Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New test: control over setting attributes, JS properties, events, and boolean attributes #2352

Open
trusktr opened this issue Apr 29, 2024 · 6 comments

Comments

@trusktr
Copy link
Contributor

trusktr commented Apr 29, 2024

This will add a new test that shows how various frameworks allow (or don't allow) control over setting attributes (strings), boolean attributes (existence), JS properties, and events.

Now that React 19 is coming out, it will finally have better support for Custom Elements, but it is still not as good as it could be.

Specifically see this reply to the OP: facebook/react#11347 (comment)

What custom-elements-everywhere is missing is a test to verify that each library gives users control over setting DOM attributes vs JS properties.

Note, this problem is not technically limited to Custom Elements, but to all elements including built-in elements, however because Frameworks cannot possibly know all the Custom Elements that exist, they cannot hard-code special cases like they do for built-in elements, hence frameworks that do not cover special cases for Custom Elements can end up preventing the user from doing what they need to do to Custom Elements.

The problem becomes more pronounced with Custom Elements because frameworks are not automatically handling a variety of special cases for every Custom Elements that could ever exist (and there are quite many of them out there). For example React 18 and below has hard-coded onClick, onInput, and similar event handlers, but no ability to handle onWhatever event handlers on custom elements, which React 19 finally fixed.

Besides this, it is not ideal for a frameworks to have to update their hard-coded or limited heuristics for handling new built-in elements as they get released over time.

See the Why? section below for more examples of scenarios in which users need control over (custom) elements.

Current results (manual testing)

Framework Lit Lume Solid.js Pota Vue Angular Svelte React Stencil
Score 100% 100% 100% 100% 75% 75% 12.5% 12.5% 0%

Features being tested

To pass with 100%, a framework needs to have templating syntax for all of the following:

  • the ability to set a specific attribute
  • the ability to set a boolean attribute (adding or removing the attribute based on the truthiness of a given value)
  • the ability to set a JS property
  • the ability to set an event listener
  • the ability to set all four of the above at the same time (in Lit, that's foo=, .foo=, ?foo=, and @foo=)

Frameworks details:

Lit (100% passing):

Lit's html template tag syntax is fully explicit, no conditional checks.

  • foo=${value} unconditionally sets the foo attribute exactly as written (always, no magic heuristic to detect properties)
  • onfoo=${value} unconditionally sets the onfoo attribute exactly as written (always, no magic heuristic to detect properties, or to add event listeners)
  • .foo=${value} unconditionally sets the .foo JS property
  • ?foo=${value} unconditionally toggles the existence of the foo attribute based on the Boolean value
  • @foo=${value} unconditionally adds an event listener for a "foo" event.

(Lit has no conditional heuristics for detecting attributes vs properties, the user fully decides, which I great, the DOM is the DOM and Lit isn't changing how we work with it but only providing the declarative-reactive interface for it)

Solid.js (100% passing):

Similarly, Solid.js JSX has the following syntax:

  • foo={value} is dynamic, currently it sets the foo attribute on a non-custom element (no hyphen in name) or the foo JS property in a custom element (more likely to be what CE users want). Update Feb 2025: this is being revised for Solid 2.0, to make the behavior consistent regardless if the element is builtin or custom.
  • onfoo={value} adds an event listener for a "foo" event when the value is a function. When the value is a literal primitive (onfoo={"foo"}) it will set a prop (confusing) but when the value is any value from a variable (onfoo={value}) it will always try to add an event listener (which fails if the value is not a function).
  • prop:foo={value} unconditionally sets the foo JS property
  • attr:foo={value} unconditionally sets the foo attribute
  • it does not have a special Boolean attribute syntax Update Feb 2025: Solid.js has since added support for bool:foo={value}
  • on:foo={value} unconditionally adds an event listener for a "foo" event.

Solid's html tagged template function has similar syntax (but with ${} interpolations). Update Feb 2025: We're talking about adding Lit syntax to Solid's html to be compatible with existing tooling that can type check Lit html templates.

Lume Element (100% passing)

  • Uses Solid JSX/html, so same results as Solid.js.

Pota (100% passing):

  • supports both Solid JSX/html and Lit html explicit syntaxes
  • foo={value} uses a heuristic to set an attribute vs a prop
  • onclick={value} not allowed by the type definitions but currently works (back compat), documentation states to use on: only, so moving forward there will only be explicit on: (or @) syntax for events.

React 19 (12.5% passing)

React 19 does not have any explicit syntax:

  • foo={value} sets a foo property if it exists, otherwise falls back to setting a foo attribute
  • onfoo={value} adds an event listener for a "foo" event if the value is a function, otherwise falls back to the previous point to set either a property or an attribute with the given value. (12.5%)

Vue (75% passing):

  • foo="value" uses conditions to determine which to set.
  • onfoo="value" seems to always set an attribute (tested on custom elements)
  • @foo="value" adds an event listener for a "foo" event
  • foo.attr="value" always set an attribute.
  • foo.prop="value" always set a property.
  • missing explicit boolean syntax (25%)

Svelte (12.5% passing)

Svelte currently has no explicit syntax:

  • foo={value} sets a property or an attribute bassed on conditions
  • onfoo={value} adds an event listener for a "foo" event (12.5%)
  • onfoo="console.log(event)" throws a compiler error that strings are not allowed

For automatic conditional attribute vs property setting across frameworks, the conditions vary a lot, making it a bit difficult to reason about what actually gets set across frameworks for foo= and onfoo=. Using explicit syntax when available is a way to avoid the fogginess.

Stencil (0% passing)

Stencil has no explicit syntax:

  • foo={value} set a JS prop is it exists, falls back to attributes.
  • onClick={value} sets up a "click" event listener (same with known events)
  • onfoo={fn} passes the value to the JS property for unknown events (Lume elements will handle this)
  • onfoo="console.log(event)" passes the string to the JS prop (Lume elements also handle this, similar to native onclick="" in the DOM).
  • onfoo={"console.log(event)"} passes it to the JS prop too

Note the onfoo example don't count as any percent because they are passing values along, rather than calling addEventListener, which means they only work on custom elements that handle the values (not just merely use dispatchEvent).

Angular (75% passing)

  • foo="string" seems to set attributes only in my testing so far.
  • [foo]="value" unconditionally binds value to the foo JS property
  • [attr.foo]="" unconditionally binds value to the foo attribute
  • onfoo="string" seems to unconditionally set the DOM attribute
  • [onfoo]="value" is forbidden by Angular during compile for "security reasons", will not be able to set an onfoo JS property
  • (foo)="value($event)" adds a listener for "foo" events
  • missing explicit boolean syntax (25%). [attr.foo]="false" sets the DOM attribute foo="false" which is still a truthy boolean attribute because it exists (a "false" string is truthy).

Etc

Please comment below about other frameworks you know about.

Why?

This is a necessity for avoiding the situation of a framework user being forced to escape out of the framework to manually use element references in JavaScript. We cannot guarantee custom elements are written a certain way, so frameworks would ideally allow full control of the DOM:

  • some elements accept attributes only
  • some accept JS properties only
  • some accept both
  • some accept a mix
  • some authors rely on attributes for styling :host with attribute selector
  • some users want to set an attribute for styling from the outside (they could set the JS prop, and the element would work, but their CSS would not be able to select the element)
  • etc

Consider this React 19 example:

function MyComp() {
  return <>
    <some-el foo="123" />
    <other-el foo="123" />

    {/* Style any elements that have a foo attribute. */}
    <style>{`
      [foo] {
        color: cornflowerblue;
      }
    `}</style>
  </>
}

In React 19, if the user uses multiple custom elements and puts an attribute on them, the styling might work for some elements, but not for others, which is confusing. In the above example,

  • if some-el has a .foo JS property, it will not be styled (if the custom element does not also reflect the property back to attributes)
  • if other-el on the other hand does not have a .foo JS property, it will be stayled as expected.
  • if other-el adds a JS property, later, what happens? The style disappears?

For this reason, and others, Custom Element users need full control of the DOM so they do not have to resort to escaping out of the framework to manually use element references in JavaScript.

It is impossible for any templating system to heuristically guess if a value should be set as attribute or as property (i.e. to guess the CE author's or CE user's intent).

Template systems could have defaults (for example like Solid.js and React both have dynamic rules to determine whether to set an attribute or a property), but ultimately the user needs control for when those rules get in the way (Lit has no rules, just simple explicit syntax, so it never gets in the way), and authors also need control on telling users how to use their custom elements regardless of how frameworks work.

Here are a couple examples:

Custom element user example:

  • custom element only accepts a property, however I will also set the attribute for styling. <some-el prop:foo="value" attr:foo="value">

Custom element author example:

  • Author: My elements accept only properties within my element framework, however I do not define them up front and I use animation-frame-based rendering to detect when values exist.
    • User: Oh no, it does not work in React 19 because when I try to set props React sets attributes instead and the author doesn't care about React so I have to use refs. :(

It may not be ideal if an custom element author has non-existent properties on their elements and they should strive to do things in a way that in interoperable with all frameworks, but the point is, users still need full control regardless what authors have done with their custom elements because users can't control all authors (and frameworks are the middle ground between them and all custom elements).

@mrginglymus
Copy link

I've added a PR here demonstrating (one of the many) issues with react's implementation - it's actually regressed from react 18, for support for what is a fairly bare-bones custom element: #2466

I have written a suite of custom elements largely for use where a reactive framework is inappropriate, so it's only moderately annoying to have react 19 not work with them, but it's quite surprising nonetheless.

@trusktr
Copy link
Contributor Author

trusktr commented Dec 15, 2024

Yay! Nice you got something going on this @mrginglymus!

@trusktr
Copy link
Contributor Author

trusktr commented Mar 5, 2025

Manual test results have been added for Lit, Lume, Solid.js, Pota, Vue, Svelte, React, with more on the way.

@trusktr trusktr changed the title New test: libraries should offer control over setting attributes vs JS properties New test: framework template systems should offer control over setting attributes, JS properties, events, and boolean attributes Mar 5, 2025
@trusktr trusktr changed the title New test: framework template systems should offer control over setting attributes, JS properties, events, and boolean attributes New test: control over setting attributes, JS properties, events, and boolean attributes Mar 5, 2025
@NullVoxPopuli
Copy link

This is exciting!

Tho, I'm curious how Ember compares to the others: #2438

I'm using vite tho, and I'm not sure how to integrate the test results

@trusktr
Copy link
Contributor Author

trusktr commented Mar 5, 2025

I'm curious how Ember compares

Me too! I updated the OP to make it more clear what a framework has to do to get 100%. Wanna give it a try manually?

I'm using vite tho, and I'm not sure how to integrate the test results

Currently there's no test, it's just manual testing at the moment. Need to start making the actual test for CEs-everywhere, and I have some good experience with the manual testing above to get started now.

@NullVoxPopuli
Copy link

it's just manual testing at the moment

So my PR is fine? 🥳 (the tests I've implemented pass -- tho, tbh I'm a few months behind haha)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants