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 documentation chapter for event handling including
Key
-API
Migration Guide
If the compiler complains about a missing function, we recommend to switch to the same named factory (with the init
-parameter):
// before
keydowns.stopPropagagtion().preventDefault().map { ... } handledBy someHandler
keydowns.map { ... }.stopPropagagtion().preventDefault() handledBy someHandler
// now
keydowns {
// Remember: receiver type of `init`-lambda is an `Event`, so we can call all functions directly:
stopPropagagtion()
preventDefault()
}.map { ... } handledBy someHandler
Besides compiler errors, there might also be code sections which should be refactored in order to behave reliably:
// before: Will lead to compiler errors
keydowns.mapNotNull { ... }.stopPropagagtion().preventDefault() handledBy someHandler
// ^^^^^^^^^^^^^^^^
// Caution: If inside there is a branch that returns `null`,
// the further operations are not called.
// This must be adapted inside the `selector`-logic.
keydowns.filter { ... }.stopPropagagtion().preventDefault() handledBy someHandler^
// ^^^^^^^^^^^^^^^^
// Caution: Depending on the filtering, further operations are not called!
// This must be adapted inside the `selector`-logic.
// before: no compiler error - but no more recommended
keydowns.mapNotNull {
if(someCondition) {
it.stopPropagagtion()
it.preventDefault()
null
} else it
} handledBy someHandler
Use the If
-suffix based event-factories to replace such calls:
// now
keydownsIf {
if(someCondition) {
stopPropagagtion()
preventDefault()
false // this is fully dependant on the previous code: Here, the event should not be processed any further,
// so we need to make the `selector` fail
} else true
} handledBy someHandler
Further Bugs
- PR #856: This PR fixes some Flow-based focus-trap issue