Constructing a Frontend Framework; Reactivity and Composability With Zero Dependencies
Constructing a Frontend Framework; Reactivity and Composability With Zero
Dependencies
Earlier than I begin—to set some context—by frontend framework what I imply is, a
framework that enables us to keep away from having to put in writing common
outdated HTML and JavaScript resembling this:
<p id="cool-para"></p>
<script>
const coolPara = 'Lorem ipsum.';
const el = document.getElementById('cool-para');
el.innerText = coolPara;
</script>
and instead allows us to write magical HTML and JavaScript code such as
this (Vue):
<script setup>
const coolPara = 'Lorem ipsum.';
</script>
<template>
<p>{{ coolPara }}</p>
</template>
or this (React):
and the benefit of such a framework is understandable. Remembering words
or phrases such as document
, innerText
, and
getElementById
are difficult—so many syllables!
Okay, syllable count isn’t the main reason.
Reactivity ✨
The first main reason is that, in the second and third examples,
we can just set or update the value of the variable
coolPara
and the markup—i.e. the <p>
element—is updated without without explicitly having to set its
innerText
.
This is called reactivity, the UI is tied to the data in such a
way that just changing the data updates the UI.
Composability ✨
The second main reason is the ability to define a component and
reuse it without having to redefine it every time we need to use it. This
is called composability.
Regular HTML + JavaScript does not have this by default. And so the
following code does not do what it feels like it should:
<!-- Defining the component -->
<component name="cool-para">
<p>
<content />
</p>
</component>
<!-- Using the component -->
<cool-para>Lorem ipsum.</cool-para>
Reactivity and composability are the two main things the usual frontend
frameworks such as Vue, React, etc give us.
These abstractions aren’t granted for free, one has to front-load a bunch
of framework specific concepts, deal with their leakiness when things work
in inexplicably magical ways, and not to mention, a whole load of
failure-prone dependencies.
But, it turns out that using modern Web APIs these two things aren’t very
hard to achieve. And most use cases we might not actually need the usual
frameworks and their cacophony of complexities…
Reactivity
A simple statement that explains reactivity is
when the data updates, update the UI automatically.
The first part is to know when the data updates. This
unfortunately is not something a regular object can do. We can’t
just attach a listener called ondataupdate
to listen to data
update events.
Fortunately JavaScript has just the thing that would allow us to do this,
it’s called
Proxy
.
Proxy
Objects
Proxy
permits us to create a proxy object from a
common object:
and this proxy object can then listen to changes to the data.
In the example above we have a proxy object, but it is not really
doing anything when it comes to know that name
has changed.
For that we need a handler, which is an object that tells the
proxy object what to do when the data is updated.
// Handler that listens to data assignment operations
const handler = {
set(user, value, property) {
console.log(`${property} is being updated`);
return Reflect.set(user, value, property);
},
};
// Creating a proxy with the handler
const user = { name: 'Lin' };
const proxy = new Proxy(user, handler);
Now whenever we update name
using the proxy
object, we’ll get a message saying
"name is being updated"
.
If you’re wondering,
What’s the big deal, I could’ve done this using a regular old
setter, I’ll let you know the deal:
-
The proxy technique is generalized, and handlers may be reused, which suggests
that… -
Any worth you set on a proxied object may be recursively
transformed right into a proxy, which implies that… -
You now have this magical object with the power to react to
information updates irrespective of how nested it’s.
Aside from this you possibly can
handle
a number of different entry occasions resembling when a property is
read,
updated,
deleted, and so on.
Now that we have now the power to hearken to hearken to operations, we have to
react to them in a significant approach.
Updating the UI
In case you recall, The second a part of reactivity was
replace the UI mechanically. For this we have to fetch the
acceptable UI component to be up to date. However earlier than that that we
must first mark a UI component as acceptable.
To do that we’ll use
data-attributes, a characteristic that enables us to set arbitrary values on a component:
The nicety of data-attributes are that we can now find all the
appropriate elements using:
Now we just set the innerText
of all the
appropriate elements:
const handler = {
set(user, value, property) {
const query = `[data-mark="${property}"]`;
const elements = document.querySelectorAll(query);
for (const el of elements) {
el.innerText = value;
}
return Reflect.set(user, value, property);
},
};
// Regular object is omitted cause it's not needed.
const user = new Proxy({ name: 'Lin' }, handler);
That’s it, that’s the crux of reactivity!
Because of the general nature of our handler
, for
any property of user
that is set, all the
appropriate UI elements will be updated.
That’s how powerful the JavaScript Proxy
features are, with
zero dependencies and some cleverness it can give us these magical
reactive objects.
Now onto the second main thing…
Composibility
Turns out, browsers already have an entire feature dedicated to this
called
Web Components, who knew!
Few use it trigger it’s a little bit of a ache within the ass to make use of (and in addition as a result of
most attain out for the standard frameworks as a default when beginning a
challenge, no matter the scope).
For composability we first must outline the parts.
Defining parts utilizing template
and slot
The <template>
tags are used to include markup which is
not rendered by the browser. As an illustration, you possibly can add the next
markup in your HTML:
and it won’t be rendered. You can think of them as invisible containers
for your components.
The next building block is the <slot>
element which
defines where the content of a component will be placed in it. This
enables a component to be reused with different content, i.e it becomes
composable.
For example, here’s an h1 element that colors its text red.
Before we get to using our components—like the red h1 above, we need to
register them.
Registering the Components
Before we can register our red h1 component, we need a name to register it
by. We can just use the name
attribute for that:
And now, using some JavaScript we can get the component and its name:
const template = document.getElementsByTagName('template')[0];
const componentName = template.getAttribute('name');
and then finally register it using
customElements.define
:
customElements.define(
componentName,
class extends HTMLElement {
constructor() {
super();
const component = template.content.children[0].cloneNode(true);
this.attachShadow({ mode: 'open' }).appendChild(component);
}
}
);
There is a lot going on in the block above:
-
We are calling
customElements.define
with two arguments. -
First argument is the component name (i.e.
"red-h1"
). -
Second argument is a class that defines our custom component as an
HTMLElement
.
What we are doing in the class constructor is using a copy of the template
red-h1
to set the shadow DOM tree.
What’s the Shadow DOM?
The shadow DOM is what sets the styling of a several default elements
such as a
range input, or a
video element.
The shadow DOM of a component is hidden by default which is why we will’t
see it within the dev console, however right here’re we’re setting the
mode
to 'open'
.
This enables us to examine component and see that the purple coloured h1 is
hooked up to the
#shadow-root
.
Calling customElements.outline
will permit us to make use of the
outlined element like a daily HTML component.
Onto putting these two concepts together!
Composability + Reactivity
A quick recap, we did two things:
-
We created a reactive data structure i.e. the proxy objects
which on setting a value can update any element we have marked as
appropriate. -
We defined a custom component
red-h1
which will render it’s
content as a red h1.
We can now put them both together:
<div>
<red-h1 data-mark="name"></red-h1>
</div>
<script>
const user = new Proxy({}, handler);
user.name = 'Lin';
</script>
and have a custom component render our data and update the UI when we
change the data.
Of course the usual frontend frameworks don’t just do this, they have
specialized syntax such the
template syntax
in Vue, and
JSX
in React that makes writing complicated frontends comparatively extra concise that
it in any other case could be.
Since this specialised syntax is just not common JavaScript or HTML, it’s not
parsable by a browser and they also all want specialised instruments to compile
them right down to common JavaScript, HTML, and CSS earlier than the browser can
perceive them. And so,
no body writes JavaScript any more.
Even with out specialised syntax, you are able to do loads of what the standard
frontend framework does—with related conciseness—simply through the use of
Proxy
and WebComponents
.
The code right here is an over simplification and to transform it right into a framework
you’d need to flesh it out. Right here’s my try at doing simply that: a
framework known as
Strawberry.
As I develop this, I plan on sustaining two onerous constraints:
- No dependencies.
- No build-step earlier than it may be used.
And a gentle constraint of holding the code base tiny. On the time of
writing it’s only a
single file
with fewer than 400
CLOC, let’s see the place it goes. ✌️