Enhance your frontend state management with view models

Tobias Uhlig
ITNEXT
Published in
13 min readApr 21, 2021

--

You have most likely used MobX, Redux or looked into the React Context API.

To quote the Context API landing site:

“Context provides a way to pass data through the component tree without having to pass props down manually at every level.”

This is basically the common ground for most state management solutions:

How to make data accessible for child components and how to dynamically update the state.

This article will cover some of the possibilities and advantages of using view models. We will talk about code. A lot!

While using view models inside neo.mjs is extremely powerful and fairly easy to do, understanding the internal logic on how it works is challenging, even for experienced Javascript developers.

The article is split into two parts:

  1. “Enhance your frontend state management with view models”
    including the code for several example apps as well as videos & online demos
  2. “Enhance your frontend state management with view models — Part2”
    Under the hood: design goals & talking about the view model implementation

Welcome to part 1!

[Side note] This article is an extended rewrite of “Introducing view models for the neo.mjs Javascript UI framework”. For the neo.mjs version 2 release, the syntax on how to use binding formatters got switched from strings to functions, which is not reflected inside the previous article. In case you have read the previous article, take a look into the view definitions (arrow functions) and dive into the new content section 8.

Content

  1. Introduction
  2. What is neo.mjs?
  3. The simple demo app without using a view model
  4. The simple demo app using a view model class
  5. The simple demo app using an inline view model
  6. The simple demo app using nested data
  7. The advanced demo app
  8. The dialog app: accessing a view model outside of the parent tree
  9. Online demos
  10. Source code location
  11. TL-BR
  12. Your feedback is appreciated!

1. Introduction

Components (classes extending component.Base) and view controllers (classes extending controller.Component) have been inside the framework for a long time, so the missing piece to support the MVVM design pattern were view models.

I am excited to announce that the framework now provides an elegant solution for this topic. Elegant is especially related to the binding formatters, which work out of the box without creating or using a templating engine. The buzz-word here is Template literals.

View models can help you with adjusting the state of complex component trees and reducing boiler-plate code.

I think a good way to dive into this topic is to create a simple demo app without using view models at all first and then convert it into a version which is using them. From there we can move to more advanced use cases.

2. What is neo.mjs?

In case you are already familiar with the framework, skip to 3.

neo.mjs is a MIT licensed open source project which enables you to build multithreaded frontends without taking care about the workers setup or communication layer.

An extended ES8+ class config system helps you with creating Javascript driven UI code on a professional level.

One unique aspect is that the development mode runs directly inside your Browser, without any builds or transpilations. This can be a big time saver when it comes to debugging.

You can switch into a SharedWorkers mode with changing just 1 top level framework config. This mode enables you to create next generation UIs which would be extremely hard to achieve otherwise.

You don’t need any knowledge on web workers to follow this article.

Just keep in mind that all component instances live within the application worker realm. This means that view models live inside this scope as well.

3. The simple demo app without using a view model

We are creating a viewport (MainContainer), which includes a panel.

We want to connect both textfields to the matching buttons, so that changing the input value will update the button text. Clicking on a button should reset the matching field value to its original value.

Online demo (dist/production)

This demo app has a very simple architecture:

Let us take a look at the MainContainer code:

The view definition does not contain any business logic as it should be.

The JSON based item definitions follow the architecture diagram.

You will notice string based button handlers and listeners → those will get mapped into our view controller, expecting to find real methods with the same name there (or inside a parent view controller).

We are also using 4 references, which makes it easier to access specific items. There are several different ways to access child items, e.g. using manager.Component.

MainContainerController:

We are using configs to store our 2 data properties button1Text_ and button2Text_. Using the trailing underscore, the config system will enable us to optionally use beforeGetName(), beforeSetName() and afterSetName(). “Name” equals to an uppercase version of the config name.

Inside afterSetButton1Text() we are updating the text config of our button1 reference as well as the value config of our textfield1 reference.

afterSetButton2Text() does the same for button2 and textfield2.

We are ignoring the first initial call (oldValue === undefined), since at this point in time, the view controller has not parsed the view configs yet. We are doing this instead inside the onViewParsed()method.

You might wonder if we should add more checks into updateReferences() , e.g. in case you type into textfield1, the method will not only adjust the button1 text config, but also set the value config of textfield1 itself to the same value. This is fine! In case the same value does get set, there won’t be a change event → the related afterSet()method won’t get triggered → no checks for delta updates or even DOM manipulations.

onButtonClick1() will set our button1Text controller config to the initial value, onTextField1Change() will set the controller config to the new input value. The same happens for “2”.

You could as well add the 2 data properties into the view class (MainContainer). It does not make sense for this example, but this is the way to create new components in general (extending the desired base class and adding new configs, new methods and / or overrides into it).

I hope everything is clear until now. The first demo app is intentionally very easy. If not, please ask questions!

4. The simple demo app using a view model

Obviously we are keeping the same view architecture.
Well, I added one more button to log the model instance :)
(Excluded in the diagram, since not relevant)

The only difference is that we are adding the new model.Component class into the mix:

We are extending model.Component and are adding our 2 data properties into a data config instead of adding each one as a top level config. This is important to support deeper nested data structures.

Of course you could add new methods or overrides here as well. One of the beautiful aspects of neo.mjs is that you can extend and change pretty much everything.

MainContainer next:

We are importing our new model class (line 2) and simply drop the imported module into the model config.

Inside the view definition you will notice that the 4 references are gone.
We no longer need them.

Instead, we are using 4 bind objects.

bind: {
text: data => data.button1Text
},

Keys to use inside your bind objects are supposed to match class config names.

Our view model will automatically add the bound config key values super early inside the component lifecycle. This happens before the component constructor is done.

Our view model will also update the component configs on every related data property change out of the box.

Of course you can add multiple bindings to each component.

Bound values can be arrow functions (better readability) or real functions (for a slightly better performance, since functions will get bound to the closest view model scope).

View models will parse the used data attributes and only trigger an update in case one of the used variables changes. The amount of re-rendering calls (check for delta updates inside the virtual dom web worker) is minimal.

MainContainerController:

You will notice that the logic inside the controller got simpler.

The 2 data property configs are gone.

We are using our 2 button handlers and the 2 textfield change listeners to simply change view model data properties.

controller.getModel() will return the closest model inside the connected components parent tree, so not each component has to use an own model.

I am using 2 different ways to change data properties just for demo purposes.

this.getModel().data['button1Text'] = value;

or

this.getModel().data.button1Text = value;

In case you are sure the data property does exist on the closest model inside the parent tree chain, you can simply assign a new value to it.

This “assignment” triggers a setter under the hood.

this.getModel().setData({
button2Text: value
});

setData() is the better way to do it. You can change multiple data properties at once and the method will search each key inside the parent chain of the components view models. In case no match is found for a given key, a new data property will get registered on the closest model level.

What we have learned so far:

  1. Using view models is optional, you can create the same business logic without them.
  2. View models can reduce boiler-plate code:
    Using bindings, you only need to update the vm data properties

5. The simple demo app using an inline view model

MainContainer:

In case we only want to set data properties on our view model, we do not need to extend the class.

Instead, we can assign an object to our component model config.

module: ComponentModel is optional here, however the model import (line 1) is not. Since view models are optional, the framework won’t import their base class automatically. However, you only need to import the model.Component base class once (for the most “top level” view).

6. The simple demo app using nested data

MainContainer:

[side note] I am using Nils name here, i hope this is fine :) Nils is an expert in Ext JS and one of the early neo.mjs contributors. Thanks again for helping with the RealWorld demo app!

I skipped the module: ComponentModel inside our model object definition.

We are nesting our data 3 levels deep here.

bind: {
text: data => data.user.details.firstname
}

Only the 2 data property setters did change.

You can still simply change a data property leaf as an assignment.
(even nested structures get created via Object.defineProperty() → get() & set())

Using string based data paths inside thesetData() method is important. Values could be objects, in which case we could not know where a property ends and the value starts.

7. The advanced demo app

At the end of the video you can see that your app code is used “as it is” (no builds) inside the dev mode and it is running inside the application worker.

The advanced demo architecture is an extended version of the previous demos. We are using 2 view models this time.

The body container is using a vertical box (vbox) layout and since we now want to show each button formatter as well, we are wrapping each “row” into a container (horizontal box (hbox) layout containing one textfield and one displayfield).

I am using a green color for some items → those will get added dynamically when you click on the “Add a third button & textfield” button.

MainContainer:

The really important part here is that the top level view model contains the data properties button1Text & button3Text, while the panel (child) view model contains button2Text.

Looking at the first 2 button formatters:

text: data => `Hello ${data.button2Text} ${1+2} ${data.button1Text + data.button2Text}`

The button1 formatter contains the MainContainer model button1Text data property as well as the panel model button2Text data property.

Obviously we expect the text to update in case any of these 2 properties change, regardless of the model level. This is exactly what happens :)

text: data => data.button2Text.toLowerCase()

You can call functions on each data property. The data property parser is smart enough to figure out that your bound property name is “data.button2Text”.

Since button2Text is a string, using .toLowerCase() is fine.

MainContainerController:

Inside the first method onAddButtonTextfieldButtonClick() , we are adding a new “row” container into the panel body. The itemDefaults config inside our view definition is still in use, so we don’t need to specify the module or layout.

We are also adding button3 into the header toolbar at the same time.

The key part here is that both item definitions contain bound configs and they should work the same way as inside our initial view definition.

The good news: dynamically adding bindings works perfectly fine as well :)

A quick look into the 3 data property updating methods:

updateButton2Text(value) {
this.getReference('panel').getModel().setData({
button2Text: value
});
}

Accessing the panel (child) model is the best way to do it. The other 2 methods are just for testing different ways.

updateButton3Text(value) {
this.getModel().data['button3Text'] = value;
}

Our MainContainerController is connected to our MainContainer view. Calling this.getModel() will access the top level view model. Since button1Text and button2Text are defined on this level, it works fine.

updateButton1Text(value) {
this.getReference('panel').getModel().setData('button1Text', value);
}

This call is basically a test: we are calling setData() on the panel (child) model, knowing that the button1Text data property does exist on the parent level instead. Works fine.

8. The dialog app: accessing a view model outside of the parent tree

This new example is special and definitely worth a closer look.

Both, the MainContainer as well as the EditUserDialog are direct child nodes of the document.body. There is no parent — child relation between them.

We are also lazy loading the EditUserDialog when clicking on the “Edit User” Button.

MainContainer:

Pretty straight forward: We are defining an an inline view model with nested data and display the data inside the label on the top left.

EditUserDialog:

The class definition of the dialog does not contain a view model at all. However, we are using bound values for the TextFields, containing data properties of the MainContainer model.

EditUserDialogController:

The dialog view controller listens to TextField change events and will adjust the data inside the closest model.

MainContainerController:

Now here is the real deal: When clicking on the “Edit User” button, we check if we have already created a dialog. If not, we use a dynamic import to lazy load the file (which will as well lazy load the static imports inside the dialog file).

We assign an empty model to the dialog instance(!) and use a parent config to map it to our MainContainer model.

This is it :)

9. Online demos

dist/production:
Webpack based build (minified). Runs in all major Browsers.

examples/model/advanced/

examples/model/dialog/

examples/model/extendedClass/

examples/model/inline/

examples/model/inlineNoModel/

examples/model/nestedData/

development mode:
Uses the real code directly inside your Browser, without any builds or transpilations. This mode is limited to Chromium (Chrome & Edge), since other Browsers do not support JS modules inside the worker scope yet.

examples/model/advanced/

examples/model/dialog/

examples/model/extendedClass/

examples/model/inline/

examples/model/inlineNoModel/

examples/model/nestedData/

You can dynamically log the model instances into your console
(buttons at the bottom left):

Your focus should be on the bindings & data configs.

[Hint] The console logs are a bit more meaningful inside the dev mode
→ non minified class names.

10. Source code location

You can find the source code of all examples here:
examples/model

We will cover the internal logic of model.Component inside part 2 of this article. So far, it is “just” 586 lines of code and I tried my best to keep it very clean and structured.

In case you are an Javascript expert and curious to dive into it right now:
src/model/Component.mjs

11. TL-BR

The possibilities of what you can do using the new neo.mjs view models are amazing! You can not only significantly reduce your boiler-plate application code, but got an elegant way to modify your view related state at your fingertips.

To summarise it:

  1. Using view models is optional, you can create the same business logic without them.
  2. View models will only push changes to bound values in case one of the actually used data properties did change.
  3. View models can reduce boiler-plate code:
    Using bindings, you only need to update the vm data properties.
  4. You can dynamically change your views, the bindings are persistent.
  5. You can dynamically add views which contain bindings, even in case they don’t have their own view model(s).
  6. Not every view needs its own view model, you can always access the closest parent model calling myComponent.getModel() .
  7. I personally recommend to not register models for “leaf nodes” inside your application view structure (e.g. buttons, form fields or other simple components).
  8. I also recommend to apply state related data at the lowest possible model level.

12. Your feedback is appreciated!

I am looking forward to see what talented developers like you can achieve using this new technology.

In case you build something nice or have questions,
make sure to give me a ping!

[Edit] Quick update: part 2 is online now:

Tomorrow (April 23, 2021), the neo.mjs version 2 release announcement will get published :)

Best regards & happy coding,
Tobias

--

--