DEV Community

Cover image for Reactivity?
Matt Ellen-Tsivintzeli
Matt Ellen-Tsivintzeli

Posted on • Edited on

Reactivity?

Introduction

What am I doing here? That's a fair question.

I have been using Vue.js for years. I like it. I've also tried angular. Recently I read this post about how different frameworks deal with state, and it got me thinking about how they might work under the hood.

Now, I know I could read through the source code, and figure it out that way, and it's possible during this series that I will, but I like tinkering and going from the abstract to the concrete by making things. So I thought I'd try to figure out how I'd do it.

I'd like, fair reader, for you to limit your expectations. First of all, I will not be making a CLI like any of the frameworks out there have. Secondly, this won't be as good as any of the others. So don't be surprised if when you compare mine to anyone elses, that it's slower or buggier 😋. (If you do find bugs, let me know!)

Finally, I don't get a lot of free time, so I think I will be able to manage a monthly update schedule.

On with the show!

Step 1 – A name

I won't dwell too long on naming, as this isn't going to be a project for the ages, so I don't have to think about marketing it. I will go with a straightforward initialism: RJSF. That is: Reactive JavaScript Framework. Bland enough that noöne else will pick it, and simple to understand and remember.

If you want to see how I'm getting on, and get a sneak peak at what future articles will explore:

GitHub logo Mellen / rjsf

An exploration into how I'd make a reactive javascript framework

RJSF logo. It's the letters arranged in a square on a dark background with blue squiggles.

Reactive Javascript Framework

RJSF for short.

What! Why?

I know. There are other, better frameworks out there. We don't need any more. I agree.

This is not about making a framework to replace Vue.js or anything like that. It's about creating something to learn how it could be created.

Javascript frameworks often seem to me like incredible works, and they are, and making anything similar, despite being a waste of time, is something I couldn't do. But, well, I could. I not a bad programmer, I can figure these things out. So I will.

I am blogging about this on dev.to.

There are examples of use in the examples folder.




Step 2 – can I do a reactive?

What should a reactive javascript framework do? The first thing I want to explore is how I can change the value of a variable, and have that variable be shown in the UI without having to document.getElementById('myspan').textContent = value;.

For this proof of concept, I'll just update the text in a span from nothing to the evergreen "Hello, World!".

As I'm sure you're aware, HTML elements have a key/value store. All attributes that start data- add to this store, the key being what comes after data-, and the value being what is stored in the attribute, for example:

<span id="example" data-flavour="cranberry">super tasty</span>
Enter fullscreen mode Exit fullscreen mode

In the above, the key is "flavour" and the value is "cranberry". This can be accessed in javascript using the dataset property of the element:

const span = document.getElementById('example');
console.log(span.dataset.flavour);
Enter fullscreen mode Exit fullscreen mode

So, rather than coming up with my own prefix for attributes, I'm going to use this.

This is what the HTML looks like:

<div id="app">
  <span data-model="message"></span>
</div>
Enter fullscreen mode Exit fullscreen mode

Now for the reactive bit. I'll put the whole thing and then try to break it down:

const rjsf = (function()
{
  function AppBuilder(baseElement)
  {
    this.base = baseElement;
  }

  AppBuilder.prototype.init = function(viewmodel)
  {
    const elements = this.base.getElementsByTagName('*');

    this.originalViewmodel = viewmodel;

    this.elements = {};

    const _internal = this;

    this.data = new Proxy({}, 
    {
      get(target, name, receiver) 
      {
        if (!(name in _internal.originalViewmodel.data))
        {
          return undefined;
        }

        return _internal.originalViewmodel.data[name];
      },

      set(target, name, value, receiver) 
      {
        if (!(name in _internal.originalViewmodel.data))
        {
          console.warn(`Setting non-existent property '${name}', initial value: ${value}`);
        }

        _internal.originalViewmodel.data[name] = value;

        for(let el of _internal.elements[name])
        {
          _internal.updateElement(el);
        }     

        return _internal.originalViewmodel.data[name];
      }
    });

    for(let el of elements)
    {
      if('model' in el.dataset)
      {
        if(el.dataset.model in this.elements)
        {
          this.elements[el.dataset.model].push(el);
        }
        else
        {
          this.elements[el.dataset.model] = [el];
        }

        this.updateElement(el);
      }
    } 

  };

  AppBuilder.prototype.updateElement = function(el)
  {
    const property = el.dataset.model;
    el.textContent = this.data[property];
  };

  return AppBuilder;

})();

Enter fullscreen mode Exit fullscreen mode

So, the first thing is our constructor:

function AppBuilder(baseElement)
{
  this.base = baseElement;
}
Enter fullscreen mode Exit fullscreen mode

Basic stuff. Just like in Vue.js, you pass in the base element, the children of which will be subject to the whims of the framework.

Next up is the initialisation function AppBuilder.prototype.init = function(viewmodel). The first step is a little bit of setup

    const elements = this.base.getElementsByTagName('*');
Enter fullscreen mode Exit fullscreen mode

Get all of the elements in the base element.

    this.originalViewmodel = viewmodel;
Enter fullscreen mode Exit fullscreen mode

Store the original data that gets passed in.

    this.elements = {};
Enter fullscreen mode Exit fullscreen mode

Create a map for the elements that will be controlled by the framework.

    const _internal = this;
Enter fullscreen mode Exit fullscreen mode

Avoid any shenanigans with this.

Now we come to the meat and potatoes, setting up a proxy (this.data = new Proxy(...) of an object, intercepting getting and setting of properties:

get(target, name, receiver) 
{
  if (!(name in _internal.originalViewmodel.data))
  {
    return undefined;
  }

  return _internal.originalViewmodel.data[name];
},
Enter fullscreen mode Exit fullscreen mode

Quite straight forward. If a property exists in the data section of the original viewmodel, return the value of that, otherwise return undefined.

set(target, name, value, receiver) 
{
  if (!(name in _internal.originalViewmodel.data))
  {
    console.warn(`Setting non-existent property '${name}', initial value: ${value}`);
  }

  _internal.originalViewmodel.data[name] = value;

  for(let el of _internal.elements[name])
  {
    _internal.updateElement(el);
  }     

  return _internal.originalViewmodel.data[name];
}
Enter fullscreen mode Exit fullscreen mode

A little bit of monkey business. Assigning to things that don't currently exist in the original viewmodel data section creates them in the section, then all the elements whose data-model is set to the name of the property being set get updated with the new value. Finally the value is returned.

Finally the child elements of the base elements that have the data-model attribute set get added to the this.elements map:

for(let el of elements)
{
  if('model' in el.dataset)
  {
    if(el.dataset.model in this.elements)
    {
      this.elements[el.dataset.model].push(el);
    }
    else
    {
      this.elements[el.dataset.model] = [el];
    }

    this.updateElement(el);
  }
} 
Enter fullscreen mode Exit fullscreen mode

OK! That's the biggest chunk. One last function and we can wrap up.

AppBuilder.prototype.updateElement = function(el)
{
  const property = el.dataset.model;
  el.textContent = this.data[property];
};
Enter fullscreen mode Exit fullscreen mode

This is the updateElement method that is being called in the setter function.

It gets the model name from data-model and then uses that to get the value from the viewmodel's data section and write that value into the textContent of the supplied element.

Step 3 – Put it into action

const appElement = document.getElementById('app');

const app = new rjsf(appElement);

const viewmodel = 
{
  data:
  {
    message: 'Hello, World!',
  },
};

app.init(viewmodel);
Enter fullscreen mode Exit fullscreen mode

A quick refresher of the HTML this applies to:

<div id="app">
  <span data-model="message"></span>
</div>
Enter fullscreen mode Exit fullscreen mode

So put it all together and you will see that the span has the value "Hello, World!"!

Next time I will investigate the onclick handler.

Please let me know any thoughts or questions you have in the comments below.

❤️, 🦄, share, and follow for the next instalment!

Top comments (0)