Error Handling in RxJS
Rate Limiting an Express App
How RxJS groupBy()
Works
Part 2: JSX
So in part 1, I described why I decided to embark on coding yet another frontend framework. If you haven't checked it out, I highly recommend reading it first, as it outlines our goals for this (and subsequent parts).
The most essential part of any frontend framework is describing the frontend elements (duh), which in our case means HTML trees. While HTML itself is a really appropriate syntax for describing those trees (again, duh), its own syntax does not support incorporation of a separate data-source, which is essential for creating dynamic UIs.
So, to get started with our awesome Yet Another Frontend Framework, we need an easy way of describing HTML imbued with dynamic data. Additionally our solution should be:
Composable and extensible
i.e. it doesn't unnecessarily limit you,
i.e. you can re-use stuff,
Pretty easy to learn
The classic answer to that question is templating. If you don't know what templating is, think of it as an HTML file with some holes (called a template). You can fill those holes with your data, getting a final, static HTML the browser can display.
For example, this is an HTML template:
1link<div>Hellow {{ name }}!</div>
Now if you combine it with
1link{ name: 'World' }
You get
1link<div>Hellow World!</div>
If you have ever seen Angular, Vue.js, or Svelte, they are all based on templating, using actually pretty similar templating syntax:
1link<div>Hellow {{name}}!</div>
1link<div>Hellow {{name}}!</div>
1link<div>Hellow {name}!</div>
There are a wide variety of templating languages out there, but to keep the chase short, let's just go ahead with Nunjucks's syntax:
Its mostly HTML, so relatively easy to learn
It allows for template re-use, and custom filters, which means it also satisfies our composability and extensibility requirements.
We are not going to actually use Nunjucks, since it is designed for rendering HTML strings while we need to create our elements using DOM APIs, as discussed in part 1. We are just going to assume a templating solution with Nunjuck's syntax.
With such a design, our sample timer code from part 1 would look like this:
1linkimport { render, fromTemplate } from 'yet-another-frontend-framework';
2linkimport { timer } from 'rxjs';
3link
4linkrender(
5link fromTemplate(
6link 'timer.html', // @see tab:timer.html
7link { count: timer(0, 1000 )}
8link ),
9link document.body
10link);
1link<div>You have been here for {{count}} seconds!</div>
2link<!-- thats it folks! -->
Before going through the practical issues with templating, lets get the philosophical discussion out of the way. The main argument in favor of templating vs non-templating solutions (i.e. language extensions such as JSX), is Rule of Least Power.
Simply put, the rule says web stuff should be described using the least powerful description syntax possible (to avoid unnecessarily complicating it). The argument then goes: HTML trees imbued with data are web stuff, and Javascript is in the most powerful class of languages, so we should avoid using it for describig HTML trees and instead use a less powerful but powerful enough syntax.
While the rule itself is a respectible sentiment that I personally subscribe to, it is hard to pin-point what does powerful enough mean, at least for our use case. For example, all of the frameworks and templating tools described above have given up on stringently enforcing that limitation, by supporting custom filters and arbitrary Javascript functions.
So instead of delving into such hypothetical musings of what should things be idealistically, lets focus on how things do work with different solutions and how does that look like in practice:
In the example code above, we are referring to timer.html
:
1linkrender(
2link fromTemplate(
3link 'timer.html',
4link { count: timer(0, 1000 )}
5link ),
6link document.body
7link);
Since we want our templates to be re-usable, we must also be able to refer (and include) templates from within templates as well:
1link{% include 'some-other.html' %}
This simply means we are introducing another module system to our framework (alongside Javascript's) with its own module resolution strategy. This problem is further compounded by the fact that we need to (somehow) include Javascript functions within our templates as well (to satisfy the extensibility requirement).
We can of-course shift this additional complexity from our code to our tool-chain, for example using some Webpack loaders, but the complexity is still there, now in form of tool-chain the framework is dependent on.
A 'pro' of templating is that it separated the view from the logic. However, that separation is not
as clear-cut as some like to believe. For example, imagine we want to display a text that alters between Even
and Odd
every second:
1linkimport { render, fromTemplate } from 'yet-another-frontend-framework';
2linkimport { timer } from 'rxjs';
3linkimport { map } from 'rxjs/operators';
4link
5linkrender(
6link fromTemplate(
7link 'alter.html', // @see tab:alter.html
8link { parity: timer(0, 1000).pipe(map(x => x % 2 === 0 ? 'Even' : 'Odd')) }
9link ),
10link document.body
11link);
1link<div>{{parity}} seconds have passed since you've been here!</div>
2link<!-- thats it folks! -->
It looks like the strings 'Even'
and 'Odd'
are view stuff that belong to alter.html
,
but we would need to make this code much more complicated to move them there.
Also, its not clear whether the x % 2 === 0
part is view or logic.
The separation brings a constant cognitive burden of deciding what is view and what is
logic when writing code, or makes it hard to find something when debugging, because you
do not know whether the person who wrote the code thought of it as view or as logic.
Ok what if there is a typo in the template, e.g. writing counter
instead of count
?
1link<div>You have been here for {{counter}} seconds!</div> <!--> ERRR, counter is not defined? -->
Well the default Javascript linter doesn't provide any hints by default, so best case scenario I will get an error for this after I've written the template and executed it. Furthermore, when I'm consuming the template in Javascript, I need to constantly go back to the template to check whether I am providing all the needed variables with correct names or not.
We can also create a linter that fixes these issues, but again, we're just shifting the complexity from the code to the tool-chain our nice framework is going to be dependent on.
If you want to loop over an array in Nunjucks syntax, you should do something like this:
1link{% for item in items %}
2link<p>{{item.name}}</p>
3link{% endfor %}
Each of the frameworks mentioned above also have their own special syntax for this:
1link<p *ngFor="let item in items">{{item.name}}</p>
1link<p v-for="item in items">{{item.name}}</p>
1link{# each items as item}
2link<p>{item.name}</p>
3link{/each}
Additionally, the code between {{
and }}
is not Javascript, so what happens if I wanted
to call a method on each item
instead of just accessing a property?
Well, someone needed to know Javascript and HTML to be able to work with our nice framework, now they also need to learn these extra syntaxes and find the specific answers to these questions.
To resolve all these issues, we would need a solution that:
Well, there is this extension of Javascript called JSX, which specifically satisfies all of these requirements. It is basically a syntactic sugar for Javascript that allows creating objects (for example, DOM Objects) using HTML syntax:
1linkconst myDiv = <div>Hellow there!</div>;
This would make our timer code look like this:
1linkimport { render } from 'yet-another-frontend-framework';
2linkimport { timer } from 'rxjs';
3link
4link
5linkrender(<div>You have been here for {timer(0, 1000)} seconds!</div>, document.body);
But doesn't JSX itself require additional tooling on top?
It does. However:
It is just one tool in the tool-chain that solves all of the problems (i.e. module resolution, scopes, etc), instead of multiple ones.
It is just a syntactic sugar, meaning that people could use our framework without it, without getting any of the aforementioned issues (though it will be less convenient),
TypeScript supports it (almost) out of the box, which means zero additional tooling for anyone using Typescript.
As mentioned above, enforcing a general separation between description of the DOM tree and the logic behind it can cause more problems than it solves.
However, there are valid concerns about unchecked mixing of HTML tree representation and Javascript code, as it can lead to code that is pretty difficult to read/understand/debug (simply because there are two interwoven syntaxes instead of one).
With JSX, we are NOT enforcing a separation strategy, but we are also not barring it by any means. If anything, people can actually be much more flexible on how they split their codes, depending on their project's needs, and how they re-use view code or integrate it into logic code, while still benefiting from explicit scoping, a unified module resolution strategy, and a familiar syntax.
For example, we can still break our timer code into view and logic bits:
1linkimport { render } from 'yet-another-frontend-framework';
2linkimport { timer } from 'rxjs';
3linkimport view from './timer.view'; // @see tab:View Code
4link
5linkrender(view(timer(0, 1000))).on(document.body);
1linkexport const counter => (
2link <div>You have been here for {counter} seconds!</div>
3link);
JSX (or TSX, the TypeScript version) code is converted to some straight-forward function calls based on your configurations. Since it was introduced alongside React, JSX code is by default converted to React function calls. So this:
1linkconst myDiv = <div>Hellow World!</div>
is by default translated to this:
1linkconst myDiv = React.createElement('div', {}, 'Hellow World!');
React by default doesn't create HTML Elements directly, as it first renders to a virtual DOM and diffs that with the actual DOM. However, as discussed in Part 1, one of the main goals of our framework was to render everything once and instead subscribe the changing nodes to the changing values.
To that end, we need to provide a replacement for React.createElement()
that works with the same interface (so TypeScript or Babel can convert JSX to its invokations)
but produces DOM Objects using browser's own APIs:
1linkexport class Renderer {
2link public create(tag, props, ...children) { // --> So this is our main function
3link // --> STEP 1: create the element
4link const el = document.createElement(tag); // --> STEP 1: create the element
5link
6link // --> STEP 2: set its properties
7link if (props) // --> STEP 2: set its properties
8link Object.entries(props) // --> STEP 2: set its properties
9link .forEach(([prop, target]) => this.setprop(prop, target, el)); // --> STEP 2: set its properties
10link
11link // --> STEP 3: add its children
12link children.forEach(child => this.append(child, el)); // --> STEP 3: add its children
13link
14link return el;
15link }
16link
17link public setprop(prop, target, host) {
18link if (typeof target === 'boolean' && target) host.setAttribute(prop, ''); // --> Yep boolean attributes are a bit different
19link else host.setAttribute(prop, target.toString()); // --> Set other attributes normally
20link }
21link
22link public append(target, host) {
23link if (target instanceof Node) host.appendChild(target); // --> So append another Node casually
24link else if (Array.isArray(target)) target.forEach(_ => this.append(_, host)); // --> Append each member of an array
25link else host.appendChild(document.createTextNode(target.toString())); // --> Append the string value for other stuff
26link }
27link}
Now we can use this class to have JSX that uses browser's APIs to directly creat DOM Elements:
1link/** @jsx renderer.create */ // --> So this line tells our JSX parser how to work with JSX. It is typically configurable globally for a project as well
2linkimport { Renderer } from 'yet-another-frontend-framework';
3linkconst renderer = new Renderer();
4link
5linkconst myDiv = <div>Hellow World!</div>;
Which will be converted to:
1linkimport { Renderer } from 'yet-another-frontend-framework';
2linkconst renderer = new Renderer();
3link
4linkconst myDiv = renderer.create('div', {}, 'Hellow World!');
touch_app NOTE
In React,
React
itself is a singleton object andReact.createElement()
a static function. In our case however, we are not using a singleton renderer, but instead binding the JSX create function (sometimes called the JSX factory function) to a variable (renderer
) that must be defined in each scope.This might seem like more work (import and create instead of just import), but it gives us the additional flexibility to use different renderers in different scopes. For example, a component can use a scoped renderer that applies some implicit styling. Since that renderer is limited to the scope of that component, the styles it applies would also naturally be scoped, which means we've solved style scoping simply using Javascript scopes.
With our brand new Renderer
, our timer component would look like this:
1link/** @jsx renderer.create */
2linkimport { Renderer } from 'yet-another-frontend-framework';
3linkimport { timer } from 'rxjs';
4link
5linkconst renderer = new Renderer();
6linkrenderer.append(<div>You have been here for {timer(0, 1000)} seconds!</div>, document.body);
Well this actually would not render our beloved timer, but instead it would render something like this:
You have been here for [object Object] seconds!
Why? Because our Renderer
uses .toString()
method to render all objects, and
1linkconsole.log(timer(0, 1000).toString());
2link// > [object Object]
In other words, our Renderer
class is a static renderer, as it cannot render dynamic content on the DOM tree.
There is still much work to be done to complete our Yet Another Frontend Framework.
In the next part, we'll design a mechanism that allows extending the behaviour of our Renderer
,
for example to allow it to properly render Observable
s. You can check the actual, final code of
the static (or raw) renderer here,
and stay tuned for upcoming parts!
Hero Image by Zbysiu Rodak from Unsplash.
Templating vs JSX, which one is the best option for our frontend framework?
Part 2: JSX
React, Angular, Vue.js, Svelte, Nunjucks, Templates, JSX, Templating, Frontend, Javascript, TypeScript