Releases: jwstegemann/fritz2
Version 1.0-RC19.5
This version is a patch release backporting important changes from master so they can be used in Kotlin 1.9 projects without needing to migrate to Kotlin 2.0 first.
Improvements
- PR #926: Simplify the combobox's internal state handling of the open-state
Version 1.0-RC19.4
This version is a patch release backporting important changes from master so they can be used in Kotlin 1.9 projects without needing to migrate to Kotlin 2.0 first.
Fixed Bugs
- PR #921: Fix setting an initial value for the ComboBox
Version 1.0-RC19.3
This version is a patch release backporting important changes from master so they can be used in Kotlin 1.9 projects without needing to migrate to Kotlin 2.0 first.
Fixed Bugs
- PR #913: Combobox: Only accept distinct values via data-binding
Version 1.0-RC19.2
This version is a patch release backporting important changes from master so they can be used in Kotlin 1.9 projects without needing to migrate to Kotlin 2.0 first.
Improvements
- PR #911: Improves the API doc for
renderIf
Fixed Bugs
Version 1.0-RC19.1
This version is a patch release backporting important changes from master
so they can be used in Kotlin 1.9 projects without needing to migrate to Kotlin 2.0 first.
Fixed Bugs
PR #908 - Allows manual computation of PopUpPanel's position, fixes initial positioning of the combobox's dropdown
- Adds a method to manually (re-)compute the position of a PopUpPanel's dropdown if needed
- Fixes a bug where the combobox's dropdown appears off-position when the dropdown is aligned at the end of its reference and has a changing size
Version 1.0-RC19
This release adds some important functionality to fritz2. The highlights are defintely:
- adds the long awaited ComboBox headless component.
- improves support for sealed type hierarchies by new
lenses-annotation-processor
-capabilities and dedicated documentation, explaining typical use cases and solving idioms.
Breaking Changes
PR 879 - Removes unsafe export-API
Problem
Until this PR, the export
function was not safe to use:
class Exporter<T : Any>(initialize: Exporter<T>.() -> Unit) {
lateinit var payload: T // this is bad and dangerous!
fun export(payload: T): T {
this.payload = payload
return payload
}
init {
initialize()
}
}
As you can see there is the lateinit va payload: T
, which gets only set, if export()
-function gets calles in the initialize
-Lambda.
This cannot be forced! And that leads to a brittle API :-(
Imagine some control structures like if
, which prohibits the call if some value fails its condition. Then the outer export
call will try to access the payload
-field in order to return it. But it is still not initialized:
val something= false
export {
if(something) {
export("This wont get called always!")
}
}.also { /* an exception would be thrown before this block. Hard to find the precise code section on top! */ }
Solution
We have decided to remove this function from fritz2!
It is simply not possible to reach for a good design, that would fit all kind of situations.
One idea could be to introduce some default
-parameter to cover the case, where no export
is called inside the Exporter
-scope. But this would force people to always provide some senseless default value, that will never be used in the end.
As this function is often used to return some Tag<*>
from a nested UI-code section, one would have to create an "empty"-tag-object, which will be rendered inside the DOM anyways...
You can guess, there is no good default - that's probably why there is nothing like this built into Kotlin's standard lib ;-)
As you can see this is unfortunately API breaking, but there was no other solution.
Migration Guide
Thus said, it should be quite simple to bypass the use, by just
// with `export`
fun RenderContext.component(): HtmlTag<HTMLInputElement> = export {
div(id = id) {
export(
input(id = inputId) {
type("text")
}
)
}
}
// without `export`
// before
fun RenderContext.component(): HtmlTag<HTMLInputElement> {
lateinit var result // we are save to use this here, as the `input`-factory will always be called.
div(id = id) {
result = input(id = inputId) {
type("text")
}
}
return result
}
If you have code, where it is not guarantueed, that the var
will be initialized, you cant use lateinit
, but should rather use a "normal" var
oder make the type of the var
nullable like this:
// without `export`
fun RenderContext.component(flag: Boolean): HtmlTag<HTMLInputElement>? {
var result: HtmlTag<HTMLInputElement>? = null // we not safe, that `input` will be called!
div(id = id) {
if(flag) {
result = input(id = inputId) {
type("text")
}
}
}
return result
}
Improvements
PR #880 - Adds a headless combobox component
This PR adds a headless combobox
component inspired by the HeadlessUI Combobox.
The component can be created via the combobox
factory.
For the full documentation visit the online documentation.
API-sketch:
combobox<T> {
val items: ItemsHook()
// params: List<T> / Flow<List<T>>
var itemFormat: (T) -> String
val value: DatabindingProperty<T?>
var filterBy: FilterFunctionProperty
// params: (Sequence<T>, String) -> Sequence<T> / T.() -> String
val openDropdown: DropdownOpeningHook
// methods: lazily() / eagerly()
val selectionStrategy: SelectionStrategyProperty
// methods: autoSelectMatches() / manual()
var maximumDisplayedItems: Int = 20
var inputDebounceMillis: Long = 50L
var renderDebounceMillis: Long = 50L
comboboxInput() { }
comboboxPanelReference() {
// this brick is often used with a nested
// comboboxInput() { }
}
comboboxLabel() { }
comboboxItems() {
// inherited by `PopUpPanel`
var placement: Placement
var strategy: Strategy
var flip: Boolean
var skidding: Int
var distance: int
val results: Flow<QueryResult.ItemList<T>>
// state.render {
// for each QueryResult.ItemList<T>.Item<T> {
comboboxItem(Item<T>) { }
// }
// }
}
comboboxValidationMessages() {
val msgs: Flow<List<ComponentValidationMessage>>
}
}
PR #876: Introduce automatic Generation of Delegating Lenses: Improves Validation Support for Sealed Class Hierarchies
Motivation
Currently the support for validating sealed class hierachies is rather limited, as our provided mechanisms wont work as the following example shows:
import dev.fritz2.core.Lens
import dev.fritz2.core.Lenses
import dev.fritz2.core.lensOf
import dev.fritz2.headless.validation.ComponentValidationMessage
import dev.fritz2.headless.validation.warningMessage
import dev.fritz2.validation.Validation
import dev.fritz2.validation.validation
import dev.fritz2.validation.invoke
sealed interface Product {
val name: String
companion object {
// Try to implement validation for the common property `name` at a central place.
// (Have a look at the validation code of `WebFramework` data class, which calls this central validation)
val validation: Validation<Product, Unit, ComponentValidationMessage> = validation { inspector ->
val name = inspector.map(Product.name()) // won't compile -> there is no Lens generated.
// ...
}
fun <T : Product> createGenericValidation(): Validation<T, Unit, ComponentValidationMessage> =
validation { inspector ->
val name = inspector.map(T.name()) // won't compile -> generic types dont carry companion objects!
}
inline fun <reified T> createGenericValidationInlined(): Validation<T, Unit, ComponentValidationMessage> where T : Product =
validation { inspector ->
val name = inspector.map(T.name()) // won't compile -> generic types dont carry companion objects!
}
}
}
@Lenses
data class WebFramework(override val name: String, val technology: Technology) : Product {
companion object {
val validation: Validation<WebFramework, Unit, ComponentValidationMessage> = validation { inspector ->
val technology = inspector.map(WebFramework.technology())
if (technology.data == Technology.PHP) {
add(technology.warningMessage("Consider a better language for this task ;-)"))
}
// call the validation of the parent type
addAll(Product.validation(inspector.data))
}
}
}
@Lenses
data class Pizza(override val name: String, val toppings: List<String>) : Product {
companion object {
// analogous procedure:
// val validation ...
}
}
enum class Technology {
Kotlin,
PHP,
FSharp,
}
We cannot access the generated Lenses from within the interface scope, as we cannot refer to the companion objects of the implementing classes.
We could create the needed lenses by hand of course:
sealed interface Product {
val name: String
companion object {
// generate `Lens` by hand:
fun manualNameLens(): Lens<Product, String> = lensOf("name", Product::name) { _, _ -> TODO() }
// ^^^^^^^^^^^^^^
// This is ugly!
val validationWithManualLens: Validation<Product, Unit, ComponentValidationMessage> = validation { inspector ->
val name = inspector.map(manualNameLens())
// ...
}
}
}
We cannot really provide some good setter
though!
Warning
This is very dangerous, as anyone could use the Lens
for store-mapping, which would fail then!
making this lens private
would minimize the risk of leaking outside, but a small risk remains of course...
Solution
The solution is kinda simple: We have to implement the lens within the sealed base type, but delegate the "work" to the implementing types! This way we can use the polymorphic aspect in a safe way, as the sealed
property guarantees alle types are known at compile time:
sealed interface Product {
val name: String
companion object {
fun name(): Lens<Product, String> = lensOf(
"name",
{ parent ->
when (parent) {
is WebFramework -> parent.name
is Pizza -> parent.name
}
},
{ parent, value ->
when (parent) {
is WebFramework -> parent.copy(name = value)
is Pizza -> parent.copy(name = value)
}
...
Version 1.0-RC18
Highlight of this release might be a complete new chapter of our documentation dedicated to event handling including Key
-API, because of the work on PR #861.
Improvements
PR #862 - Improve disclosure component for state creating content, add options to control a transition's initial and after-end classes
Overview
This PR changes the headless disclosure to only render its panel's content once and then show/hide it based on styling. Currently, the panel's content is rendered every time the disclosure is opened/closed.
Warning
In order to achieve this, the headless component no longer controls the panel's visibility. All styling is now solely done by the implementor of a headless discloure and needs to be adjusted accordingly. Thus, the changes introduced in this PR are api breaking!
Additionally, the transition
function for Flow
-based transitions has been extended to allow optional control over initial classes and classes that should be present after the leave-transition has ended. This is useful to allow transitions on an element's visibility which are normally not possible using pure CSS.
Disclosure
As mentioned earlier, the headless disclosure no longer creates a mount-point for its panel to re-render based on the open-state. Instead, it now only provides the state and leaves the styling to the implementor.
This is necessary due to multiple reasons:
- Creating a mount-point is not related to styling, thus it can easily be done in headless. Hiding/showing an element is - that's why it cannot be done in headless.
- Leaving the entire styling (including the styles used for hiding/showing the element) to the implementor allows for greater flexibility on how a concrete disclosure component might work. For example, it might behave similar to a spoiler element on social media sites and provide a blurred preview of some content that is revealed when clicked.
- By leaving the styling to the user, all styling can be applied in a single place whithout some internal styling working against you.
Migration
In order to migrate an existing disclosure component, styling information for the opened/closed states must be provided.
Find different migration scenarios below.
Note
Most of the examples below use Tailwind CSS. However, you are free to use any CSS framework, or even plain CSS, as well.
Example using plain CSS:
disclosurePanel {
inlineStyle(opened.map {
if (it) "display: block;" else "display: none;"
})
}
1) The disclosure to migrate does not use transitions
In this case, the appropriate styling for the opened/closed states needs to be added. No existing styles have to be adjusted.
This can be as easy as simply providing basic CSS styling:
Before:
disclosurePanel {
// some content
}
After:
disclosurePanel {
className(
opened.map { if (it) "" else "hidden" },
initial = "hidden"
)
// some content
}
2) The existing disclosure makes use of transitions
In this case, the existing transition
call needs to be adjusted in order to provide visible/hidden classes. As mentioned in the beginning, this function has been adjusted accordingly.
Before:
disclosurePanel {
transition(
"transition transition-all duration-500 ease-in",
"opacity-0 scale-y-95 max-h-0",
"opacity-100 scale-y-100 max-h-[100vh]",
"transition transition-all duration-500 ease-out",
"opacity-100 scale-y-100 max-h-[100vh]",
"opacity-0 scale-y-95 max-h-[0]"
)
// some content
}
After:
disclosurePanel {
transition(
opened,
// ^^^^^^
// change the transition to be flow based by specifing a triggering flow via the `on` parameter
"transition transition-all duration-500 ease-in",
"opacity-0 scale-y-95 max-h-0",
"opacity-100 scale-y-100 max-h-[100vh]",
"transition transition-all duration-500 ease-out",
"opacity-100 scale-y-100 max-h-[100vh]",
"opacity-0 scale-y-95 max-h-[0]",
afterLeaveClasses = "hidden",
// ^^^^^^
// provide classes that should be present when the leave transition is over (panel hidden)
initialClasses = "hidden"
// ^^^^^^
// provide an initial styling. In this case: `hidden`, since `opened` is false by default
)
// some content
}
Further Improvements
- PR #859: Rename classes utility function to
joinClasses
in order to reduce naming confusion
Fixed Bugs
PR #861 - Repairs event handling with focus of manipulation and filtering
Current State
fritz2 has supported event-handling since its beginning, but with the release of RC1, we introduced dedicated handling for the internal DOM-API event manipulation with stopPropagation
, stopImmediatePropagation
or preventDefault
(see also PR #585). These were part of the Listener
-class and implemented as kind of builder
-pattern to provide a fluent API:
keydowns.stopPropagagtion().preventDefault().map { ... } handledBy someHandler
// ^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^ ^^^^^
// each call returns `Listener` with "normal" flow-transformation
// modified event state.
Those three functions were also provided with extensions on Flow<T>
in order to make them callable, like shown above, as an intermediate Flow
-operation:
keydowns.map { ... }.stopImmediatePropagagtion() handledBy someHandler
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^
// after `map`, the type is no longer `Listener`, but `Flow<T>`
Caution
This did not work reliably! It might sometimes work as desired, sometimes it might fail.
The big fundamental problem with Kotlin's Flow
s is that the emit - collect
-cycle comes with a time delay which can be crucial for operations like the above. The browser's rendering engine keeps on working while the first Event
-value is emitted and consumed on its way to the handledBy
-collector. So it might have already been propagated further, or the default action has already been triggered, by the time those functions are applied. This leads to unwanted und often volatile effects during the application lifecycle.
Solution
To bypass the flow-delay, we needed a way to apply those event-manipulating functions as soon as the DOM calls the registered listener function. This now happens inside a new function called subscribe
with the following signature:
fun <E : Event, T : EventTarget> T.subscribe(
name: String,
capture: Boolean = false,
selector: E.() -> Boolean = { true }
): Listener<E, T> = ...
In this function, we emit the Event
-object from the DOM into a Flow
, which can then be processed further.
Until the emit
-function is called, all processing is strictly sequential, which means that the order of the registered event-listeners is guaranteed.
So in short, the solution is to apply those functions right onto the Event
-object before emitting it to the flow.
In order to enable this, we have introduced two factory functions for each predefined (property based) Listener
-object. For example, for the clicks
-Listener, the following two additional factories exist:
- one is named exactly like the
property
itself:clicks(init: MouseEvent.() -> Unit)
- the other adds an
If
-suffix like this:clicksIf(selector: MouseEvent.() -> Boolean)
Those two factories enable a user to control the further processing besides the custom written Flow
-Handler
-binding.
The first is a substitution for simply calling event manipulating functions, the second enables filtering the event-emitting based on the Event
-object. This is a common pattern used inside the headless components.
Now it is possible to write code like this:
div {
+"Parent"
button {
+"stopPropagation"
// We use the `clicks(init: MouseEvent.() -> Unit)` variant here:
clicks { stopPropagation() } handledBy { console.log("stopPropagation clicked!") }
// ^^^^^^^^^^^^^^^^^
// We want the event processing to stop bubbling to its parent.
// As the receiver type is `MouseEvent`, which derives from `Event`, we can call
// its method directly.
clicks handledBy { window.alert("Button was clicked!") }
}
// no value will appear on this `clicks`-Listener anymore!
clicks handledBy { console.log("click reached Parent!") }
}
The last click
upon the outer <div>
-tag will never be processed, due to the call of the stopPropagation
inside the <button>
-tag.
The filtering works like this:
keydownsIf { shortcutOf(this) == Keys.Space } handledBy { ... }
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// Boolean expression: If "Space" is pressed resolve to `true` -> emit the event, so it can be handled.
// (This syntax is explained in a following section about "Keyboard-Events and Shortcut-API"!)
Important
We strongly recommend to manipulate the event handling only inside the lambda-expressions of the new Listener
-factory-functions! Strive to move such manipulations from inside some mapping / filtering into such an init
or selector
-lambda-expression.
Further Improvements
- simplify
Listener
-type. It remains more or less a marker type in order to dispatch the convenience functions to grab values out of specific DOM elements. - get rid of unnecessary convenience functions
- remove
@ignore
from one test case as the cause is now fixed - add a new [dedicated doc...
Version 1.0-RC17
Version 1.0-RC16
This release fixes some leftover issues with event handling for nested portals (a dropdown inside a pop-over, for example) and improves the focus management for some headless components.
Improvements
- PR #853: Improves Focus Management of RadioGroups
Fixed Bugs
Version 1.0-RC15
Improvements
The most important change for the project is the upgrade to the latest Kotlin version, that is 1.9.22 at the time of this release and gradle 8.5.
- PR #789: Kotlin 1.9.20 & refreshVersions plugin
- PR #841: Refresh Versions for upcoming RC13 Release (Kotlin 1.9.22 for example)
- PR #845: Update Gradle to v8.5
- PR #811: Move docs for new serialization module to docs sites
- PR #817: Update the headless documentation of portalling
- PR #819: Make RootStore.runWithJob public
- PR #822: Improves PopUpPanel Rendering
- PR #823: Improves and enhances the reactive styling
- PR #824: Improve event handling in dataCollection
- PR #826: Improves the validation message filtering and resets the default behaviour
- PR #829: Add Scope handling for Portal Containers
- PR #830, #834: Avoid events in flatMapLatest
- PR #835: Improves PopUpPanel
- PR #836: Improves TicTacToe example
- PR #838: Add UI-Tests to GitHub Build-Process
- PR #839: Add documentation for Stores current property
Fixed Bugs
- PR #847: Fix implicit dependency error in Gradle for publishing
- PR #843: Fix javadoc jar publication for publishing to MavenCentral
- PR #842: Fix Selection Behaviour of ListBox
- PR #812: Fixes WebComponent example
- PR #833: Fix nested Portals
- PR #837: Fix Mountpoint in Portals
- PR #838: Fix some regression in DataCollection
Credits
Special thanks to @jillesvangurp for his amazing work on #789!