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:
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>
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);
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>
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;
})();
So, the first thing is our constructor:
function AppBuilder(baseElement)
{
this.base = baseElement;
}
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('*');
Get all of the elements in the base element.
this.originalViewmodel = viewmodel;
Store the original data that gets passed in.
this.elements = {};
Create a map for the elements that will be controlled by the framework.
const _internal = this;
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];
},
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];
}
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);
}
}
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];
};
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);
A quick refresher of the HTML this applies to:
<div id="app">
<span data-model="message"></span>
</div>
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)