4.2 preview - simplified view model API in JS Modules

Continuing the informal series about news in DotVVM 4.2, let me show how we have improved the JavaScript API for modifying the view model, specifically in JS View Modules.

From the very first DotVVM version, we had the dotvvm.viewModels.root.viewModel “API” - the view model is simply available to anyone as an object wrapped in Knockout observables. Modifying a single property is easy, just remember to unwrap each observable by calling it - dotvvm.viewModels.root.viewModel.Collection()[5]().MyProperty("new-value"). However, it gets quite cumbersome to unwrap everything in a more complex logic. Moreover knockout observables are rather expensive to create, so in DotVVM 3.0 we internally switched to manage the state using a plain immutable object. Knockout observables are obviously still available, but only created lazily, avoiding unnecessary allocations.

With this rewrite, we introduced the dotvvm.state property and dotvvm.patchState function for accessing and updating the immutable view model object. The patchState function accepts a partial object, updating only the specified properties, leaving the rest unchanged. For example, patchState({ User: { Name: "new-name" } }) updates the User.Name property.

The state/patchState API is also available on each knockout observable, enabling us to use the state API in knockout bindings and binding handlers. Knockout observables also have a setState function, which simply replaces the entire object. This function was missing from the global dotvvm object, because, well, we forgot to add it.

In 4.2 we fix this disparity, with the same API available on dotvvm, knockout observables and JS module context (DotvvmModuleContext):

  • state - returns the current value of the view model
  • setState(newValue) - replaces the current value
  • patchState(newValues) - replaces only the specified properties, for example patchState({ Title: "new title" })
  • updateState(currentValue => computeNewValue(currentValue)) - applies the specified function onto the current view model

JS Modules

In previous versions, we noticed that it’s often useful to directly modify the client-side view model inside View Modules (instead of calling a named command). If the module is loaded into a page, we can simply use the global dotvvm.state/dotvvm.patchState, however, it was quite cumbersome in markup controls. In 4.2, the API is unified as context.state/context.setState/… In markup controls, it works on the current viewmodel (the type declared using @viewModel directive). The same API available for properties in context.properties.MyProperty.state.

A simple usage might look as follows. In the example we download new data from a REST endpoint and place it into a view model using the context.updateState function.

export default context => {
    return {
        async myNamedCommand() {
            const newData = await (await fetch("/api/newData")).json()
            context.updateState(vm => ({...vm, Data: newData }))
        }
    }
}

We also changed that modules are initialized after dotvvm is fully initialized, making it safe to access state, setState, … in the constructor

Lenses

In simple cases using the updating the immutable objects in not very complicated, one just has to get used to the object spread syntax: {...oldVM, MyProperty: newValue }. However, when the object gets nested heavily, the code gets rather annoying to write. You might use the knockout observable API for this, but mixing the two is sometimes problematic.

In functional programming, all objects are immutable – there is nothing like simply assigning the value to a nested object, you have to go through the trouble of updating each object on the path to it. There has to be a better way, since this code quite repetitive and Haskell programmers are known for other things than writing repetitive code :grin:

The “Haskell” way is called lenses or more generally optics. There is number of implementations in Javascript and you can use it quite easily with dotvvm. I won’t go into details about lenses and optics here, there is enough of it on the web. Some libraries are powerful, general optics are much more than a way to do immutable assignments, but even I’d say this comes at quite steep cost of complexity. However, there are reasonably sober implementations, for example shades - a quick example how to use it with updateState.

const lens = shades.mod("Questions", "Items", questionIndex, "Votes")
dotvvm.updateState(lens(votes => votes + 1))

The lens is essentially a function which upgrades a modification function (votes => votes + 1) from working on a number to working on an object containing properties Questions.Items[index].Votes. dotvvm.updateState expects the function to accept the entire view model and return the new one, so when we upgrade the number => number using the lens, the types will match.

Just to peak into the more powerful constructs, you can also simply update all elements of the collection :wink:

const lens = shades.mod("Questions", "Items", shades.all(), "Votes")

or just elements matching a condition:

const lens = shades.mod("Questions", "Items", shades.matching(q => q.Title.startsWith("A")), "Votes")