Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Repairs event handling with focus of manipulation and filtering #861

Merged
merged 1 commit into from May 2, 2024

Conversation

Lysander
Copy link
Collaborator

@Lysander Lysander commented Apr 19, 2024

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 Flows 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-lamba.

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

@Lysander Lysander added the documentation Improvements or additions to documentation label Apr 19, 2024
@Lysander Lysander added this to the 1.0-RC18 milestone Apr 19, 2024
@Lysander Lysander self-assigned this Apr 19, 2024
@Lysander Lysander force-pushed the chausknecht/add-event-handling-doc branch from d6940d5 to d0d5d6c Compare April 22, 2024 09:04
@Lysander Lysander force-pushed the chausknecht/add-event-handling-doc branch 2 times, most recently from db3876a to 37f877e Compare April 29, 2024 16:06
@Lysander Lysander changed the title Add initial text for event handling docs Repairs event handling with focus of manipulation and filtering Apr 29, 2024
@Lysander Lysander added bug Something isn't working api breaking Forces client sources to change labels Apr 29, 2024
@Lysander Lysander force-pushed the chausknecht/add-event-handling-doc branch from 37f877e to 9745bbe Compare April 30, 2024 08:46
@Lysander Lysander marked this pull request as ready for review April 30, 2024 08:47
@Lysander Lysander force-pushed the chausknecht/add-event-handling-doc branch 2 times, most recently from 52bfefe to 10e0145 Compare April 30, 2024 14:46
@Lysander Lysander requested a review from haukesomm April 30, 2024 14:49
haukesomm
haukesomm previously approved these changes Apr 30, 2024
- enables filtering and manipulating events immediately after occurrence due to specialized event factories. Prevent `Flow` based delay we had until now.
- simplifies `Listener`-type. It remains more or less as 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
@Lysander Lysander force-pushed the chausknecht/add-event-handling-doc branch from 3dfa1b2 to 0b78b92 Compare May 2, 2024 10:35
@Lysander Lysander requested a review from haukesomm May 2, 2024 10:36
@Lysander Lysander merged commit de114fe into master May 2, 2024
2 checks passed
@Lysander Lysander deleted the chausknecht/add-event-handling-doc branch May 2, 2024 10:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api breaking Forces client sources to change bug Something isn't working documentation Improvements or additions to documentation
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants