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 validation result is valid or not
val messages: List<MyMessage> = Person.validation(Person())
messages.valid // == false
New ValidatingStore
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
PR #580: Transitions controlled by Flow
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.
PR #587: Add new functions to the core
DomNodeList
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()
Extension function whenever
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))
Further Improvements
- PR #592: Added toString() method to Scope.Key class
- PR #594: Improve error handling when dealing with lenses
Fixed Bugs
- PR #568:
Authentication.complete()
function sets the principal also without running auth-process
- PR #571:
Authentication.getPrincipal()
function returns the principal also when pre-setting it before any process is started
- PR #579: Fixed exception handling in Handlers
Source code(tar.gz)
Source code(zip)