Skip to content

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

Open
@trusktr

Description

@trusktr

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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions