It helps to understand what fritz2 is in the first place. What does it do? What problems does it help me solve? Why would I want to use it?
We would like to show you the basic concepts, explain what they are used for, and show you the beauty and elegance of the applied framework API.
fritz2 is a lightweight, yet fully functional framework to build reactive web apps in Kotlin.
The main concepts of fritz2 are:
Store
s take care of the data and offer functions to update it by (UI-) events (Handler
s). They
also use the data to reactively render the affected parts of your UI.Sounds simple, right? In fact, it is. The main principles and concepts are that simple, that's why we consider fritz2 lightweight. After getting the first overview in this chapter, the following chapters will teach you more about convenience functions which make writing real world apps more pleasant.
You will also be able to understand the following picture which shows the above-mentioned concepts embedded into the overall data flow. We will refer to this later on.
Please have a look at our working code getting started example - it goes nicely with this documentation.
Let's dive into a small example to demonstrate the fundamental concepts and functions.
Assume an input field where a user can enter text which will be displayed in a section below the input. An additional button will then capitalize the input:
import dev.fritz2.core.*
// some styling is omitted; have a look at the complete example at the end of this chapter
fun main() {
render {
val store = storeOf("Hello, fritz2!")
div("w-48 m-4 flex flex-col gap-2") {
label {
+"Input"
`for`(store.id)
}
input(id = store.id) {
placeholder("Add some input")
value(store.data)
changes.values() handledBy store.update
}
p { +"Value" }
store.data.render { content ->
p{ +content }
}
button {
+"Capitalize"
clicks handledBy store.handle { it.uppercase() }
}
}
}
}
This is the rendered structure of the main part below the body
tag:
<div class="w-48 m-4 flex flex-col gap-2">
<label for="dp9C88">Input</label>
<input id="dp9C88" placeholder="Add some input" value="Hello, fritz2!">
<p>Value</p>
<div class="mount-point" data-mount-point="">
<p>Hello, fritz2!</p>
</div>
<button>Capitalize</button>
</div>
Although this is an enhanced "hello-world"-example, it demonstrates almost all the fundamental concepts and parts of fritz2.
Let's have a look at the result in a browser:
Now that we have a good notion of the app and how it works, let's dive into the code and discover the various building blocks driving this app.
Note: The styling in this example uses tailwindcss. As fritz2 is completely agnostic of any CSS styling framework, you are free to use any CSS framework, or even handcrafted CSS, that fits your needs. The running examples are often built with bootstrap.
The fritz2 framework requires very low ceremony to set up an application. Just call the global
render
function inside your code once to create an initial so-called RenderContext
. Think of it as a context
where you can place all your UI elements, which in the end are HTML tags
.
import dev.fritz2.core.*
fun main() {
// call the render function *once* to create an initial `RenderContext` you can render your UI into
render { /* `this` is a `RenderContext` */
// starting from within this root context, declare the whole application's UI
}
}
Inside a RenderContext
, fritz2 offers factory functions for all HTML5 elements, like div
, span
, p
, and so on.
These functions create a special Tag
implementation which in itself is just a new RenderContext
. This
enables the nesting of factories and enables declarative UI definition.
The declarative calling of tag factories resembles the natural HTML representation of the DOM and thus makes the code very pleasant to read and understand.
render { /* creates the initial `RenderContext` */
// call a tag-factory, like a `div` tag and optionally pass some CSS classes as first parameter
div("w-48 m-4 flex flex-col gap-2") { /* exposes the `div` as new `RenderContext` */
// create another tag inside the `div`
label {
// create a text-node with the plus operator
+"Input"
// set tag specific attributes - they are predefined for all HTML tags
// (the backticks are needed here because `for` is an identifier in Kotlin, so the function call needs
// to be escaped)
`for`("SomeId")
}
// as second parameter, you can pass an ID to the tag
input(id = "SomeId") {
// set another tag specific attribute
placeholder("Add some input")
}
}
}
State handling is probably the key aspect of any reactive web application, so fritz2 supports this with a simple, but
mighty concept: Store
s.
A Store stores all your application data, no matter whether domain- or UI state data. In contrast to other approaches, there might be an arbitrary number of stores which you can connect to link the handling of all individual states.
For our example, we only need one store which we declare with the storeOf
factory.
render {
// a store can be created anywhere in your application
// pass some data as initial state to it
val store = storeOf("Hello, fritz2!")
}
Once you have created the store, it can be used for...
Using the state to render parts of your UI is quite easy: Every store offers a predefined data
property - a Flow
holding the current data. The framework offers an extension method on flows called render
which
creates a reactive UI part.
p { +"Value" }
store.data.render { content -> // the data from store
// create a paragraph tag and a text node with the data inside it
p { +content }
}
How is this reactive? Here is a simplified answer to this question: The store holds a Flow
which literally
resembles a real flow, as inside the flow data is transported to a drain. The drain in this case is a
well-defined node inside the DOM of your browser which is created by the render
call. Every time the stored data
changes, the new data will be applied to the rendering code, which then creates a new dom subtree accordingly.
So the call of render on a flow connects the store with a node of the DOM: the so-called "mount-point".
Now the "magic" can happen: Every time the data inside the store changes, the new value will appear at the target
node and change the whole subtree based upon the code you write inside the render
functions parameter.
In case of our example above, the p
tag will be removed from the DOM and a new tag will be rendered with the new text
value inside.
But how can the state of a store change? Often this is due to user input which creates an event
inside the DOM. The tag
s offer all events as predefined properties, like the changes
event of an input
tag.
fritz2 uses an extension function of this event called values
, which simply reduces the raw DOM event to a Flow
of String
s. The data on the flow is the current input value, and all the meta information about the event we do not
need here is omitted.
Once we got our flow of data in the right shape, we can connect it to the store in order to update its state.
This is done by so-called Handler
s. A handler is a method that produces the new state of the store.
In this case we want the new input value to completely replace the old one. This can be done with the predefined,
built-in update
handler on every store:
input {
changes.values() handledBy store.update
// ^^^^^^^^^^^^^^^^ ^^^^^^^^ ^^^^^^^^^^^^
// use event with connect to reference to a handler which
// new value handler changes the state of the store
}
In order to reactively pass the current value of the store to the input element, we have to set the tag-specific
value
property to the data-flow of the store:
input {
// the current state will be rendered into the input (at first the initial value of course)
// also any external state change (see the Capitalize button below) will update the input itself
value(store.data)
}
The connection between a store and the UI is called data binding. If both the UI-rendering and the UI's event of those rendered HTML element are connected to a store, this is called two-way data binding. This is the preferred way of data binding for most form elements, for example. If only one aspect is connected to a store, we call this one-way data binding.
Let's have a look at the teaser for custom handlers of a store: Capitalizing the store's state with a button click.
button {
+"Capitalize"
clicks handledBy store.handle { it.uppercase() }
// ^^^^^^ ^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^
// use connect to change the state
// event handler of the store
}
The predefined update
handler (which simply replaces the store's content with a new value) is often not sufficient
for all use cases. So fritz2 allows the definition of custom handlers like the one above, which simply takes the old
state, capitalizes it, and sets the result as new value.
We have one last basic concept to show you. In addition to reactive state handling, fritz2 stores also support the semantic relation between data and UI sections. This is achieved using identifiers.
Every store has an id
property which is implicitly initialized with a random value when no parameter is passed:
render {
val storeWithExplicitId = storeOf("Data", id = "42")
val storeWithRandomId = storeOf("Data") // id is created by the `Id.next()` factory
}
This id is useful for linking data to the semantically corresponding part of the UI. Since the store holds the data, it makes sense to also have it keep track of its identity.
In the following example, we use the id to connect the label
to the input
field so that clicking the label text
will focus the input - this is a well known HTML function.
label {
+"Input"
`for`(store.id)
}
input(id = store.id) {
// ...
}
We will discover more use cases for this concept in later chapters. But, to tease you: this concept enables the automatic association of validation messages to their corresponding form-elements.
Let's have another look at the so-called circle of life:
After reading through the teasing example explanations, you now have an understanding about how this picture reflects the main concepts of fritz2 within the cycling flow of data.
The store on the left side keeps track of the application data. The DOM on the right side
holds all HTML tags and is in charge of emitting events. By calling the render
extension method on the store's
data
-flow, its data can be connected to a well-defined node (the "mount-point").
Every time the stored data changes, the new data will be applied to the rendering code, which then creates a new dom
subtree accordingly.
In most cases, the user can interact with the UI and produce events from within the DOM on the right side.
The events are used to call Handler
s to update the store's data. All handlers have access to the current
store state and the value passed by the event-flow and use both to create the new store state.
The new state will then appear on the data
-flow and finally result in a change of the UI. Et voilĂ , the circle of life
is complete!
That's the core of fritz2's way of building reactive web applications. No more magic left, just some helpful tools to make writing apps more pleasant.
Armed with this basic knowledge, we suggest you go on reading about UI-Rendering and the chapters explaining the essentials of stores.
This is the full source of the example above, including all styling to make it look like the screenshot.
You can just copy and paste it into the vanilla
tailwind template project, replacing the main
function, and
then running the app.
import dev.fritz2.core.*
fun main() {
render {
val store = storeOf("Hello, fritz2!")
div("w-48 m-4 flex flex-col gap-2") {
label {
+"Input"
`for`(store.id)
}
input("p-2 border border-1 border-gray-300 rounded-sm", id = store.id) {
placeholder("Add some input")
value(store.data)
changes.values() handledBy store.update
}
p { +"Value" }
store.data.render { content ->
p("p-2 bg-gray-100 border border-1 border-gray-300 rounded-sm") {
+content
}
}
button("p-2 bg-blue-400 text-white border border-1 border-gray-300 rounded-md") {
+"Capitalize"
clicks handledBy store.handle { it.uppercase() }
}
}
}
}