As an aside, I really like the class name and text content ergonomics (e.g div.someclass, span:some content). Reminiscent of pug/jade
I 'invented' the concept for this back in 2011, and it was used (as a proprietary lib) in various startups. Even though many similar open source libs have been released since, and boy have I tried a lot of them, none have been able to capture the elegance and DX of what we had back then. I might be biased though. :-)
So I started creating a cleaned-up, modern, TypeScript, open source implementation for the concept about five years ago. After many iterations, working on the project on and off, I'm finally happy with its API and the developer experience it offers. I'm calling it 1.0!
The concept: It uses many small, anonymous functions for emitting DOM elements, and automatically reruns them when their underlying proxied data changes. This proxied data can be anything from simple values to complex, typed, and deeply nested data structures.
As I'm currently free to spend my time on labors of love like this, I'm planning to expand the ecosystem around this to include synchronizing data with a remote server/database, and to make CRUD apps very rapid and perhaps even pleasurable to implement.
I've celebrated 1.0 by creating a tutorial with editable interactive examples! https://aberdeenjs.org/Tutorial/
I would love to hear your feedback. The first few people to actually give Aberdeen a shot can expect fanatical support from me! :-)
As an aside, I really like the class name and text content ergonomics (e.g div.someclass, span:some content). Reminiscent of pug/jade
Also, in order to transform JSX into individual rerunnable functions, we'd need a whole different transpiler. I like being able to code browser-runnable JavaScript directly.
To each their own. :-)
The beauty of the OP’s approach is the immediate mode render approach. JSX is a data structure, immediate mode implies a structure from the order of execution of computation.
You need JSX to pass around fragments of a view, unless your view is made entirely of computation, in which case you can just pass around functions.
You can do "immediate mode" JSX. There's nothing technical stopping you, and there are at least a few libraries out there that do.
I don't think you would. `<Component prop="example" />` gets converted by current transpilers into `jsx(Component, { prop: "example" })`. The `Component` itself is passed as is. In the case of Components that are just functions, that passes the function as-is, as a function you can just call as needed.
JSX was built for "rerunnable functions". It's a lot of how React works under the hood.
This JSX:
<section><h1>Welcome</h1>{data.enabled ? <input /> : "disabled"}</section>
Which becomes this with Babel: _jsx("section", {children: [
_jsx("h1", {children: "Welcome"}),
data.enabled ? _jsx("input", {}) : "disabled"
]})
But we'd need something like this to fit Aberdeen: _jsx("section", ()=>{
_jsx("h1", ()=>{_jsx("Welcome");});
if (data.enabled) _jsx("input", {}) else _jsx("disabled");
}})
In React you can, with some effort, limit virtual DOM rerenders to a single component. Aberdeen rerenders fractions of components by default, if that's all that needs to happen. That's why it works well operating directly on the actual DOM, without a virtual DOM inbetween. <section><h1>Welcome</h1>{ () => data.enabled ? <input /> : "disabled" }</section>
That doesn't even look half bad to me. It looks really close to many of your other examples.> In React you can, with some effort, limit virtual DOM rerenders to a single component. Aberdeen rerenders fractions of components by default, if that's all that needs to happen. That's why it works well operating directly on the actual DOM, without a virtual DOM inbetween.
A lot of that depends on what your model of a "component" is. React will rerender fractions of a component in advanced areas such as a Error Boundaries and Suspense.
Also, for what little it is worth, my JSX-based library isn't a virtual DOM, operates directly on the actual DOM, and generally renders components once and only once in their lifetime, because it binds all changes even more directly to specific DOM properties.
render() {
if (this.dontNeedToRender) {
return null
}
}
Now, because hooks can't be skipped, react developers jump through many hoops to use an if statement during rendering.If you find the syntax ugly, you can create a function like "run(expr)" to wrap it, similar to Kotlin's method of the same thing.
<div>
{(() => {
switch (status) {
case Status.LOADING: return <div className="loading">Loading...</div>
case Status.ERROR: return <div className="error">Error: {error.message}</div>
case Status.SUCCESS: return <MyComponent data={data} />
}
})()}
</div>
It's less pure in that it doesn't run directly in the browser today, but it is also way more pure than the template compilers in Angular and Vue and others.
Yes, but this happens at build time, not run time.
I have a POC syntax extension (babel parser fork) I named JSXG where I introduced "generator elements" which treats the body of the element as a JS generator function that yields JSX elements.
The simple/naive implementation of just running the generator was okay, but I (perhaps prematurely) worried that it would be not ideal to have the resulting list of child elements be actually dynamic-- as opposed to being fixed size but have false/null in place of "empty" slots and also using arrays for lists made by loops.
So, I also had a transform that followed conditional branching and loops etc. and made a template of "slots" and that resulted in a stable count of children, and that improved things a whole lot.
It's been a while since I revisited that, I should try and find it!
Comparisons below.
Aberdeen:
$('div', () => {
if (user.loggedIn) {
$('button.outline:Logout', {
click: () => user.loggedIn = false
});
} else {
$('button:Login', {
click: () => user.loggedIn = true
});
}
});
$('div.row.wide', {$marginTop: '1em'}, () => {
$('div.box:By key', () => {
onEach(pairs, (value, key) => {
$(`li:${key}: ${value}`)
});
})
$('div.box:By desc value', () => {
onEach(pairs, (value, key) => {
$(`li:${key}: ${value}`)
}, value => invertString(value));
})
})
JSX: <div>
{user.loggedIn ? (
<button
className="outline"
onClick={() => user.loggedIn = false}
>
Logout
</button>
) : (
<button onClick={() => user.loggedIn = true}>
Login
</button>
)}
</div>
<div
className="row wide"
style={{ marginTop: '1em' }}
>
<div className="box">
By key
<ul>
{Object.entries(pairs).map(([key, value]) => (
<li key={key}>
{key}: {value}
</li>
))}
</ul>
</div>
<div className="box">
By desc value
<ul>
{Object.entries(pairs).map(([key, value]) => (
<li key={key}>
{key}: {invertString(value)}
</li>
))}
</ul>
</div>
</div>
JSXG: <*div>
if (user.loggedIn) {
yield <button
className="outline"
onClick={() => user.loggedIn = false}
>
Logout
</button>
} else {
yield <button onClick={() => user.loggedIn = true}>
Login
</button>
}
</*div>
<div
className="row wide"
style={{ marginTop: '1em' }}
>
<div className="box">
By key
<*ul>
for (const [key, value] of Object.entries(pairs)) {
yield <li key={key}>
{key}: {value}
</li>
}
</*ul>
</div>
<div className="box">
By desc value
<*ul>
for (const [key, value] of Object.entries(pairs)) {
yield <li key={key}>
{key}: {invertString(value)}
</li>
}
</*ul>
</div>
</div>
Edit:Come to think of it, I think it may have been <div*>...</div*> or even (:O gasp) <div*>...</div>.
The first was using </* as the marker for the end tag of a JSXG element. It worked, but it seemed like it wouldn't be too well received as parsers today treat /* as a comment in that spot iirc.
Edit: The other premature concern/feature was the ability to have a for loop NOT render to an array and rather be inline.
Normaly
for (...) { yield <>...</> }
// becomes
[[<>...</>, <>...</>, ...]]
But I added a mechanism using string directives that would inline it: "use jsxg:inline"
for (...) { yield <>...</> }
// becomes
[<>...</>, <>...</>, ...]
I think it would be pretty easy to transpile this (or something like it) to code Aberdeen can consume. In fact, it would probably be a lot easier than transpiling for React, as Aberdeen uses immediate mode the `for` problem in your comment below wouldn't be a problem at all, so no return values to worry about.
I'd use something like this myself for larger projects, I think. But asking other developers to use a different programming language just to use your library usually doesn't fly well. :-) Coming to think of it, I find it actually kind of surprising that JSX succeeded.
It doesn't.
In my[1] framework, there is JSX, but control flow like map is done with a function.
<ul>
{ ForEach(model, item => <li>Fruit: { item }</li>) }
</ul>
There is a lambda there, yes, but at the top level it's a ForEach() function call.Likewise, it is possible to use get conditional elements in JSX without using react's ugly approach.
It unfortunately can't, because by the time the jsx function gets a chance to run, the expression has already been evaluated, whereas the whole point of a framework like this is to be able to run that evaluation repeatedly.
So what you'd need to write to get that to work would be something like
<div>{() => user.name}</div>
for every single case where you're interpolating a value, which starts looking kind of ugly. It also has its own pitfalls. For example, your example with a ternary in it would unnecessarily rerender (i.e. completely unmount and recreate) the child elements if the input changes but the conditional still evaluates to the same thing. (E.g. if data.enabled was 1 and we set it to 2, what we want is to have no change in the DOM, but what we'd get the input element being unmounted and remounted.)There's also just the general issue that JSX requires some sort of transform. This isn't necessarily bad - I use SolidJS a lot which explicitly makes use of the chance to optimise the JSX rendering at compile time - but there are situations where you want to avoid that, and having a syntax that's focused on the "no compile step" case is useful.