Introducing view models for the neo.mjs Javascript UI framework

Tobias Uhlig
ITNEXT
Published in
11 min readApr 7, 2021

--

[Important update] This article is already deprecated. For the neo.mjs version 2 release, the logic got significantly improved. Here is the new version:

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

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

The article is split into two parts:

  1. How to use view models for your apps, including the code for several example apps as well as videos & online demos
  2. Under the hood: design goals & talking about the view model implementation

Welcome to part 1!

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. Online demos
  9. Source code location
  10. TL-BR
  11. 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 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: {
value: '${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.

Looking close at the binding value → ”${data.button1Text}”, you will notice that we are using exactly the same syntax as Template literals. The only difference is that this is a real string and not ``.

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.

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.user.details.firstname}'
}

Our binding values still follow the Template literals syntax.

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. More about this topic in part 2.

7. The advanced demo app

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: '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.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. Online demos

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

examples/model/advanced/

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

9. Source code location

You can find the source code of all examples here:
https://github.com/neomjs/neo/tree/dev/examples/model

We will cover the internal logic of model.Component inside part 2 of this article. So far, it is “just” 500 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:
https://github.com/neomjs/neo/blob/dev/src/model/Component.mjs

10. 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 can reduce boiler-plate code:
    Using bindings, you only need to update the vm data properties.
  3. You can dynamically change your views, the bindings are persistent.
  4. You can dynamically add views which contain bindings, even in case they don’t have their own view model(s).
  5. Not every view needs its own view model, you can always access the closest parent model calling myComponent.getModel() .
  6. I personally recommend to not register models for “leaf nodes” inside your application view structure (e.g. buttons, form fields or other simple components).

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

Part 2 (Under the hood: design goals & talking about the view model implementation) should be ready within the next couple of days. This one will also contain a discussion about future enhancements (e.g. using methods inside formatters), for which your feedback is crucial.

Best regards & happy coding,
Tobias

--

--