Bot releases are visible (Hide)
Published by over 2 years ago
Authentication.current
property returns the current principal also when pre-setting it before any process is startedfiles
component for multi file upload. It has been accidentally deactivated during upgrade to Kotlin 1.6.0.Published by over 2 years ago
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!
Published by almost 3 years ago
aarch64
(see JEP 391). Starting with Java 17 all major distributors of JDKs should support this.build.gradle.kts
:
rootProject.plugins.withType<org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin> {
rootProject.the<org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension>().nodeVersion = "16.0.0"
}
This is already included in the current version of our fritz2-template project.RenderContext
is now an Interface offering all functions to render both dynamic and static contentTag
is an implementation of this interface which adds everything needed to handle a DOM-elementTagContext
has been removedrender*
-methods now by default add a new Div
-Tag into the DOM which acts as the mountpoint. Those div
s are marked with the attribute data-mount-point
and the static default class mount-point
. The latter simply sets the display: contents
rule in order to hide this element from the visual rendering:
data.render {
// render something
}
will result in the following HTML structure:
<div class="mount-point" data-mount-point=""
<!-- your dynamic content -->
</div>
render*
-methods in RenderContext
now offer a new parameter into
which can be used to bypass the creation of a default parent Div
-tag as described above. If there is already an element which could serve as mountpoint, simply pass its Tag<HTMLElement>
as the into
parameter. Rule of thumb: Just pass this
, since the call to render
should appear directly below the desired parent element in most cases:
ul { // this == Tag<Ul>
items.data.render(into = this) {
// ^^^^^^^^^^^
// pass the parent tag to use it as mount-point
li { +it }
}
}
This results in the following DOM:
<ul data-mount-point=""> // no more CSS-class, as the tag *should* appear on screen in most cases
<li>...</li>
...
<li>...</li>
</ul>
But beware: the mount-point controls its siblings, meaning that all other content will be removed by every update of the flow. So never apply the into = this
pattern if you want to render multiple different flows into a parent, or a mixture of dynamic and static content.asText
use renderText
. It will create a Span
-Tag as parent element and a child text-node for the text. Analogous to the render*
-methods of RenderContext
, it also accept the into = this
parameter, which directly renders the text-node into the parent provided without creating the extra <span>
.Lenses are still automatically created by the @Lenses
annotation for data classes, but a companion object must be declared even though it might be left empty. The new processor now supports generic data classes as a new feature.
The API for accessing a lens has changed:
L
-object holding all lenses for all domain types anymoreFor accessing a lens, consider the following example:
// somewhere in commonMain
@Lenses
data class Language(
val name: String,
val supportsFP: Boolean
) {
companion object // important to declare - KSP can't create this
}
// accessing code - could also be in jsMain
val language = storeOf(Language("Kotlin", true))
// old: val name = kotlin.sub(L.Language.name)
val name = language.sub(Language.name())
The lenses are created as extension functions of the companion object, so no dedicated object is needed. We believe this to be more comfortable to work with: When using a lens, the domain type is already present, so this should be intuitive. The L
-object wasn't too complex either, but it seemed a bit "magical" to new users, and a bit artificially named.
To get the lens, just call the function named exactly like the corresponding property.
This introduces one restriction to the design of a custom implemented companion object: You are not allowed to implement such a function yourself. The name is required by the processor and defiance will lead to an expressive compilation error.
The following changes must be applied to the build.gradle.kts
plugins {
// Kotlin 1.6.x version
kotlin("multiplatform") version "1.6.10"
// Add KSP support
id("com.google.devtools.ksp") version "1.6.10-1.0.2"
// Remove fritz2-plugin
}
// Add further settings for KSP support:
dependencies {
add("kspMetadata", "dev.fritz2:lenses-annotation-processor:$fritz2Version")
}
kotlin.sourceSets.commonMain { kotlin.srcDir("build/generated/ksp/commonMain/kotlin") }
tasks.withType<org.jetbrains.kotlin.gradle.dsl.KotlinCompile<*>>().all {
if (name != "kspKotlinMetadata") dependsOn("kspKotlinMetadata")
}
// Needed to work on Apple Silicon. Should be fixed by 1.6.20 (https://youtrack.jetbrains.com/issue/KT-49109#focus=Comments-27-5259190.0-0)
rootProject.plugins.withType<org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin> {
rootProject.the<org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension>().nodeVersion = "16.0.0"
}
Migrating the code can be done quite easily and reliably with simple regular expressions via search and replace:
L
-object:
^import .*\.L$
L\.([\w\.]+)
(with activated case sensitivity!)$1\(\)
There is one trap you don't want to step in when replacing via regexp: If you have a receiver that is or ends with a big "L", this will be mistakenly removed:
class Foo<L> {
fun L.doSomething() = ... // The regexp will change this line as well
}
There could be more false positives we have not encountered yet, so watch out for compiler errors after applying those regexps.
Internally, a Router
is now a Store
, so you can use everything a Store
offers and create your own Handler
s for your Router
instance (compared to RootStore
):
object MyRouter : MapRouter(mapOf("page" to "overview")) {
val overview = handle {
it + ("page" to "overview")
}
val details = handle<String> { route, id ->
route + mapOf("page" to "details", "detailsId" to id)
}
}
// Navigate to overview page
clicks handledBy MyRouter.overview
// Navigate to details page with detailsId=12
clicks.map { "12" } handledBy MyRouter.details
This PR improves the handling of shortcuts when dealing with KeyboardEvent
s:
Key
-classKey
-class named Shortcut
based upon a new concept of Modifier
-interface which enables constructing shortcuts, e.g. "Strg + K" or "Shift + Alt + F".Keys
-object with predefined common keys like Tab
, Enter
, also add the modifier keys like Alt
, Shift
, Meta
and Control
key()
as deprecated in favor of relying on standard flow functions like filter
or map
.The new shortcut API allows easy combination of shortcuts with modifier shortcuts, constructing those from a KeyboardEvent
, and also prevents meaningless combinations of different shortcuts:
// Constructing a shortcut by hand
Shortcut("K")
// -> Shortcut(key = "K", ctrl = false, alt = false, shift = false, meta = false)
// Or use factory function:
shortcutOf("K")
// Set modifier states, need to use constructor:
Shortcut("K", ctrl = true) // Shortcut(key= "K", ctrl = true, alt = false, shift = false, meta = false)
// Constructing a shortcut from a KeyboardEvent
div {
keydowns.map { shortcutOf(it) } handledBy { /* use shortcut-object for further processing */ }
// ^^
// use KeyboardEvent to construct a Shortycut-object with all potentially
// modifier key states reflected!
}
// Using predefined shortcuts from Keys object
Keys.Enter // named-key for the enter key stroke, is a `Shortcut`
Keys.Alt // `ModifierShortcut` -> needs to be combined with a "real" shortcut in order to use it for further processing
// The same but more cumbersome and prone to typos
Shortcut("Enter")
// Not the same (!)
Shortcut("Alt") // -> Shortcut(key= "Alt", ..., alt = false)
Keys.Alt // -> ModifierKey-object with alt = true property!
// Constructing a shortcut with some modifier shortcuts
Shortcut("K") + Keys.Control
// Same result, but much more readable the other way round:
Keys.Control + "K"
// Defining some common combination:
val searchKey = Keys.Control + Keys.Shift + "F"
// ^^^^^^^^^^^^
// You can start with a modifier shortcut.
// Appending a String to a ModifierKey will finally lead to a `Shortcut`.
val tabbing = setOf(Keys.Tab, Keys.Shift + Keys.Tab)
// API prevents accidently usage: WON'T COMPILE because real shortcuts can't be combined
Shortcut("F") + Shortcut("P")
// Shortcut is a data class β equality is total:
Keys.Control + Keys.Shift + "K" == Shortcut("K", shift = true, ctrl= true, alt = false, meta = false)
// But
Keys.Control + Keys.Shift + "K" != Shortcut("K", shift = false, ctrl= true, alt = false, meta = false)
// ^^^^^^^^^^ ^^^^^^^^^^^^^
// +-----------------------------------+
// Case sensitive, too. Further impact is explained in next section.
shortcutOf("k") != shortcutOf("K")
Be aware of the fact that the key
-property is taken from the event as it is. This is important for all upper case keys: The browser will always send an event with shift
-property set to true
, so in order to match it, you must construct the matching shortcut with the Shift
-Modifier:
// Goal: Match upper case "K" (or to be more precise: "Shift + K")
keydowns.events.filter { shortcutOf(it) == shortcutOf("K") } handledBy { /* ... */ }
// ^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^
// | Shortcut(key = "K", shift = false, ...)
// | ^^^^^^^^^^^^
// | +-> will never match the event based shortcut!
// | | the modifier for shift needs to be added!
// Shortcut("K", shift = true, ...)--+
// upper case "K" is (almost) always send with enabled shift modifier!
// Working example
keydowns.events.filter { shortcutOf(it) == Keys.Shift + "K" } handledBy { /* ... */ }
Since most of the time you will be using the keys within the handling of some KeyboardEvent
, there are some common patterns relying on the standard Flow
functions like filter
, map
or mapNotNull
to apply:
// All examples are located within some Tag<*> or WithDomNode<*>
// Pattern #1: Only execute on specific shortcut:
keydowns.events.filter { shortcutOf(it) == Keys.Shift + "K"}.map { /* further processing if needed */ } handledBy { /* ... */ }
// Variant of #1: Only execute the same for a set of shortcuts:
keydowns.events.filter { setOf(Keys.Enter, Keys.Space).contains(shortcutOf(it)) }.map { /* further processing if needed */ } handledBy { /* ... */ }
// Pattern #2: Handle a group of shortcuts with similar tasks (navigation for example)
keydowns.events.mapNotNull{ event -> // better name "it" in order to reuse it
when (shortcutOf(event)) {
Keys.ArrowDown -> // create / modify something to be handled
Keys.ArrowUp -> //
Keys.Home -> //
Keys.End -> //
else -> null // all other key presses should be ignored, so return null to stop flow processing!
}.also { if(it != null) event.preventDefault() // page itself should not scroll up or down! }
// ^^^^^^^^^^
// Only if a shortcut was matched
} handledBy { /* ... */ }
(The final result is based upon the PR #565 too)
In the fritz2 http
API you can now add Middleware
s which have the following definition:
interface Middleware {
suspend fun enrichRequest(request: Request): Request
suspend fun handleResponse(response: Response): Response
}
You can add a Middleware
to all your http calls by using the new use(middleware: Middleware)
function.
val logging = object : Middleware {
override suspend fun enrichRequest(request: Request): Request {
console.log("Doing request: $request")
return request
}
override suspend fun handleResponse(response: Response): Response {
console.log("Getting response: $response")
return response
}
}
val myAPI = http("/myAPI").use(logging)
...
You can add multiple Middleware
s in one row with .use(mw1, mw2, mw3)
. The enrichRequest
functions will be called from left to right (mw1, mw2, mw3), the handleResponse
functions from right to left (mw3, mw2, mw1). You can stop the processing of a Middleware
's Response
further down the chain with return response.stopPropagation()
.
Also, we built a pre-implemented Authentication
middleware to enrich all request with authentication information and handle bad authentication responses in general:
abstract class Authentication<P> : Middleware {
// List of status codes forcing authentication
open val statusCodesEnforcingAuthentication: List<Int> = listOf(401, 403)
// Add your authentication information to all your request (e.g. append header value)
abstract fun addAuthentication(request: Request, principal: P?): Request
// Start your authentication process (e.g. open up a login modal)
abstract fun authenticate()
val authenticated: Flow<Boolean>
val principal: Flow<P?>
}
For more information have a look at the docs.
The nearest MountPoint
is now available on scope when rendering (some convenience methods to access it). It allows the registration of lifecycle-handlers after mounting and before unmounting a Tag from the DOM:
div {
afterMount(someOptionalPayload) { tag, payload ->
// Do something here
}
beforeUnmount(someOptionalPayload) { tag, payload ->
// Do something here
}
}
This new feature is used for the transition support (see next item).
This PR adds transition-support to fritz2. You now can define a css-transition by the css classes describing the transition itself as well as the start- and endpoint like..
// CSS is tailwindcss (https://tailwindcss.com)
val fade = Transition(
enter = "transition-all duration-1000",
enterStart = "opacity-0",
enterEnd = "opacity-100",
leave = "transition-all ease-out duration-1000",
leaveStart = "opacity-100",
leaveEnd = "opacity-0"
)
..and apply it to a tag:
div {
inlineStyle("margin-top: 10px; width: 200px; height: 200px; background-color: red;").
transition(fade)
}
Recommendation: Do not apply transitions to tags that are also styled dynamically (Flow
based). This might introduce race conditions and therefore unwanted visual effects.
Published by almost 3 years ago
All this was only necessary to enable to mix mount-points with static DOM tags below the same parent node.
β Optimization only for one edge case! β not a good idea!
The new rendering is based upon mount-points, that are always represented by a dedicated tag within the DOM tree! The dynamic content is then rendered below this mount-point-tag. This is true for all render variations, so for render
as well as for renderEach
variants.
To be more precise, there is one div
-tag inserted at the location where the render
method is called:
// within some RenderContext
section {
flowOf("Hello, World!").render {
span { +it }
}
}
This will result in the following DOM structure:
<section>
<div class="mount-point" data-mount-point>
<span>Hello World</span>
</div>
</section>
The CSS class mount-point
makes the div
"invisible" to the client (by display="contents"
), the data attribute data-mount-point
is primarely added to support readability or debugging.
It is worth to emphasize, that this mount-point-tag remains under full control of the framework. So all rendered tags below this tag, will be cleared out every time a new value appears on the flow. So do not try to use or touch this tag or any child from outside of the render
function!
This works similar for dynamic lists:
ul {
flowOf(listOf("fritz2", "react", "vue", "angular")).renderEach {
li { +it }
}
}
Which will result in this DOM structure:
<ul>
<div class="mount-point" data-mount-point>
<li>fritz2</li>
<li>react</li>
<li>vue</li>
<li>angular</li>
</div>
</ul>
If it is absolutely clear that the mount-point will be the only element of some parent tag, then the render methods offer the optional into
parameter, which accepts an existing RenderContext
as anchor for the mount-point. In this case the rendering engine uses the existing parent node as reference for the mount-point:
render {
ul { // `this` is <ul>-tag within this scope
flowOf(listOf("fritz2", "react", "vue", "angular")).renderEach(into = this) {
// ^^^^^^^^^^^
// define parent node as anchor for mounting
li { +it }
}
}
}
This will result in the following DOM structure:
<ul data-mount-point> <!-- No more explicit <div> needed! Data attribute gives hint that tag is a mount-point -->
<li>fritz2</li>
<li>react</li>
<li>vue</li>
<li>angular</li>
</ul>
If you are in a situation where you absolutly have to mix static elements with dynamic (flow based) content within the same DOM level, then the new rendering offers a solution too: Try to integrate the static aspect within a map
expression!
Let's consider the following example sketch we would like to achieve:
<ul>
<!-- static elements within the list items, always on top -->
<li>fritz2</li>
<!-- dynamic content from a flow -->
<li>react</li>
<li>vue</li>
<li>angular</li>
</ul>
The simplest solution would be to just call the renderEach
method directly within the <ul>
context after the static <li>
portions. But this would violate the constraint, that all <li>
tags must appear on the same DOM level (refer to the first example output to see, that an extra <div>
would be insterted after the static portion).
So the correct way is to provide the into = this
parameter in order to lift up the dynamic portion into the surrounding <ul>
tag and to integrate the static portion within the flow by some map
expression:
val frameworks = flowOf(listOf("react", "vue", "angular")) // might be a store in real world applications
ul {
frameworks
.map { listOf("fritz2") + it } // prepend the static part to the dynamic list
.renderEach(into = this) { // do all the rendering in the "dynamic" part of the code
li { +it }
}
}
The result is exactly the same as the scetch from above.
You might habe recognized that the into
parameter could be omitted, if the extra <div>
does not affect the overall structure (in this case all <li>
elements would still remain on the same level within the DOM!).
For the most if not all parts of your application nothing has to be changed!
So first of all please compile and test your application. If there are no compiler errors and the application appears and functions as it did before, do nothing!
There are only few exceptions to this rule:
renderElement
does not exist anymore. You will get compile errors of course, so this is easy to detect. Change all occurrences to render
instead to solve this problem. If needed, apply the next two patterns on top.into = this
parameter at the render functions calls.map
as shown in the recipe section before).StackUp
or LineUp
component with dynamic content, make sure to set the into = this
parameter in order to make the spacing
property work again.For more details have a look at the documentation
Added new easy to use handledBy
functions which have a lambda parameter. Anytime a new value on the Flow
occurs the given function gets called.
render {
button {
+"Click me!"
clicks handledBy {
window.alert("Clicked!")
}
}
}
Outside the HTML DSL you can do the same:
flowOf("Hello World!") handledBy {
window.alert(it)
}
That is why the watch()
function at the end of a Flow
is not needed anymore and is now deprecated.
// before
flowOf("Hello World!").onEach {
window.alert(it)
}.watch()
// now
flowOf("Hello World!") handledBy {
window.alert(it)
}
This way the handling of data in fritz2 (using Flows
s) gets more consistent.
Be aware that per default every Throwable
is caught by those handledBy
functions and a message is printed to the console. The flow gets terminated then, so no more possibly following values will be processed:
flowOf("A", "B", "C").map {
delay(2000)
it
} handledBy {
if (it == "B") error("error in B") // provoke an exception
window.alert(it)
}
// will open one window with "A" and then print a message top the console:
// Object { message: "error in B", cause: undefined, name: "IllegalStateException", stack: "captureStack@webpack-internal:...
The application itself will continue to work, which is the main motivation for the enforced default error handling though!
We encourage you to handle possible exceptions explicitly within the handler code, so the flow will keep on working; at least for the following valid values:
flowOf("A", "B", "C").map {
delay(2000)
it
} handledBy {
try {
if (it == "B") error("error in B")
} catch (e: Exception) { // handle exception within handler -> flow will be further consumed!
}
window.alert(it)
}
// will open three alert windows with all three chars "A", "B" and "C" as expected
merge
function to merge multiple DomListenersThis convenience functions reduces duplicate code for handling different DomListener
s with the same handler:
button {
merge(mouseenters, focuss) handledBy sameHandler
}
This PR enhances the API of the submenu
component's icon defintion. Instead of just apssing some static IconDefinition
it is now possible to pass some Flow<IconDefinition>
too. This enables one to realize an "accordeon style" menu, where the icon reflects the open / close state of the submenu:
// some store to hold the open/close state of a submenu
val toggle = storeOf(false)
menu {
header("Entries")
entry {
text("Basic entry")
}
submenu(value=toggle) {
text("Sub")
// use the state to select the appropriate sub-menu icon
icon(toggle.data.map { if (it) Theme().icons.chevronDown else Theme().icons.chevronLeft })
entry {
text("A")
}
entry {
text("B")
}
entry {
text("C")
}
}
}
It is now possible to craft SVG images much easier within fritz2, its element DSL now supports the <path>
-Tag and the following attributes:
xmlns
fill
viewBox
svg {
// quite common for each SVG declaration, so directly supported
xmlns("http://www.w3.org/2000/svg")
fill("none")
viewBox("0 0 24 24")
// too special, so no explicit wrapping function provided
custom("circle", "opacity-25") {
attr("cx", "12")
attr("cy", "12")
attr("r", "10")
attr("stroke", "currentColor")
attr("stroke-width", "4")
}
path {
attr("fill", "currentColor")
// `d` is explictily supported by `path` tag
d("M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z")
}
}
Published by haukesomm about 3 years ago
sub()
on all Store
sRemoves the RootStore
dependency in the SubStore
, so that SubStore
s can be created by calling sub()
function implemented in Store
interface. Therefore it is now possible to create a SubStore
from every Store
object.
Because one SubStore
generic type gets obsolete, it will break existing code, but it is easy to fix that. Just remove the first not type parameter.
// before
class SubStore<R, P, T> {}
// now
class SubStore<P, T> {}
// before
val addressSub: SubStore<Person, Person, Address> = store.sub(addressLens)
// now
val addressSub: SubStore<Person, Address> = store.sub(addressLens)
direction
-api from RadioGroupComponent
and CheckboxGroupComponent
This PR removes the deprecated direction
-api from RadioGroupComponent
and CheckboxGroupComponent
.
Use orientation instead
.
FormSizeSpecifier
values uppercase, so more Kotlin alikeinit
block, there is now a new private method initRenderStrategies
that produces the mapping and is directly called at beginning of the rendering process. This way no this
pointer is leaked as before. For custom implementations there is now a protected hook function finalizeRenderStrategies
that can be used to extend or change the renderer strategies!If a custom renderer should be applied or a new factory should be added, custom implementation of FormControlComponent
must replace the old registerRenderStrategy
within the init
block by a new mechanism:
// old way, no more possible!
class MyFormControlComponent : FormControlComponent {
Β Β Β init {
Β Β Β Β Β Β Β registerRenderStrategy("radioGroupWithInput", ControlGroupRenderer(this))
Β Β Β }
}
// new way, override `finalizeRenderStrategies` method:
class MyFormControlComponent : FormControlComponent {
Β Β Β // some new factory: `creditCardInput` with key of same name; used with `SingleControlRenderer`
Β Β Β // another new factory: `colorInput` with key of same name and new renderer
Β Β Β override fun finalizeRenderStrategies(
Β Β Β Β Β Β Β strategies: MutableMap<String, ControlRenderer>,
Β Β Β Β Β Β Β single: ControlRenderer,
Β Β Β Β Β Β Β group: ControlRenderer
Β Β Β Β ) {
Β Β Β Β Β Β Β Β // override setup for a built-in factory:
Β Β Β Β Β Β Β Β strategies.put(ControlNames.textArea, MySpecialRendererForTextAreas(this))
Β Β Β Β Β Β Β Β // register new factory
Β Β Β Β Β Β Β Β strategies.put("creditCardInput", single)
Β Β Β Β Β Β Β Β // register new factory with new renderer
Β Β Β Β Β Β Β Β strategies.put("colorInput", ColorInputRenderer(this))
Β Β Β Β }
}
FormSizesAware
has been introducedFormSizes
interface is renamed to FormSizesStyles
If a component supports a size property and relies on the old FormSizes
interface, just rename the receiver type appropriate to `FormSizesStyles``:
// old
val size = ComponentProperty<FormSizes.() -> Style<BasicParams>> { Theme().someComponent.sizes.normal }
// new
val size = ComponentProperty<FormSizesStyles.() -> Style<BasicParams>> { Theme().someComponent.sizes.normal }
//Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β ^^^^^^^^^^^^^^^
//Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β choose new name
label
property of a FormControl now accepts also Flow<String>
. Thus the label can dynamically react to some other state changing.The code for a ControlRenderer
implementation needs to be adapted. Use the following recipe:
class ControlGroupRenderer(private val component: FormControlComponent) : ControlRenderer {
Β Β Β override fun render(/*...*/) {
Β Β Β Β Β Β Β // somewhere the label gets rendered (label / legend or alike)
Β Β Β Β Β Β Β label {
Β Β Β Β Β Β Β Β Β Β Β // old:
Β Β Β Β Β Β Β Β Β Β Β +component.label.value
Β Β Β Β Β Β Β Β Β Β Β // change to:
Β Β Β Β Β Β Β Β Β Β Β component.label.values.asText()
Β Β Β Β Β Β Β }
Β Β Β }
}
value
like as within the main textArea
factory.for
attribute of the label correctly.Until now the close
handler was injected directly into the main configuration scope of a modal. With repsect to EfC 416 this is now changed. The handler only gets injected into the content
property, where it is considered to be used.
Just move the handler parameter from the build
expression of modal into the content
property:
// old
clickButton {
text("Custom Close Button")
} handledBy modal { close -> // injected at top level
content {
clickButton {icon { logOut } } handledBy close
}
}
// new
clickButton {
text("Custom Close Button")
} handledBy modal {
content { close -> // injected only within content
clickButton {icon { logOut } } handledBy close
}
}
ToastComponent
This PR cleans up the ToastComponent
code. This includes:
Theme().toast.base
has been renamed to Theme().toast.body
and might break themes that have custom toast-stylesTooltipComponent
This PR deals with the new TooltipComponent
A tooltip
should be used to display fast information for the user.
The individual text
will be shown on hover the RenderContext
in which be called.
This class offers the following configuration features:
text
can be a vararg
, a flow, a list, a flow of list of String or a simple string, optional can be use the @property textFromParam
.placement
of the text
around the RenderContext
in which be called. Available placements are top
, topStart
, topEnd
, bottom
, bottomStart
, bottomEnd
, left
, leftStart
, leftEnd
, right
, rightStart
, rightEnd
.Example usage:
span {
+"hover me"
tooltip("my Tooltip on right side") {
placement { right }
}
}
span {
+"hover me to see a multiline tooltip"
tooltip("first line", "second line"){}
}
span {
+"hover me for custom colored tooltip"
tooltip({
color { danger.mainContrast }
background {
color { danger.main }
}
}) {
text(listOf("first line", "second line"))
placement { TooltipComponent.PlacementContext.bottomEnd }
}
}
The old Tooltip component is based upon pure CSS. This is fine for basic usage, but it fails when it comes to advanced features like automatic positioning. Also the API does not fit into the "fritz2 component style". That's why there is the need to rework the tooltip.
PopupComponent
The PopupComponent
should be used for to positioning content
like tooltip
or popover
automatically in the right place near a trigger
.
A popup
mainly consists of a trigger
(the Element(s)) which calls the content
.
It can cen configured by
offset
the space (in px) between trigger
and content
flipping
if no space on chosen available it will be find a right placement automaticallyplacement
of the content
around the trigger
The trigger
provides two handler which can be used, the first is important to open/toggle the content
the second close it.
content
provides one handler which can be used to close it.
Example:
popup {
offset(10.0)
flipping(false)
placement { topStart }
trigger { toggle, close ->
span {
+"hover me"
mouseenters.map { it.currentTarget } handledBy toggle
mouseleaves.map { } handledBy close
}
}
content { close ->
div {
+"my content"
clicks.map{ } handledBy close
}
}
}
PaperComponent
and CardComponent
This PR adds a new CardComponent
that behaves similar to the old PopoverComponent
s content.
Component displaying content in a card-like box that can either appear elevated or outlined and scales with the specified size
of the component.
paper {
size { /* small | normal | large */ }
type { /* normal | outline | ghost */ }
content {
// ...
}
}
A component displaying the typical sections of a card inside a PaperComponent
.
The available sections are a header, a footer and the actual content.
card {
size { /* small | normal | large */ }
type { /* normal | outline | ghost */ }
header {
// ...
}
content {
// ...
}
footer {
// ...
}
}
In fritz2 you can now put some arbitrary payload data to every fritz2 html element (for styled elements too) and receive this payload data later (or deeper) in your html tree. This possibility can be use to know in with kind of context your own component get rendered or you can provide some additional information to your context for making decision on styling or rendering.
Example:
enum class Sizes {
SMALL, NORMAL, LARGE;
companion object {
val key = keyOf<Sizes>()
}
}
fun main() {
render {
div {
div(scope = {
set(Sizes.key, Sizes.SMALL)
}) {
section {
scope.asDataAttr()
when (scope[Sizes.key]) {
Sizes.SMALL -> div({ fontSize { small } }) { +"small text" }
Sizes.NORMAL -> div({ fontSize { normal } }) { +"normal text" }
Sizes.LARGE -> div({ fontSize { large } }) { +"large text" }
else -> div { +"no size in scope available" }
}
}
}
p {
scope.asDataAttr()
// scope is context-based and therefore scope is here empty
+"no scope entries here (context-based)"
}
}
}
}
Results in:
TypeAheadComponent
Adds a TypeAhead component to fritz2's component portfolio.
A TypeAhead offers the possibility to input some string and get some list of proposals to choose from. This is reasonable for large static lists, that can't be managed by SelectFields or RadioGroups or where the proposals rely on a remote resource.
Example usage:
val proposals = listOf("Kotlin", "Scala", "Java", "OCaml", "Haskell").asProposals()
val choice = storeOf("")
typeAhead(value = choice, items = proposals) { }
For further details have a look at our KitchenSink
TooltipMixin
and TooltipProperties
to use TooltipComponent
in Components
more easilyWith this PR it beeing easier to integrate a tooltip in a component and the user get a same usage of a tooltip.
appFrame
and added submenu
in menu
componentThe appFrame
component uses now different ColorScheme
s to coloring itself.
The menu
component can now contain submenu
s:
menu {
header("Entries")
entry {
text("Basic entry")
}
divider()
custom {
pushButton {
text("I'm a custom entry")
}
}
submenu {
icon { menu }
text("Sub Menu")
entry {
icon { sun }
text("Entry with icon")
}
entry {
icon { ban }
text("Disabled entry")
disabled(true)
}
}
}
box
in favor of div
This PR deprecates the box
factory method in favor of div
because, in combination with fritz2.styling, it offers the exact same functionality. All calls of box
will have to be replaced eventually.
This PR adds two convenience functions alertToast
and showAlertToast
which can be used to easily create toasts with alert's as their content.
New functions:
fun showAlertToast(
styling: BasicParams.() -> Unit = {},
baseClass: StyleClass = StyleClass.None,
id: String? = null,
prefix: String = "toast-alert",
build: AlertComponent.() -> Unit
)
fun alertToast(
styling: BasicParams.() -> Unit = {},
baseClass: StyleClass = StyleClass.None,
id: String? = null,
prefix: String = "toast-alert",
build: AlertComponent.() -> Unit
): SimpleHandler<Unit>
Example usage:
showAlertToast(buildToast = {
}) {
// toast-properties:
duration(6000)
// setup of the alert
alert {
title("Alert-Toast")
content("Alert in a toast")
severity { /* some severity */ }
variant { leftAccent }
}
}
// 'alertToast' is used similarly and bound to a flow like 'toast'
val alertComponent = AlertComponent()
.apply {
content("...")
stacking { toast }
}
showToast {
// adjust the close-button to match the alert's color-scheme:
closeButtonStyle(Theme().toast.closeButton.close + {
color {
val colorScheme = alertComponent.severity.value(Theme().alert.severities).colorScheme
when(alertComponent.variant.value(AlertComponent.VariantContext)) {
AlertComponent.AlertVariant.SUBTLE -> colorScheme.main
AlertComponent.AlertVariant.TOP_ACCENT -> colorScheme.main
AlertComponent.AlertVariant.LEFT_ACCENT -> colorScheme.main
else -> colorScheme.mainContrast
}
}
})
content {
alertComponent.render(this, styling, baseClass, id, prefix)
}
}
Result:
discreet
to ghost
This PR renames the discreet
alert-variant to ghost
in order to match the common naming scheme of fritz2.
All references of discreet
have to be changed:
alert {
variant { discreet } // <-- previously
variant { ghost } // <-- now
}
radio
, checkbox
and switch
componentsToastComponent
Published by jamowei over 3 years ago
This PR adds the option to use any element or component for the label of the items in a radio- or checkbox-group.
Previously it was only possible to generate Strings from the underlying objects by using the label
property:
val label = ComponentProperty<(item: T) -> String> { it.toString() }
Both components now feature a dedicated labelRendering
property that can be used to override the default rendering process and specify custom layouts.
The labelRendering
property is a lambda that takes each underlying item: T
and renders the respective label.
By default the label is rendered based on the label
property.
// RadioGroupComponent:
val labelRendering = ComponentProperty<Div.(item: T) -> Unit> { /* rendering based on 'label' by default */ }
// CheckboxGroupComponent:
val labelRendering = ComponentProperty<RenderContext.(item: T) -> Unit> { /* rendering based on 'label' by default */ }
Usage example:
checkboxGroup(values = /* ... */, items = /* ... */) {
labelRendering { item ->
span({
// styling
}) {
+item
}
}
}
reset()
function to Validator
Now you can easily clean the list of validation messages by calling the new reset()
function. If you want you can specific a list of validation messages to reset to.
val save = handleAndEmit<Person> { person ->
// only update the list when new person is valid
if (validator.isValid(person, "add")) {
emit(person)
validator.reset() // new reset function
Person()
} else person
}
This PR improves the following aspects of a modal:
width
instead of size
to make the intention of the property more expressive. The predefined values remains the same, but it is also possible now to provide a custom width value like 30rem
or alike. The old API calls will be applied and result in the same visual apperance for now.variant
property, which won't lead into an error, but the functionality is disabled. Instead use the placement
property to align a modal vartically: top
(default), center
, bottom
and stretch
are the possible values.Example:
clickButton {
text("Open")
} handledBy modal({
minHeight { "30rem" }
}) {
width { "800px" }
placement { center }
content {
// some content
}
}
The old API calls for size
and variant
don't break your code, but they will be removed in future versions. So please follow the mrigration guide to recplace them.
Be aware that also the interfaces ModalVariants
and ModalSizes
from the theme are deprecated and will disappear in future versions.
For a complete overview have a look at our KitchenSink.
size
to width
:
modal {
// old:
// size { small }
// new
width { small }
}
variant
to placement
:
auto
β top
verticalFilled
β stretch
centered
β center
modal {
// old:
// variant { verticalFilled }
// new
placement { stretch }
}
Theming migration:
ModalWidths
with the values taken from ModalSizes
.ModalVariants
just disappears. There is no more theme element that replaces this one. For probably adavanced settings applied here, integrate those into ModalStyles
properties base
, width
and internalScrolling
.getElementById()
callid
and prefix
placement in DOM (analog to dropdown
)Icons
in DefaultTheme
track()
function of tracker
safe for unsafe operationsselect.kt
and nav.kt
Published by haukesomm over 3 years ago
Upgrading to Kotlin 1.5.10 version and update dependencies to latest version.
This API change for pushButton
, clickButton
and linkButton
harmonizes the way for setting an icon to it.
// before
pushButton {
type { info }
icon { fromTheme { circleInformation } }
text("Info")
}
// now
pushButton {
type { info }
icon { circleInformation }
text("Info")
}
In most cases it is enough to remove the fromTheme()
function.
Components which need a z-index an their default level values defined in DefaultTheme
:
tableHeader (10)
tooltip (100)
dropdown (200)
popover (300)
appFrame (1000)
navbar (1000) -- will be replaced by appFrame
toast (2000)
modal (3000)
New component to build menus consisting of clickable entries, headers, dividers and other items.
menu {
header("Menu")
entry {
text("Clickable item")
icon { notification }
disabled(false)
events {
// standard events context
}
}
divider()
header("Other")
custom {
pushButton {
text("I'm a custom entry")
}
}
}
New component to display dropdown-content floating around a toggle-element, similar to a popover.
dropdown {
toggle {
pushButton {
text("Toggle")
}
}
placement { bottom }
alignment { start }
content {
// ...
}
}
OrientationMixin
for componentsorientation
API, old direction
marked as deprecatedorientation
API, old direction
marked as deprecateddirection
β orientation
row
β horizontal
column
β vertical
// old one with deprecation warning as:
// 'direction: ComponentProperty<CheckboxGroupComponent.DirectionContext.() -> CheckboxGroupComponent.Direction>' is deprecated. Use orientation instead
// 'row: CheckboxGroupComponent.Direction' is deprecated. Use orientation { horizontal } instead
checkboxGroup(values = someStore, items = someItems) {
direction { row }
}
// change like this
checkboxGroup(values = someStore, items = someItems) {
orientation { horizontal }
}
Published by jamowei over 3 years ago
linkButton
component
linkButton {
text("Show me awesome")
href("www.fritz2.dev")
target("_blank")
}
slider
component
val valueStore = storeOf(100)
slider(value = valueStore) {}
menu
component
menu(demoMenuStyle) {
header("Items")
entry {
text("Basic item")
}
divider()
entry {
icon { sun }
text("Item with icon")
}
}
Inter
fontinspectEach()
for Inspector on collectionspublishing.gradle.kts
alert
APIappFrame
componentPublished by WestHuus over 3 years ago
The following things has been changed:
KeyboardEvent.key
instead of deprecated KeyboardEvent.keyCode
for comparisonKeys.Alt
, Keys.Enter
, Keys.Esc
)Key
by calling the constructor with the given KeyboardEvent
equals()
method which compares two Key
s by Key.key
attribute.toString()
returns the key
attributeinput {
keydowns.key().map {
when (it) {
Keys.Enter -> //...
Keys.Escape -> //...
else -> //..
}
} handledBy store.myHandler
}
Swapping the implementations of BorderContext.horizontal(...) and BorderContext.vertical(...) so the borders created by them follow the semantics of the respective method names.
This means: horizontal now creates borders at the top and at the bottom of an element and vertical now creates borders to the left and right of an element.
ColorScheme
got a function named inverted()
it returns a ColorScheme
which switches base
with highlight
and baseContrast
with highlightContrast
. A common use case might be a creation of inverted theme.info
, success
, warning
, danger
and neutral
changed to ColorScheme
color
of our button
component refactors to type
and based on the ColorScheme
approach now.primary
, secondary
, info
, success
, warning
, danger
but you can use any other ColorScheme
.
// Until 0.9.x
clickButton {
text("danger")
color { danger }
}
// New
clickButton {
text("danger")
type { danger }
}
clickButton {
text("custom")
type { ColorScheme("#00A848", "#2D3748", "#E14F2A", "#2D3748") }
}
button
schemes independently of the theme ColorSchemes
// inside your custom theme implementation
override val button = object : PushButtonStyles {
override val types: PushButtonTypes = object : PushButtonTypes {
override val primary
get() = ColorScheme(
main = "#00A848",
mainContrast = "#2D3748",
highlight = "#E14F2A",
highlightContrast = "#2D3748"
)
//...
}
}
inputField
and textArea
now use background
and font
color of Colors
interface as default.alert
component got a rework by ColorScheme
too. Calling the component is the same as before, with the addition that a ColorScheme
can now also be passed into it.
alert {
content("Severity: success")
severity { success }
}
alert {
content("Severity: custom")
severity {
ColorScheme("#00A848", "#2D3748", "#E14F2A", "#2D3748")
}
}
ColorScheme
Improves up the AlertComponent API by allowing more flexible customization via the AlertSeverity
-style of the theme. Alerts mirroring the severity of a ComponentValidationMessage
can still be created via the ComponentValidationMessage.asAlert()
extension method. This PR also contains a lot of code cleanup in general.
Alerts now also utilize the new ColorScheme
approach for color shades and typography.
The actual usage of the AlertComponent
does not change - the underlying styling within the theme does, however.
This means if you are using a custom theme you need to update it in order to implement the altered AlertStyles
interface. In case you did not do any modifications to the AlertComponent
'-theme your app should continue to work as intended.
AlertSeverity
interface now includes support for the new ColorScheme
class:
interface AlertSeverity {
val colorScheme: ColorScheme
val icon: IconDefinition
}
AlertVariants
now contains regular styles that are no longer split into styles for the background, text, icons, etc.
interface AlertVariants {
val subtle: BasicParams.(AlertSeverity) -> Unit
val solid: BasicParams.(AlertSeverity) -> Unit
val leftAccent: BasicParams.(AlertSeverity) -> Unit
val topAccent: BasicParams.(AlertSeverity) -> Unit
val discreet: BasicParams.(AlertSeverity) -> Unit
}
AlertVariantStyles
interfaceColors
interfaceMove backgroundColor and fontColor from the Theme
interface to the Colors
interface.
// before
Theme().fontColor
Theme().backgroundColor
// now
Theme().colors.font
Theme().colors.background
// or in color styling DSL
{
background {
color {
font //or background
}
}
}
New API for using the fritz2 styling DSL on standard HTML Tag
s.
Important: add the following import to get the new extension functions: import dev.fritz2.styling.*
The currently approach gets deprecated with v0.10 and will be removed in next major version. So please migrate to the new version as shown below.
// Until 0.9.x
render {
val textColor = style { color { "yellow" } }
(::div.styled(baseClass = textColor, id = "hello", prefix = "hello") {
background { color { "red" } }
}) {
+"Hello World!"
}
}
// New
// don't forget the import dev.fritz2.styling.* !!!
render {
val textColor = style { color { "yellow" } }
div({
background { color { "red" } }
}, baseClass = textColor, id = "hello", prefix = "hello") {
+"Hello World!"
}
}
The DataTable component provides a way to visualize tabular data and offers interaction for the user in order to sort by columns or select specific rows.
The API is designed to scale well from the simplest use case of a read only table, towards a fully interactive table with live editing a cell directly within the table itself.
The component is also very flexible and offers lots of customization possibilities like for:
For a detailed overview have a look at our KitchenSink project.
both
for textArea componentFor a better readability and intelligibility the parameter store refactores to value or rather values.
value is intended for components with a Store<T>
and values is intended for Store<List<T>>
.
// Until 0.9.x
inputField(store = myStore) {}
checkboxGroup(store = myListStore, items = myItems) {}
// New
inputField(value = myStore) {}
checkboxGroup(values = myListStore, items = myItems) {}
Especially the inputField and textArea get a better intelligibility .
// Unitl 0.9.x
inputField(store = myStore) {} // Store variant
inputField { value(myFlow) } // Flow variant
// New
inputField(value= myStore) {} // Store variant
inputField { value(myFlow) } // Flow variant
EventContexts
Published by haukesomm over 3 years ago
Published by over 3 years ago
The approach to specify colors in a theme had some serious issues up to version 0.9:
primary
and primaryEffect
)That's why starting from version 0.9.1 a more expressive approach regarding the semantic and structural side is introduced.
As central concept the class ColorScheme
encapsulates the necessary and strongly related colors as a quadrupel:
open class ColorScheme(
val base: ColorProperty, // background, border, ...
val baseContrast: ColorProperty, // text, icons, ... rendered on ``base``
val highlight: ColorProperty, // instead of base for effects like hovering and so on
val highlightContrast: ColorProperty // text, icons ... rendered on ``highlight``
)
There is also a complete new color slot for a tertiary color scheme and we have enhanced the palette of gray to ten different
predefined shades from gray50
to gray900
instead of six named ones.
Remark: This new concept is not applied to all relevant aspects and components, yet, so there will be adaption towards this quadrupel concept in future releases!
Some color properties have been removed obviously. So here is a short migration guide:
primary
and primaryEffect
put those values into the new primary
quadrupel:
// old:
override val primary = "primaryColor"
override val primaryEffect = "primaryEffectColor"
// new:
override val primary = ColorScheme(
base = "primaryColor",
baseContrast = "someNew" ,
highlight="primaryEffectColor",
highlightContrast = "someNew"
)
secondary
and secondaryEffect
lightestGray, lighterGray, ...
properties to appropriate new gray{number}
properties. Fill the gaps with fitting shades.base
to neutral
dark
property. Choose a fitting gray{number}
or probably some sub-color from primary
, secondary
or tertiary
quadrupel.invoke
function inside the Hander interfaceNo import is needed by calling a unit handler directly anymore. To fix compile issues just remove the obsolete import as follows:
import dev.fritz2.binding.invoke // just delete this
// somewhere
val handler = myStore.handle { model ->
// ...
model
}
// call this ``Unit`` handler directly
handler()
basicInputStyles
of inputField
Component to default themeStylingClass
& Remove trailing space from CSS class namesPublished by jamowei over 3 years ago
This release contains changes that break code written with earlier versions.
Up until fritz2 version 0.8, we offered two global render functions:
render {}: List<Tag<E>>
- create RenderContext
for multiple root elementsrenderElement {}: Tag<E>
- create RenderContext
for single root elementmount()
function.With version 0.9, we simplified this process by supplying one global render {}
function which has no return value, but receives an HTMLElement
/ selector
-String to specify your mount-target (which defaults to the body) as first parameter instead. The mount()
function has been removed.
The former global renderElement {}
function has been removed as well, since it's executed exactly once and there were no performance gains in using it. Flow<T>.renderElement {}
can and should of course still be used when you are sure that you render exactly one root element in it's RenderContext
.
See the changes to render functions in the following example code:
// version 0.8
render {
h1 { +"My App" }
div(id = "myDiv") {
store.data.render { value ->
if (value) div { +"on" } else span { +"off" }
}
}
}.mount("target") // mount to ID "target"
// version 0.9
render("#target") { // mount to QuerySelector "#target"
h1 { +"My App" }
//div(id = "myDiv") { // no need to wrap a Flow before rendering
store.data.render { value ->
if (value) div { +"on" } else span { +"off" }
}
//}
}//.mount("target")
Mounting fritz2-HTML directly to the document.body
works without passing the first parameter to the render function because it's the default. With the additional override
flag, you can specify whether or not you want to overwrite existing HTML content. This value defaults to true.
Note: A QuerySelecor must be used instead of an id
now, which is why you need to start your target string with "#".
We made some key changes to simplify the usage of fritz2 repositories (localstorage and REST). The Resource
class and the ResourceSerializer
interface where combined into the new interface Resource
, which is now the only interface that needs to be implemented to create a repository. We also renamed the (de-)serialization functions:
write
-> serialize
read
-> deserialize
writeList
-> serializeList
- needed only for QueryRepository
readList
-> deserializeList
- needed only for QueryRepository
The following example demonstrates the changes to fritz2 Repositories:
@Lenses
@Serializable
data class ToDo(val id: Long = -1, val text: String = "", val completed: Boolean = false)
// version 0.8
object ToDoSerializer : Serializer<ToDo, String> {
override fun read(msg: String): ToDo = Json.decodeFromString(ToDo.serializer(), msg)
override fun readList(msg: String): List<ToDo> = Json.decodeFromString(ListSerializer(ToDo.serializer()), msg)
override fun write(item: ToDo): String = Json.encodeToString(ToDo.serializer(), item)
override fun writeList(items: List<ToDo>): String = Json.encodeToString(ListSerializer(ToDo.serializer()), items)
}
val toDoResource = Resource(ToDo::id, ToDoSerializer, ToDo())
val query = restQuery<ToDo, Long, Unit>(toDoResource, "/api/todos")
// version 0.9
object ToDoResource : Resource<ToDo, Long> {
override val idProvider: IdProvider<ToDo, Long> = ToDo::id
override fun deserialize(msg: String): ToDo = Json.decodeFromString(ToDo.serializer(), msg)
override fun deserializeList(msg: String): List<ToDo> = Json.decodeFromString(ListSerializer(ToDo.serializer()), msg)
override fun serialize(item: ToDo): String = Json.encodeToString(ToDo.serializer(), item)
override fun serializeList(items: List<ToDo>): String = Json.encodeToString(ListSerializer(ToDo.serializer()), items)
}
val query = restQuery<ToDo, Long, Unit>(ToDoResource, "/api/todos", initialId = -1)
As you can see in this example, it is no longer necessary to supply Resource
with a default instance.
Instead, using a REST-Repository requires you to pass initialId
to allow the repository to derive from a given instance whether a POST or PUT should be sent. This parameter is not needed for localstorage repositories.
For the following fritz2 features we streamlined our API a bit:
msgs
to data
)They now all have a data: Flow<X>
and a current: X
property which gives a dynamic Flow<X>
or the static current value X
of its state.
In order to harmonize our component's API we have changed some fundamental aspects:
All component's properties follow a clear semantic: Just pass a value T
or a Flow<T>
directly as parameter and omit manually wrapping with flowOf
:
// version 0.8
inputField {
value { +"Hello world" }
}
inputField {
value { flowOf("Hello world") }
}
// version 0.9
inputField {
value("Hello World")
}
the base
property for simple form wrapping components like inputField
, pushButton
and so on has been replaced by the element
property.
instead of handling events directly within the component's top level configuration context, there is now an events
context. So events must now be set up therein.
the items for all grouping forms have to be passed now directly as function parameter instead of setting it within the configuration context:
// version 0.8
checkboxGroup {
items { items= listOf("A", "B", "C") }
}
// version 0.9
checkboxGroup(items= listOf("A", "B", "C")) {
}
The following component's are affected by this:
checkboxGroup
radioGroup
selectField
(new component)AppFrame
and navigation-components PR#262
FormControl
PR#272
Validator
PR#242
Published by jwstegemann almost 4 years ago
This release contains changes that break code written with earlier versions. Hopefully these are the last major api-changes prior to fritz2 1.0:
In fritz2 0.8 we decided to use functions to set attribute values instead of vars with delegation.
That way you do not have to wrap constant values in a Flow
anymore. This yields better performance and the const()
-function could be removed. For convenience reasons we also added a new function asString
for Flow
s to
convert a Flow
to a Flow<String>
by calling the toString()
method internally.
input {
type("text") // native
value(myStore.data) // flow
name(otherStore.data.asString()) // otherStore.data is not a Flow of String
}
RenderContext
replaces HtmlElements
We renamed the HtmlElements
interface to RenderContext
, because we think this name better fits the Kotlin DSL approach.
The idea behind it is that every render
function creates a new RenderContext
in which
new Tag
s can be created. This also means that you must replace the receiver type in your custom component-functions accordingly:
val errorStore = storeOf("some text")
// own component
fun RenderContext.errorText(text: String): P {
return p("error") {
+text
}
}
errorStore.data.render { //this: RenderContext
errorText(it)
}
We clarified the creation of TextNodes in Tag
s. Now you use unary +
-operator for constant String
s
to append text at this position to your Tag
. If you have a Flow
, call asText()
instead.
To create a CommentNode, you can use the !
-operator and asComment()
analogous. This intentionally follows a different approach in contrast to the attribute functions so it can be distinguished more easily.
p {
+"Hello "
myStore.data.asText()
!"this is a comment"
myStore.data.asComment()
}
render()
and renderEach()
Using former fritz2-versions you mapped a Flow
of data to a Flow
of Tag
s and created a MountPoint
explicitly by calling bind()
at some place in your rendering. This was error prone. Since nobody would do anything with a Flow<Tag>
other than binding it, all render
functions now implicitly create the mount point and therefore no bind()
is necessary anymore. It has been removed completely.
val myStore = storeOf(listOf("a","b","c"))
render {
ul {
myStore.data.renderEach {
li { +it }
} // no .bind() here anymore
}
}
For performance reasons the render-functions prior to version 0.8 did not allow more than one root-element. In version 0.8 the standard render
allows you to add as many root elements to your context as you want or even none:
val myStore = storeOf(42)
// renders multiple root-elements
myStore.data.render {
repeat(it) {
div { +"one more" }
}
}
// does only render something if value is large enough
myStore.data.render {
if (it > 100) {
div { +"number" }
}
}
If you you do not need this feature (because you know you will always have exactly one root-element) use renderElement()
instead to get (slightly) improved performance.
render()
and renderElement()
now reserve their place in the DOM until the content is rendered by using a temporary placeholder. Since this costs some performance you can disable it when you are sure that there are no sibling-elements on the same level in your DOM-tree by setting renderElement(preserveOrder = false)
. Use this when you have to render lots of elements (in huge lists, tables, etc.).
Instead of someListFlow.each().render {...}.bind()
you now simply write someListFlow.renderEach {...}
. This is analog for all flavors of renderEach
on Store
s and Flow
s with and without an idProvider
.
Please note that renderEach()
still allows only one root-element (like renderElement
)!
Flow<Boolean>
Tracker
now implements Flow<Boolean>
instead of Flow<String?>
so it adopts better to most use-cases. Find an example here.
Published by jwstegemann about 4 years ago
Small patch resolving a memory issue related to coroutine scopes.
Published by jwstegemann about 4 years ago
Just a small patch to be compatible with Kotlin 1.4.0.
No new features or bug fixes included.
Published by jwstegemann about 4 years ago
This release contains changes that break code written with earlier versions:
Handler
s are now suspendable, so you can call suspend-methods directly inside your Handler
. There is no need for Applicator
anymore. Therefore this class and its utility-functions have been removed. (PR#124 & PR#126)FormateStore
and interface Format
have been removed. Use format
-factory-function inside lenses
package to create a formatting Lens
and create a normal SubStore
(by using sub
). (PR#139 & PR#146)val df: DateFormat = DateFormat("yyyy-MM-dd")
// converts a Date into String in vice versa
val dateFormat = format(
parse = { df.parseDate(it) },
format = { df.format(it) }
)
//using the dateLens
val birthday = personStore.sub(L.Person.birthday + dateFormat)
// or
val birthday = personStore.sub(L.Person.birthday).sub(dateFormat)
in commonMain
data class Message(val id: String, val status: Status, val text: String) : ValidationMessage {
override fun isError(): Boolean = status > Status.Valid // renamed from failed() -> isError()
}
object PersonValidator : Validator<Person, Message, String>() {
// return your validation messages here
override fun validate(data: Person, metadata: String): List<Message> {
...
}
}
in jsMain
val personStore = object : RootStore<Person>(Person()) {
// only update when it's valid
val addOrUpdate = handle<Person> { oldPerson, newPerson ->
if (PersonValidator.isValid(newPerson, "update")) new else oldPerson
}
}
...
// then render the validation message list in your html
PersonValidator.msgs.render { msg ->
...
}.bind()
in jvmMain
if (PersonValidator.isValid(newPerson , "add")) {
//e.g. save your new Person to Database
...
} else {
// get the messages, only available after isValid() was called
val msgs = PersonValidator.msgs
...
}
Handler
s (e.g. to show process indicator). (PR#147)Store
s and provide back()
function. (PR#152)Repository
to offer CRUD-functionality for entities and dealing with queries. Implementations are available for REST and LocalStorage (see example). (PR#141, PR#144, PR#155 & PR#153)storeOf()
function to create a minimal RootStore
(without Handler
s) (PR#144)render
on Seq
, so you can directly write each(...).render { ... }
(and leave out map
) (PR#142)render
on Flow
, so you can directly write flow.render { ... }
(and leave out map
) (PR#154)Handler
s (PR#137)append
function to remote (PR#127)IdProvider
to generic type (PR#123)Inspector
(created by inspect()
-function) to navigate through your model in validation and test and have data and corresponding ids available at any point (PR#118)Published by jwstegemann over 4 years ago
This release contains changes that break code written with earlier versions:
WithId
in your model-classes (the interface has been removed from fritz2 entirely). Instead, you need to provide a function which returns the id of a certain instance. This function can be used when calling each
or creating a SubStore
for a certain element (PR#94):// in commonMain
@Lenses
data class Model(val id: String, val value: String)
// in jsMain
val store = RootStore<List<Model>>(listOf(...))
render {
ul {
store.each(Model::id).map { modelStore ->
render {
li { modelStore.sub(L.Model.value).data.bind() }
}
}.bind()
}
}.mount("target")
All of the each
methods (PR#113) were unified:
use Flow<T>.each()
to map each instance of T to your Tag
s. It uses Kotlin's equality function to determine whether or not two elements are the same, and therefore re-renders the whole content you mapped when an element is changed or moved.
with Flow<T>.each(idProvider: (T) -> String)
you can also map each instance of T to your Tag
s, but it uses the given idProvider to determine whether or not two elements are the same. In your mapping, you can get a SubStore
for an element using listStore.sub(id, idProvider)
, so only the parts that actually changed will be re-rendered.
use Store<List<T>>.each()
to map a SubStore<T>
to Tag
s. It uses the list position of the element to determine whether or not two elements are the same. This means that when inserting something into the middle of the list, the changed element AND ALL following elements will be re-rendered.
with Store<List<T>>.each(idProvider: (T) -> String)
you can also map a SubStore<T>
to Tag
s, but it uses the given idProvider to determine whether or not two elements are the same`, so only the parts that actually changed will be re-rendered.
renamed handleAndEmit
to handleAndOffer
(PR#109)
renamed ModelIdRoot
to RootModelId
to follow the naming of stores (PR#96)
+"yourText"
(PR#95)comment("yourText")
or !"yourText"
(PR#108)action
function to dispatch an action at any point in your code (PR#117)value
and checked
attributes (PR#81)MapRouter
to use Map<String,String>
(PR#82)kotlin
-block in gradle build-file (PR#97)bind(preserveOrder = true)
(PR#102)SingleMountPoint
for Boolean
(leaving out the attribute if false) (PR#105)Published by jwstegemann over 4 years ago
This release contains changes, that break code written with earlier versions:
plugins {
id("dev.fritz2.fritz2-gradle") version "0.5"
}
repositories {
jcenter()
}
kotlin {
kotlin {
jvm()
js().browser()
sourceSets {
val commonMain by getting {
dependencies {
implementation(kotlin("stdlib"))
}
}
val jvmMain by getting {
dependencies {
}
}
val jsMain by getting {
dependencies {
}
}
}
}
}
checked
attribute at HTMLInputElement
Published by jwstegemann over 4 years ago
This release contains changes, that break code written with earlier versions:
Tag
s (formerly html
) to render
:render {
div("my-class") {
// ...
}
}
<=
to bind a Flow
of actions or events to a Handler
was definitely not Kotlin-like, so we replaced it by the handledBy
infix-function (please note the reversed order):button("btn btn-primary") {
text("Add a dot")
clicks handledBy store.addADot
}
Kotlin style
dependencies {
implementation("io.fritz2:fritz2-core-js:0.4")
}
Groovy style
dependencies {
implementation 'io.fritz2:fritz2-core-js:0.4'
}
Published by jwstegemann over 4 years ago