sgo.to

Templates and Block Params

TL;DR; This an investigation of using block params in template languages (Lit's HTML and JSX) via a babel plugin. You can try a live demo at the end.

I recently tried lit for a personal project and got reminded of an old pet peeve: I still really hate the status quo of templating languages for the Web.

What I fundamentally dislike comes down to the fact that statements in JS can't be used as expressions.

Yes, do and throw expressions solve some of this problem.

For example, in JSX or lit, because if-statements can't be used as expressions, you are forced to break that into an IFFE:

class HelloWorld extends React.Component {
render() {
return (
<div>
{(() => {
if (user) {
return <div>Welcome {user}!</div>;
}
})}
</div>
);
}
}

For loops aren't great either, here is what we have in lit:

@customElement("hello-world")
class HelloWorld extends LitElement {
render() {
return html`
${this.colors.map((color) =>
html`<li style="color: ${color}">${color}</li>`
)}

`
;
}
}

I still feel that what we really want is for ifs and fors statements to be expressions, and for them to evaluate using the same algorithm as eval uses. That is, what I really want (I believe), is for the following to be possible:

class HelloWorld extends React.Component {
render() {
return (
<div>
{if (user) {
<div>Welcome {user}!</div>
}}
</div>
);
}
}

Block params won't take us this far (I think), but they may take us close enough. Here is how they work:

// ... this is what you write ...
foobar(1) {
// ...
}

// ... this is what you get ...
foobar(1, () => {
// ...
})

Which would allow us to write the following:

function iff(condition, f) {
if (condition) {
return f();
}
}
class HelloWorld extends React.Component {
render() {
return (
<div>
{iff (user) {
return <div>Welcome {user}!</div>;
}}
</div>
);
}
}

for-loops are a little more complicated because they need some form of binding. Here is my favorite variation that @bterlson suggested:

// ... this is what you write ...
foreach (let {key, value} in map) {
}
// ... this is what you get ...
foreach (map, ({key, value}) => {
})

So that I could write:

function foreach(color, body) {
return color.map(body);
}

@customElement("hello-world")
class HelloWorld extends LitElement {
render() {
return html`
${foreach(let color of colors) {
html`<li style="color: ${color}">${color}</li>`
}}

`
;
}
}

Since I was looking for an excuse to code, I figure it could be a fun hack day project.

Playground

I started by following the wonderful guide at creating custom syntax with babel which got me far enough to have a custom parser.

Then, I hooked that up with the standalone babel build by creating a plugin:

function blockParams() {
return {
parserOverride(code, opts) {
return exports.parse(code, opts);
},
};
}
Babel.registerPlugin("blockparams", blockParams);

Which now allows me to write this in my blog:

<script type="text/babel"
data-type="module"
data-plugins="blockparams">


class HelloWorld extends React.Component {
render() {
return (
<div>
{iff (true) {
return <div>This is a conditional statement</div>;
}}
</div>
);
}
}
</script>

And get this:



As well as this:

<script type="text/babel"
data-type="module"
data-plugins="blockparams"
data-presets="env-plus">

@customElement("block-param")
class BlockParams extends LitElement {
render() {
return html`
${iff (true) {
return html`This is a conditional statement`
}}

`
;
}
}
</script>

<block-param></block-param>

And get this:



Cute, heh?