Yet Another Frontend Framework 

Part 2: JSX

linkPreviously ...

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).


linkThe Question

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:


linkTemplating

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:

1<div>Hellow {{ name }}!</div>

Now if you combine it with

1{ name: 'World' }

You get

1<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:

1<div>Hellow {{name}}!</div>

1<div>Hellow {{name}}!</div>

1<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:

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:

timer.js
1import { render, fromTemplate } from 'yet-another-frontend-framework';

2import { timer } from 'rxjs';

3

4render(

5 fromTemplate(

6 'timer.html', // @see tab:timer.html

7 { count: timer(0, 1000 )}

8 ),

9 document.body

10);

timer.html
1<div>You have been here for {{count}} seconds!</div>

2<!-- thats it folks! -->


linkIssues With Templating

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:

linkParallel Module Resolution

In the example code above, we are referring to timer.html:

1render(

2 fromTemplate(

3 'timer.html',

4 { count: timer(0, 1000 )}

5 ),

6 document.body

7);

Since we want our templates to be re-usable, we must also be able to refer (and include) templates from within templates as well:

1{% 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.

linkUnclear Separation

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:

alter.js
1import { render, fromTemplate } from 'yet-another-frontend-framework';

2import { timer } from 'rxjs';

3import { map } from 'rxjs/operators';

4

5render(

6 fromTemplate(

7 'alter.html', // @see tab:alter.html

8 { parity: timer(0, 1000).pipe(map(x => x % 2 === 0 ? 'Even' : 'Odd')) }

9 ),

10 document.body

11);

alter.html
1<div>{{parity}} seconds have passed since you've been here!</div>

2<!-- 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.

linkUnclear Scope

Ok what if there is a typo in the template, e.g. writing counter instead of count?

1<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.

linkExtra Stuff To Learn

If you want to loop over an array in Nunjucks syntax, you should do something like this:

1{% for item in items %}

2<p>{{item.name}}</p>

3{% endfor %}

Each of the frameworks mentioned above also have their own special syntax for this:

1<p *ngFor="let item in items">{{item.name}}</p>

1<p v-for="item in items">{{item.name}}</p>

1{# each items as item}

2<p>{item.name}</p>

3{/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.


linkJSX To The Rescue

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:

1const myDiv = <div>Hellow there!</div>;

This would make our timer code look like this:

1import { render } from 'yet-another-frontend-framework';

2import { timer } from 'rxjs';

3

4

5render(<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:

linkSeparation

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:

timer.logic.js
1import { render } from 'yet-another-frontend-framework';

2import { timer } from 'rxjs';

3import view from './timer.view'; // @see tab:View Code

4

5render(view(timer(0, 1000))).on(document.body);

timer.view.jsx
1export const counter => (

2 <div>You have been here for {counter} seconds!</div>

3);


linkA JSX/TSX Renderer

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:

1const myDiv = <div>Hellow World!</div>

is by default translated to this:

1const 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:

1export class Renderer {

2 public create(tag, props, ...children) { // --> So this is our main function

3 // --> STEP 1: create the element

4 const el = document.createElement(tag); // --> STEP 1: create the element

5

6 // --> STEP 2: set its properties

7 if (props) // --> STEP 2: set its properties

8 Object.entries(props) // --> STEP 2: set its properties

9 .forEach(([prop, target]) => this.setprop(prop, target, el)); // --> STEP 2: set its properties

10

11 // --> STEP 3: add its children

12 children.forEach(child => this.append(child, el)); // --> STEP 3: add its children

13

14 return el;

15 }

16

17 public setprop(prop, target, host) {

18 if (typeof target === 'boolean' && target) host.setAttribute(prop, ''); // --> Yep boolean attributes are a bit different

19 else host.setAttribute(prop, target.toString()); // --> Set other attributes normally

20 }

21

22 public append(target, host) {

23 if (target instanceof Node) host.appendChild(target); // --> So append another Node casually

24 else if (Array.isArray(target)) target.forEach(_ => this.append(_, host)); // --> Append each member of an array

25 else host.appendChild(document.createTextNode(target.toString())); // --> Append the string value for other stuff

26 }

27}


Now we can use this class to have JSX that uses browser's APIs to directly creat DOM Elements:

1/** @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

2import { Renderer } from 'yet-another-frontend-framework';

3const renderer = new Renderer();

4

5const myDiv = <div>Hellow World!</div>;

Which will be converted to:

1import { Renderer } from 'yet-another-frontend-framework';

2const renderer = new Renderer();

3

4const myDiv = renderer.create('div', {}, 'Hellow World!');

touch_app NOTE

In React, React itself is a singleton object and React.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.


linkThe Show Must Go On ...

With our brand new Renderer, our timer component would look like this:

1/** @jsx renderer.create */

2import { Renderer } from 'yet-another-frontend-framework';

3import { timer } from 'rxjs';

4

5const renderer = new Renderer();

6renderer.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

1console.log(timer(0, 1000).toString());

2// > [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 Observables. 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.

Previously ...The QuestionTemplatingIssues With TemplatingParallel Module ResolutionUnclear SeparationUnclear ScopeExtra Stuff To LearnJSX To The RescueSeparationA JSX/TSX RendererThe Show Must Go On ...

Home

On Reactive Programmingchevron_right
Yet Another Frontend Frameworkchevron_right
Other Articleschevron_right