Releases: jwstegemann/fritz2
Version 1.0-RC3
Breaking Changes
PR #728: Streamline API of Stores, Inspector and Lenses
We changed our API of Store
, Inspector
and Lens
to a more Kotlin-like style of functional programming, making them more similar to the Flow
-API.
Migration Guide
The following tables show the difference:
Stores mapping
current | new |
---|---|
Store<P>.sub(lens: Lens<P, T>): Store<T> | Store<P>.map(lens: Lens<P, T>): Store<T> |
Store<P?>.sub(lens: Lens<P & Any, T>): Store<T> | Store<P?>.map(lens: Lens<P & Any, T>): Store<T> |
Store<List<T>>.sub(element: T, idProvider): Store<T> | Store<List<T>>.mapByElement(element: T, idProvider): Store<T> |
Store<List<T>>.sub(index: Int): Store<T> | Store<List<T>>.mapByIndex(index: Int): Store<T> |
Store<Map<K, V>>.sub(key: K): Store<V> | Store<Map<K, V>>.mapByKey(key: K): Store<V> |
Store<T?>.orDefault(default: T): Store<T> | Store<T?>.mapNull(default: T): Store<T> |
MapRouter.sub(key: String): Store<String> | MapRouter.mapByKey(key: String): Store<String> |
The same applies for the Inspector
API as well.
Lens creation
current | new |
---|---|
lens(id: String, getter: (P) -> T, setter: (P, T) -> P): Lens<P, T> | lensOf(id: String, getter: (P) -> T, setter: (P, T) -> P): Lens<P, T> |
format(parse: (String) -> P, format: (P) -> String): Lens<P, String> | lensOf(parse: (String) -> P, format: (P) -> String): Lens<P, String> |
lensOf(element: T, idProvider: IdProvider<T, I>): Lens<List, T> | lensForElement(element: T, idProvider: IdProvider<T, I>): Lens<List, T> |
lensOf(index: Int): Lens<List, T> | lensForElement(index: Int): Lens<List, T> |
lensOf(key: K): Lens<Map<K, V>, V> | lensForElement(key: K): Lens<Map<K, V>, V> |
defaultLens(id: String, default: T): Lens<T?, T> | not publicly available anymore |
Lens mapping
current | new |
---|---|
Lens<P, T>.toNullableLens(): Lens<P?, T> | Lens<P, T>.withNullParent(): Lens<P?, T> |
PR #735: Optimize Efficiency of render
and renderText
Until now, the Flow<V>.render
and Flow<V>.renderText
functions collected every new value on the upstream flows and started the re-rendering process.
In order to improve performance however, a new rendering should only happen if there is not just a new value, but a changed one. If the value equals the old one, there is no reason to discard the DOM subtree of the mount-point.
This effect was previously achieved by adding distinctUntilChanged
to the flow. But it is cumbersome in your code and easy to forget,
so we added this call to the provided flow for both functions automatically.
As a result, the user gets automatic support for efficient precise rendering approaches by custom data-flows.
PR #731: Remove FetchException
from http
-API
No more FetchException
is thrown when execute()
gets called internally for receiving the Response
object in fritz2 http
-API.
Migration Guide
For this reason, it is not needed to catch the FetchException
exception anymore to receive a Response
with status-code != 200
. The only exceptions that can occur now are the ones from the underlying JavaScript Fetch-API (e.g. if status-code = 404
).
PR #739: Fixes focus-trap functions
This PR repairs the functionality of the trapFocusWhenever
-function. Before, it behaved incorrectly, as setting the initial focus and restoring would not work properly. Now it is explicitly targeted to its condition Flow<Boolean>
for its internal implementation.
Also, it renames trapFocus
to trapFocusInMountpoint
to improve its semantic context - enabling the trap inside a reactively rendered section and disabling it on removal.
The so called "testdrive" was added to the headless-demo
project, which offers (and explains) some samples in order to test and explore the different focus-traps. Also, some UI-Tests were added which stress those samples.
Migration Guide
Just rename all occurences of trapFocus
to trapFocusInMountpoint
:
// before
div {
trapFocus(...)
}
// now
div {
trapFocusInMountpoint(...)
}
PR #740 Simplify Tracker
Until now, a tracker was able to distinguish several different transactions which were passed as a parameter to the track
function. This rarely needed functionality can still be implemented by using multiple trackers (whose data
streams can be combined if needed).
Specifying a transaction as a parameter of track
is no longer possible. Appropriately, no defaultTransaction
can be defined in the factory either. Likewise, obtaining the flow which checks whether a certain transaction is running by invoking the Tracker
is omitted. All of this significantly simplifies the tracker's implementation.
Further Improvements
- PR #732: Improves the documentation a lot to fit different needs better.
- PR #734: Adds data-mount-point attribute to renderText generated mount-point tag.
- PR #733: Fixes CORS problems in JS-tests when requesting the
test-server
API. - PR #738: Removes the incorrect
text
attribute extension functions.
Fixed Bugs
- PR #741: Fix bug stopping handler on re-rendering
Version 1.0-RC2
Breaking Changes
PR #718: Remove Repositories from Core
As we have considered repositories to add no real value as abstraction, this commit will remove them entirely from fritz2.
Migration Guide
Just integrate the code form any repository implementation directly into the handler's code, that used to call the repository. Of course all Kotlin features to structure common code could be applied, like using private methods or alike.
PR #707: Repair remote auth middleware - prevent endless loop for 403 response
The default status code for a failed authentication is reduced to only 401.
Rational
Before also the 403 was part of the status codes and would trigger the handleResponse interception method and starts a new authentication recursively. This is of course a bad idea, as the authorization will not change by the authentication process. Therefore the default http status for launching an authentication process should be only 401.
Migration Guide
If you have some service that really relies on the 403 for the authentication, please adopt to the http semantics and change that to 401 instead.
PR #712: Simplify history feature
Simplifying the history feature, which includes the following changes:
- history is synced with
Store
by default
// before
val store = object : RootStore<String>("") {
val hist = history<String>().sync(this)
}
// now
val store = object : RootStore<String>("") {
val hist = history() // synced = true
}
- renamed
reset()
method toclear()
- renamed
add(entry)
method topush(entry)
- removed
last()
method, cause withcurrent: List<T>
every entry is receivable - changed default
capacity
to0
(no restriction) instead of10
entries
PR #715: Exposing Store interface instead of internal RootStore and SubStore
Exposing only the public Store<T>
type in fritz2's API, instead of the internal types RootStore
or SubStore
for simplifying the use of derived stores.
// before
val person: RootStore<Person> = storeOf(Person(...))
val name: SubStore<Person, String> = person.sub(Person.name())
// now
val person: Store<Person> = storeOf(Person(...))
val name: Store<String> = person.sub(Person.name())
Migration Guide
Just change the type of some field or return type from RootStore<T>
to Store<T>
and SubStore<T, D>
to Store<D>
.
PR #727: Resolve bug with alsoExpression on Hook with Flow
In order to make the also-expression work with Flow
based payloads, we had to tweak the API.
The Effect
now gets the alsoExpr
from the Hook
injected into the applied function as second parameter besides the payload itself. This way the expression can and must be called from the value
assigning code sections, which a hook implementation typically implements.
As the drawback we can no longer expose the return type R
to the outside client world. An effect now returns Unit
.
typealias Effect<C, R, P> = C.(P, (R.() -> Unit)?) -> Unit
^^^^^^^^^^^^^^^
alsoExpr as 2nd parameter
migration guide
The client code of some hook initialization does not need any changes.
The code for hook execution should almost always stay the same, as long as the code did not rely on the return type. If that was the case, you have the following options:
- move operating code into the hook implementation itself
- if some external data is needed, enrich the payload with the needed information and the proceed with 1.
The assignment code to Hook.value
will need a second parameter. Often this is done by some functional expression, which can be solved like this (example taken from TagHook
):
// before
operator fun invoke(value: I) = this.apply {
this.value = { (classes, id, payload) ->
renderTag(classes, id, value, payload)
}
}
// now
operator fun invoke(value: I) = this.apply {
this.value = { (classes, id, payload), alsoExpr ->
// ^^^^^^^^
// add 2nd parameter
renderTag(classes, id, value, payload).apply { alsoExpr?.let { it() } }
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// apply the expression onto the specific result (`R`)
// is is always specific to the hook's implemetation
}
}
New Features
PR #701: Add headless Toast component
A toast is a component that can be displayed in specific areas of the screen for both a fixed or indefinite amount of time, similar to notifications. fritz2's headless components now offer a nice and simple abstraction for this kind of functionality.
Have a look into our documentation to get more information.
PR #719: Enable Event capturing
It is now possible to listen on events in capture-phase (suffixed by Captured
):
render {
div {
clicksCaptured handledBy store.save
}
}
For this we added new options to the subscribe()
function which gives you a Listener
for your event:
subscribe<Event>(name: String, capture: Boolean, init: Event.() -> Unit)
Using the init
-lambda you can make settings to the captured event that have to be applied immediately.
We also fixed a bug when using stopPropagation()
on a Listener
which sometime did not work as expected.
Further New Features
- PR #716: Integrate fritz2 examples into the web site
Improvements
PR #711: Improve handling of nullable values in Stores
Handling nullable values in Store
s
If you have a Store
with a nullable content, you can use orDefault
to derive a non-nullable Store
from it, that transparently translates a null
-value from its parent Store
to the given default-value and vice versa.
In the following case, when you enter some text in the input and remove it again, you will have a value of null
in your nameStore
:
val nameStore = storeOf<String?>(null)
render {
input {
nameStore.orDefault("").also { formStore ->
value(formStore.data)
changes.values() handledBy formStore.update
}
}
}
In real world, you will often come across nullable attributes of complex entities. Then you can often call orDefault
directly on the SubStore
you create to use with your form elements:
@Lenses
data class Person(val name: String?)
//...
val applicationStore = storeOf(Person(null))
//...
val nameStore = applicationStore.sub(Person.name()).orDefault("")
Calling sub
on a Store
with nullable content
To call sub
on a nullable Store
only makes sense, when you have checked, that its value is not null:
@Lenses
data class Person(val name: String)
//...
val applicationStore = storeOf<Person>(null)
//...
applicationStore.data.render { person ->
if (person != null) { // if person is null you would get NullPointerExceptions reading or updating its SubStores
val nameStore = customerStore.sub(Person.name())
input {
value(nameStore.data)
changes.values() handledBy nameStore.update
}
}
else {
p { + "no customer selected" }
}
}
Further Improvements
- PR #696: Upgrades to Kotlin 1.7.20
- PR #677: Improve textfield API
- PR #681: Improve Headless Input API
- PR #680: Make render's lambda run on Tag instead of RenderContext
- PR #686: Add default data-binding as fallback for headless components
- PR #692: Improve DataCollection behavior: Let selections be updated by filtered data flow
- PR #699: Added link for docs to edit the content on Github
- PR #705: Improve http example in documentation
- PR #706: Rework documentation for Webcomponents
- PR #708: Detect missing match in RootStore for IdProvider based derived stores
- PR #726: Improve the Focustrap for Flow based sections
Fixed Bugs
- PR #663: Fix structure info in validation handling
- PR #679: Fix for Attribute referenced id
- PR #687: Repair aria-haspopup for PopUpPanel based components
- PR #688: Add default z-Index for PopUpPanel
- PR #689: Fix ModalPanel id being overridden
- PR #690: Improve Focus Management on various headless Components
- PR #694: Improves OpenClose's toggle behaviour
Version 0.14.4
Improvements
- PR #664: Upgrade to Kotlin 1.7.10
Version 1.0-RC1
Breaking Changes
PR #567: Drop old Components
We are sorry to announce, that we have dropped our so far developped components. As this is a huge move, we have written an article where we explain our motivation and introduce the new approach we take from now on.
They will remain of course part of the long term supporting 0.14 release line, which we plan to support until the end of this year approximately. This should offer you enough time to migrate to the new headless based approach.
Nevertheless, if you really want to keep those components alive and dare the task to maintain them on your own, feel free to extract them out of fritz2 and provide them as your own project. Feel free to contact us if you need some help.
PR #582: Change Structure of basic Types
Tag<>
is now an Interface- There are two implementations available for
HtmlTag<>
andSvgTag<>
- The specialized classes for individual Tags like
Div
,Input
, etc. have been removed - Attributes specific for individual tags are available as extension functions (and have to be imported).
Migration-Path
Wherever you used specialized classes that inherited from Tag<>
like Div
, Input
, etc., just exchange this by Tag<HTMLDivElement>
or Tag<HTMLInputElement>
.
If you access specific attributes of a certain Tag<>
like value
on an input, just import it from dev.fritz2.core.*
.
PR #596: New package structure
We simplyfied our package structure, we used in fritz2, to minimze the amount of import statements.
This means that you can now often use the wildcard import (import dev.fritz2.core.*
), which makes calling the new attribute extension functions on the Tag<>
interface (#582) much easier.
before:
import dev.fritz2.binding.RootStore
import dev.fritz2.binding.SimpleHandler
import dev.fritz2.binding.Store
import dev.fritz2.dom.html.Div
import dev.fritz2.dom.html.RenderContext
import dev.fritz2.dom.html.render
import dev.fritz2.dom.states
import dev.fritz2.dom.values
now:
import dev.fritz2.core.*
PR #584: API-streamlining of fritz2 core
Following changes takes place:
- global
keyOf
function for creating aScope.Key
is moved toScope
class
// before
val myKey = keyOf<String>("key")
// now
val myKey = Scope.keyOf<String>("key")
- all repository factory-functions ends with
Of
appendix
// before
val localStorage = localStorageEntity(PersonResource, "")
// now
val localStorage = localStorageEntityOf(PersonResource, "")
- renaming
buildLens
function tolens
andelementLens
andpositionLens
tolensOf
for lists
// before
val ageLens = buildLens(Tree::age.name, Tree::age) { p, v -> p.copy(age = v) }
val elementLens = elementLens(element, id)
val positionLens = positionLens(index)
// now
val ageLens = lens(Tree::age.name, Tree::age) { p, v -> p.copy(age = v) }
val elementLens = lensOf(element, id)
val positionLens = lensOf(index)
- replacing
UIEvent
byEvent
which solvesClassCastExceptions
whenUIEvent
is explicitly needed you have to cast it (see #578) - removed special
attr
function forMap
andList
. Convert them by yourself to aString
orFlow<String>
and use then theattr
function.
// before
attr("data-my-attr", listOf("a", "b", "c")) // -> data-my-attr="a b c"
attr("data-my-attr", mapOf("a" to true, "b" to false)) // -> data-my-attr="a"
// now
attr("data-my-attr", listOf("a", "b", "c").joinToString(" "))
attr("data-my-attr", mapOf("a" to true, "b" to false).filter { it.value }.keys.joinToString(" "))
PR #585: Rework fritz2 core event concept
By using delegation a Listener
is now a Flow
of an event, so you can directly use it without the need to use the events
attribute. Also the distinction between DomListener
and WindowListener
is not needed anymore.
// before
keydowns.events.filter { shortcutOf(it) == Keys.Space }.map {
it.stopImmediatePropagation()
it.preventDefault()
if (value.contains(option)) value - option else value + option
}
// now
keydowns.stopImmediatePropagation().preventDefault()
.filter { shortcutOf(it) == Keys.Space }
.map { if (value.contains(option)) value - option else value + option }
PR #591: Job handling improvements
- we removed the
syncBy()
function, as it was not useful enough and easily to misunderstand. - to prevent possible memory leaks, we moved
syncWith
toWithJob
interface
PR #622: Fix invocation of Handlers
invoke
-extensions to directly call handlers have been moved toWIthJob
and can easily be called from the context of aStore
or aRenderContext
only.
New Features
New Webpage
We are happy to announce that we have reworked our whole web presence. We have moved to 11ty as base, so we are able to integrate all separate pieces into one consistent page:
- landing page
- documentation
- new headless components
- blog / articles
We are planning to integrate also the remaining examples and to add further sections like recipes.
Besides the pure visual aspects (and hopefully improvements) this improves our internal workflows a lot; it is much easier to coordinate the development of changes and new features along with documentation, examples and possibly some recipe we have identified. Also issues and pull requests will reside inside the fritz2 project itself and thus improve the overall workflow.
We hope you enjoy it :-)
Headless Components
We are proud to announce a new way to construct UIs and possibly reusable components: Headless Components
We are convinced those will improve the creation of UIs with consistent functionality combinded with context fitting structure and appearance.
If you are interested how we have arrived to this paradigm shift, we encourage you to read this blog post.
PR #641: Add structural information for headless components
In order to improve the usage of headless components all components and its bricks will render out HTML comments that name their corresponding component or brick name. This way the matching between the Kotlin names and its HTML equivalents is much easier.
Most hint comments are located as direct predecessor of the created HTML element. For all bricks that are based upon some Flow
this is not possible due to their managed nature. In those cases the comment appears as first child-node within the created element and its text startes with "parent is xyz" to clarify its relationship.
In order to activate those helpful structural information, one must put the SHOW_COMPONENT_STRUCTURE
key into the scope with a true
value.
Example:
div(scope = { set(SHOW_COMPONENT_STRUCTURE, true) }) {
switch("...") {
value(switchState)
}
}
// out of scope -> structural information will not get rendered
switch("...") {
value(switchState)
}
Will result in the following DOM:
<div>
<!-- switch -->
<button aria-checked="false" ...></button>
</div>
<button aria-checked="false" ...></button>
PR #570: New Validation
In order to reduce the boilerplate code, reduce the dependencies to fritz2's core types and to offer more freedom to organize the validation code, we have created a new set of validation tools within this release.
First, you need to specify a Validation
for your data-model. Therefore, you can use one of the two new global convenience functions:
// creates a Validation for data-model D with metadata T and validation-messages of M
fun <D, T, M> validation(validate: MutableList<M>.(Inspector<D>, T?) -> Unit): Validation<D, T, M>
// creates a Validation for data-model D and validation-messages of M
fun <D, M> validation(validate: MutableList<M>.(Inspector<D>) -> Unit): Validation<D, Unit, M>
These functions are available in the commonMain
source set, so you can create your Validation
object right next to your data classes to keep them together. Example:
@Lenses
data class Person(
val name: String = "",
val height: Double = 0.0,
) {
companion object {
val validation = validation<Person, String> { inspector ->
if(inspector.data.name.isBlank()) add("Please give the person a name.")
if(inspector.data.height < 1) add("Please give the person a correct height.")
}
}
}
Then you can call your Validation
everywhere (e.g. JVM- or JS-site) to get a list of messages which shows if your model is valid or not. We recommend extending your validation messages from the ValidationMessage
interface. Then your validation message type must implement the path
which is important for matching your message to the corresponding attribute of your data-model and the isError
value which is needed to know when your model is valid or not:
data class MyMessage(override val path: String, val text: String) : ValidationMessage {
override val isError: Boolean = text.startsWith("Error")
}
// change your Validation to use your own validation message
val validation = validation<Person, MyMessage> { inspector ->
val name = inspector.sub(Person.name())
if (name.data.isBlank())
add(MyMessage(name.path, "Error: Please give the person a name."))
val height = inspector.sub(Person.height())
if (height.data < 1)
add(MyMessage(height.path, "Error: Please give the person a correct height."))
}
// then you can use the valid attribute to check if your vali...
Version 0.14.3
Fixed Bugs
- PR #649: Don't react to
blur
-events caused by child elements of aPopoverComponent
Version 0.14.2
Fixed Bugs
- PR #571:
Authentication.current
property returns the current principal also when pre-setting it before any process is started - Reactivate the
files
component for multi file upload. It has been accidentally deactivated during upgrade to Kotlin 1.6.0.
Version 0.14.1
Improvements
Improve rendering speed of data table
This small patch version just improves the rendering speed of the data table component. It tremendously reduces the speed of the cell rendering, by sacrificing the evaluation of changes of a single <td>
. Instead, the whole row will be scanned for changes, which is a far better solution in the tradeoff between precise rendering and creating the fitting flow of data from the bunch of pure data, sorting and selection information.
This does not affect the API of the component, nor the functionality at all, so there is no need to change anything in client code!
Other Improvements
- PR #568: Authentication.complete() function sets the principal also without running auth-process
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 combi...
Version 0.13
Breaking Changes
PR #530: Alternative approach to rendering
Motivation
- There were four bugs that dealt with rendering problems, so obviously the current solution was not good / mature enough yet:
- The code was rather complex
- The mechanism itself was rather cumbersome, as there was some placeholder tag rendered into the DOM, which would be replaced by the actual flow evaluated DOM fragement.
All this was only necessary to enable to mix mount-points with static DOM tags below the same parent node.
→ Optimization only for one edge case! → not a good idea!
New Solution
The new rendering is based upon mount-points, that are always represented by a dedicated tag within the DOM tree! The dynamic content is then rendered below this mount-point-tag. This is true for all render variations, so for render
as well as for renderEach
variants.
To be more precise, there is one div
-tag inserted at the location where the render
method is called:
// within some RenderContext
section {
flowOf("Hello, World!").render {
span { +it }
}
}
This will result in the following DOM structure:
<section>
<div class="mount-point" data-mount-point>
<span>Hello World</span>
</div>
</section>
The CSS class mount-point
makes the div
"invisible" to the client (by display="contents"
), the data attribute data-mount-point
is primarely added to support readability or debugging.
It is worth to emphasize, that this mount-point-tag remains under full control of the framework. So all rendered tags below this tag, will be cleared out every time a new value appears on the flow. So do not try to use or touch this tag or any child from outside of the render
function!
This works similar for dynamic lists:
ul {
flowOf(listOf("fritz2", "react", "vue", "angular")).renderEach {
li { +it }
}
}
Which will result in this DOM structure:
<ul>
<div class="mount-point" data-mount-point>
<li>fritz2</li>
<li>react</li>
<li>vue</li>
<li>angular</li>
</div>
</ul>
Recipe: Mixing dynamic and static content within the same level
If it is absolutely clear that the mount-point will be the only element of some parent tag, then the render methods offer the optional into
parameter, which accepts an existing RenderContext
as anchor for the mount-point. In this case the rendering engine uses the existing parent node as reference for the mount-point:
render {
ul { // `this` is <ul>-tag within this scope
flowOf(listOf("fritz2", "react", "vue", "angular")).renderEach(into = this) {
// ^^^^^^^^^^^
// define parent node as anchor for mounting
li { +it }
}
}
}
This will result in the following DOM structure:
<ul data-mount-point> <!-- No more explicit <div> needed! Data attribute gives hint that tag is a mount-point -->
<li>fritz2</li>
<li>react</li>
<li>vue</li>
<li>angular</li>
</ul>
If you are in a situation where you absolutly have to mix static elements with dynamic (flow based) content within the same DOM level, then the new rendering offers a solution too: Try to integrate the static aspect within a map
expression!
Let's consider the following example sketch we would like to achieve:
<ul>
<!-- static elements within the list items, always on top -->
<li>fritz2</li>
<!-- dynamic content from a flow -->
<li>react</li>
<li>vue</li>
<li>angular</li>
</ul>
The simplest solution would be to just call the renderEach
method directly within the <ul>
context after the static <li>
portions. But this would violate the constraint, that all <li>
tags must appear on the same DOM level (refer to the first example output to see, that an extra <div>
would be insterted after the static portion).
So the correct way is to provide the into = this
parameter in order to lift up the dynamic portion into the surrounding <ul>
tag and to integrate the static portion within the flow by some map
expression:
val frameworks = flowOf(listOf("react", "vue", "angular")) // might be a store in real world applications
ul {
frameworks
.map { listOf("fritz2") + it } // prepend the static part to the dynamic list
.renderEach(into = this) { // do all the rendering in the "dynamic" part of the code
li { +it }
}
}
The result is exactly the same as the scetch from above.
You might habe recognized that the into
parameter could be omitted, if the extra <div>
does not affect the overall structure (in this case all <li>
elements would still remain on the same level within the DOM!).
Migration Guide
For the most if not all parts of your application nothing has to be changed!
So first of all please compile and test your application. If there are no compiler errors and the application appears and functions as it did before, do nothing!
There are only few exceptions to this rule:
renderElement
does not exist anymore. You will get compile errors of course, so this is easy to detect. Change all occurrences torender
instead to solve this problem. If needed, apply the next two patterns on top.- If the additional mount-point-tag leads to unwanted effects (some styling applied to children won't work for example), just provide the parent tag as mount-point-tag by setting the
into = this
parameter at the render functions calls. - If there are parts of your application where dynamic and static content absolutely needs to coexist within the same DOM level, strive to include the static portion into the flow (for example by using
map
as shown in the recipe section before). - If you use our
StackUp
orLineUp
component with dynamic content, make sure to set theinto = this
parameter in order to make thespacing
property work again.
Further Information
For more details have a look at the documentation
New Features
PR #532: Adding new Adhoc-Handler
Added new easy to use handledBy
functions which have a lambda parameter. Anytime a new value on the Flow
occurs the given function gets called.
render {
button {
+"Click me!"
clicks handledBy {
window.alert("Clicked!")
}
}
}
Outside the HTML DSL you can do the same:
flowOf("Hello World!") handledBy {
window.alert(it)
}
That is why the watch()
function at the end of a Flow
is not needed anymore and is now deprecated.
// before
flowOf("Hello World!").onEach {
window.alert(it)
}.watch()
// now
flowOf("Hello World!") handledBy {
window.alert(it)
}
This way the handling of data in fritz2 (using Flows
s) gets more consistent.
Be aware that per default every Throwable
is caught by those handledBy
functions and a message is printed to the console. The flow gets terminated then, so no more possibly following values will be processed:
flowOf("A", "B", "C").map {
delay(2000)
it
} handledBy {
if (it == "B") error("error in B") // provoke an exception
window.alert(it)
}
// will open one window with "A" and then print a message top the console:
// Object { message: "error in B", cause: undefined, name: "IllegalStateException", stack: "captureStack@webpack-internal:...
The application itself will continue to work, which is the main motivation for the enforced default error handling though!
We encourage you to handle possible exceptions explicitly within the handler code, so the flow will keep on working; at least for the following valid values:
flowOf("A", "B", "C").map {
delay(2000)
it
} handledBy {
try {
if (it == "B") error("error in B")
} catch (e: Exception) { // handle exception within handler -> flow will be further consumed!
}
window.alert(it)
}
// will open three alert windows with all three chars "A", "B" and "C" as expected
PR #529: Add merge
function to merge multiple DomListeners
This convenience functions reduces duplicate code for handling different DomListener
s with the same handler:
button {
merge(mouseenters, focuss) handledBy sameHandler
}
PR #528: Add dynamic icons for sub-menus
This PR enhances the API of the submenu
component's icon defintion. Instead of just apssing some static IconDefinition
it is now possible to pass some Flow<IconDefinition>
too. This enables one to realize an "accordeon style" menu, where the icon reflects the open / close state of the submenu:
// some store to hold the open/close state of a submenu
val toggle = storeOf(false)
menu {
header("Entries")
entry {
text("Basic entry")
}
submenu(value=toggle) {
text("Sub")
// use the state to select the appropriate sub-menu icon
icon(toggle.data.map { if (it) Theme().icons.chevronDown else Theme().icons.chevronLeft })
entry {
text("A")
}
entry {
text("B")
}
entry {
text("C")
}
}
}
PR #522: Expose open/close handlers or an external store for submenu component
PR #519: Add often used attributes for svg and path element
It is now possible to craft SVG images much easier within fritz2, its element DSL now supports the <path>
-Tag and the foll...
Version 0.12
Breaking Changes
PR #494: Allow sub()
on all Store
s
Removes the RootStore
dependency in the SubStore
, so that SubStore
s can be created by calling sub()
function implemented in Store
interface. Therefore it is now possible to create a SubStore
from every Store
object.
Because one SubStore
generic type gets obsolete, it will break existing code, but it is easy to fix that. Just remove the first not type parameter.
// before
class SubStore<R, P, T> {}
// now
class SubStore<P, T> {}
// before
val addressSub: SubStore<Person, Person, Address> = store.sub(addressLens)
// now
val addressSub: SubStore<Person, Address> = store.sub(addressLens)
PR #492: Remove deprecated direction
-api from RadioGroupComponent
and CheckboxGroupComponent
This PR removes the deprecated direction
-api from RadioGroupComponent
and CheckboxGroupComponent
.
Use orientation instead
.
PR #489: Some Improvements and Bugfixing around Forms
Improve FormControl code
- make
FormSizeSpecifier
values uppercase, so more Kotlin alike - improve render strategies initialization and customization: Instead of setting all up within the
init
block, there is now a new private methodinitRenderStrategies
that produces the mapping and is directly called at beginning of the rendering process. This way nothis
pointer is leaked as before. For custom implementations there is now a protected hook functionfinalizeRenderStrategies
that can be used to extend or change the renderer strategies!
Migration Guide:
If a custom renderer should be applied or a new factory should be added, custom implementation of FormControlComponent
must replace the old registerRenderStrategy
within the init
block by a new mechanism:
// old way, no more possible!
class MyFormControlComponent : FormControlComponent {
init {
registerRenderStrategy("radioGroupWithInput", ControlGroupRenderer(this))
}
}
// new way, override `finalizeRenderStrategies` method:
class MyFormControlComponent : FormControlComponent {
// some new factory: `creditCardInput` with key of same name; used with `SingleControlRenderer`
// another new factory: `colorInput` with key of same name and new renderer
override fun finalizeRenderStrategies(
strategies: MutableMap<String, ControlRenderer>,
single: ControlRenderer,
group: ControlRenderer
) {
// override setup for a built-in factory:
strategies.put(ControlNames.textArea, MySpecialRendererForTextAreas(this))
// register new factory
strategies.put("creditCardInput", single)
// register new factory with new renderer
strategies.put("colorInput", ColorInputRenderer(this))
}
}
Improve sizes aspect for components in theme
- in respect to EfC 502 a new mixin alike interface
FormSizesAware
has been introduced - the
FormSizes
interface is renamed toFormSizesStyles
Migration Guide:
If a component supports a size property and relies on the old FormSizes
interface, just rename the receiver type appropriate to `FormSizesStyles``:
// old
val size = ComponentProperty<FormSizes.() -> Style<BasicParams>> { Theme().someComponent.sizes.normal }
// new
val size = ComponentProperty<FormSizesStyles.() -> Style<BasicParams>> { Theme().someComponent.sizes.normal }
// ^^^^^^^^^^^^^^^
// choose new name
Make FormControl Labels dynamic
- the
label
property of a FormControl now accepts alsoFlow<String>
. Thus the label can dynamically react to some other state changing.
Migration Guide
The code for a ControlRenderer
implementation needs to be adapted. Use the following recipe:
class ControlGroupRenderer(private val component: FormControlComponent) : ControlRenderer {
override fun render(/*...*/) {
// somewhere the label gets rendered (label / legend or alike)
label {
// old:
+component.label.value
// change to:
component.label.values.asText()
}
}
}
Adapt FormControl's textArea function parameters
- the optional store parameter now also is named
value
like as within the maintextArea
factory.
Repairs label behaviour for SelectField
- a click onto a label within a FormControl now directs to the SelectField.
- the id of a SelectField is now correctly set, so that a FormControl can now fill the
for
attribute of the label correctly.
PR #490: Move Modal’s close handler into content property
Until now the close
handler was injected directly into the main configuration scope of a modal. With repsect to EfC 416 this is now changed. The handler only gets injected into the content
property, where it is considered to be used.
Migration
Just move the handler parameter from the build
expression of modal into the content
property:
// old
clickButton {
text("Custom Close Button")
} handledBy modal { close -> // injected at top level
content {
clickButton {icon { logOut } } handledBy close
}
}
// new
clickButton {
text("Custom Close Button")
} handledBy modal {
content { close -> // injected only within content
clickButton {icon { logOut } } handledBy close
}
}
PR #487: Rework ToastComponent
Changes
This PR cleans up the ToastComponent
code. This includes:
- Adding styling-options to the theme
- Use correct z-indizes
- General code-cleanup
What's API breaking?
Theme().toast.base
has been renamed toTheme().toast.body
and might break themes that have custom toast-styles
New Features
PR #505: Add TooltipComponent
This PR deals with the new TooltipComponent
A tooltip
should be used to display fast information for the user.
The individual text
will be shown on hover the RenderContext
in which be called.
This class offers the following configuration features:
text
can be avararg
, a flow, a list, a flow of list of String or a simple string, optional can be use the @PropertytextFromParam
.placement
of thetext
around theRenderContext
in which be called. Available placements aretop
,topStart
,topEnd
,bottom
,bottomStart
,bottomEnd
,left
,leftStart
,leftEnd
,right
,rightStart
,rightEnd
.
Example usage:
span {
+"hover me"
tooltip("my Tooltip on right side") {
placement { right }
}
}
span {
+"hover me to see a multiline tooltip"
tooltip("first line", "second line"){}
}
span {
+"hover me for custom colored tooltip"
tooltip({
color { danger.mainContrast }
background {
color { danger.main }
}
}) {
text(listOf("first line", "second line"))
placement { TooltipComponent.PlacementContext.bottomEnd }
}
}
Migration
- The old tooltip component remains as deprecated for the next releases, so clients have time to migrate.
Motivation
The old Tooltip component is based upon pure CSS. This is fine for basic usage, but it fails when it comes to advanced features like automatic positioning. Also the API does not fit into the "fritz2 component style". That's why there is the need to rework the tooltip.
PR #493: Add PopupComponent
The PopupComponent
should be used for to positioning content
like tooltip
or popover
automatically in the right place near a trigger
.
A popup
mainly consists of a trigger
(the Element(s)) which calls the content
.
It can cen configured by
offset
the space (in px) betweentrigger
andcontent
flipping
if no space on chosen available it will be find a right placement automaticallyplacement
of thecontent
around thetrigger
The trigger
provides two handler which can be used, the first is important to open/toggle the content
the second close it.
content
provides one handler which can be used to close it.
Example:
popup {
offset(10.0)
flipping(false)
placement { topStart }
trigger { toggle, close ->
span {
+"hover me"
mouseenters.map { it.currentTarget } handledBy toggle
mouseleaves.map { } handledBy close
}
}
content { close ->
div {
+"my content"
clicks.map{ } handledBy close
}
}
}
PR #496: Add PaperComponent
and CardComponent
This PR adds a new CardComponent
that behaves similar to the old PopoverComponent
s content.
Paper Component
Component displaying content in a card-like box that can either appear elevated or outlined and scales with the specified size
of the component.
Example
paper {
size { /* small | normal | large */ }
type { /* normal | outline | ghost */ }
content {
// ...
}
}
CardComponent
A component displaying the typical sections of a card inside a PaperComponent
.
The available sections are a header, a footer and the actual content.
Example
card {
size { /* small | normal | large */ }
type { /* normal | outline | ghost */ }
header {
// ...
}
content {
// ...
}
footer {
// ...
}
}
![Bildschirmfoto 2021-08-17 um 19 17 18](https://...