10

Enhance your frontend state management with view models — Part2

 3 years ago
source link: https://tobiasuhlig.medium.com/enhance-your-frontend-state-management-with-view-models-part2-5a9384bd863c
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Enhance your frontend state management with view models — Part2

Under the hood: design goals & talking about the view model implementation based on Template literals.

In case the name does not ring a bell, template literals were previously called Template strings. Syntax-wise they look like a string, just wrapped into ``.

Example: `<button>${myButtonText}</button>`

Template literals resolve variables like ${myButtonText} right away inside the given scope.

We can do a lot more with them!

Welcome to part 2!

[Side note] This article is an extended rewrite of “Using template literals to create a binding engine”. 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 it, take a look into the new content sections 3 & 4.

Content

  1. Introduction
  2. Examples of how you can improve your frontend architectures with using view models
  3. How to extract data variables from a string?
  4. How to extract data variables from a template literal?
  5. Where to store data properties used inside binding formatters?
  6. How can we get change events for nested data properties?
  7. How do the bindings get saved internally?
  8. Source code location
  9. Support for methods inside binding formatters
  10. Final thoughts

Appendix

  1. What is neo.mjs?

1. Introduction

For the neo.mjs view model implementation, my goal was to support common features of a templating engine without building or using a templating engine at all.

You do not need any experience in using neo.mjs to follow this article.

Template literals are a perfect fit, since the browser support is amazing at this point:

0*Sn_Z4QdiYNvKhdOt.png?q=20
enhance-your-frontend-state-management-with-view-models-part2-5a9384bd863c

We do want to manually parse binding formatters to figure out which variables are included. We need to to set up bindings between used variables and the component config which is using the binding formatter to dynamically update the value in case any of the used variables changes.

Due to the nature of template literals which resolve variables right away, we can not define our formatters using ``.

E.g.:

`Hello ${1+2} ${data.button1Text + data.button2Text}`

would not contain any information about the used variables.

Instead, we are wrapping our literals into functions, to prevent the direct execution:

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

You can use arrow functions for better readability, real function have a slightly better performance, since we are executing them on the scope of the closest view model to a bound component.

data: function(data) {
return `Hello ${1+2} ${data.button1Text + data.button2Text}`
};

2. Examples of how you can improve your frontend architectures with using view models

In case you are curious to see examples (code, videos, online demos) of the view model implementation in action, I strongly recommend to read part 1 of this article:

The obvious answer: regular expressions.

For the neo.mjs context, all variables are stored inside a data property.

I went for:

/(data|[a-z])((?!(\.[a-z_]\w*\(\)))\.[a-z_]\w*)+/gi

I am most definitely not an expert on regex, so I used an online tool to test and modify it:

Just change the “flavor” to ECMAScript and you are good to go:

  1. We do want to support nested variables
  2. Variables could end with a function like toLowerCase() which we do want to ignore (the reason for the negative lookahead).

For those who have read the previous version of this article:
The start of the regex got changed:

It was data before and now it is (data|[a-z)]) .

While the old regex worked fine inside the dev mode and dist/development, it no longer did work in dist/production after the string to literal fn change.

The reason is simple:

bind: {
value: data => `${data.button3Text}`
}

functions do get minified.

bind:{value:e=>`${e.button3Text}`}

So the regex needs to support any 1 char long variable names for the starting point as well. The model.Component logic got enhanced to still map found data properties to this.data .

4. How to extract data variables from a template literal?

In short: it is not possible.

This is the reason the previous view model version was using strings in a template literal format, parsing those and then converting them into literals.

const fn = new Function('data', 'return `' + formatter + '`;');

Forget that you have seen this trick, since it is a security issue.
new Function()is calling eval() under the hood and you could exploit this to add “mean” code.

The good news: There is an elegant solution.

In case we define our formatters as functions, we do have the option to call function.toString().

Try this inside your console:

let bind = {
value: data => `${data.button3Text}`
};

bind.value.toString();// logs: "data => `${data.button3Text}`"

This is definitely good enough to use as an input for the regex parsing.

The previous resolveFormatter() method got replaced with:

It will get called on the closest view model of the bound component
(scope → this).

5. Where to store data properties used inside binding formatters?

In neo.mjs you can now use view models to store data properties. You can simply define them as objects inside your components or extend model.Component . Part 1 of this article covers this.

One easy example:

Inside “real life” apps, you most likely have a deeply nested view structure.

0*j5x3CDU6iKw8UIYL.png?q=20
enhance-your-frontend-state-management-with-view-models-part2-5a9384bd863c
  1. Each view can optionally have a view model
  2. We need access to data properties inside parent models
  3. Binding formatters can contain data properties of different view models at the same time

6. How can we get change events for nested data properties?

Inside our view controllers, we want to be able to directly assign a new value like:

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

We also want to be able to change multiple data properties at once:

this.getModel().setData({
button1Text: value1,
button2Text: value2
});

model.Component is using a data_ config. Thanks to the class system enhancements (config system), the trailing underscore enables us to use an afterSet() method:

This one will trigger in case you assign a value for data inside your model class or instance definition.

We are recursively parsing nested data properties.

In case a property of any level did not get converted yet, we are calling createDataProperty()

This is already it: we are transforming each property on any level via get() and set() . The values get stored with a leading underscore and we now have our change handler method → onDataPropertyChange() in place.

In case a model data property does change, we parse the stored bindings to get all components which are bound. We then access the closest model of the component and call getHierarchyData() which gives us a merged object of all data properties inside the components parent model chain.

Components can have multiple config bindings including the changed data property, so we are calling component.set(config) to assign all changes at once. This way we will only trigger the virtual dom engine once.

7. How do the bindings get saved internally?

The constructor of every component.Base (including all class extensions) will check for a closest model and in case there is a match call parseConfig() on this one.

E.g. inside a button definition, you can add:

bind: {
text: data => `${data.button2Text.toLowerCase()}`
}

We are calling createBindings() and set the resolved value on each config right away.

In case we are not binding a Neo.data.Store , we are calling createBindingByFormatter() .

Since formatters can contain multiple variables, we are doing the (previously mentioned in 3.) regex parsing and call createBinding() for each of them.

We are storing bindings in the following format:

1*en1QYRx_ahaHnhFrSKCb8Q.png?q=20
enhance-your-frontend-state-management-with-view-models-part2-5a9384bd863c

dataPropertyPath → componentId → configName: formatter

createBinding() will always use the closest data property match inside the model parent chain (you could have the same name inside a parent model).

Of course a component will ping the closest model in case it does get destroyed to remove all bindings for it inside the model parent chain. We don’t want to get memory leaks.

8. Source code location

You can dive into the full source code here:
src/model/Component.mjs

The full view model implementation only needed 600 lines of code so far.

Without using template literals, it would have been a lot(!) more.

9. Support for methods inside binding formatters

I started a new discussion on this topic here:
https://github.com/neomjs/neo/discussions/1754

Your feedback is appreciated!

10. Final thoughts

Template literals enable us to do great things in case we think a little bit outside of the box.

I hope this article was not too far ahead. I am aware that the code is not easy to understand, even for experienced Javascript developers.

I tried to keep the logic as simple as possible though.

With the view models implementation, you can now fully follow the MVVM design pattern, in case you want to.

Again: Your feedback is appreciated!

Best regards & happy coding,
Tobias

P.S.: The neo.mjs version 2 release announcement will get published tomorrow!

Appendix

1. What is neo.mjs?

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.

0*Gl--YjXm23XW7ZQW.png?q=20
enhance-your-frontend-state-management-with-view-models-part2-5a9384bd863c

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 can find the repository here:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK