fritz2 offers a rich DSL to create the HTML for your application. Simply call the global render
function to
create an initial RenderContext
, then use the HTML-Tag factory-functions provided by fritz2,
like div
. All of those factories have to be nested by intention, so this results in a declarative way of creating
UIs.
fun main() {
render { // offers created root `RenderContext` - start your UI code in here
// create an HTML tag; `div` produces a `Tag` which is also a `RenderContext`.
// This enables nested calling and therefore the declarative approach
div(id = "header") {
}
div(id = "container") {
h1 { +"Hello World!" }
// ^^^^^^^^^^^^^^^
// create a text node inside a tag
}
div(id = "footer") {
}
}
}
This code results in:
<body id="target">
<div id="header"></div>
<div id="container">
<h1>Hello World!</h1>
</div>
<div id="footer"></div>
</body>
If you compare the result with the code, you will immediately recognize that the DOM structure is reflected by the declaring code. This leads to easy to read UI definitions and is a core feature of fritz2.
fritz2 supports reactive UIs as one of its core features, so let us enhance the example with some dynamic content.
First of all, we need a so-called Store
for holding the dynamic data of our application. Such stores are the heart
of every fritz2 application; they provide the current value in a reactive way and handle all the data changes.
The store creation and its core functionalities will be explained in-depth in chapter. So do not fret about the details - the facts relevant to understanding this chapter are explained here.
The store's data
-property offers a Flow
of the stored value T
. To reactively bind this value to the DOM,
use one of the render*
-functions of the data flow on it. The function creates a so-called mount-point which manages
the automatic update of the DOM on every change of the store's data. The mount-point uses a dedicated tag created in
the DOM as reference to the node where the deletion and recreation of the defined UI-fragment happens.
To react to (user) events like the click onto a button, a store provides so-called Handler
s, which create the new
value of the store. The default and built-in handler update
simply substitutes the old state with a new value.
Events and reacting to them will be explained in-depth in another chapter. The next example contains short commentary on this topic only to help you understand the example.
fun main() {
render {
// define a store to hold the dynamic data, in this case a `String`
val storedName = storeOf("World")
div(id = "header") {
}
div(id = "container") {
storedName.data.render { name -> // current value is provided
// ^^^^^^
// create a "mount-point" to bind the store's data to a node in the DOM
// every time the data changes, this inner UI-subtree gets deleted and newly created
h1 {
+"Hello, "
+name // use the provided data here
+"!"
}
}
}
div(id = "footer") {
button {
+"Greet fritz2"
}.clicks.map { "fritz2" } handledBy storedName.update
// ^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^
// use the event to send a new use store's default handler
// value to the store to replace the old with the new value
// Event handling will be explained in the chapter about store creation!
}
}
}
The DOM structure now looks as follows:
<body id="target">
<div id="header"></div>
<div id="container">
<div class="mount-point" data-mount-point="">
<h1>Hello, World!</h1>
</div>
</div>
<div id="footer">
<button>Greet fritz2</button>
</div>
</body>
When you click the button, the whole h1
-subtree will be removed and changed to <h1>Hello, fritz2!</h1>
As a final teaser, we would like to demonstrate the styling of a UI and setting attributes of a tag.
The tag-factories accept static CSS-classes as a String
as first parameter, as this is such a common use case (this is
why we used the named parameter for the ids throughout previous chapters).
fritz2 is totally agnostic of any CSS-framework or even handcrafted CSS - use whatever fits your needs!
fritz2 is totally agnostic of any specific CSS technology. At least there are the following possibilities you can apply:
jsMain/resources
and embed it in
the jsMain/resources/index.html
file.jsMain/resources/index.html
file.Since being reactive is such an important aspect of fritz2, styling and attributes can be set based upon the store's
state. Example:
A button is reactively disabled when it is clicked because the click changes the state of the store.
The changed state (its data) is evaluated inside the disabled
-function which sets the related property of
the <button>
-tag.
All the building blocks we showcased here make fritz2 a fully reactive web framework.
fun main() {
render {
val storedName = storeOf("World")
div("w-48 m-4 flex flex-col gap-2") {
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^
// set (static) CSS classnames as first parameter of a tag factory
div(id = "header") {
}
div(id = "container") {
h1 {
+"Hello, "
storedName.data.render { name ->
span(if (name == "fritz2") "font-bold" else "") {
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// set classname depending on the store's content
+name
}
+"!"
}
}
}
div(id = "footer") {
button("p-2 text-white border border-1 border-gray-300 rounded-md") {
+"Greet fritz2"
className(storedName.data.map { if (it == "fritz2") "bg-gray-500" else "bg-blue-400" })
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// set CSS classes reactively
disabled(storedName.data.map { it == "fritz2" })
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// set disabled attribute reactively
}.clicks.map { "fritz2" } handledBy storedName.update
}
}
}
}
The final DOM structure now looks like this:
<body id="target">
<div class="w-48 m-4 flex flex-col gap-2">
<div id="header"></div>
<div>
<h1>Hello,
<div class="mount-point" data-mount-point="">
<span>World</span>!
</div>
</h1>
</div>
<div>
<button class="p-2 bg-blue-400 text-white border border-1 border-gray-300 rounded-md">
Greet fritz2
</button>
</div>
</div>
</body>
Clicking the button will change the button section to this:
<button disabled class="p-2 bg-gray-500 text-white border border-1 border-gray-300 rounded-md">
Greet fritz2
</button>
Pay attention to the changed CSS-classes and the added disabled
attribute!
(tailwindcss users might recognize a better approach for this case: the usage of the disabled:
prefix. This would make
the className
call obsolete and shorten the code - so please accept this solution for demonstration purposes
only.)
Before we dive into the essential topics, let us introduce some model types we will use in our upcoming examples:
// example of an entity
data class Person(
val id: Int, // stable identifier
val name: String,
val age: Int
)
// example of some value type
enum class Interest {
Programming,
Sports,
History,
WritingDocumentation
}
As you already know, all state handling is done with Store
s in fritz2.
Based upon the data
-property, which provides a Flow
of the store's generic data type, there are a variety of
render*
-functions which can be used to create reactive UIs:
Render-Function | Additional parameters | Description | Default Tag |
---|---|---|---|
Flow<T>.render |
- | Creates a mount-point providing the whole store's data value T inside content expression |
div |
Flow<T>.renderIf |
predicate | Creates a mount-point providing the whole store's data value T inside content expression when predicate is true |
div |
Flow<Boolean>.renderTrue |
- | Creates a mount-point rendering the content expression with no store data provided when the flow's value is true |
div |
Flow<Boolean>.renderFalse |
- | Creates a mount-point rendering the content expression with no store data provided when the flow's value is false |
div |
Flow<T>.renderNotNull |
- | Creates a mount-point providing the whole store's data value T inside content expression when T is not null |
div |
Flow<T>.renderIs |
klass | Creates a mount-point providing the whole store's data value T inside content expression when T is of type klass |
div |
Flow<String>.renderText |
- | Creates a mount-point creating a text-node | span |
Flow<List<T>>.renderEach |
- | Creates a mount-point optimizing changes by T.equals . Provides a T inside the content expression. Use for value objects |
div |
Flow<List<T>>.renderEach |
idProvider | Creates a mount-point optimizing changes by idProvider . Provides a T inside the content expression. Use for entities |
div |
There is one more renderEach
variant which is defined as an extension to a Store
instead of a Flow
.
This special variant and its application are described in the
chapter about store mapping.
Render-Function | Additional parameters | Description | Default Tag |
---|---|---|---|
Store<List<T>>.renderEach |
idProvider | Creates a mount-point optimizing changes by idProvider . Provides a Store<T> inside the content expression. Use for entities |
div |
T
Use the render
-function to render the store's data type as a whole.
It requires a lambda expression with a Tag
as receiver (remember that a Tag
is a RenderContext
), providing the
data as a parameter and returning Unit
. Inside this content
parameter, you then have access to the current data
and can use all HTML tag factories to create the desired UI-fragment.
render {
// define a Person and "store" it
val storedPerson = storeOf(Person(1, "Fritz", 42))
storedPerson.data.render { person -> // the current store's value is injected
dl {
dt { +"Id" }
// use the data type to render its contents by accessing its properties
// as the DOM consists only of strings, we must take care of the needed type conversion from `Int` to `String`
dd { +person.id.toString() }
dt { +"Name" }
dd { +person.name }
dt { +"Age" }
dd { +person.age.toString() }
}
}
}
As a result, the following DOM-fragment is rendered:
<div class="mount-point" data-mount-point="">
<dl>
<dt>Id</dt>
<dd>1</dd>
<dt>Name</dt>
<dd>Fritz</dd>
<dt>Age</dt>
<dd>42</dd>
</dl>
</div>
The render
-function creates a mount-point which reactively connects a store's data with
a node in the DOM-tree. The created mount-point now takes care of reacting to new values and keeps the UI-fragment up
to date. This is the upper part of fritz2's circle of life.
It is important to know the following facts about render
and mount-points:
<div>
tag is rendered which is marked with the purely informational data-mount-point
attribute. It also has a special CSS class mount-point
which simply sets the display mode to contents
in order to
exclude this artificial tag from the visible UI.<div>
tag is dropped and rebuilt with the
new data's content.As a direct consequence of the last fact, we recommend keeping the reactive UI-fragments as minimal as you can, since re-rendering a subtree is work for the browser.
fritz2 supports you in doing so, for example by
As rule of thumb, the smaller the changing parts are, the faster the result will be.
Also, take a look at our basic example which demonstrates the render
function.
Since creating a reactive UI with dynamically rendered texts is a common use case,
fritz2 offers a dedicated render variant called renderText
on data flows of type String
:
render {
val storedText = storeOf("fritz2")
div {
// attention: needs a *Tag*, not just a `RenderContext`
storedText.data.renderText()
}
}
As result the following DOM-fragment is rendered:
<div>
<span>fritz2</span>
</div>
There is also an extension function asString
which converts a Flow<T>
to a Flow<String>
by calling the
toString
method internally:
render {
val storedCount = storeOf<Int>(0)
div {
storedCount.data.asString().renderText()
}
}
The renderText
function is useful when long text has smaller dynamic parts.
Since only the smaller parts will change, the other parts must not be part of the mount-point:
render {
val storedText = storeOf("fritz2")
p {
+"There is an excellent Kotlin based framework named "
storedText.data.renderText() // only the dynamic part is here; the other text-nodes are static
+" which empowers one to easily create reactive SPAs in pure Kotlin."
}
}
Imagine a render
based solution on the contrary:
render {
val storedText = storeOf("fritz2")
p {
storedText.data.render { frameworkName ->
+"There is an excellent Kotlin based framework named "
+frameworkName
+" which empowers one to easily create reactive SPAs in pure Kotlin."
}
}
}
Of course the former could also have been written with String
-templating instead of separate text-nodes.
The important advantages of the dedicated renderText
solution compared to the general render
based solution are:
renderText
solution fits better to the declarative UI approach, as it better reflects the node structure
and thus is better to read.Since a store of List<T>
is a common use case, fritz2 offers a special renderEach
function for this as well:
render {
// define a store with some type of `List<T>`
val storedInterests = storeOf(Interest.values().toList())
ul {
// for every value of store's interest list, the provided `content` expression is executed
storedInterests.data.renderEach { interest -> // the current applied value of the data flow
// just declare the UI for one item to render the complete list accordingly
li {
+interest.toString()
}
}
}
}
As a result, the following DOM-fragment is rendered:
<div class="mount-point" data-mount-point="">
<li>Programming</li>
<li>Sports</li>
<li>History</li>
<li>WritingDocumentation</li>
</div>
It is important to spot the main difference to the former render-functions: The store's data type is an (ordered)
collection type, so its value consists of an arbitrary amount of elements of the same type.
An inherent property of equal types is that their UI-representation is also of the same type. renderEach
supports
this by describing only the UI-fragment for one item of the list with the content
parameter.
The UI-container which holds those items is therefore not part of the reactive expression and thus the
mount-point.
Another important aspect of handling the reactive rendering of List
s is performance
optimization.
In order to gain some understanding for this technical aspect, consider the above example realized with the standard
render
-function:
render {
val storedInterests = storeOf(Interest.values().toList())
ul {
storedInterests.data.render { interests -> // name suggests a list
// "manually" create <li> tags for each item
interests.forEach {
li {
+it.toString()
}
}
}
}
}
You cannot spot the difference by looking at the rendered DOM structure because it remains the same for both solutions.
But looking at the code, we can conclude that the second approach will definitely delete the whole subtree of the
mount-point and re-create all <li>
-tags, even if only one element of the list changes.
renderEach
instead creates a specialized mount-point in order to identify elements for re-rendering.
This mount-point compares the current version of your list with the new one on every change and applies the minimum
necessary patches to the DOM. Assuming only one item changes, the untouched items can remain in the DOM,
and only the changed item's DOM subtree needs to be deleted and recreated. As you can imagine, this is a game changer
concerning performance.
This renderEach
implementation does the patch-determination by applying the standard equals
-method of the list's
type T
. This is the reason why it is targeted to value objects - they are implicitly defined by their equality.
Have a look at its usage in our master detail example.
Since a List<T>
as value of a store is a common use case, T
being an entity with a stable identity, fritz2
offers a specialized renderEach
-function for it.
When dealing with entities, you can pass an additional parameter called idProvider
. This expression takes
one item T
and determines its stable identity. Armed with this information, the internal patch-determination is then
simply based upon the id and not upon the equals
-method anymore.
// define some person-entities and store them
val persons = listOf(
Person(1, "Fritz", 42),
Person(2, "Caesar", 66),
Person(3, "Cleopatra", 50)
)
render {
val storedPersons = storeOf(persons)
ul {
storedPersons.data.renderEach(Person::id) { person ->
// ^^^^^^^^^^
// provide a function to determine the stable identity of one `T`
li {
dl {
dt { +"Id" }
dd { +person.id.toString() }
dt { +"Name" }
dd { +person.name }
dt { +"Age" }
dd { +person.age.toString() }
}
}
}
}
}
As the identity is stable, the following properties hold for the rendered items:
The latter is an important aspect to consider before the use of renderEach
for entities. If you still need to reflect
changes to the data within an entity and want to apply precise rendering, use this application of renderEach
,
but add additional mount-points inside the elements subtrees. You will learn about those in the
chapter about store mapping.
Otherwise, it might be a better choice to stick to the default renderEach
application relying on equality.
Have a look at its application in our todomvc example.
The class
attribute of a Tag
for working with CSS style-classes is somewhat special. You can set the static values
of each Tag
for class
and id
by using the optional parameters of its factory function:
render {
div("some-static-css-class") {
button(id = "someId")
}
}
Use this one-liner to add styling and meaning to your elements by using semantic CSS class-names. Also, it keeps your code clean when using CSS frameworks like Bootstrap, Tailwind, etc.
Additionally, the className
-function can be used to set static classes:
render {
div {
className("some-static-css-class")
button(id = "someId")
}
}
To reactively change the styling of a rendered element, you can add dynamic classes by assigning a Flow
of strings to
the className
-attribute (like with any other attribute).
render {
val enabled = storeOf(true)
div("common-css-class") {
className(enabled.data.map {
if (it) "enabled-css-class"
else "disabled-css-class"
})
+"Some important content"
}
}
You can also combine all of these approaches with arbitrary usages of className
-variants to handle different aspects
separately:
render {
val enabled = storeOf(true)
val readonly = storeOf(true)
div("common-css-class") {
className("some-other-static-class")
className(enabled.data.map {
if (it) "enabled-css-class"
else "disabled-css-class"
})
className(readonly.data.map { if(it) "readonly-csss-class" else "" })
+"Some important content"
}
}
Keep in mind that the first value of a Flow
might take some time to be consumed and applied to the class
attribute of a tag. This could lead to flicker effects, for example with floating elements which only become
visible when activated. The following section explains how to overcome these effets.
In order to avoid flicker effects caused by the delay of the first value becoming available on the flow, an initial value must be provided to be applied immediately within the rendering process.
The previously discussed className
-function offers an initial
parameter:
fun className(value: Flow<String>, initial: String): Unit
For single classes or short class name groups, simply pass the appropriate initial classnames as second parameter:
render {
val enabled = storeOf(true)
div("common-css-class") {
className(enabled.data.map {
if (it) "enabled-css-class"
else "disabled-css-class"
},
initial = "enabled-css-class"
)
+"Some important content"
}
}
For simple use cases this is fine, as long as the classnames for the initial state remain unchanged.
Otherwise the initial = "..."
-parameter would also have to be changed, which can easily be overlooked.
That's why there is another className
-function variant which might be better suited for more complex or volatile
initial class name values:
fun <T> className(value: Flow<T>, initial: T, transform: (T) -> String): Unit
This function takes three parameters in order to solve the above problem:
value
is a Flow
which provides arbitrary values of T
. This can be a simple Flow<Boolean>
, but also
combinations or any other type can be provided.initial
: a value of T
representing the initial state to be applied immediately.transform
: a lambda expression which uses one value of T
in order to generate the appropriate class names for
this specific value.The above problem is now solved by this function as the transform
-expression is the single source of truth of all
class names. First, the initial
-parameter is passed to the transform
-expression to create the initial class names
which are immediately applied. Further on, each value appearing on the value
-Flow
will be used with transform
to create the appropriate class names.
render {
val enabled = storeOf(true)
div("common-css-class") {
className(enabled.data, initial = true) {
if (it) "enabled-css-class"
else "disabled-css-class"
}
+"Some important content"
}
}
CSS classes strings can become very long and may exceed the limit of a code line. This is especially true when working with utility based CSS frameworks like tailwindcss.
For such situations you need to split up your string into feasible, shorter parts. For this use-case fritz2 offers
the dedicated function joinClasses(vararg classes: String?): String
. This functions accepts an arbitrary
amount of String
s and concatenates them together taking care of the needed white spaces:
// example uses tailwindcss classes
render {
div(
joinClasses(
"relative z-10 flex justify-between items-start w-full my-2 p-4",
"bg-primary-800 rounded-lg hover:bg-primary-900",
"text-left text-white",
"focus:outline-none focus:ring-4 focus:ring-primary-600",
)
) {
// some content
}
}
Using this function, it is also possible to conditionally construct classes strings without having to do dangerous string concatenation:
val someState = ...
val classes = joinClasses(
"class1",
"class2".takeIf { it.length > 10 },
when(someState) {
something.A -> "class3 class4 class5"
something.B -> "class5"
else -> "class6 class7"
}
)
You may be tempted to use Kotlin's default multiline strings, which is generally speaking totally ok. But be aware, that you have to deal with the correct handling of separating white spaces by your own! This can be quite error-prone though:
"""grid grid-rows-3 grid-cols-[auto_1fr] gap-1 p-4
| text-base font-sans cursor-pointer rounded-md
|focus:outline-none focus-visible:ring-4 focus-visible:ring-primary-600""".trimMargin()
// ^
// Missing Space -> would lead to class with name `rounded-mdfocus:outline-none` which will
// lead to an error in appearance and might be hard to identify
A better approach could be the joinToString
-Method of collection types, but it is unlikely that you already have
those classes inside some collection, so you would have to create some collection in place first, which is quite
expensive and ineffective.
To create rich HTML interfaces, styling alone is not sufficient - you will need to use a variety of attributes. In fritz2 there are several easy ways to do this, depending on your use case.
You can set all HTML attributes inside the Tag
's content by calling a function of the according name. Every standard
HTML attribute has two functions. One sets a static value every time the element is re-rendered, the second collects
dynamic data coming from a Flow
. When coming from a Flow
, the attribute's value will be updated in the
DOM whenever a new value appears on the Flow
, no re-rendering required:
val flowOfInts = ... // i.e. get it from a store
render {
input {
placeholder("a text")
maxLength(flowOfInts)
disabled(true)
}
}
If you want to set a Boolean
value, you can use the optional parameter trueValue
which will be set as the
attribute-value when your data is true
:
val isLow = myStore.data.map { i -> i <= 0 }
render {
button {
+"My button"
attr("data-low", isLow, trueValue = "true")
// isLow == true -> <button data-low="true">My button</button>
// isLow == false -> <button>My button</button>
}
}
This is sometimes needed for CSS-selection or animations.
The className
function you have already encountered is just another way to set a common attribute on a tag.
Internally, it offers more convenience variants and works a bit differently, due to its overall importance.
To set a value for a custom (data-) attribute, use the attr()
-function. It works for static and dynamic (from
a Flow
) values:
render {
div {
attr("data-something", "value")
attr("data-something", flowOf("value"))
}
}
Sometimes it is important for an attribute to only appear if a certain condition is true
. For example, some
ARIA properties like
aria-controls should
preferably appear only if the dependent element exist. The attr
functions for Flows
behave in such a way - they
only set an attribute if the value is not null
. This behaviour can be used to achieve the desired effect:
render {
val isOpened = storeOf(true)
button {
+"Toggle"
clicks handledBy isOpened.handle { !it }
attr("aria-controls", isOpened.data.map { if (it) "disclosure" else null })
// ^^^^
// attribute disappears if disclosure-div is not rendered
}
isOpened.data.render {
if (it) {
div(id = "disclosure") {
+"I am open!"
}
}
}
}
As the first value of a Flow
might appear some time after the DOM portion of some tag is already rendered, it is
possible that the initial value of an attribute is not set or set with the wrong value. This could lead to
further unwanted side effects if other behaviour or rendering is derived by those attributes. Think of a falsely
enabled disabled
attribute of an input
element, for example.
To immediately set any attribute, the respective attribute-method is simply called twice: First with the
static value to be set immediately, then with the Flow
providing the dynamic values:
val disabled: Flow<Boolean> = ...
attr("disabled", "false")
attr("disabled", disabled) // the first value of the `Flow` will override the static value set before.
In order to improve performance and memory-footprint, you should always try to keep the reactive parts of your
UI as small as possible. This can be achieved by putting the render*
-functions as close to the dynamic subtree
as possible.
Let's recap the first reactive rendering example:
render {
val storedPerson = storeOf(Person(1, "Fritz", 42))
storedPerson.data.render { person ->
dl {
dt { +"Id" }
dd { +person.id.toString() }
dt { +"Name" }
dd { +person.name }
dt { +"Age" }
dd { +person.age.toString() }
}
}
}
This code will recreate the whole dl
-block when the storedPerson
state changes, even if only one of its properties
changes.
Let's analyze how this object might change by looking at each property:
id
: this must be stable by definition, so it will never change.name
: name changes are very unlikely.age
: this is in fact the only regularly changing property - each passing year it must be increased.So we only need to react to changes of one portion of the Person
model: the age
-property.
The following example uses some concepts that have not yet been explained.
The function of handlers and custom data-flows are explained in the upcoming chapter Store Creation. We do need to tease these functions here in order to show a working example. Please accept their function without a deeper understanding of those techniques for now. It's sufficient to grasp the general concept.
The age being the only attribute likely to change lets us put the reactive rendering code much closer to the corresponding UI-elements. As a result, the whole dynamic subtree will become much smaller. This will improve performance and reduce memory-usage:
// define a small helper type to hold all static parts of a `Person`
data class StaticPerson(val id: Int, val name: String)
val storedPerson = object : RootStore<Person>(Person(1, "Fritz", 42), job = Job()) {
// collect all static properties of the person into the helper class object
val staticPart: Flow<StaticPerson> = data.map { StaticPerson(it.id, it.name) }
// create specific data flow for the `age`-property
val age: Flow<Int> = data.map { it.age }
// For now, just accept that this element will change the store's state.
// The concept of handlers is described in the next chapter "Creating Stores"
val increaseAge = handle { state -> state.copy(age = state.age + 1) }
}
render {
dl {
storedPerson.staticPart.render { person ->
// ^^^^^^^^^^
// the upcoming `render`-function will only react to changes to this data part.
// as this data won't change, this will be rendered only once!
dt { +"Id" }
dd { +person.id.toString() }
dt { +"Name" }
dd { +person.name }
dt { +"Artificial random value to show that this UI part will not react to age changes." }
dd { +Id.next() } // This changes on re-rendering. It won't change in this app though.
}
dt { +"Age" }
dd {
storedPerson.age.renderText()
// ^^^
// Same here: the render function will only react to changes of the `age`-portion
// of some `Person`-object. As this might change quite often, this part of the
// UI will also change in the same manner.
}
}
button {
+"Increase Age"
clicks handledBy storedPerson.increaseAge
}
}
In the above example, the static aspects are exposed via the separate data-flow staticPart
and will be configured
internally by the render
-function. The latter will filter out all values which are equal to their predecessors.
This way, every change to the store's value exposed by its data
-flow will only appear on this flow if and when
some relevant properties have changed.
As the example only allows the changing of the age
-property, which is not part of the static-parts, the staticPart
flow will not emit a new value, so the mount-point will not re-render ist subtree.
On the other hand, the age
-property is exposed by some special data-flow storedPerson.age
. This one will emit a
new value on every change to the main model. The latter is realized by the <button>
-tag below, which will trigger
a handler in the store which increases the Person.age
-property.
As this value is atomic, we want to place the mount-point as deep into the static UI-part as possible. In this case,
directly as text-node inside the <dd>
-tag. This is quite precise, that's why we call this concept
precise rendering.
You can verify the two different behaviours of rendering in the example by clicking the Increase Age-button.
After each click, a new Person
-object is created by the handler. The "Age" data will show the increased value, but
the "Artificial random value" remains the same, which proves that the first mount-point does not get an update.
The above concept applies also to the renderText
-function or mapped stores.
Let's recap the first reactive rendering example:
render {
val storedPerson = storeOf(Person(1, "Fritz", 42))
storedPerson.data.render { person ->
dl {
dt { +"Id" }
dd { +person.id.toString() }
dt { +"Name" }
dd { +person.name }
dt { +"Age" }
dd { +person.age.toString() }
}
}
}
As result the following DOM-fragment is rendered:
<div class="mount-point" data-mount-point="">
<dl>
<dt>Id</dt>
<dd>1</dd>
<dt>Name</dt>
<dd>Fritz</dd>
<dt>Age</dt>
<dd>42</dd>
</dl>
</div>
Since the preliminary <dl>
tag groups all its child UI-elements, the artificially created
<div>
-tag as mount-point reference to the DOM is overhead. We can improve ths DOM structure by telling the
render
-function to use an existing tag as mount-point reference instead of creating a dedicated one:
dl {
// `this` is the <dl>-Tag in this scope
// pass the existing tag *into* `render` to use it as the mount-point reference
storedPerson.data.render(into = this) { person ->
dt { +"Id" }
dd { +person.id.toString() }
// ...
}
}
As a result, the following reduced DOM-fragment is rendered:
<dl data-mount-point="">
<dt>Id</dt>
<dd>1</dd>
<dt>Name</dt>
<dd>Fritz</dd>
<dt>Age</dt>
<dd>42</dd>
</dl>
As you know, the mount-point has full control over the DOM-subtree below its reference tag and will drop it
completely on every change of the data, so be cautious to never ever put other tags around this render
-expression!
It will be deleted sooner or later by the mount-point. The two additional div
s in the following code are located
inside the dl
, which is the this
we put into the render-function, and will thus disappear on re-render.
dl {
div { +"Do not put any elements between a referenced tag and its related `render` function!" }
storedPerson.data.render(into = this) { person ->
dt { +"Id" }
dd { +person.id.toString() }
// ...
}
div { +"Even though this might appear on initial rendering, it will be dropped after first change." }
}
In fact all render*
-variants offer the into
parameter, so this applies to renderText
and renderEach
too.
It's very easy to create a lightweight reusable component with fritz2. All you have to do is write a function
with RenderContext
as its receiver type:
fun RenderContext.myComponent() {
p {
+"This is the smallest valid stateless component."
}
}
render {
myComponent()
}
Of course, you can also use a subtype of RenderContext
, like a Tag
, as receiver if you want to limit the
usage of your component to this parent type.
Using plain functions, it's also straight forward to parametrize your component:
fun RenderContext.myOtherComponent(person: Person) {
p {
+"Hello, my name is ${person.name}!"
}
}
val somePerson = Person(...)
render {
div {
myOtherComponent(somePerson)
}
}
To allow nested components, use a lambda with RenderContext
as its receiver, or the type of the element you are
calling the lambda in:
// return an HTML element if you need it
fun RenderContext.container(content: RenderContext.() -> Unit) {
div("container") {
content()
}
}
render {
container {
p {
+"Hello World!"
}
}
}
Using Div
as receiver type in the example above allows you to access the specific attributes and events of your
container-element from your content-lambda. Use RenderContext
where this is not necessary or intended.
fritz2 also offers a function for setting the inline style
attribute to your elements:
render {
p {
inlineStyle("color: red")
+"this is red text"
}
}
Of course, it is also possible to dynamically style an element by passing a Flow
of CSS styles to inlineStyle
:
render {
val enabled = storeOf(true)
div {
inlineStyle(enabled.data.map {
if (it) "background-color: lightgreen;"
else "opacity: 0.5; background-color: lightgrey;"
})
+"Important content"
}
}
fritz2 offers the use of Scope
s to add more information to a tag. It can then be received by any
child-tag of the corresponding DOM-subtree and will not be rendered out by default. The values in the Scope
are
only available for tags inside the context of the tag which sets them.
To append something to the Scope
, you have to create a Scope.Key
by using the
Scope.keyOf()
function.
val myKey = Scope.keyOf<String>("myKeyName")
The key is needed to set or get a value of the Scope
.
val fooKey = Scope.keyOf<String>("foo")
render {
div(scope = {
set(fooKey, "bar")
}) {
div {
// div is child of scope owner, so key is accessible
+(scope[fooKey] ?: "")
}
}
div {
// this div is not a child, so key is not in its (empty) scope
+(scope[fooKey] ?: "")
}
}
The result is the following:
<div>
<div>bar</div>
</div>
<div></div>
For debugging purposes, you can use the scope.asDataAttr()
function to set the current scope to the tag and see it
in the DOM-Tree.
As you already know, you need to call the global render
function once in order to create an initial
RenderContext
:
fun main() {
render {
// access to the created root `RenderContext`
// start your UI code from within here
}
}
This of course only works in combination with a matching index.html
in the jsMain/resources
-folder, which is just a
normal web-page. To set it up correctly,
document.body
tag is used.<script>
tag beneath the static HTML.<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<meta content="width=device-width, initial-scale=1" name="viewport">
</head>
<body>
<div id="myAppAnchor">
Loading...
</div>
<script src="<project-name>.js"></script>
</body>
</html>
The global render
factory accepts a
selector
string (see querySelector),
or alternatively a HTMLElement,
to select the target HTML-tag:
fun main() {
render("#myAppAnchor") { // using id selector here, leave blank to use document.body by default
h1 { +"My App" }
div("fix-css-class") {
p(id = "someId") {
+"Hello World!"
}
}
}
}
When calling render
like that, your content will be mounted to an HTMLElement
with id="myAppAnchor"
.
If you want to mount your content to the body
of your index.html
instead, you can omit this parameter.
The second option is to set the override
parameter to false
, which means that your content will be appended.
By default, all child elements will be removed before your content is appended to the target HTML-tag.
Run the project by calling ./gradlew jsRun
in your project's main directory. Add -t
to enable automatic
building and reloading in the browser after changing your code.
fritz2 also lets you manage multiple classes in a List<String>
or Flow<List<String>>
with the classList
-attribute.
Additionally, you can build a Map<String, Boolean>
from your model data which enables and disables single classes
dynamically:
render {
div {
classMap(toDoStore.data.map {
mapOf(
"completed" to it.completed, // a boolean-attribute in the data-model
"editing" to it.editing
)
})
}
}
Documentation coming soon
Documentation coming soon