ryansolid / dom-expressions Goto Github PK
View Code? Open in Web Editor NEWA Fine-Grained Runtime for Performant DOM Rendering
License: MIT License
A Fine-Grained Runtime for Performant DOM Rendering
License: MIT License
Currently, directives are always guaranteed to run first, regardless if the dynamic attribute is declared first. We can consider this issue in the future however we can't do this right now considering that dom-expressions does not do true fine-grained attributes, as dynamic attributes are grouped into a single effect rather than their own independent effects. With ordered directives, it requires these dynamic attributes to either be split into their own independent effects or their group sequences (longest sequence of dynamic attributes before and after a directive, much like mergeProps
is right now.), but of course we don't want this right now as this trades the creation/memory cost against the update cost.
When implementing a function like the one discussed in Solid Discussions #451, I noticed that this line results in an error being thrown if the resource promise resolves to undefined
(i.e. method "replace" doesn't exist on type undefined
). Returning an empty string seems to be a simple work-around.
I'm using the setup provided by solid-start
. The error isn't always triggered, but is generally triggered if you reload the page (I don't think it's triggered by hot module reloading).
I'm using Shoelace together with Solid and noticed that Shoelace's custom events can't be used because it leads to invalid JavaScript syntax.
Example usage of such Shoelace component and custom event handler:
const TitleInput = () => {
const [title, setTitle] = createSignal("");
return (
<sl-textarea
value={title()}
onSl-input={(e) => setTitle(e.target?.value)}
/>
);
};
That yields the following syntax:
_el$4.__sl-input = e => setTitle(e.target?.value);
And error:
SyntaxError: invalid assignment left-hand side
Because of the non-quoted __sl-input
property.
Is there any reason why empty attributes <div empty />
are getting transpiled into empty=""
? As I understand by default HTML understands empty
as ""
value. Getting rid of =""
could reduce the build size while still persisting the same effect.
- results.template += value ? `="${value.value}"` : `=""`;
+ results.template += value ? `="${value.value}"` : "";
I also noticed another case that could prevent generating more templates.
In many cases the templates differs only by attributes.
<>
<div>
<div class="bang">
Hello
<span>!</span>
</div>
</div>
<div>
<div class="h1">
Hello
<span>!</span>
</div>
</div>
</>
Currently this syntax would generate 2 the same templates with just a different attribute value:
const _tmpl$ = template(`<div><div class="bang">Hello<span>!</span></div></div>`, 6),
_tmpl$2 = template(`<div><div class="h1">Hello<span>!</span></div></div>`, 6);
We could optimize it by treating attributes always as dynamic:
setAttribute(_el$2, "my-attr", "value");
That why we could reduce build size drastically reducing the amount of templates.
I am aware that currently this can be achieved by wrapping each attr with template literals but this is inconvenient for DX
I would like to add defer so I do not need to move the script to the end of the body.
Is there a reason that template literals are generated here instead of regular strings? Unless I am missing something there are never any expressions in the strings.
Add an SSR feature called renderToJSON
/serialize
that instead of rendering an HTML markup, renders the JSON representation of the markup.
This is a very wild idea. Obviously, the SSR formats right now only support HTML so this feature might only work if the compiler itself is compatible to the runtime. Supposedly, renderToJSONAsync
and renderToJSONStream
are also similar ideas that can be added.
Hey Ryan :)
I really like the direction you are moving to with dom-expression. The latest feature use
directive is very similar to modifiers known from Swift UI or Ember.js. You literally read in my mind with that as I was going to fork the repo and implement it this week.
I was wondering, would you be keen on exposing an alternative syntax? The current one is not fully JSX and types friendly. Have you considered a syntax like this?
<button $={[on('click', clickFn)]}>Click Me</button>
the $
is one of not reserved and allowed by JSX. Also passing the modifiers as an array, makes this syntax fully supported by typescript and JSX.
the example on
function would generate a new function that will be used exactly like the current use:on
directive. I am really curious about your opinion on that.
Solid is eating whitespace inside components.
Code:
function Pre(props) {
return <pre {...props} />;
}
<pre><span>1</span> <span>2</span> <span>3</span></pre>
<Pre><span>1</span> <span>2</span> <span>3</span></Pre>
Expected results:
1 2 3
1 2 3
Actual results:
1 2 3
123
The recent update to make this a generator looks great. An example would be even greater :-)
I've managed to build a runtime using s-js. It only exports a single {wrap} function.
Next I copy that runtime as and use it in a new project 's-jsx/index.js' (using rollup + the babel-jsx.. plugin).
A simple build where I import
import S from 's-js';
import { wrap } from './s-jsx';
and run
console.log('Hello World')
works fine...
When I create a "dumb" component (no data just a static message) and use S.root(()=>...)
it also works fine
As soon as I start using S to add some data and render it in my component, it screams out "Can't find variable: r"
Last week, r, root, selectEach and selectWhen would all have been exported by the runtime I built; but no longer. The question is - where's it hiding now?
I was working on a Solid.js project and stumble upon an error when classList
key has leading/trailing spaces.
Error:
Uncaught DOMException: An invalid or illegal string was specified dev.js:306:65
After some research, it seems like the issue is coming from the split
regex on toggleClassKey
which will split the trailing/leading space as an empty string and try to toggle it.
e.g.: " red"
will split up to ["", "red"]
which will throw the error when trying to node.classList.toggle("", value)
Here is the specific line:
Uncaught DOMException: An invalid or illegal string was specified dev.js:306:65
toggleClassKey dev.js:306
classList dev.js:151
Loading icons.tsx:40
Loading icons.tsx:47
c dev.js:475
untrack dev.js:400
c dev.js:475
runComputation dev.js:612
updateComputation dev.js:600
devComponent dev.js:481
createComponent dev.js:1069
get children index.tsx:64
Switch dev.js:1225
runComputation dev.js:612
updateComputation dev.js:600
createMemo dev.js:215
Switch dev.js:1222
c dev.js:475
untrack dev.js:400
c dev.js:475
runComputation dev.js:612
updateComputation dev.js:600
devComponent dev.js:481
createComponent dev.js:1069
get children index.tsx:54
get dev.js:1103
get dev.js:1077
get children index.js:68
res dev.js:834
runComputation dev.js:612
updateComputation dev.js:600
createMemo dev.js:215
children dev.js:523
res dev.js:834
untrack dev.js:400
provider dev.js:830
runComputation dev.js:612
updateComputation dev.js:600
createComputed dev.js:194
provider dev.js:830
c dev.js:475
untrack dev.js:400
c dev.js:475
runComputation dev.js:612
updateComputation dev.js:600
devComponent dev.js:481
createComponent dev.js:1069
Auth0 index.js:37
c dev.js:475
untrack dev.js:400
c dev.js:475
runComputation dev.js:612
updateComputation dev.js:600
devComponent dev.js:481
createComponent dev.js:1069
index.tsx:36
render dev.js:90
createRoot dev.js:169
runUpdates dev.js:684
createRoot dev.js:169
render dev.js:88
index.tsx:36
InnerModuleEvaluation self-hosted:2384
evaluation self-hosted:2335
Right now it looks like spreads go in first which is a bit awkward. Reactivity will cause last thing to win anyways and if it's dynamic versus static, well dynamic wins regardless of attribute order but can we do better with dynamic things. I kept spreads out because of awkwardness of diffing but I think we've made improvements here lately so that they could be inserted in order.
Otherwise TypeScript users will get errors like:
node_modules/lit-dom-expressions/types/index.d.ts:1:83 - error TS2307: Cannot find module 'dom-expressions/src/runtime'.
1 import { effect, insert, createComponent, delegateEvents, classList, style } from "dom-expressions/src/runtime";
I think it's nicer if we cleanup container when user call disposer.
const dispose = render(App, document.getElementById("app"));
dispose(); // exepect remove inserted component
SSR breaks when I pass undefined
to a html components style
prop because ssrStyle doesn't handle undefined
:
dom-expressions/packages/dom-expressions/src/server.js
Lines 206 to 209 in 9c41669
SWC is an alternative to Babel that boasts having much better performance, so it would be nice to see an SWC equivalent to babel-plugin-jsx-dom-expressions
(and also babel-preset-solid
, though that's technically not relevant here ๐) in order to speed up the development cycle. However, I haven't looked into the similarities/differences between the two plugin APIs, so I'm not sure how much of a project this would be.
I forgot to make an issue about this last month, this can easily be reproduced.
For expressions like
<For each={[...array()]}>
it is compiled into
each: [...array()]
which is like a false-positive. The hotfix would be to recursively check each item in the ArrayExpression
to check for dynamic and static expressions. I believe ObjectExpression
happens to behave like this.
dom-expressions/packages/dom-expressions/src/client.js
Lines 97 to 101 in ad095a1
In various places in the helper you use prev[s]
, suggesting that the key of prev
should be a css property name. However, in line 100 you're using the property value as key. To me, that looks like a typo...
The issue can be seen here
The expression this.shouldStay
here
should be transformed into _self$.shouldStay
here
this
is going to refer to the object literal being passed as the second argument to _$createComponent()
instead of the this
in the render
function, which is what would be expected.
The bug was introduced in commit f8ea5cc
I'm in the process of migrating every project I have related to solid to yarn 2
. One of the good thing about yarn 2
is that it's stricter with its module resolution and when using babel-preset-solid
which relies on babel-plugin-jsx-dom-expression
, it gets stuck because it can't find any reference to @babel/types
in the package.json
dependencies/dev dependencies despite being imported multiple times within the source code as seen in the below screenshot.
Would it be possible to add it or is there a specific reason for it to be missing? (or am I just blind and I missed it)
Here's the error, for reference.
[!] (plugin babel) Error: babel-plugin-jsx-dom-expressions tried to access @babel/types, but it isn't declared in its dependencies; this makes the require call ambiguous and unsound.
As an alternative, I can always install it within my poject, but that's not ideal since I'm not really the one relying on it directly.
What does this if statement do?
if ("_DX_DEV_" && check && t.innerHTML.split("<").length - 1 !== check)
throw `The browser resolved template HTML does not match JSX input:\n${t.innerHTML}\n\n${html}. Is your HTML properly formed?`;
I just get a super annoying error when creating tables like this (in production no doubt about it)
<table>
<tr>Do you really wanna listen to her large mouth instead of playing video games?</tr>
<tr>Get the hell out of there, dude</tr>
</table>
When I remove these four lines of code everything works. Like, when I remove all the <tr>
it works. As I said, in build version everything works just fine, I think there should be a bug, or why should this break?
the problematic code is:
i think it should be i && attributes.splice(classAttributes[i].key, 1);
but that doesn't fully fix it, with that change if there are multiple attributes only one goes missing.
Is it intentional that style={undefined}
in Solid clears all inline styles?
I expected only previously set styles to be cleared, which is how style={{}}
behaves with the current code. For point of comparison, this is how React behaves: undefined
just clears previously set styles.
My intuition is this: In most of Solid and React, foo={undefined}
is equivalent to omitting/removing the foo
attribute in the HTML, which in the reactive case means undoing whatever was done by setting it. So I found it confusing that style={undefined}
did something very different (.style.cssText = undefined
) in Solid.
The context is that I wanted to write style={hide() ? {display: 'none'} : undefined}
(mainly because this is easy to write as style={display: 'none' if hide()}
in CoffeeScript), but currently I need to write style={hide() ? {display: 'none'} : {}}
. It took me a long time to debug this, as I couldn't figure out why my style attributes set manually in an effect were getting destroyed โ I didn't expect it to be relevant to the display
setting.
For example, @mdynnl suggests a replacement function signature of
function style(node, value = {}, prev = {}) {
Hi Ryan,
I am digging through the code, learning and trying understand things. Would you mind sharing a bit what is the purpose of having owner attached to elements in rendering context? :)
Not sure what to do yet but would be great if we could validate templates in a way that we ensure the browser won't mess with them when we instantiate them. Things like how browsers insert <tbody>
if you omit it. Or the way certain elements aren't allowed certain children. This is critical because it breaks walks during render/hydration.
Right now I just count the tags at runtime which is super rudimentary and it only runs in dev mode since we don't want the additional code/overhead. In theory this can be done at compile time but I don't know of any existing packages. Maybe failing that we could just develop something ourselves?
Currently, the code <Counter use:example="Hello World" />
compiles into
createComponent(Counter, {
"use:example": "Hello World"
})
We could probably make it work into
createComponent(Counter, {
ref(node) {
example(node, 'Hello World');
},
});
since ref
is already a special prop for both host and components.
What do you think?
The only design challenge would be that props.ref
can be assigned anytime, anywhere which makes it lack constraint unlike host nodes, posing some memory leaks.
I couldn't find any instructions on how to use the plugin in the documentation. I tried the following:
// .babelrc.json
{
"plugins": [
"babel-plugin-jsx-dom-expressions"
]
}
And I run it with @babel/cli:
npx babel index.js -o compiled.js
But I keep getting a syntax error because of the JSX:
TypeError: /home/me/library/index.js: Property left of AssignmentExpression expected node to be of a type ["LVal"] but instead got "ArrowFunctionExpression"
How do I use this with Babel?
In one of our projects that uses babel-jsx-dom-expressions, we are getting a crash due to path not begin defined.
I looked at the code and it seems that path
is used in a function but its never sent as an argument.
function transformClasslistObject(expr, values, quasis) {
expr.properties.forEach((prop, i) => {
const isLast = expr.properties.length - 1 === i;
let key = prop.key;
if (t.isIdentifier(prop.key) && !prop.computed) key = t.stringLiteral(key.name);
else if (prop.computed) {
registerImportMethod(path, "escape"); <-------------------- this is where path is not defined
...
The line of code is here: https://github.com/ryansolid/dom-expressions/blob/main/packages/babel-plugin-jsx-dom-expressions/src/ssr/element.js#L355
From what I can see, the function transformClasslistObject
is only used twice in the file and it would be easy to pass the path
argument to it, but since I'm not familiar enough with the code, I might miss something crucial. I would otherwise submit a pull request.
Cheers and thank you!
I've discussed this on Discord temporarily.
Provide a way for the client to batch updates inside event handlers. React is already doing this internally with react-dom
, and I believe others as well. This is useful specially if there's multiple signal updates in a single event call. Solid, compostate and other libraries out there currently provides a way to do so.
import { batch } from 'rxcore';
krausest/js-framework-benchmark#559
Shows all libraries took a hit in clear all rows. I've done major refactoring over the past couple months, so I need to do some work to pinpoint where this happened.
First - Thank you very much for all that toolchain
its very impressive :)
I wondering around that field, and i came across your projects and https://github.com/jridgewell/babel-plugin-transform-incremental-dom
And i have a feeling that some of it overlaps, and maybe using incremental-dom for the actual dom manipulation can work very nicely with this model
Have you made any evaluations and comparison of the approaches?
A simple demo of jsx -> incremental dom + mobx:
https://codesandbox.io/s/jsx-incremental-dom-mobx-6ugg1
I'm still trying to wrap my head around how to make granular updates
When spreading an object with computed keys, the object is wrongly assumed to be static and transformed as such.
I ran into this code recently: https://playground.solidjs.com/?hash=-743945147&version=1.3.9; notice due to how mergeProps
and Dynamic
work, the textarea is recreated whenever props.value
changes. There are a few areas where compilation is broken or could be better with regards to inline objects:
Bugs:
Possible improvements:
This is obviously quite specific to spreading objects declared inline, since no transformations will be done to explicit mergeProps
calls or when spreading objects declared elsewhere.
Ever since the setProperty
changes in solidjs/solid#143, we have to use hyphenated names like style={{'flex-direction': 'column'}}
, but Typescript is still set to use CSSStyleDeclaration
so I get errors like:
Type '{ 'flex-direction': string; }' is not assignable to type 'string | Partial<CSSStyleDeclaration> | undefined'.
Object literal may only specify known properties, and ''flex-direction'' does not exist in type 'Partial<CSSStyleDeclaration>'.
Maybe use CSS.PropertiesHyphen
from https://github.com/frenic/csstype instead?
I tried re-exporting html
from my LUME lib, and tried this simple demo:
const {html, variable} = LUME
const count = variable(0)
setInterval(() => count(count() + 1), 1000)
const div = html`
<div>${count}</div>
`
setTimeout(() => {
document.body.append(div)
}, 1000)
What happens is that this (somehow?) outputs two div elements to the page. So I see the number in count
twice on the screen (it works, just there's two of them).
As per README:
All attributes are props (so use className) and to indicate attributes wrap in 'attrs' object
I did:
h('label', { attrs: { for: 'someId' } })
but it doesn't create attributes, instead leaves attrs
prop with the stringified object. Can we clarify it?
see:
I am trying to disable event delegation by setting babel preset as
[
"babel-preset-solid",
{
"delegateEvents": false
}
]
It works for most case, but I found when there is props-spreading syntax in the component, the global delegated handler occurs again. By researching the codebase, I found in the babel-preset it indeed considered the deleteEvents option, but there is no peer consideration in the spread situation.
repro
the class name for <div className={x1} class={x2}></div>
is transformed to `${x1} ${x2}`
which is "null undefined"
if x1=null, x2=undefined
.
I've taken a look at
dom-expressions/packages/babel-plugin-jsx-dom-expressions/src/dom/element.js
Lines 132 to 150 in ba1ea6b
The possible solutions I have in mind are:
`${x1 ?? ""} ${x2 ?? ""}`
but the class name would end up having extra spaces.`${(c = x1) == null ? "" : c + " "}${(c = x2) == null ? "" : c}`
but there can also end up being an extra space at the endclassList
runtime function or one similar to itClick handlers aren't removed from spreads in SSR. They just get spread in and the JS literally serialized. We should fix this.
Thinking of css in js type scenarios but built into the compiler... very much like css prop in those libraries.
The basic event for onSubmit should be Event
instead of FocusEvent
. I guess its a bug :)
Am I missing something or is that doubled && el
in the expression doubled by mistake?
As mentioned in Discord, the code below won't apply px
, py
, mx
, my
const Btn: Component<Props> = (props) => {
return (
<button
type="button"
classList={{
"px-2.5 py-1.5 text-xs": props.size === "xs",
"px-3 py-2 text-sm": props.size === "sm",
"px-4 py-2 text-sm": props.size === "base",
"px-4 py-2 text-base": props.size === "lg",
"px-6 py-3 text-base": props.size === "xl",
[`text-${props.textColor} bg-${props.bgColor}-600 hover:bg-${props.bgColor}-700 focus:outline-none`]:
!props.disabled,
"text-gray-600 bg-gray-200 cursor-not-allowed focus:outline-none":
props.disabled,
}}
>
{/* content here */}
</button>
);
};
This is re: babel-plugin-jsx-dom-expressions v0.25.4 with Solid v0.24.7
Steps:
delegateEvents
set to truedelegateEvents
set to trueThe result is that document.addEventListener()
is called multiple times and registered events are triggered twice (or more depending on the number of imported modules)
Would it be worth somehow scoping what handlers should be triggered within a single bundle? Or making the event handler global?
Babel can tell if it can resolve the value of an expression. Maybe some basic arithmetic and all the variables are in scope and never re-assigned. Maybe a constant at the top of your file. We can push these values even though they look like variables into the static part of the template.
I was expecting to use camel case for CSS property styles in Solid (probably because of React). I'm not sure if this is documented anywhere but I just wanted to note it. I'm assuming you've given this consideration already but I'd be curious to hear your rationale...
It could be useful to support this. Currently, DX treats prop sources from spread expressions to be "static" and thus, when done this way, DX cannot reapply new props when received from sources that are reactive. Consider this SolidJS example:
function Heading(props){
return (
<h1>
{props.foo && `foo: ${props.foo}`}
{props.baz && `baz: ${props.baz}`}
</h1>
);
}
function App() {
const [customProps, setCustomProps] = createSignal({
foo: 'bar',
});
function toggleToBaz() {
setCustomProps({ baz: 'bar' });
}
function toggleToFoo() {
setCustomProps({ foo: 'bar' });
}
return (
<>
<button type="button" onClick={toggleToBaz}>Set property to baz</button>
<button type="button" onClick={toggleToFoo}>Set property to foo</button>
<Heading {...customProps()} />
</>
);
}
If we treat this with same behavior with children and props, it is expected for Heading
to behave similarly for props spreading (as is if we apply the same for libraries like React).
Currently, the only workaround is to wrap the element into an accessor:
return (
<>
<button type="button" onClick={toggleToBaz}>Set property to baz</button>
<button type="button" onClick={toggleToFoo}>Set property to foo</button>
{() => <Heading {...customProps()} />}
</>
);
However this solution is unlikely to be known or discovered by many users, and causes the component to re-render as a whole and not patch.
This can be solved easily if, by default, elements with props spreading compiles to createComponent
wrapped into an accessor, although this might be too naive in some cases. Another solution could be to allow mergeProps
to receive accessor to objects.
https://playground.solidjs.com/?hash=1390926683&version=1.3.9
Notice how the color: red
doesn't get applied in the client rendering. Reducing the number of classes to 3 gets it to apply. On the other hand, increasing the number of classes to 5 also gets it to apply, but then the click handler doesn't get applied.
Also, looking at the template it seems to apply some of the classes multiple times.
When using spread
with an SVGElement, spread
calls spreadExpression
which in turn calls assign
.
In the tree of possibilities in assign
if we are spreading a class
we end up on that branch because class
is a keyword registered in the Attributes
constant and is of type property
.
We then arrives to this expression: } else node[info.alias] = value;
. I believe this is where things are a bit weird with SVG and the className attribute. I found this stackoverflow about it. I believe you are doing something similar which causes the following error:
Cannot assign to read only property 'className' of object '#<SVGSVGElement>'
Here's a small repro: https://codesandbox.io/s/solid-svg-spreading-class-bug-vdefz?file=/src/index.tsx
Let me know if you need more details.
First off, thanks for writing this library! I've been using MobX in combination with React for quite some time and always felt like there must be a more reactive and finer grained approach to updating the DOM with an observer library like MobX. I'm really happy I found your libraries as it looks like they bring it all together in a very nice way. Congrats on your work and the new ideas!
I noticed that setting event handler props that result in delegated events do not bubble, which I assume is not by design. Here is an example:
function MyComponent() {
function onClick(e) {
console.log(e)
}
return (
<div onClick={onClick}>
Parent
<div onClick={onClick}>
Child
</div>
</div>
)
}
Clicking on the child div prints to the console only once even though stopPropagation
is not called. Using the onclick
instead of the onClick
prop works as expected and clicking on the child div results in two log entries in the console.
I would have expected the delegated events to bubble as long as stopPropagation
is not called.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.