Using template literals to create a binding engine

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

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!

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. The ugly part: How to convert a string into 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?
  2. How to create your first neo.mjs app?

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:

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 using a real String formatted like if it was a template literal to store the information:

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

In case you want to jump into the source code of the view models implementation right away:

src/model/Component.mjs

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 my previous article:

3. How to extract data variables from a string?

The obvious answer: regular expressions.

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

I went for:

/data((?!(\.[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).

4. The ugly part: How to convert a string into a template literal?

I searched a lot and it turns out that this is simply not possible.

In my opinion, a string to template literal converter should be an ECMAScript feature. I am not really sure where to create a feature request for it. In case you have an idea, please add a comment!

The only way to make it work right now is to create a new function:

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

Creating a function like this can be a security issue!

Under the hood, this is a hidden eval() call.

While it is fine to create code using binding formatters, be careful in case you want to make them editable for your application users.

Example: if you create an UI builder and add textfields into your app where users can manually edit your formatters, they could add logic into the formatters. They could trigger ajax calls or do all sorts of things.

Strong advice: if you make formatters editable for your app users, make sure to parse their input first and remove code which should not be there.

Here is the full function generator logic:

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 . The previous article covers this.

One easy example:

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

  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.button2Text.toLowerCase()}'
}

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

In case we are not binding a 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:

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

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.

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:

2. How to create your first neo.mjs app?

I received several question on how to use the new version of the app generator recently:

  1. Open your terminal (or cmd).
  2. Enter a folder where you want to store your project (I only used Desktop for this demo)
  3. Enter “npx neo-app”
  4. You can hit enter on all questions
  5. Open the new generated workspace folder in an IDE
  6. Optional: deploy it to a repository (e.g. GitHub)
  7. Open the MainContainer.mjs file
  8. Change code
  9. Reload the browser window (the dev mode does not require any builds)

Enjoy creating your first multithreaded frontend!

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store