Browse Source

rename declarativ blog, finalize content

develop
James Fenn 9 months ago
parent
commit
363214617b
1 changed files with 69 additions and 15 deletions
  1. +69
    -15
      blog/2021-08-17-Declarativ-Templating.md

blog/2021-03-10-Declarativ-Templating.md → blog/2021-08-17-Declarativ-Templating.md View File

@@ -2,10 +2,14 @@
layout: layouts/blog
title: "Declarativ: an experimental React-like HTML framework"
description: "A breakdown of the concepts used in Declarativ, a lightweight JavaScript HTML templating library."
date: "2021-03-10"
date: "2021-08-17"
links:
- name: "Declarativ"
url: "https://jfenn.me/projects/declarativ/"
- name: "React JSX"
url: "https://reactjs.org/docs/introducing-jsx.html"
- name: "Reactive programming"
url: "https://en.wikipedia.org/wiki/Reactive_programming"
tags: blog
---

@@ -17,7 +21,7 @@ Of course, this was still an experiment, will not be maintained, and isn't somet

## Design

There are many advantages to "declarative" templating in any programming language, as it can offer huge benefits to the stability and simplicity of an application. Rather than creating a layout and locating nodes with `findViewById()` to update their attributes manually, declarative programming enables automatic state updates simply by specifying in the layout where the data should come from.
There are many advantages to "declarative" templating in any programming language, as it can offer huge benefits to the stability and simplicity of an application. Rather than creating a layout and locating nodes with `getElementById()` to update their attributes manually, declarative programming enables automatic state updates simply by specifying where the data should come from in the layout.

```js
container(
@@ -31,18 +35,28 @@ container(

The process of manipulating these updates manually can be a significant source of error - if a piece of data is used in multiple places, it can be easy to forget about an element that uses it, or create an illegal state when an update shouldn't occur.

```js
```html
<div class="jumbotron">
<h1>This is a big header.</h1>
<button id="doSomething">Do something</button>
<p id="greyParagraph"></p>
</div>

<script>
$.get("https://loripsum.net/api/plaintext").then((string) => {
const element = document.getElementById('paragraph'); // does this element even exist?
const element = document.getElementById('grayParagraph'); // does this element even exist?
element.innerText = string;
});
</script>
```

Declarative programming lends itself more to an "observer" pattern, where you specify the data source, and the elements decide to 'observe' it. The Declarativ library functions similarly to this - while I focused more on the initial collection of asynchronous data, it is entirely possible to observe and update the created elements afterwards with little effort.
Declarative programming lends itself more to an "observer" pattern, where you specify the data source, and the elements decide to 'observe' it. This is also known as [Reactive Programming](https://en.wikipedia.org/wiki/Reactive_programming) - where variables and data are described as a process of change that implicitly update any components that use them.

The Declarativ library functions similarly to this; while I focused more on the initial collection of asynchronous data, it is entirely possible to observe and update the created elements with little effort.

### Variadic Functions

Commonly known as "varargs," this is the backbone of Declarativ: the ability for any function to receive an indefinite amount of arguments. This means that, rather than using a function like `createElement()` with defined parameters, I can _write a separate function for each HTML element and pass child elements to it as arguments._
Commonly known as "varargs," this is the backbone of Declarativ's API: the ability for any function to receive an indefinite amount of arguments. This means that, rather than using a function like `createElement()` with defined parameters, I can _write a separate function for each HTML element and pass child elements to it as arguments._

```js
div(
@@ -110,9 +124,9 @@ async resolveChildren(data: any) : Promise<ComponentNode[]> {

### Asynchronous Templating

With the complex polymorphism issues out of the way, I still needed to consider how elements with asynchronous components should be handled. Obviously, in the case of an `h1("Hello, world!")`, all of the information I need to create a template is immediately provided. However, what about a more complex scenario, such as `h1("Hello, ", db.fetchUserName())`? In this case, `db.fetchUserName()` returns a Promise, which the `h1` element will have to wait for before it can be templated. However, I might not want to hold up the entire element tree to await a single data source when the rest of the page can be templated without it.
With the complex polymorphism issues out of the way, I still needed to consider how elements with asynchronous components should be handled. Obviously, in the case of an `h1("Hello, world!")`, all of the information I need to create a template is immediately provided. However, what about a more complex scenario, such as `h1("Hello, ", db.fetchUserName())`? In this case, `db.fetchUserName()` might return a Promise, which the `h1` element will have to wait for before it can be templated. However, I might not want to hold up the entire element tree to await a single data source when the rest of the page can be rendered without it.

I solved this by inserting an empty placeholder element into the DOM with a unique id for each asynchronous child, and using `Promise.all` to process all child elements in parallel (or, at least, the single-threaded JavaScript equivalent of that). This means that, if the `h1()` is next to a `p()` that can be templated instantly, it will appear on the page first, while the `h1()` can wait to replace its placeholder element after it has finished loading.
I solved this by inserting an empty placeholder element into the DOM with a unique id for each asynchronous child, and using `Promise.all` to await all child elements in parallel (or, at least, the single-threaded JavaScript equivalent of that). This means that, if the `h1()` is next to a `p()` that can be templated instantly, it will appear on the page first, while the `h1()` can wait to replace its placeholder element after it has finished loading.

```typescript
let innerHtml = "";
@@ -138,21 +152,61 @@ await Promise.all(Object.keys(components).map(async (id) => {

Again - like all the previous examples - this code is still a simplified version of [what Declarativ actually does](https://github.com/fennifith/declarativ/blob/68cd92e61d39b1dcdee555c68256fc71ca1bda85/src/render/dom-render.ts#L22), as there are even more edge cases and extra conditions that it needs to handle.

## Usage
A lot of these cases were only discovered during actual use, such as the possibility for a single component to result in more than one root element - for example, if an asynchronous function returns an array of other nodes. This was also when I discovered how clunky my state-updating mechanism was when compared to other frameworks.

### Reactivity

[Svelte](https://svelte.dev/tutorial/reactive-declarations) is one example that I think approaches this really well. The framework keeps track of where any variable is used in the document, and the Svelte compiler inserts an update function after every place that its value is modified - providing automatic state updates for any variable.

(data source example)
```html
<script>
let count = 0;

function handleClick() {
count++;
}
</script>

<button on:click={handleClick}>
Clicked {count} times!
</button>
```

### Functional Abstractions
Needless to say, this isn't possible in Declarativ as I specifically wanted to avoid a compilation process. However, it would be interesting to explore different ways to accomplish this in plain JavaScript.

(function returning a Component)
Other frameworks have different mechanisms for creating state updates: with the [`useState`](https://reactjs.org/docs/hooks-state.html) hook, React allows for state "objects" to be associated with each defined component. When the state is updated, React simply re-renders the component it resides in.

### Updating Nodes
```jsx
function CountButton() {
const [count, setCount] = useState(0);

return (
<button onclick={() => setCount(count + 1)}>
Clicked {count} times!
</button>
);
}
```

The approach I used in declarativ is closer to how refs are used in React/Vue, though a little more verbose (making use of [Proxy objects](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy)):

```js
function CountButton() {
const state = declarativ.observe({ count: 0 });

return button(() => `Clicked ${state.proxy.count} times!`)
.on("click", () => {
state.proxy.count += 1;
})
.bind(state);
}
```

(similar to react refs)
While this is very flexible, it is much harder to work with than other frameworks which create these state associations automatically. Needing to invoke a specific `bind()` function, while more verbose, effectively creates the same problems as manual DOM manipulation with connecting a data source to its consumer.

## Takeaways

One of the major things I've learnt while using this library is that, while it certainly makes it fast to iterate over different designs, *mixing your application logic directly with the UI structure is a horrible idea!* Providing easy access to DOM events with the `.on('event', () => {}))` function was very intuitive, but I found that it often allowed me to write very confusing and cluttered logic directly inside the event handlers, when I should've been separating them into external functions.
One of the major things I've learnt while using this library is that, while it certainly makes it fast to iterate over different designs, *mixing your application logic directly with the UI structure is a horrible idea!* Providing easy access to DOM events with the `.on('event', () => {}))` function was very intuitive, but I found that it often allowed me to write very confusing and cluttered logic directly inside the event handlers, when I should've been separating it into external functions.

This might be why many developers like to categorize their work into architectural patterns such as MVC or MVVM - to ensure an appropriate separation of concerns and prevent applications from accumulating this "UI clutter" that can be very difficult to maintain.


Loading…
Cancel
Save