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.
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 if
s and for
s 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?