Skip to content

Releases: jwstegemann/fritz2

Version 1.0-RC6

20 Jun 10:47
Compare
Choose a tag to compare

Breaking Changes

PR #772: Add Inspector.mapNull() method and fix Store.mapNull() path

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.

Improvements

PR #765: Adds convenience execution functions for Unit metadata in validators

For convenience reasons it is now possible to call a validator with metadata type Unitwithout the metadata parameter.

Imagine a Validatorof 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`

Fixed Bugs

  • PR #767: Fixes an issue with overlapping validation messages of mapped stores
  • PR #766: Fix popups in scroll containers/modals
  • PR #771: Improves docs

Version 1.0-RC5

08 May 11:33
Compare
Choose a tag to compare

Breaking Changes

PR #763: Validation: Remove explicit nullability from metdata type

This PR changes the Validation's metadata type to not be explicitly be nullable. Nullable types are still allowed, however.
The ValidatingStore API has been changed accordingly.

Validation

Before
@JvmInline
value class Validation<D, T, M>(private inline val validate: (Inspector<D>, T?) -> List<M>) {
    operator fun invoke(inspector: Inspector<D>, metadata: T? = null): List<M> = this.validate(inspector, metadata)
    operator fun invoke(data: D, metadata: T? = null): List<M> = this.validate(inspectorOf(data), metadata)
}
Now
@JvmInline
value class Validation<D, T, M>(private inline val validate: (Inspector<D>, T) -> List<M>) {
//                                                                         ^^^
//                                                  Metadata type is no longer explicitly nullable.
//                                                  Thus, it must always be specified.
//
    operator fun invoke(inspector: Inspector<D>, metadata: T): List<M> = this.validate(inspector, metadata)
    operator fun invoke(data: D, metadata: T): List<M> = this.validate(inspectorOf(data), metadata)
}

ValidatingStore

Before
open class ValidatingStore<D, T, M>(
    initialData: D,
    private val validation: Validation<D, T, M>,
    val validateAfterUpdate: Boolean = true,
    override val id: String = Id.next()
) : RootStore<D>(initialData, id) {
    // ...
   
   protected fun validate(data: D, metadata: T? = null): List<M> = /* ... */
}
Now
open class ValidatingStore<D, T, M>(
    initialData: D,
    private val validation: Validation<D, T, M>,
    private val metadataDefault: T,
//  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
//  New parameter `metadataDefault`: Since the metadata must now be present at
//  all times, a default value needs to bespecified for the automatic validation
//  to work. During manual validation (via `validate(..)`), the metadata can still
//  be passed in as before.
    private val validateAfterUpdate: Boolean = true,
    override val id: String = Id.next()
) : RootStore<D>(initialData, id) {
    // ...
   
   protected fun validate(data: D, metadata: T): List<M> = /* ... */
//                                 ^^^^^^^^^^^
//                                 Metadata now has to be specified at all times
}

Additionally, the convenience factory storeOf(...) has been overloaded so ValidatingStore<D, Unit, M>s can be created without the need to specify Unit as the default metadata value manually.

Migration

  • Validation<D, T, M>.invoke(...) now always requires metadata to be present. Add the missing metadata if necessary.
  • The Validation<D, T, M> constructor now requires the paramer metadataDefault to be present. Add the missing metadats default if necessary.

PR #761: Inspector based Validation

Motivation

Real world domain objects are in most cases forms a deep object hierarchy by composing dedicated types in a sensefull way, for example some Person consists of fields like name or birthday but also complex fields like some Address, which itself represents some domain aspect with basic fields.

As validation is most of the time tied to its corresponding domain type, the validators mirror the same hierarchy as the domain classes. Thus they must be composable in the same way, the domain types are composed.

This was prior to this PR not supported by the 1.0-RC releases!

Solution

The API of the Validation type changes just a little: The validate lambda expression now provides no longer a (domain) type D, but an Inspector<D>! There are now two invoke functions that take as first parameter an Inspector<D> and as before just a D, which then constructs the inspector itself. So one can use the latter for the (external) call of a validation with the domain object itself and the former for calling a validator from inside another validator!

Remark: If you have used the recommended validation-factory functions, there will be no API breaking at all, as those already have used the Inspector<D>. So it is very unlikely that this change will break existing code!

Example

The following composition now works:

// if you use the `headless` components, prefer to use the `ComponentValidationMessage` instead of handcrafting your own!
data class Message(override val path: String, val text: String) : ValidationMessage {
    override val isError: Boolean = true
}

@Lenses
data class Person(
    val name: String,
    val birthday: LocalDate,
    val address: Address // integrate complex sub-model, with its own business rules
) {

    data class ValidationMetaData(val today: LocalDate, val knownCities: Set<String>)

    companion object {
        val validate: Validation<Person, ValidationMetaData, Message> = validation { inspector, meta ->
            inspector.map(Person.name()).let { nameInspector ->
                if (nameInspector.data.isBlank()) add(Message(nameInspector.path, "Name must not be blank!"))
            }
            inspector.map(Person.birthday()).let { birthdayInspector ->
                if (birthdayInspector.data > meta.today)
                    add(Message(birthdayInspector.path, "Birthday must not be in the future!"))
            }
            // call validator of `Address`-sub-model and pass mapped inspector into it as data source and for
            // creating correct paths!
            // Voilà: Validator Composition achieved!
            addAll(Address.validate(inspector.map(Person.address()), meta.knownCities))
        }
    }
}

@Lenses
data class Address(
    val street: String,
    val city: String
) {
    companion object {
        // enforce business rules for the `Address` domain
        val validate: Validation<Address, Set<String>, Message> = validation { inspector, cities ->
            inspector.map(Address.street()).let { streetInspector ->
                if (streetInspector.data.isBlank()) add(Message(streetInspector.path, "Street must not be blank!"))
            }
            inspector.map(Address.city()).let { cityInspector ->
                if (!cities.contains(cityInspector.data)) add(Message(cityInspector.path, "City does not exist!"))
            }
        }
    }
}

// and then use those:
val fritz = Person(
    "", // must not be empty!
    LocalDate(1712, 1, 24),
    Address("Am Schloss", "Potsdam") // city not in known cities list, see below
)

val errors = Person.validate(
    fritz,
    Person.ValidationMetaData(
        LocalDate(1700, 1, 1), // set "today" into the past
        setOf("Berlin", "Hamburg", "Braunschweig") // remember: no Potsdam inside
    )
)

// three errors would appear:
// Message(".name", "Name must not be blank!")
// Message(".birthday", "Birthday must not be in the future!")
// Message(".address.city", "City does not exist!")

Migration Guide

If you have used the Validation.invoke method directly, then prefer to switch to the dedicated validation-factories!
Inside your validation code just remove the inspectorOf(data) line, that you hopefully will find and change the name of the parameter to inspector.

If you have not used any inspector based validation code, you simple must change the field access in such way:

// inside validation code:
// old
data.someField

// new
inspector.data.someField

Also try to replace handcrafted path parameters of the Message objects by relying on the inspector object:

// old
add(SomeMessage(".someField", ...))

// new
add(SomeMessage(inspector.path, ...))

Improvements

  • PR #762: Generate extension functions for Lens-Chaining to enable some fluent-style-API

Fixed Bugs

  • PR #764: Fix navigation issue in Router
  • PR #755: Fixes List Index related Bug in Headless DataCollection
  • PR #752: Substitute tailwindcss class with vanilla CSS

Version 1.0-RC4

08 Feb 20:38
7b95d12
Compare
Choose a tag to compare

Improvements

  • PR #747: Improve documentation (english, typos, etc.)
  • PR #749: Add some explicit information for dealing with CSS

Fixed Bugs

  • PR #748: Fix renderEach-behaviour after regression by RC3 release

Version 1.0-RC3

24 Jan 12:47
7272256
Compare
Choose a tag to compare

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

25 Nov 14:19
10ec9c3
Compare
Choose a tag to compare

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 to clear()
  • renamed add(entry) method to push(entry)
  • removed last() method, cause with current: List<T> every entry is receivable
  • changed default capacity to 0 (no restriction) instead of 10 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:

  1. move operating code into the hook implementation itself
  2. 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 Stores

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

03 Aug 12:17
Compare
Choose a tag to compare

Improvements

  • PR #664: Upgrade to Kotlin 1.7.10

Version 1.0-RC1

17 Jun 15:15
Compare
Choose a tag to compare

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<> and SvgTag<>
  • 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 a Scope.Key is moved to Scope 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 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)
  • replacing UIEvent by Event which solves ClassCastExceptions when UIEvent is explicitly needed you have to cast it (see #578)
  • removed special 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(" "))

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 to WithJob interface

PR #622: Fix invocation of Handlers

  • invoke-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.

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...
Read more

Version 0.14.3

13 Jun 10:35
Compare
Choose a tag to compare

Fixed Bugs

  • PR #649: Don't react to blur-events caused by child elements of a PopoverComponent

Version 0.14.2

31 Jan 13:42
Compare
Choose a tag to compare

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

19 Jan 11:06
Compare
Choose a tag to compare

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