Quentin Delcourt

Writing a reactive framework in JS - Part 1

I recently worked a lot with Vue.js, which I enjoy because of its simple declarative style. Having seen recently a great tutorial about recreating a simple database system, I thought I'd try to do the same with a JS reactive framework.

I'm not sure where this will lead me, but let's give it a try...

You can find the source for this post on GitHub.

Giving it a name

If I flip Vue.js logo upside down, it makes me think of a hut. Therefore, here's the name of this new framework: Hut.js :)

Setting up the project

Starting a js project with Webpack 4 is now fairly trivial as it does not require any config. We can use the default behaviour and already compile everything we need. To get started, let's create the directory, initalize the package.json and install webpack.

mkdir hut
cd hut
npm init -y
npm i --save-dev webpack webpack-cli

Let's create an entry JS file as well as a test HTML page:

mkdir src
touch index.html src/index.js

Writing the first lines

Before I start writing some code I usually begin with the client code, which I think is an excellent way of thinking of what you need exactly. The return value and the parameters are what really matters. It's also a really good way to work in TDD. You write the test first and then you write the implementation.

In this case, I'd like to be able to render a simple "Hello world" template where "world" is a variable that will be evaluated by our Hut application.

My dream JS framework for a small project would work with a lot of conventions and would not, just like Webpack 4, require me to create a configuration file. Just using the default values could work.

Our entry point would only need to import the framework and:

  1. Provide a template to be evaluated
  2. Provide the data that will be inserted in the template

We don't need to consider anything else at this stage.

src/index.js
------------
import Hut from './hut/hut';

const app = new Hut({
  tpl: 'Hello {{ foo }}',
  ctx: () => ({
    foo: 'world',
  })
});

We create a new Hut application and provide it with the template (tpl) and the context (ctx). One could argue these parameters names are quite obscure but I think that it's not a problem when you are used to a framework (or a language even) to use abbreviated words.

Now what? Of course this "Hut" library does not exist anywhere than in our minds right now, so we need to implement it. But at least we know the interface we need.

The application object

Let's create Hut's main file:

mkdir src/hut
touch hut.js

We are going to leverage ES6's class syntax to store the code of our framework's main object. The first thing we need to do is implement the application class constructor, which will initialize the instance of the app with the template and the context.

src/hut/hut.js
--------------
export default class Hut {

  constructor (config = {}) {
    const defaults = {
      el: '#app',
      renderer: window.document,
      tpl: '',
      ctx: () => {},
    }

    this.config = Object.assign({}, defaults, config);
    this.renderer = this.config.renderer;
    this.el = this.renderer.querySelector(this.config.el);
    this.ctx = this.config.ctx();
  }
}

We export a new class called Hut and, within the constructor, we declare an object that will hold all our default configuration values. Using Object.assign() we take the configuration variables passed to our constructor and replace the default values with these specifiv values. Using {} as the first argument to Object.assign() allows us to store the application configuration in a brand new object, and not overwrite the defaults. We then store some elements of the configuration directly as properties of the object, so as to easily access them.

Let's take a closer look at the configuration variables:

OK, great, now we have a configuration. We now need to:

  1. Parse the template
  2. Render the template using the context
  3. Mount the rendered element on the mounting point

This rendering and mounting should all be triggered by the constructor so that client code remains as small as possible. Therefore, let's add a line of dream code to our constructor:

this.mount(this.el, this.prepare(this.config.tpl, this.ctx));

=> On the target element, we want to mount the result of our rendered template. We need to implement these two methods now:

  1. prepare will return a rendered, but not yet mounted HTML element
  2. mount, which will replace the target element by the rendered element

The preparefunction should not be too complex right now, we just want to render some text, that's all.

prepare (template, context) {
  const appNode = this.renderer.createElement('div');
  const tokens = template.match(/\{\{\s?([^}]+)\}\}/).slice(1);
  const evaluatedContent = tokens.reduce( (acc, token) => {
    token = token.trim();
    return acc.replace(new RegExp(`\\{\\{\\s?${token}\\s?\\}\\}`, 'gi'), context[token]);
  }, template);
  const content = this.renderer.createTextNode(evaluatedContent);
  appNode.appendChild(content);

  return appNode;
}

Parse and render the template using the context

Here we use the renderer defined in the constructor to create a root element for our app. After that we "parse" the template to find variables names wrapped in double curly braces, using a good old regular expression. Obviously this is a fairly basic and inefficient way of parsing the template, but this will do for now. (Note: when you are using Vue in production, this parsing step is done at compilation time, so the performance impact is minimal).

Once we have all the variables we replace them by their value as defined in the context object. We then append the text node to our main node and return the latter.

Mount the rendered element on the target node

The mount function immitates Vue in that it will completely replace the original element with the rendered element:

mount (mountingNode, applicationNode) {
  return mountingNode.parentNode.replaceChild(applicationNode, mountingNode);
}

Nothing complicated here: we can use the replaceChild method to insert our newly rendered node in the document.

That's it, we now have all the element to render our template. We can now write a simple HTML page that will hold our target element and load the main script:

<!doctype html>
<html lang="en">
<head>
	<meta charset="utf-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>Hello world!</title>
</head>
<body>
	<div id="app"></div>
	<script src="../dist/main.js"></script>
</body>
</html>

We now have the boilerplate for our framework, but we cannot do a lot except render some text, which isn't really exciting. The next part will focus on the parsing and rendering of the template, and see how we could display more interesting things.