Why not JSX? There’s no real cost to making the API JSX compatible, from what I can tell, and tsc has builtin support for transpiling JSX. It would also make porting code a lot easier. I’m only saying this because the type signature of $ is so similar to createElement.
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 don't particularly like how control logic needs to be embedded within JSX using ?ternary : operators and .map(() => stuff) within the HTML.
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. :-)
> I don't particularly like how control logic needs to be embedded within JSX using ?ternary : operators and .map(() => stuff) within the HTML.
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.
You can just write an IIFE expression, and use regular imperative logic like if/else and switch statements.
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>
> Also, in order to transform JSX into individual rerunnable functions, we'd need a whole different transpiler.
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.
The problem is that JSX transpilers will put child nodes in an array, instead of in an anonymous function, meaning there is no easy way (without transpiler magic) to rerender just a part of the component.
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. Your `_jsx` function can auto-wrap children in a function. Also, you can just pass functions as children, too if you really want to make the JSX author work for it:
<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.
Modern react developers forget that if statements exist. When react was class based it was trivial to do:
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. I might be missing your point, can you elaborate? If you want to write an if statement you just do it at the end of a component, after the hooks. It's a common pattern.
You don't have to do that in the html if you don't want to. You can easily assign those operations to a variable then insert the variable into the html.
re: syntax, I agree, it's stopped me from ever trying React/JSX-based frameworks, which I am sure is an over-reaction.
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>.
That looks cool!
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.
Ah, the roadblocks are coming back to me.
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
[<>...</>, <>...</>, ...]
JSX is a better data structure than strings for expressing views, but this doesn’t use strings or any other data structure.
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.
JSX does not have any inherent data structure, it immediately converts to function calls. React is well known for taking the JSX calls and converting them to a reusable data structure, but that's React's thing and React's data structure is somewhat unique to React, other virtual DOM's have different data structures.
You can do "immediate mode" JSX. There's nothing technical stopping you, and there are at least a few libraries out there that do.
No matter how one turns it, JSX is still something different from plain JS. It is a kind of format, that needs to be parsed and interpreted. Writing only plain JS from that perspective is a purer approach than having JSX anywhere.
There's no embedded interpretation in JSX, it's a direct "sugar" for a function call pattern. If you are already using Typescript, it's a "free" syntax sugar that Typescript supports well.
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.
> It is a kind of format, that needs to be parsed and interpreted
Yes, but this happens at build time, not run time.
The signature looked compatible with JSX to me. You could probably easily create JSX functions that just use $ internally, and make your web build tool's react-auto-import setting point to it.