Bot releases are hidden (Show)
renderTrue
and renderFalse
convenience functionsPublished by Lysander 7 months ago
renderTrue
and renderFalse
convenience functionsPublished by Lysander 8 months ago
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.
Published by Lysander 9 months ago
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.
Special thanks to @jillesvangurp for his amazing work on #789!
Published by Lysander 10 months ago
The most important change for the project is the upgarde to the latest Kotlin version, that is 1.9.22 at the time of this release.
Special thanks to @jillesvangurp for his amazing work on #789!
Published by Lysander 12 months ago
ComponentValidationMessage
a data classCurrently there is no real lifecycle-management for Jobs
of Handler
s and Stores
. The initial idea was to hide those details from the user in order to make the API and overall user-experience as easy and pleasant as possible.
Sadly this was a bad idea - not only does it lead to memory leaks, but also to unexpected behaviour due "handledBy" invocations running endlessly. Imagine a delayed change of a store's data initiated by a handler which is no longer "visible", because the Mountpoint
in which the handler was called was already destroyed. This is definitely a problem, as it contradicts the reliable reactive behaviour of fritz2.
That being said, especially the latter problem arises very seldomly, which is the reason why we encountered and subsequently adressed this problem so late on the final route to 1.0 version of fritz2.
Nevertheless we have to fix it, so here we go.
There is a need for an explicit lifecycle-management for all fritz2 elements which are related to reactive behaviour such as:
Store
sHandler
sHistory
- more or less for streamlining reasons. Appears rarely outside of a Store
oder WithJob
, so there are no behaviour changes)Router
- just for streamlining reasons. Appears only once inside an application, so there are no behaviour changes)Within a RenderContext
, those elements should rely on the managed Job
of their context to be finished, as the RenderContext
gets dropped by any render*
-function. This solves the memory leaks as well as the unexpected behaviour of a neverending local store or handledBy
call.
Outside of any RenderContext
or other receivers that offer some Job
, the job-handling should be made explicit. Typically this applies for global stores that hold application states. Those elements are discrete and rather small in number and are intended to run for the whole lifecycle of an application, so there is no harm in creating new jobs for those cases.
Previously, RootStores created their own jobs which were never cancelled. With this PR, every store initialization needs a job: Job
parameter.
class RootStore<D>(initialData: D, **job: Job**, override val id: String = Id.next())
fun <D> storeOf(initialData: D, **job: Job**, id: String = Id.next())
Because the jobs within a RenderContext
are already managed, we also added convenience functions which directly use the RenderContext
-job if a store is created with the storeOf
-factory:
// `WithJob` is a base interface of `RenderContext`
fun <D> WithJob.storeOf(initialData: D, job: Job = this.job, id: String = Id.next())
// ^^^^^^^^
// the job is taken from its receiver context
Each store which is created outside of a WithJob
-Scope or directly using the constructor needs a user managed Job
. The user himself is responsible to take care of the job lifecycle and cancelling the job when it is not needed anymore. We suggest using the job of the surrounding RenderContext, because that one is always properly managed.
In real world applications, it is normal to have some globally defined Store
s. Don't be afraid to simply create new Job
s for those without any "management" by yourself. Those stores are intended to run for as long as the application runs. So there is no need to stop or cancel them.
Forever running jobs inside such global stores were a normal occurance before this PR and will remain so after this PR.
Previously, handledBy
could be called everywhere. In a WithJob
-context (e.g. a RenderContext
), it used that job, otherwise it created a new job which was endlessly running within the application-lifecycle, which is what might have caused some unexpected side effects.
Now you can only run handledBy
within
WithJob
-scope (a RenderContext
implements WithJob
)RootStore
-InstanceThis ensures that the Job
used inside of handledBy
is always properly managed, which includes both cases:
Store
which is intended to run foreverThe 'handledBy'-functions within a RootStore
are protected
and can therefore only be used within the RootStore
itself or in any derived custom store-implementation. A RootStore
as receiver - e.g. using extension functions or apply/run - is not sufficient!
If you explicitely want to use the store-job outside the RootStore
, you have to create an extension function with the WithJob
receiver and call that function within the RootStore
wrapped with the new runWithJob
-function.
Example:
object MyStore : RootStore<String>("", Job()){
init {
runWithJob{ myFunction() }
}
}
fun WithJob.myFunction() {
flowOf("ABC") handledBy MyStore.update
}
Alongside with this change, a handledBy
-call will also be interrupted if:
handledBy
was called) has been cancelledAlso, the store's data
-Flow will be completed when the store-job has been cancelled to prevent further side effects of the store.
When using reactive rendering, e.g. using Flow<T>.render
or Flow<T>.renderEach
, the render-operation was previously never interrupted if a new flow-value was emitted. The render-operations where queued after each other. This behaviour has changed with this PR. When the Flow
emits a new value, the current render-task will be interrupted and it will directly re-render the content with the latest value. This will improve performance.
In rare circumstances, this might also cause different behaviour. In our opinion this could only happen when the reactive rendering is abused for state handling outside of Store
s, for example with mutating some var foo
outside of the rendering block. Since we consider such implementations bad practise, we recommend to change these constructs.
For all global Store
s of an application, just add some newly created Job
:
// previously
object MyApplicationStore : RootStore<AppState>(AppState(...)) {
}
// now
object MyApplicationStore : RootStore<AppState>(AppState(...), job = Job()) {
}
// previously
val storedAppState = storeOf(AppState())
// now
val storedAppState = storeOf(AppState(), job = Job())
If you encounter a compiler error due to missing job
-parameter inside a RenderContext
, please have a look at the last section about "Chasing Memory Leaks" where the dangers and solutions as explained in depth.
You simply have to move those calls inside some Store
or some RenderContext
- there is no standard solution which fits all situations. You have to decide for yourself where the data handling fits best.
If you have two stores and changes to one should also change the other, consider handleAndEmit
as alternate approach or move the call to the dependent store (the one that holds the handler passed to handledBy
).
The history
-function without receiver has been removed. So you either have to move the history code inside some Store
(which is the common and recommended approach) or you have to invoke the constructor manually:
// previously
val myHistory = history()
// now
object MyStore : RootStore<MyData>(MyData(), job = Job()) {
val history = history()
}
// or if really needed outside a store:
val myHistory = History(0, emptyList(), Job())
If you really need to call the Router
-constructors manually, you have to provide a Job
now:
// previously
val router = Router(StringRoute(""))
// now
val router = Router(StringRoute(""), job = Job())
Consider using the routerOf
-factories, they will create a Job
automatically.
Besides the previously shown dedicated proposals for fixing compiler errors, we encourage you to scan your code for potential memory-issues we simply cannot prevent by our framework:
Imagine the creation of a Store
inside a RenderContext
where a new Job()
is created:
// previously
fun RenderContext.renderSomething() = div {
object MyApplicationStore : RootStore<AppState>(AppState(...)) {
}
}
// now - satisfies compiler but is wrong!
fun RenderContext.renderSomething() = div {
object MyApplicationStore : RootStore<AppState>(AppState(...), job = Job()) {
// ^^^^^^^^^^^
// This fixes the compiler error, but in most cases
// this is *wrong*. The job-object is never cancelled
// -> memory leak!
}
}
// now - recommended approach
fun RenderContext.renderSomething() = div {
object MyApplicationStore : RootStore<AppState>(AppState(...), job = [email protected]) {
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// Reuse job of the surrounding RenderContext
// and you will be fine.
}
}
Last but not least: If you really want such a store to live forever, then refactor the code and define it as a global value.
There are variations of the pattern above which are even worse:
If you encounter any job-creation - thus manually created new Job()
s - inside a looping-construct, this is very likely a mistake. Typical looping constructs appears inside renderEach
or simple for
-loops inside a render
. Inside such loops, you should also strive to re-use the Job
of the surrounding RenderContext
.
For example, you should absolutely change code section like this one:
val storedCustomers = storeOf<List<Customer>>(emptyList(), job = Job())
fun RenderContext.renderCustomers() = section {
storedCustomers.renderEach { customer ->
val editStore = object : RootStore<Customer>(customer, job = Job()) {
// ^^^^^^^^^^^
// This is evil! A new job is created on every customer
// and every re-rendering from the outer store.
// The jobs are never canceled -> memory leak!
}
// render one customer somehow
}
}
You should change such a spot by reusing the RenderContext
's job
:
fun RenderContext.renderCustomers() = section {
storedCustomers.renderEach { customer ->
val editStore = object : RootStore<Customer>(customer, job = [email protected]) {
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// This is good! The `job` is managed by `renderEach`
// so it is cancelled on re-rendering and the objects
// can be deleted by the garbage collector.
}
// render one customer somehow
}
}
Keep in mind that the first shown example may reside inside a loop, from where the renderSomething
-function gets called: in that case, there is also a leak inside a loop, which is even harder to encounter - so always consider the job-creation inside a RenderContext
as code smell and bad practise.
Special thanks to @serras and @realqdbp for their effords and contributions to this release!
Published by Lysander about 1 year ago
beforeUnmount
Crashes due to Job CancellationsrenderEach
fast deleting PredecessorsPublished by Lysander about 1 year ago
beforeUnmount
CallsPublished by Lysander about 1 year ago
setFocus
from PopUpPanelPublished by Lysander about 1 year ago
PR #783 and PR #788 add new utility render
-functions inside RenderContext
. @serras initially brought up the idea and implementations to reduce boilerplate code by adding some convenience render-functions for common UI-patterns.
Often only some specific state of the data model should be rendered, while for other states the UI artefact should disappear. Typical cases are nullable types, where only for the none null
part something should be rendered. The following new functions are added to the fritz2's core to adress those cases.
Please regard: If you ever need to render some UI also for the other state-variants, then just refer to the standard render
-function and use explicit case analysis like if-else
or when
expressions inside the render-block.
The renderIf(predicate: (V) -> Boolean)
function only renders the given content
, when the given predicate
returns true
for the value inside the Flow<V>
.
val store = storeOf<String>("foo")
render {
store.data.renderIf(String::isNotBlank) {
p { +it }
}
}
The renderNotNull()
function only renders the given content
, when the value inside the Flow<V?>
is not null.
val store = storeOf<String?>(null)
render {
store.data.renderNotNull {
p { +it }
}
}
The renderIs(klass: KClass<W>)
function only renders the given content
, when the value inside the Flow<V>
is a type of klass
.
interface I
class A: I
class B: I
val store = storeOf<I>(A())
render {
store.data.renderIs(A::class) {
p { +"A" }
}
}
This PR adresses two closely related aspects within our headless components:
The first one is quite simple to explain: popper.js is outdated and therefor it makes sense to upgrade to the successor project. Floating-UI fits so much better than popper.js for fritz2 and our headless-components, as it is in fact by itself only a headless component. It offers functionality to calculate the position of some floating element, but it does no more render some styling by itself to the DOM. This obviously is great news for our implementations of popup-elements, as the integration removes some pain points and works much smoother together with fritz2's core mechanics!
For the user there are not much changes; the core approach to implement PopUpPanel
as base for some floating brick, does not change. It is only about some properties that have different names and sometimes slightly different values. But we gain much more flexibility like the new concept of midlewares, that can be easily added by custom bricks. So in the end the user has more power to control and modify the floating element for his custom needs.
The other aspect of this PR adresses fundamental problems with overlayed elements in general (this includes the headless modals and toast on top of the PopupPanel
based components): There will always be a chance for clipping errors, if the overlay is rendered close to the trigger, which often is kinda deep down inside the DOM hierarchy. Please read about in the description of the Floating-UI project.
That said, nothing will change on API level for this aspect. It is a pure implementation aspect, that has changed now: All headless-overlay components will now render their overlay-bricks inside the original target-node of the outer render
-function, which acts as a portal-root. The trigger will create only some placeholder node into the DOM, whereas the real overlay content will be rendered inside the portal-root quite upside in the DOM. The placeholder acts as a portal to the final rendering place.
This is a common pattern and so we stick to the wording inside our code and name everything about it with portal
prefix.
Be aware that this PR might be API breaking for the headless
package and the components, that implements the PopupPanel
-class for some of their bricks. Also your own Implementation that relies on PopupPanel
might break.
We do not consider this API breaking for the whole project, so we prefer to present this under the improvement aspect. We have no knowledge about users, using our headless-component yet, so obviously this will affect primarely ourselves.
There is one action needed to keep the headless components working: To use portalling you have to render the portalRoot
manually in our your render {} Block like this:
fun main() {
//...
render {
// custom content
// ...
portalRoot() // should be the last rendered element
}
}
After this patch, the Headless-Components listBox
, menu
, modal
, popOver
, toast
and tooltip
should be working again.
If you should against all expectations encounter a break, please simply refer to the updated list of properties within the headless documentation. This list and the following explanations should enable you to adapt to the new properties.
Special thanks to @serras for his effords and contributions to this release!
Published by jamowei about 1 year ago
Published by jamowei about 1 year ago
PopUpPanel
's arrow would initially be visible regardless of the popup being hidden.Published by Lysander over 1 year ago
This PR changes the behavior of Store.mapNull()
so that the path of the derived Store is the same as in the parent Store. This is the correct behavior, as a Store created via mapNull()
does not technically map to another hierarchical level of the data model. As a result, mapNull()
works the same on both Stores and Inspectors, making the validation process more straight-forward. Previously, the Store's id has been appended to the path upon derivation.
This might potentially be API breaking as the behavior of Store.mapNull()
regarding the resulting path changes.
Additionally, it adds an Inspector.mapNull()
extension function that works like Store.mapNull()
.
Just like on a Store, a default value is passed to the method that is used by the resulting store when the parent's value is null
.
For convenience reasons it is now possible to call a validator with metadata type Unit
without the metadata parameter.
Imagine a Validator
of type Validator<SomeDomain, Unit, SomeMessage>
:
data class SomeDomain(...) {
companion object {
val validator: Validator<SomeDomain, Unit, SomeMessage> = validation { inspector -> ... }
// ^^^^
// no "real" metadata needed
}
}
val myData: SomeDomain = ...
// now invoke the execution process...
// old
SomeDomain.validator(myData, Unit) // need to pass `Unit`!
// new
SomeDomain.validator(myData) // can omit `Unit`
Published by jamowei over 1 year ago
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.
The following tables show the difference:
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.
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 |
current | new |
---|---|
Lens<P, T>.toNullableLens(): Lens<P?, T> | Lens<P, T>.withNullParent(): Lens<P?, T> |
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.
FetchException
from http
-APINo more FetchException
is thrown when execute()
gets called internally for receiving the Response
object in fritz2 http
-API.
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
).
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.
Just rename all occurences of trapFocus
to trapFocusInMountpoint
:
// before
div {
trapFocus(...)
}
// now
div {
trapFocusInMountpoint(...)
}
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.
test-server
API.text
attribute extension functions.Published by jwstegemann almost 2 years ago
As we have considered repositories to add no real value as abstraction, this commit will remove them entirely from fritz2.
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.
The default status code for a failed authentication is reduced to only 401.
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.
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.
Simplifying the history feature, which includes the following changes:
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
}
reset()
method to clear()
add(entry)
method to push(entry)
last()
method, cause with current: List<T>
every entry is receivablecapacity
to 0
(no restriction) instead of 10
entriesExposing 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())
Just change the type of some field or return type from RootStore<T>
to Store<T>
and SubStore<T, D>
to Store<D>
.
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
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:
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
}
}
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.
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.
Store
sIf 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("")
sub
on a Store
with nullable contentTo 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" }
}
}
Published by jamowei over 2 years ago
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.
Tag<>
is now an InterfaceHtmlTag<>
and SvgTag<>
Div
, Input
, etc. have been removedWherever 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.*
.
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.*
Following changes takes place:
keyOf
function for creating a Scope.Key
is moved to Scope
class// before
val myKey = keyOf<String>("key")
// now
val myKey = Scope.keyOf<String>("key")
Of
appendix// before
val localStorage = localStorageEntity(PersonResource, "")
// now
val localStorage = localStorageEntityOf(PersonResource, "")
buildLens
function to lens
and elementLens
and positionLens
to lensOf
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)
UIEvent
by Event
which solves ClassCastExceptions
when UIEvent
is explicitly needed you have to cast it (see #578)attr
function for Map
and List
. Convert them by yourself to a String
or Flow<String>
and use then the attr
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(" "))
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 }
syncBy()
function, as it was not useful enough and easily to misunderstand.syncWith
to WithJob
interfaceinvoke
-extensions to directly call handlers have been moved to WIthJob
and can easily be called from the context of a Store
or a RenderContext
only.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:
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 :-)
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.
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>
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 validation result is valid or not
val messages: List<MyMessage> = Person.validation(Person())
messages.valid // == false
We introduce a new type of Store
which we call ValidatingStore
. This Store
has the same properties as a RootStore
and additionally a Validation
which it uses to validate the stored model. With these additional information you get next to your known data
flow also a messages
flow which you can use to render out your list of validation messages. Furthermore, you can decide on store creation if you want to automatically validate your model after an update to your store takes place. Then you have to set the validateAfterUpdate
flag and the flow of messages gets accordingly updated. Otherwise, you can call the validate(data: D, metadata: T? = null)
function inside your own handlers to update the list of messages by your own. For cases, you want to reset your validation state to an empty list or to a specific list of messages, you can use the resetMessages(messages: List<M> = emptyList())
function. Example:
// create your ValidatingStore
val myValidationStore = object : ValidatingStore<Person, Unit, MyMessage>(
Person(), Person.validation, validateAfterUpdate = true, id = "myPerson"
) {
// check if your model is valid and then save
val save = handle { person ->
if(validate(person).valid) {
localStorage.setItem(person.name, person.height.toString())
Person()
} else person
}
//...
}
render {
//...
input {
val name = myValidationStore.sub(Person.name())
value(name.data)
changes.values() handledBy name.update
}
//...
button {
+"Save"
clicks handledBy myValidationStore.save
}
// render out the messages
myValidationStore.messages.renderEach(MyMessage::path) {
p {
+it.text
if(it.isError) inlineStyle("color: red")
}
}
}
For further information, have a look at our documentation
In addition to transitions that are run, when a Tag
is added to or removed from the DOM, you can now also apply a Transition
to a mounted Tag
whenever a new value appears on a Flow[Boolean]
:
tag.transition(someFlowOfBoolean,
"transition duration-100 ease-ease-out",
"opacity-0 scale-95",
"opacity-100 scale-100",
"transition duration-100 ease-ease-out",
"opacity-100 scale-100",
"opacity-0 scale-95"
)
The enter-transition will be executed when true
appears on the [Flow]
The leave-transition will be executed when false
appears on the [Flow]
We also added a suspending extension-function WithDomNode<*>.waitForAnimation
, that allows you to wait for running transitions to be finished. This is for example useful, if you want to make a parent element invisible only after a the fading-out of it's children is done.
In order to integrate the NodeList
type as the result of lots of DOM-queries better into Kotlin's List
-API, the new type DomNodeList
was added to fritz2. It acts as an adapter on top of a NodeList
and enables the applications of the common List
operations.
In order to constuct such a DomNodeList
from some query result, the extension function asElementList
is offered:
val amount = document
.querySelectorAll("p")
.asElementList() // create `DomNodeList`
.filter { it.innerText.contains("fritz2") }
.count()
As common pattern a tag attribute should appear or disappear based upon some Flow<Boolean>
based condition. In order to achieve this effect, the new pair of extension functions T.whenever(condition: Flow<Boolean>): Flow<T?>
and Flow<T>.whenever(condition: Flow<Boolean>): Flow<T?>
were added:
val messages: Flow<List<ValidationMessage>> = TODO() // some validation result
// attribute should only appear if there are errors!
attr("aria-invalid", "true".whenever(!messages.valid))
Authentication.complete()
function sets the principal also without running auth-processAuthentication.getPrincipal()
function returns the principal also when pre-setting it before any process is startedPublished by haukesomm about 3 years ago
sub()
on all Store
sRemoves 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)
direction
-api from RadioGroupComponent
and CheckboxGroupComponent
This PR removes the deprecated direction
-api from RadioGroupComponent
and CheckboxGroupComponent
.
Use orientation instead
.
FormSizeSpecifier
values uppercase, so more Kotlin alikeinit
block, there is now a new private method initRenderStrategies
that produces the mapping and is directly called at beginning of the rendering process. This way no this
pointer is leaked as before. For custom implementations there is now a protected hook function finalizeRenderStrategies
that can be used to extend or change the renderer strategies!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))
}
}
FormSizesAware
has been introducedFormSizes
interface is renamed to FormSizesStyles
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
label
property of a FormControl now accepts also Flow<String>
. Thus the label can dynamically react to some other state changing.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()
}
}
}
value
like as within the main textArea
factory.for
attribute of the label correctly.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.
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
}
}
ToastComponent
This PR cleans up the ToastComponent
code. This includes:
Theme().toast.base
has been renamed to Theme().toast.body
and might break themes that have custom toast-stylesTooltipComponent
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 a vararg
, a flow, a list, a flow of list of String or a simple string, optional can be use the @property textFromParam
.placement
of the text
around the RenderContext
in which be called. Available placements are top
, 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 }
}
}
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.
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) between trigger
and content
flipping
if no space on chosen available it will be find a right placement automaticallyplacement
of the content
around the trigger
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
}
}
}
PaperComponent
and CardComponent
This PR adds a new CardComponent
that behaves similar to the old PopoverComponent
s content.
Component displaying content in a card-like box that can either appear elevated or outlined and scales with the specified size
of the component.
paper {
size { /* small | normal | large */ }
type { /* normal | outline | ghost */ }
content {
// ...
}
}
A component displaying the typical sections of a card inside a PaperComponent
.
The available sections are a header, a footer and the actual content.
card {
size { /* small | normal | large */ }
type { /* normal | outline | ghost */ }
header {
// ...
}
content {
// ...
}
footer {
// ...
}
}
In fritz2 you can now put some arbitrary payload data to every fritz2 html element (for styled elements too) and receive this payload data later (or deeper) in your html tree. This possibility can be use to know in with kind of context your own component get rendered or you can provide some additional information to your context for making decision on styling or rendering.
Example:
enum class Sizes {
SMALL, NORMAL, LARGE;
companion object {
val key = keyOf<Sizes>()
}
}
fun main() {
render {
div {
div(scope = {
set(Sizes.key, Sizes.SMALL)
}) {
section {
scope.asDataAttr()
when (scope[Sizes.key]) {
Sizes.SMALL -> div({ fontSize { small } }) { +"small text" }
Sizes.NORMAL -> div({ fontSize { normal } }) { +"normal text" }
Sizes.LARGE -> div({ fontSize { large } }) { +"large text" }
else -> div { +"no size in scope available" }
}
}
}
p {
scope.asDataAttr()
// scope is context-based and therefore scope is here empty
+"no scope entries here (context-based)"
}
}
}
}
Results in:
TypeAheadComponent
Adds a TypeAhead component to fritz2's component portfolio.
A TypeAhead offers the possibility to input some string and get some list of proposals to choose from. This is reasonable for large static lists, that can't be managed by SelectFields or RadioGroups or where the proposals rely on a remote resource.
Example usage:
val proposals = listOf("Kotlin", "Scala", "Java", "OCaml", "Haskell").asProposals()
val choice = storeOf("")
typeAhead(value = choice, items = proposals) { }
For further details have a look at our KitchenSink
TooltipMixin
and TooltipProperties
to use TooltipComponent
in Components
more easilyWith this PR it beeing easier to integrate a tooltip in a component and the user get a same usage of a tooltip.
appFrame
and added submenu
in menu
componentThe appFrame
component uses now different ColorScheme
s to coloring itself.
The menu
component can now contain submenu
s:
menu {
header("Entries")
entry {
text("Basic entry")
}
divider()
custom {
pushButton {
text("I'm a custom entry")
}
}
submenu {
icon { menu }
text("Sub Menu")
entry {
icon { sun }
text("Entry with icon")
}
entry {
icon { ban }
text("Disabled entry")
disabled(true)
}
}
}
box
in favor of div
This PR deprecates the box
factory method in favor of div
because, in combination with fritz2.styling, it offers the exact same functionality. All calls of box
will have to be replaced eventually.
This PR adds two convenience functions alertToast
and showAlertToast
which can be used to easily create toasts with alert's as their content.
New functions:
fun showAlertToast(
styling: BasicParams.() -> Unit = {},
baseClass: StyleClass = StyleClass.None,
id: String? = null,
prefix: String = "toast-alert",
build: AlertComponent.() -> Unit
)
fun alertToast(
styling: BasicParams.() -> Unit = {},
baseClass: StyleClass = StyleClass.None,
id: String? = null,
prefix: String = "toast-alert",
build: AlertComponent.() -> Unit
): SimpleHandler<Unit>
Example usage:
showAlertToast(buildToast = {
}) {
// toast-properties:
duration(6000)
// setup of the alert
alert {
title("Alert-Toast")
content("Alert in a toast")
severity { /* some severity */ }
variant { leftAccent }
}
}
// 'alertToast' is used similarly and bound to a flow like 'toast'
val alertComponent = AlertComponent()
.apply {
content("...")
stacking { toast }
}
showToast {
// adjust the close-button to match the alert's color-scheme:
closeButtonStyle(Theme().toast.closeButton.close + {
color {
val colorScheme = alertComponent.severity.value(Theme().alert.severities).colorScheme
when(alertComponent.variant.value(AlertComponent.VariantContext)) {
AlertComponent.AlertVariant.SUBTLE -> colorScheme.main
AlertComponent.AlertVariant.TOP_ACCENT -> colorScheme.main
AlertComponent.AlertVariant.LEFT_ACCENT -> colorScheme.main
else -> colorScheme.mainContrast
}
}
})
content {
alertComponent.render(this, styling, baseClass, id, prefix)
}
}
Result:
discreet
to ghost
This PR renames the discreet
alert-variant to ghost
in order to match the common naming scheme of fritz2.
All references of discreet
have to be changed:
alert {
variant { discreet } // <-- previously
variant { ghost } // <-- now
}
radio
, checkbox
and switch
componentsToastComponent
Published by jamowei over 3 years ago
This PR adds the option to use any element or component for the label of the items in a radio- or checkbox-group.
Previously it was only possible to generate Strings from the underlying objects by using the label
property:
val label = ComponentProperty<(item: T) -> String> { it.toString() }
Both components now feature a dedicated labelRendering
property that can be used to override the default rendering process and specify custom layouts.
The labelRendering
property is a lambda that takes each underlying item: T
and renders the respective label.
By default the label is rendered based on the label
property.
// RadioGroupComponent:
val labelRendering = ComponentProperty<Div.(item: T) -> Unit> { /* rendering based on 'label' by default */ }
// CheckboxGroupComponent:
val labelRendering = ComponentProperty<RenderContext.(item: T) -> Unit> { /* rendering based on 'label' by default */ }
Usage example:
checkboxGroup(values = /* ... */, items = /* ... */) {
labelRendering { item ->
span({
// styling
}) {
+item
}
}
}
reset()
function to Validator
Now you can easily clean the list of validation messages by calling the new reset()
function. If you want you can specific a list of validation messages to reset to.
val save = handleAndEmit<Person> { person ->
// only update the list when new person is valid
if (validator.isValid(person, "add")) {
emit(person)
validator.reset() // new reset function
Person()
} else person
}
This PR improves the following aspects of a modal:
width
instead of size
to make the intention of the property more expressive. The predefined values remains the same, but it is also possible now to provide a custom width value like 30rem
or alike. The old API calls will be applied and result in the same visual apperance for now.variant
property, which won't lead into an error, but the functionality is disabled. Instead use the placement
property to align a modal vartically: top
(default), center
, bottom
and stretch
are the possible values.Example:
clickButton {
text("Open")
} handledBy modal({
minHeight { "30rem" }
}) {
width { "800px" }
placement { center }
content {
// some content
}
}
The old API calls for size
and variant
don't break your code, but they will be removed in future versions. So please follow the mrigration guide to recplace them.
Be aware that also the interfaces ModalVariants
and ModalSizes
from the theme are deprecated and will disappear in future versions.
For a complete overview have a look at our KitchenSink.
size
to width
:
modal {
// old:
// size { small }
// new
width { small }
}
variant
to placement
:
auto
→ top
verticalFilled
→ stretch
centered
→ center
modal {
// old:
// variant { verticalFilled }
// new
placement { stretch }
}
Theming migration:
ModalWidths
with the values taken from ModalSizes
.ModalVariants
just disappears. There is no more theme element that replaces this one. For probably adavanced settings applied here, integrate those into ModalStyles
properties base
, width
and internalScrolling
.getElementById()
callid
and prefix
placement in DOM (analog to dropdown
)Icons
in DefaultTheme
track()
function of tracker
safe for unsafe operationsselect.kt
and nav.kt
Published by haukesomm over 3 years ago
Upgrading to Kotlin 1.5.10 version and update dependencies to latest version.
This API change for pushButton
, clickButton
and linkButton
harmonizes the way for setting an icon to it.
// before
pushButton {
type { info }
icon { fromTheme { circleInformation } }
text("Info")
}
// now
pushButton {
type { info }
icon { circleInformation }
text("Info")
}
In most cases it is enough to remove the fromTheme()
function.
Components which need a z-index an their default level values defined in DefaultTheme
:
tableHeader (10)
tooltip (100)
dropdown (200)
popover (300)
appFrame (1000)
navbar (1000) -- will be replaced by appFrame
toast (2000)
modal (3000)
New component to build menus consisting of clickable entries, headers, dividers and other items.
menu {
header("Menu")
entry {
text("Clickable item")
icon { notification }
disabled(false)
events {
// standard events context
}
}
divider()
header("Other")
custom {
pushButton {
text("I'm a custom entry")
}
}
}
New component to display dropdown-content floating around a toggle-element, similar to a popover.
dropdown {
toggle {
pushButton {
text("Toggle")
}
}
placement { bottom }
alignment { start }
content {
// ...
}
}
OrientationMixin
for componentsorientation
API, old direction
marked as deprecatedorientation
API, old direction
marked as deprecateddirection
→ orientation
row
→ horizontal
column
→ vertical
// old one with deprecation warning as:
// 'direction: ComponentProperty<CheckboxGroupComponent.DirectionContext.() -> CheckboxGroupComponent.Direction>' is deprecated. Use orientation instead
// 'row: CheckboxGroupComponent.Direction' is deprecated. Use orientation { horizontal } instead
checkboxGroup(values = someStore, items = someItems) {
direction { row }
}
// change like this
checkboxGroup(values = someStore, items = someItems) {
orientation { horizontal }
}
Published by jamowei over 3 years ago
linkButton
component
linkButton {
text("Show me awesome")
href("www.fritz2.dev")
target("_blank")
}
slider
component
val valueStore = storeOf(100)
slider(value = valueStore) {}
menu
component
menu(demoMenuStyle) {
header("Items")
entry {
text("Basic item")
}
divider()
entry {
icon { sun }
text("Item with icon")
}
}
Inter
fontinspectEach()
for Inspector on collectionspublishing.gradle.kts
alert
APIappFrame
component