Version 0.14
Important Notes for Apple Users
- If you use an Apple computer with Apple silicon architecture please make sure to use a JDK targeted to this architecture. It is officially named
aarch64
(see JEP 391). Starting with Java 17 all major distributors of JDKs should support this. - You will need the following piece of code in your
build.gradle.kts
:This is already included in the current version of our fritz2-template project.rootProject.plugins.withType<org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin> { rootProject.the<org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension>().nodeVersion = "16.0.0" }
This workaround should be needed only until Kotlin version 1.6.20
Breaking Changes
PR #544: Rework RenderContext
RenderContext
is now an Interface offering all functions to render both dynamic and static content- A
Tag
is an implementation of this interface which adds everything needed to handle a DOM-element TagContext
has been removedrender*
-methods now by default add a newDiv
-Tag into the DOM which acts as the mountpoint. Thosediv
s are marked with the attributedata-mount-point
and the static default classmount-point
. The latter simply sets thedisplay: contents
rule in order to hide this element from the visual rendering:will result in the following HTML structure:data.render { // render something }
<div class="mount-point" data-mount-point="" <!-- your dynamic content --> </div>
render*
-methods inRenderContext
now offer a new parameterinto
which can be used to bypass the creation of a default parentDiv
-tag as described above. If there is already an element which could serve as mountpoint, simply pass itsTag<HTMLElement>
as theinto
parameter. Rule of thumb: Just passthis
, since the call torender
should appear directly below the desired parent element in most cases:This results in the following DOM:ul { // this == Tag<Ul> items.data.render(into = this) { // ^^^^^^^^^^^ // pass the parent tag to use it as mount-point li { +it } } }
But beware: the mount-point controls its siblings, meaning that all other content will be removed by every update of the flow. So never apply the<ul data-mount-point=""> // no more CSS-class, as the tag *should* appear on screen in most cases <li>...</li> ... <li>...</li> </ul>
into = this
pattern if you want to render multiple different flows into a parent, or a mixture of dynamic and static content.- instead of
asText
userenderText
. It will create aSpan
-Tag as parent element and a child text-node for the text. Analogous to therender*
-methods ofRenderContext
, it also accept theinto = this
parameter, which directly renders the text-node into the parent provided without creating the extra<span>
.
PR #559: Apply KSP as substitute for KAPT for automatic lenses generation
Lenses are still automatically created by the @Lenses
annotation for data classes, but a companion object must be declared even though it might be left empty. The new processor now supports generic data classes as a new feature.
The API for accessing a lens has changed:
- there is no
L
-object holding all lenses for all domain types anymore - the lens is now created by a factory function instead by a property
For accessing a lens, consider the following example:
// somewhere in commonMain
@Lenses
data class Language(
val name: String,
val supportsFP: Boolean
) {
companion object // important to declare - KSP can't create this
}
// accessing code - could also be in jsMain
val language = storeOf(Language("Kotlin", true))
// old: val name = kotlin.sub(L.Language.name)
val name = language.sub(Language.name())
The lenses are created as extension functions of the companion object, so no dedicated object is needed. We believe this to be more comfortable to work with: When using a lens, the domain type is already present, so this should be intuitive. The L
-object wasn't too complex either, but it seemed a bit "magical" to new users, and a bit artificially named.
To get the lens, just call the function named exactly like the corresponding property.
This introduces one restriction to the design of a custom implemented companion object: You are not allowed to implement such a function yourself. The name is required by the processor and defiance will lead to an expressive compilation error.
Migration guide
Tweak buildfile
The following changes must be applied to the build.gradle.kts
plugins {
// Kotlin 1.6.x version
kotlin("multiplatform") version "1.6.10"
// Add KSP support
id("com.google.devtools.ksp") version "1.6.10-1.0.2"
// Remove fritz2-plugin
}
// Add further settings for KSP support:
dependencies {
add("kspMetadata", "dev.fritz2:lenses-annotation-processor:$fritz2Version")
}
kotlin.sourceSets.commonMain { kotlin.srcDir("build/generated/ksp/commonMain/kotlin") }
tasks.withType<org.jetbrains.kotlin.gradle.dsl.KotlinCompile<*>>().all {
if (name != "kspKotlinMetadata") dependsOn("kspKotlinMetadata")
}
// Needed to work on Apple Silicon. Should be fixed by 1.6.20 (https://youtrack.jetbrains.com/issue/KT-49109#focus=Comments-27-5259190.0-0)
rootProject.plugins.withType<org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin> {
rootProject.the<org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension>().nodeVersion = "16.0.0"
}
Code Migration
Migrating the code can be done quite easily and reliably with simple regular expressions via search and replace:
- remove all imports of the
L
-object:- search:
^import .*\.L$
- replace: by nothing
- search:
- change invocation of the lens:
- search:
L\.([\w\.]+)
(with activated case sensitivity!) - replace:
$1\(\)
- search:
There is one trap you don't want to step in when replacing via regexp: If you have a receiver that is or ends with a big "L", this will be mistakenly removed:
class Foo<L> {
fun L.doSomething() = ... // The regexp will change this line as well
}
There could be more false positives we have not encountered yet, so watch out for compiler errors after applying those regexps.
Further Cross-functional Constraints
- fritz2 now requires Kotlin 1.6.10 in order to run
- fritz2 now supports all Java versions again (up to 17 right now)
PR #543: Router is now a Store
Internally, a Router
is now a Store
, so you can use everything a Store
offers and create your own Handler
s for your Router
instance (compared to RootStore
):
object MyRouter : MapRouter(mapOf("page" to "overview")) {
val overview = handle {
it + ("page" to "overview")
}
val details = handle<String> { route, id ->
route + mapOf("page" to "details", "detailsId" to id)
}
}
// Navigate to overview page
clicks handledBy MyRouter.overview
// Navigate to details page with detailsId=12
clicks.map { "12" } handledBy MyRouter.details
Smaller API breaking changes
- PR #560: Removed deprecated uniqueId() function → use Id.next() instead
New Features
PR #556: Improve Shortcut Handling of KeyboardEvents
This PR improves the handling of shortcuts when dealing with KeyboardEvent
s:
- remove
Key
-class - add improved
Key
-class namedShortcut
based upon a new concept ofModifier
-interface which enables constructing shortcuts, e.g. "Strg + K" or "Shift + Alt + F". - add improved
Keys
-object with predefined common keys likeTab
,Enter
, also add the modifier keys likeAlt
,Shift
,Meta
andControl
- mark the special interest functions
key()
as deprecated in favor of relying on standard flow functions likefilter
ormap
.
The new shortcut API allows easy combination of shortcuts with modifier shortcuts, constructing those from a KeyboardEvent
, and also prevents meaningless combinations of different shortcuts:
// Constructing a shortcut by hand
Shortcut("K")
// -> Shortcut(key = "K", ctrl = false, alt = false, shift = false, meta = false)
// Or use factory function:
shortcutOf("K")
// Set modifier states, need to use constructor:
Shortcut("K", ctrl = true) // Shortcut(key= "K", ctrl = true, alt = false, shift = false, meta = false)
// Constructing a shortcut from a KeyboardEvent
div {
keydowns.map { shortcutOf(it) } handledBy { /* use shortcut-object for further processing */ }
// ^^
// use KeyboardEvent to construct a Shortycut-object with all potentially
// modifier key states reflected!
}
// Using predefined shortcuts from Keys object
Keys.Enter // named-key for the enter key stroke, is a `Shortcut`
Keys.Alt // `ModifierShortcut` -> needs to be combined with a "real" shortcut in order to use it for further processing
// The same but more cumbersome and prone to typos
Shortcut("Enter")
// Not the same (!)
Shortcut("Alt") // -> Shortcut(key= "Alt", ..., alt = false)
Keys.Alt // -> ModifierKey-object with alt = true property!
// Constructing a shortcut with some modifier shortcuts
Shortcut("K") + Keys.Control
// Same result, but much more readable the other way round:
Keys.Control + "K"
// Defining some common combination:
val searchKey = Keys.Control + Keys.Shift + "F"
// ^^^^^^^^^^^^
// You can start with a modifier shortcut.
// Appending a String to a ModifierKey will finally lead to a `Shortcut`.
val tabbing = setOf(Keys.Tab, Keys.Shift + Keys.Tab)
// API prevents accidently usage: WON'T COMPILE because real shortcuts can't be combined
Shortcut("F") + Shortcut("P")
// Shortcut is a data class → equality is total:
Keys.Control + Keys.Shift + "K" == Shortcut("K", shift = true, ctrl= true, alt = false, meta = false)
// But
Keys.Control + Keys.Shift + "K" != Shortcut("K", shift = false, ctrl= true, alt = false, meta = false)
// ^^^^^^^^^^ ^^^^^^^^^^^^^
// +-----------------------------------+
// Case sensitive, too. Further impact is explained in next section.
shortcutOf("k") != shortcutOf("K")
Be aware of the fact that the key
-property is taken from the event as it is. This is important for all upper case keys: The browser will always send an event with shift
-property set to true
, so in order to match it, you must construct the matching shortcut with the Shift
-Modifier:
// Goal: Match upper case "K" (or to be more precise: "Shift + K")
keydowns.events.filter { shortcutOf(it) == shortcutOf("K") } handledBy { /* ... */ }
// ^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^
// | Shortcut(key = "K", shift = false, ...)
// | ^^^^^^^^^^^^
// | +-> will never match the event based shortcut!
// | | the modifier for shift needs to be added!
// Shortcut("K", shift = true, ...)--+
// upper case "K" is (almost) always send with enabled shift modifier!
// Working example
keydowns.events.filter { shortcutOf(it) == Keys.Shift + "K" } handledBy { /* ... */ }
Since most of the time you will be using the keys within the handling of some KeyboardEvent
, there are some common patterns relying on the standard Flow
functions like filter
, map
or mapNotNull
to apply:
// All examples are located within some Tag<*> or WithDomNode<*>
// Pattern #1: Only execute on specific shortcut:
keydowns.events.filter { shortcutOf(it) == Keys.Shift + "K"}.map { /* further processing if needed */ } handledBy { /* ... */ }
// Variant of #1: Only execute the same for a set of shortcuts:
keydowns.events.filter { setOf(Keys.Enter, Keys.Space).contains(shortcutOf(it)) }.map { /* further processing if needed */ } handledBy { /* ... */ }
// Pattern #2: Handle a group of shortcuts with similar tasks (navigation for example)
keydowns.events.mapNotNull{ event -> // better name "it" in order to reuse it
when (shortcutOf(event)) {
Keys.ArrowDown -> // create / modify something to be handled
Keys.ArrowUp -> //
Keys.Home -> //
Keys.End -> //
else -> null // all other key presses should be ignored, so return null to stop flow processing!
}.also { if(it != null) event.preventDefault() // page itself should not scroll up or down! }
// ^^^^^^^^^^
// Only if a shortcut was matched
} handledBy { /* ... */ }
(The final result is based upon the PR #565 too)
PR #558: Add Middleware support to html API + pre-build stateless Authentication middleware
In the fritz2 http
API you can now add Middleware
s which have the following definition:
interface Middleware {
suspend fun enrichRequest(request: Request): Request
suspend fun handleResponse(response: Response): Response
}
You can add a Middleware
to all your http calls by using the new use(middleware: Middleware)
function.
val logging = object : Middleware {
override suspend fun enrichRequest(request: Request): Request {
console.log("Doing request: $request")
return request
}
override suspend fun handleResponse(response: Response): Response {
console.log("Getting response: $response")
return response
}
}
val myAPI = http("/myAPI").use(logging)
...
You can add multiple Middleware
s in one row with .use(mw1, mw2, mw3)
. The enrichRequest
functions will be called from left to right (mw1, mw2, mw3), the handleResponse
functions from right to left (mw3, mw2, mw1). You can stop the processing of a Middleware
's Response
further down the chain with return response.stopPropagation()
.
Also, we built a pre-implemented Authentication
middleware to enrich all request with authentication information and handle bad authentication responses in general:
abstract class Authentication<P> : Middleware {
// List of status codes forcing authentication
open val statusCodesEnforcingAuthentication: List<Int> = listOf(401, 403)
// Add your authentication information to all your request (e.g. append header value)
abstract fun addAuthentication(request: Request, principal: P?): Request
// Start your authentication process (e.g. open up a login modal)
abstract fun authenticate()
val authenticated: Flow<Boolean>
val principal: Flow<P?>
}
For more information have a look at the docs.
PR #544: Rework RenderContext
The nearest MountPoint
is now available on scope when rendering (some convenience methods to access it). It allows the registration of lifecycle-handlers after mounting and before unmounting a Tag from the DOM:
div {
afterMount(someOptionalPayload) { tag, payload ->
// Do something here
}
beforeUnmount(someOptionalPayload) { tag, payload ->
// Do something here
}
}
This new feature is used for the transition support (see next item).
PR #545: Transitions (css-animations on DOM manipulation)
This PR adds transition-support to fritz2. You now can define a css-transition by the css classes describing the transition itself as well as the start- and endpoint like..
// CSS is tailwindcss (https://tailwindcss.com)
val fade = Transition(
enter = "transition-all duration-1000",
enterStart = "opacity-0",
enterEnd = "opacity-100",
leave = "transition-all ease-out duration-1000",
leaveStart = "opacity-100",
leaveEnd = "opacity-0"
)
..and apply it to a tag:
div {
inlineStyle("margin-top: 10px; width: 200px; height: 200px; background-color: red;").
transition(fade)
}
Recommendation: Do not apply transitions to tags that are also styled dynamically (Flow
based). This might introduce race conditions and therefore unwanted visual effects.
Improvements
- PR #564: Provide annex context for creating elements as direct sibling of a tag
Fixed Bugs
- PR #557: Removed touchends-event scrolling from appFrame component (fixed jumping screen problem)
- PR #546: Use a SharedFlow for Web Component attribute changes (Many thanks to @tradeJmark for his great work!)