View on GitHub

nclearseam

front-end templating for Nim with direct DOM modification and no specific syntax

How can we combine Svelte.js with Nim as a language. More specifically, what I’d like to see is a web frontend micro-framework which:

The idea is to get inspired by Svelte v2 which was simpler, and convert it to Nim. Specifically, Svelte v2 only compiles HTML templates (where Svelte v3 compiles also your code so you can have reactive javascript). The compiled HTML template is then available as a library import.

We can imagine adding a little into the mix by using concepts from weld and pure.js (that I already taken into clearseam). The idea here is to avoid reinventing a template markup, and split the template in two:

It works in a pipeline where the description object is first compiled with the DOM as argument. The generated function is then called with the data.

But first, let’s investigate how exactly Svelte works. Here is the compiled version of:

<h1>Hello {name}!</h1>

The generated javascript has many helper functions to wrap DOM operations. For clarity, I inlined those.

/* App.html generated by Svelte v2.16.1 */

function create_main_fragment(component, ctx) {
	var h1, text0, text1, text2;

	return {
		c() {
			h1 = document.createElement("h1");
			text0 = document.createTextNode("Hello ");
			text1 = document.createTextNode(ctx.name);
			text2 = document.createTextNode("!");
		},

		m(target, anchor) {
			target.insertBefore(h1, anchor);
			h1.appendChild(text0);
			h1.appendChild(text1);
			h1.appendChild(text2);
		},

		p(changed, ctx) {
			if (changed.name) {
				text1.data = '' + ctx.name;
			}
		},

		d(detach) {
			if (detach) {
				h1.parentNode.removeChild(h1);
			}
		}
	};
}

Here, create_main_fragment is just the translation of the HTML template into an object that has 4 methods:

With this, it should be easy to create an equivalent for Nim. The key is to have every DOM node available as a variable, and when the value changes, it updates the variable. Conditionals can be handled by updating the child node, detaching them or attaching them as required.

Loops might be trickier, let’s see how the following input is handled:

<ul>
  {#each children as child}
    <li>
      name: {child.name}
    </li>
  {/each}
</ul>
/* App.html generated by Svelte v2.16.1 */

function get_each_context(ctx, list, i) {
	const child_ctx = Object.create(ctx);
	child_ctx.child = list[i];
	return child_ctx;
}

function create_main_fragment(component, ctx) {
	var ul;

	var each_value = ctx.children;

	var each_blocks = [];

	for (var i = 0; i < each_value.length; i += 1) {
		each_blocks[i] = create_each_block(component, get_each_context(ctx, each_value, i));
	}

Here, we can see that before any initialization, a create_each_block function is called for each iteration of the loop. The create_each_block function is similar to create_main_fragment by returning an object with c(), m(), p() and d() methods.

	return {
		c() {
			ul = createElement("ul");

			for (var i = 0; i < each_blocks.length; i += 1) {
				each_blocks[i].c();
			}
		},

		m(target, anchor) {
			insert(target, ul, anchor);

			for (var i = 0; i < each_blocks.length; i += 1) {
				each_blocks[i].m(ul, null);
			}
		},

At creation time (c()) and mount time (m()), each block is called with the c() or m() function too.

		p(changed, ctx) {
			if (changed.children) {
				each_value = ctx.children;

				for (var i = 0; i < each_value.length; i += 1) {
					const child_ctx = get_each_context(ctx, each_value, i);

					if (each_blocks[i]) {
						each_blocks[i].p(changed, child_ctx);
					} else {
						each_blocks[i] = create_each_block(component, child_ctx);
						each_blocks[i].c();
						each_blocks[i].m(ul, null);
					}
				}

				for (; i < each_blocks.length; i += 1) {
					each_blocks[i].d(1);
				}
				each_blocks.length = each_value.length;
			}
		},

On update, we only do something when the iteration value changes. Here, the loop is executed for each child, and if the child exists for a given index, the p() method is forwarded (the child is updated). If the child does not exists, it is created with c() and mounted with m(). After the loop, each extra block is deleted with d().

		d(detach) {
			if (detach) {
				detachNode(ul);
			}

			destroyEach(each_blocks, detach);
		}

On destroy, the child blocks are destroyed in turn.

	};
}

// (13:2) {#each children as child}
function create_each_block(component, ctx) {
	var li, text0, text1_value = ctx.child.name, text1;

	return {
		c() {
			li = createElement("li");
			text0 = createText("name: ");
			text1 = createText(text1_value);
		},

		m(target, anchor) {
			insert(target, li, anchor);
			append(li, text0);
			append(li, text1);
		},

		p(changed, ctx) {
			if ((changed.children) && text1_value !== (text1_value = ctx.child.name)) {
				setData(text1, text1_value);
			}
		},

		d(detach) {
			if (detach) {
				detachNode(li);
			}
		}
	};
}

And the create_each_block for an iteration item looks identical to a simple compiled block.

Now, let’s imagine a templating system working like that for Nim. We would have directive as Nim macros or templates that would take a CSS selector to some child DOM node, and apply behaviour. For example we can imagine the following template in Nim:

html = """
<h1>Hello <span class="name"></span>!</h1>
<ul>
  <li>
    name: <span class="name"></span>
  </li>
</ul>
"""

template(rootNode, data):
  match(rootNode, "h1 .name", span):
    if data["nameChanged"]:
      span.textContent = data["name"]
  match_iterate(rootNode, "ul li", data["children"], li, child):
    match(li, ".name", span):
      if child["nameChanged"]:
        span.textContent = child["name"]

The generated template would copy the entire DOM nodes from the template at construction time. For each declared match, a local variable will be defined corresponding to the match.

At mount time, the blocks would be mounted.

At update time, the matches will be processed in order and re-evaluated if the values are changing. In the example, the change is detected outside of the templating system. For loops, data["children"] must be iterable, and children count will be updated if the iteration yields new or fewer elements.

At destroy time, the DOM nodes can easily be detached.