fritz2 offers a rich DSL to create the HTML for your application. You just have to call the global render
function
inside to create an initial RenderContext
in which you can then call the HTML-Tag functions provided by fritz2
like div
.
fun main() {
render { // by default target = document.body
div("header") {
}
div("container") {
h1 { +"Hello World!" }
}
div("footer") {
}
}
}
This code results in:
<body id="target">
<div class="header"></div>
<div class="container">
<h1>Hello World!</h1>
</div>
<div class="footer"></div>
</body>
It's very easy to create a lightweight reusable component with fritz2. Basically 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 certain Tag
as receiver if you want to limit the usage
of your component to this type as its parent.
By 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 this lambda in:
// return a html element if you need it
fun RenderContext.container(content: RenderContext.() -> Unit) {
div("container") {
content()
}
}
render {
container {
p {
+"Hello World!"
}
clicks handledBy someHandler // you will see what this does in the next chapter
}
}
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
if this is not necessary or intended.
To create rich html-interfaces you will want to use a variety of attributes. In fritz2 there are several easy ways to achieve 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
without having to re-render the whole element:
val flowOfInts = ... // i.e. get it from some store
render {
input {
placeholder("some text")
maxLength(flowOfInts)
disabled(true)
}
}
If you want to set a Boolean
value, you can set an optional parameter trueValue
which will be set as the
attribute-value if 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.
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", "someValue")
attr("data-something", flowOf("someValue"))
}
}
Sometimes it is important for an attribute to only appear if some 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, that they
only set an attribute if the value is not null
. This behaviour could be used to achieve the desired effect:
val isOpened = storeOf(true)
render {
button {
+"Toggle"
// This mechanism is later explained in "State Management".
// Just accept for now this simply toggles the boolean value in the store by each click.
clicks.map { !isOpened.current } handledBy isOpened.update
attr("aria-controls", isOpened.data.map { if (it) "disclosure" else null })
// ^^^^
// make whole attribute disappear if disclosure-div is not rendered
}
isOpened.data.render {
if (it) {
div(id = "disclosure") {
+"I am open!"
}
}
}
}
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.
To dynamically 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 {
className(enabled.data.map {
if (it) "background-color: lightgreen;"
else "opacity: 0.5; background-color: lightgrey;"
})
+"Some important content"
}
}
The same works for List<String>
s with the classList
-attribute.
Additionally, you can build a Map<String, Boolean>
from your model data that 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
)
})
}
}
fritz2 also offers a function for setting the inline style
attribute to your elements:
render {
p {
inlineStyle("color: red")
+"this is red text"
}
}
fritz2 offers the possibility to use a Scope
to add some information to a tag which can then be received by any
child-tag of the corresponding DOM-subtree and which 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 to 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 proposes you can use the scope.asDataAttr()
function to set current scope to the tag and see it
in the DOM-Tree.