fritz2

Easily build reactive web-apps in Kotlin based on flows and coroutines.

MIT License

Stars
601
Committers
36

Bot releases are visible (Hide)

fritz2 - Version 0.14.2

Published by over 2 years ago

Fixed Bugs

  • PR #571: Authentication.current property returns the current principal also when pre-setting it before any process is started
  • Reactivate the files component for multi file upload. It has been accidentally deactivated during upgrade to Kotlin 1.6.0.
fritz2 - Version 0.14.1

Published by over 2 years ago

Improvements

Improve rendering speed of data table

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!

Other Improvements

  • PR #568: Authentication.complete() function sets the principal also without running auth-process
fritz2 - Version 0.14

Published by almost 3 years ago

Important Notes for Apple Users

  • If you use an Apple computer with Apple silicon architecture please make sure to use a JDK targeted to this architecture. It is officially named aarch64 (see JEP 391). Starting with Java 17 all major distributors of JDKs should support this.
  • You will need the following piece of code in your 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.
    This workaround should be needed only until Kotlin version 1.6.20

Breaking Changes

PR #544: Rework RenderContext

  • RenderContext is now an Interface offering all functions to render both dynamic and static content
  • A Tag is an implementation of this interface which adds everything needed to handle a DOM-element
  • TagContext has been removed
  • render*-methods now by default add a new Div-Tag into the DOM which acts as the mountpoint. Those divs 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.
  • instead of 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>.

PR #559: Apply KSP as substitute for KAPT for automatic lenses generation

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:

  • there is no L-object holding all lenses for all domain types anymore
  • the lens is now created by a factory function instead by a property

For 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.

Migration guide

Tweak buildfile

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"
}
Code Migration

Migrating the code can be done quite easily and reliably with simple regular expressions via search and replace:

  • remove all imports of the L-object:
    • search: ^import .*\.L$
    • replace: by nothing
  • change invocation of the lens:
    • search: L\.([\w\.]+) (with activated case sensitivity!)
    • replace: $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.

Further Cross-functional Constraints

  • fritz2 now requires Kotlin 1.6.10 in order to run
  • fritz2 now supports all Java versions again (up to 17 right now)

PR #543: Router is now a Store

Internally, a Router is now a Store, so you can use everything a Store offers and create your own Handlers 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

Smaller API breaking changes

  • PR #560: Removed deprecated uniqueId() function β†’ use Id.next() instead

New Features

PR #556: Improve Shortcut Handling of KeyboardEvents

This PR improves the handling of shortcuts when dealing with KeyboardEvents:

  • remove Key-class
  • add improved Key-class named Shortcut based upon a new concept of Modifier-interface which enables constructing shortcuts, e.g. "Strg + K" or "Shift + Alt + F".
  • add improved Keys-object with predefined common keys like Tab, Enter, also add the modifier keys like Alt, Shift, Meta and Control
  • mark the special interest functions 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)

PR #558: Add Middleware support to html API + pre-build stateless Authentication middleware

In the fritz2 http API you can now add Middlewares 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 Middlewares 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.

PR #544: Rework RenderContext

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).

PR #545: Transitions (css-animations on DOM manipulation)

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.

Improvements

  • PR #564: Provide annex context for creating elements as direct sibling of a tag

Fixed Bugs

  • PR #557: Removed touchends-event scrolling from appFrame component (fixed jumping screen problem)
  • PR #546: Use a SharedFlow for Web Component attribute changes (Many thanks to @tradeJmark for his great work!)
fritz2 - Version 0.13

Published by almost 3 years ago

Breaking Changes

PR #530: Alternative approach to rendering

Motivation

  • There were four bugs that dealt with rendering problems, so obviously the current solution was not good / mature enough yet:
    • #531
    • #405
    • #346
    • #538
  • The code was rather complex
  • The mechanism itself was rather cumbersome, as there was some placeholder tag rendered into the DOM, which would be replaced by the actual flow evaluated DOM fragement.

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!

New Solution

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>
Recipe: Mixing dynamic and static content within the same level

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!).

Migration Guide

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:

  1. 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.
  2. If the additional mount-point-tag leads to unwanted effects (some styling applied to children won't work for example), just provide the parent tag as mount-point-tag by setting the into = this parameter at the render functions calls.
  3. If there are parts of your application where dynamic and static content absolutely needs to coexist within the same DOM level, strive to include the static portion into the flow (for example by using map as shown in the recipe section before).
  4. If you use our StackUp or LineUp component with dynamic content, make sure to set the into = this parameter in order to make the spacing property work again.

Further Information

For more details have a look at the documentation

New Features

PR #532: Adding new Adhoc-Handler

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 Flowss) 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

PR #529: Add merge function to merge multiple DomListeners

This convenience functions reduces duplicate code for handling different DomListeners with the same handler:

button {
    merge(mouseenters, focuss) handledBy sameHandler
}

PR #528: Add dynamic icons for sub-menus

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")
        }
    }
}

2021-09-28 14_59_24-KitchenSink - fritz2 components demo

PR #522: Expose open/close handlers or an external store for submenu component

PR #519: Add often used attributes for svg and path element

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")
    }
}

Improvements

  • PR #535: Deprecate randomId function from dev.fritz2.components.foundations package
  • PR #525: Improve null value handling for attribute setting
  • PR #539: Remove obsolete watch calls

Fixed Bugs

  • PR #524: Fixed baseClass at Svg and Path
  • PR #523: Fixed not working disabled property in formControl component
  • PR #521: Handle possible ClassCastException
  • Issue #538: Popups do not work in Modals
fritz2 - Version 0.12

Published by haukesomm about 3 years ago

Breaking Changes

PR #494: Allow sub() on all Stores

Removes the RootStore dependency in the SubStore, so that SubStores 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)

PR #492: Remove deprecated direction-api from RadioGroupComponent and CheckboxGroupComponent

This PR removes the deprecated direction -api from RadioGroupComponent and CheckboxGroupComponent.
Use orientation instead.

PR #489: Some Improvements and Bugfixing around Forms

Improve FormControl code

  • make FormSizeSpecifier values uppercase, so more Kotlin alike
  • improve render strategies initialization and customization: Instead of setting all up within the init 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!
Migration Guide:

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))
Β Β Β Β  }
}

Improve sizes aspect for components in theme

  • in respect to EfC 502 a new mixin alike interface FormSizesAware has been introduced
  • the FormSizes interface is renamed to FormSizesStyles
Migration Guide:

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

Make FormControl Labels dynamic

  • the label property of a FormControl now accepts also Flow<String>. Thus the label can dynamically react to some other state changing.
Migration Guide

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()
Β Β Β Β Β Β Β  }
Β Β Β  }
}

Adapt FormControl's textArea function parameters

  • the optional store parameter now also is named value like as within the main textArea factory.

Repairs label behaviour for SelectField

  • a click onto a label within a FormControl now directs to the SelectField.
  • the id of a SelectField is now correctly set, so that a FormControl can now fill the for attribute of the label correctly.

PR #490: Move Modal’s close handler into content property

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.

Migration

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
    }
}

PR #487: Rework ToastComponent

Changes

This PR cleans up the ToastComponent code. This includes:

  • Adding styling-options to the theme
  • Use correct z-indizes
  • General code-cleanup

What's API breaking?

  • Theme().toast.base has been renamed to Theme().toast.body and might break themes that have custom toast-styles

New Features

PR #505: Add TooltipComponent

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 RenderContextin 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 }
    }
}

Migration

  • The old tooltip component remains as deprecated for the next releases, so clients have time to migrate.

Motivation

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.

PR #493: Add 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 automatically
  • placement 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
        }
    }
}

PR #496: Add PaperComponent and CardComponent

This PR adds a new CardComponent that behaves similar to the old PopoverComponents content.

Paper Component

Component displaying content in a card-like box that can either appear elevated or outlined and scales with the specified size of the component.

Example
paper {
    size { /* small | normal | large */ }
    type { /* normal | outline | ghost */ }

    content {
        // ...
    }
}

Bildschirmfoto 2021-08-17 um 19 09 11

CardComponent

A component displaying the typical sections of a card inside a PaperComponent.
The available sections are a header, a footer and the actual content.

Example
card {
    size { /* small | normal | large */ }
    type { /* normal | outline | ghost */ }

    header {
        // ...
    }
    content {
        // ...
    }
    footer {
        // ...
    }
}

Bildschirmfoto 2021-08-17 um 19 17 18

PR #481: Add possibility to pass arbitrary payload data in fritz2's DSL

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:
Screenshot_20210707_165401

PR #470: Add 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


Improvements

PR #514: Upgrade Kotlin to Version 1.5.30

PR #507: Add TooltipMixin and TooltipProperties to use TooltipComponent in Components more easily

With this PR it beeing easier to integrate a tooltip in a component and the user get a same usage of a tooltip.

PR #486: Use colors in appFrame and added submenu in menu component

The appFrame component uses now different ColorSchemes to coloring itself.

The menu component can now contain submenus:

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)
        }
    }
}

PR #483: Deprecate 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.

PR #477: Add convenience functions to create toasts with alert-content

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:
Bildschirmfoto 2021-07-06 um 10 35 19

PR #462: Rename alert variant 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
}

Fixed Bugs

  • PR #515: Handle possible CastException
  • PR #511: fix appearance of inputs, textareas and select on iOS
  • PR #502: fix type of InputEvent
  • PR #500: Fix layout issues in radio, checkbox and switch components
  • PR #471: Fix close-button margin in ToastComponent
fritz2 - Version 0.11.1

Published by jamowei over 3 years ago

Improvements

PR #443: Use any element/component as a label in radio- and checkbox-groups

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
        }
    }
}

PR #435: Added new 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
}

PR #442: Rework modal

This PR improves the following aspects of a modal:

  • scrolling behaviour: Per default there are some settings to enable scrolling automatically without any client side effort.
  • width settings (API change): new name 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.
  • vertical placement (API change): This replaces the old 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.

Migration advices:

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:

  • Define new predefined values in 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.

PR #436: Rework popover component

  • remove unnecessary getElementById() call
  • fix id and prefix placement in DOM (analog to dropdown)
  • fix and rearrange kdoc
  • refactor & reformat code

Further improvements

  • PR #452: Add formGroup component

Fixed Bugs

  • PR #434: Use explicit type specification for Icons in DefaultTheme
  • PR #430: Make track() function of tracker safe for unsafe operations
  • PR #451: Fixed slider styling with delegations
  • PR #458: Added missing package line in select.kt and nav.kt
fritz2 - Version 0.11

Published by haukesomm over 3 years ago

Breaking Changes

PR #409: Upgrade to Kotlin 1.5

Upgrading to Kotlin 1.5.10 version and update dependencies to latest version.

PR #420: Using ComponentProperty for setting an IconDefinition for a button

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.

PR #415: Use constant values for z-indices in components

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 Features

PR #326 and #424: Add Menu and Dropdown components

Menu

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")
        }
    }
}

Dropdown

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 {
        // ...
    }
}

Improvements

PR #417: Harmonize orientation settings

  • add new general concepts for the orientation aspect of a component: That is align its item(s) in an horizontal or vertical way.
  • offer a new mixin OrientationMixin for components
  • add this mixin for the following components:
    • Slider (no API change!)
    • CheckBoxGroup: new orientation API, old direction marked as deprecated
    • RadioGroup: new orientation API, old direction marked as deprecated

Migration

  • direction β†’ 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 }
}

Fixed Bugs

  • PR #423: Removed duplicated html elements which are causing errors
  • PR #422: Fixed self type loss after using global functions on Listener like preventDefault etc.
fritz2 - Version 0.10.1

Published by jamowei over 3 years ago

Improvements

  • PR #380: Added new linkButton component
    linkButton {
        text("Show me awesome")
        href("www.fritz2.dev")
        target("_blank")
    }
    
  • PR #387: Add new slider component
    val valueStore = storeOf(100)
    slider(value = valueStore) {}
    
  • PR #362: Add new menu component
    menu(demoMenuStyle) {
        header("Items")
        entry {
            text("Basic item")
        }
        divider()
        entry {
            icon { sun }
            text("Item with icon")
        }
    }
    
  • PR #370: Remove misleading Inter font
  • PR #369: Added convenience methods inspectEach() for Inspector on collections
  • PR #365: Fix default ColorScheme colors
  • PR #363: Improve publishing.gradle.kts
  • PR #361: Improve alert API

Fixed Bugs

  • PR #408: Fix problems with using media breakpoints
  • PR #385: Fix selection forwarding to external store for DataTable component
  • PR #390: Fix css error in appFrame component
fritz2 - Version 0.10

Published by WestHuus over 3 years ago

Breaking Changes

PR #334: Rework of API for Keys from KeyboardEvents

The following things has been changed:

  • Using KeyboardEvent.key instead of deprecated KeyboardEvent.keyCode for comparison
  • Extending the list of standard key (e.g. Keys.Alt, Keys.Enter, Keys.Esc)
  • Simplify the creation of a Key by calling the constructor with the given KeyboardEvent
  • Overriden equals() method which compares two Keys by Key.key attribute.
  • The toString() returns the key attribute
input {
    keydowns.key().map { 
        when (it) {
            Keys.Enter -> //...
            Keys.Escape -> //...
            else -> //..
        }
    } handledBy store.myHandler
}

PR #336: Fix horizontal and vertical convenience functions of BordersContext DSL

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.

PR #355: Extends of ColorScheme approach

  • 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.
  • The colors info , success, warning, danger and neutral changed to ColorScheme
  • The property color of our button component refactors to type and based on the ColorScheme approach now.
    Our default theme deliveres following predefinitions: 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") }
     }
    
  • You've the possibility to setup the 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.
  • The 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")
        }
    }
    

PR #356: Improve API of AlertComponent and use new 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.

  • Updated 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
    }
    
  • Removed AlertVariantStyles interface

PR #357: Move background & font colors to Colors interface

Move 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 Features

PR #337: Extend styling API

New API for using the fritz2 styling DSL on standard HTML Tags.
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!"
    }
}

PR #350 : Add DataTable component

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:

  • the styling of the header or the columns
  • the styling based upon the index or the content of a row or cell
  • the sorting mechanisms (logic and UI)
  • the selection mechanism (logic and UI)

For a detailed overview have a look at our KitchenSink project.

Further new features

  • PR #335 Added missing resizeBehavior option both for textArea component
  • PR #341 moves basicStyles of selectField and textArea to defaultTheme

Improvements

PR #349 Harmonize parameter name in components with stores

For 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

Further improvements

  • PR #340: Moved mono icons to an extra package next to the License.md
  • PR #345: Make staticStyle internal
  • PR #358: Polish DataTable
  • PR #359: Rework new Svg tag in core

Fixed Bugs

  • PR #338: Fixed problem with nested renders which contains conditions
  • PR #351: Fix problem with component-specific EventContexts
fritz2 - Version 0.9.2

Published by haukesomm over 3 years ago

Improvements

  • PR #317: Using websafe fonts in default theme
  • PR #331: Using DSLMarkers for Components

Fixed Bugs

  • PR #319: Suppressing errors messages when inserting normalize.css into CSSStyleSheet

Gradle-Plugin

  • PR #8: Fixed bug when renaming Kotlin multiplatform targets
fritz2 - Version 0.9.1

Published by over 3 years ago

Breaking Changes

PR #313: Improvement of theme colors

The approach to specify colors in a theme had some serious issues up to version 0.9:

  • no semantics for dealing with content's color on top of a colored area
  • no structural grouping of related colors (only by prefix like primary and primaryEffect)
  • no definition for separating the effect colors for filling areas and rendering something on the surfaces

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!

Migration for custom themes

Some color properties have been removed obviously. So here is a short migration guide:

  • instead of 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"
        )
    
  • choose the same approach for the old pair of secondary and secondaryEffect
  • change the lightestGray, lighterGray, ... properties to appropriate new gray{number} properties. Fill the gaps with fitting shades.
  • change base to neutral
  • there is nor more dark property. Choose a fitting gray{number} or probably some sub-color from primary, secondary or tertiary quadrupel.

PR #290: Parameterless invoke function inside the Hander interface

No 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()

New Features

  • PR #308: New convenience functions for getting current value on enter (Input & TextArea)
  • PR #293: Migration to mavenCentral

Improvements

  • PR #310: Decouple Modal and Toast components from local RenderContext
  • PR #305: Extending fritz2 DefaultTheme
  • PR #298: Move basicInputStyles of inputField Component to default theme
  • PR #287: Using offical Slack badge

Fixed Bugs

  • PR #311: Visual fix of padding in small inputField
  • PR #302: Rework of StylingClass & Remove trailing space from CSS class names
  • PR #299: Make annotation processor for lenses aware of constructor properties only
  • PR #300: Correct maven central for local development
  • PR #297: Fix styling bug in file component
fritz2 - Version 0.9

Published by jamowei over 3 years ago

Breaking Changes

This release contains changes that break code written with earlier versions.

Global Render Functions (PR#233 & PR#243)

Up until fritz2 version 0.8, we offered two global render functions:

  • render {}: List<Tag<E>> - create RenderContext for multiple root elements
  • renderElement {}: Tag<E> - create RenderContext for single root element
    The result had to be mounted to your DOM-tree by calling, the mount() 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 "#".

Changes in Repositories API (PR#232)

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.

API Streamlining (PR#283)

For the following fritz2 features we streamlined our API a bit:

  • Router
  • Tracking
  • History
  • Validator (note: renamed 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.

Components (PR#269 & PR#266)

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)

new features

  • added window event-listenersPR#277
  • added AppFrame and navigation-components PR#262
  • added file selector component PR#258
  • support for JS IR compiler PR#254
  • allow important to CSS-properties in DSL PR#248
  • components validation PR#244

improvements

  • harmonized toast behavior PR#285
  • small refactoring for Router PR#281
  • added convenience function to register WebComponent PR#279
  • opened component's stores for extension PR#276
  • improved CloseButton API PR#275
  • small improvements to FormControl PR#272
  • added convenience functions for Lenses PR#271
  • rework element property prioritization PR#268
  • added convenience methods for Validator PR#242

fixed bugs

  • fixed default styling of selectField PR#274
  • fixed placement of modals PR#249
  • adjusted WebComponents to new Render/TagContext PR#247
fritz2 - Version 0.8

Published by jwstegemann almost 4 years ago

breaking changes

This release contains changes that break code written with earlier versions. Hopefully these are the last major api-changes prior to fritz2 1.0:

Setting attributes per function

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 Flows 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 Tags 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)
}

Adding Text and Comments

We clarified the creation of TextNodes in Tags. Now you use unary +-operator for constant Strings
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()
}

Evolution of render() and renderEach()

Using former fritz2-versions you mapped a Flow of data to a Flow of Tags 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 Stores and Flows with and without an idProvider.
Please note that renderEach() still allows only one root-element (like renderElement)!

Tracker offers Flow<Boolean>

Tracker now implements Flow<Boolean> instead of Flow<String?>so it adopts better to most use-cases. Find an example here.

new features

improvements

  • update all dependencies to latest version PR#166
  • extend Router functionality PR#197
  • upgraded Dokka-version and moved to html for api-docs PR#194
  • annotation processor visibility option PR#178
  • use local test server PR#165

fixed bugs

  • fix memory leaks and performance issues PR#180 PR#185
  • no trailing slash in remote PR#167
  • fix boolean attribute delegates PR#172
fritz2 - Version 0.7.2

Published by jwstegemann about 4 years ago

Small patch resolving a memory issue related to coroutine scopes.

fritz2 - Version 0.7.1

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.

fritz2 - Version 0.7

Published by jwstegemann about 4 years ago

breaking changes

This release contains changes that break code written with earlier versions:

  • Handlers 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)
  • Validation has been extracted as a service and refactored to be more concise. (PR#149 & #157)

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
   ...
}

new features

  • added tracking-service to access process state of Handlers (e.g. to show process indicator). (PR#147)
  • added history-service to keep track of historical values in Stores and provide back() function. (PR#152)
  • added 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)
  • added storeOf() function to create a minimal RootStore (without Handlers) (PR#144)
  • added convenience-function render on Seq, so you can directly write each(...).render { ... } (and leave out map) (PR#142)
  • added convenience-function render on Flow, so you can directly write flow.render { ... } (and leave out map) (PR#154)
  • added functions to deal with errors in Handlers (PR#137)
  • snapshots are now provided on oss.jfrog.org (PR#128)
  • added append function to remote (PR#127)
  • changed IdProvider to generic type (PR#123)
  • use 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)

fixed bugs

  • added isValid on JVM (PR#135)
  • added missing factories for <dt> and <dd> (PR#134)
  • added missing SelectedAttributeDelegate (PR#131)
  • fixed some bugs in Router and minor API changes (PR#151)
fritz2 - Version 0.6

Published by jwstegemann over 4 years ago

breaking changes

This release contains changes that break code written with earlier versions:

  • You no longer need to inherit 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 Tags. 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 Tags, 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 Tags. 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 Tags, 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)

new features

  • add static text in HTML by +"yourText" (PR#95)
  • add HTML-comments by comment("yourText") or !"yourText" (PR#108)
  • use the action function to dispatch an action at any point in your code (PR#117)

fixed bugs

  • fixed handling of value and checked attributes (PR#81)
  • fixed MapRouter to use Map<String,String> (PR#82)
  • fixed double kotlin-block in gradle build-file (PR#97)
  • ensure order of children when mixing static tags with bound values on the same level by using bind(preserveOrder = true) (PR#102)
  • classes of HTML-tags are now open so you can inherit your own tags from them (PR#104)
  • SingleMountPoint for Boolean (leaving out the attribute if false) (PR#105)
fritz2 - Version 0.5

Published by jwstegemann over 4 years ago

breaking changes

This release contains changes, that break code written with earlier versions:

  • We moved all artifacts and packages to match our domain: dev.fritz2. You will have to adjust your inputs and dependencies accordingly.
  • The default project-type for fritz2 now is multiplatform (to make it easy to share models and validation between client and server). Use the new fritz2-gradle-plugin to setup your project:

build.gradle.kts

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 {
                }
            }
        }
    }
}

fixed bugs

  • fixed dom-update problem with checked attribute at HTMLInputElement
fritz2 - Version 0.4

Published by jwstegemann over 4 years ago

breaking changes

This release contains changes, that break code written with earlier versions:

  • since it was the source of much confusion we renamed the function to build a tree of Tags (formerly html) to render:
render {
    div("my-class") {
        // ...
    }
}
  • the overloaded operator <= 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
}

new features

  • improved remote-api
  • support for building and using WebComponents

bug fixes

  • improved examples
  • improved documentation

build.gradle.kts

Kotlin style

dependencies {
    implementation("io.fritz2:fritz2-core-js:0.4")
}

Groovy style

dependencies {
    implementation 'io.fritz2:fritz2-core-js:0.4'
}
fritz2 - Version 0.3

Published by jwstegemann over 4 years ago

  • several bug-fixes
  • tidyed up syntax for text, attr, const
  • better examples
  • Improved diff-algorithm for list-handling
  • better extractions on events (current value, selected item, etc.)
  • reworked structure of GitHub-projects