Skip to content

Custom Elements: Contentious Bits

Domenic Denicola edited this page Jan 22, 2016 · 11 revisions

This document is meant to summarize the more pressing issues related to the Custom Elements spec. These issues are either blocking adoption of the spec, or else significant concerns raised by experimentation with the current Google proposal.

This page is meant to document the issues and most likely options for resolving those issues, and not to argue for a specific resolution of any issue. If you see a significant issue which is not yet represented here, or your own position on the issue isn't reflected yet, or there's a pro/con on a position which should be considered, please add it.

Previously contentious bits

Some things everyone has more or less agreed to change, but the work simply hasn't happened yet (possibly because related areas are still contentious).

Constructor vs. createdCallback

The current proposal uses createdCallback, and the return value of registerElement is a different value from that passed in. However, both Constructor-Dmitry and WebKit's implementation use the constructor, and the return value is the same as the constructor passed in (or in WebKit's case returns nothing).

In general, it is possible to "call" the constructor at any time, including during upgrades, by using the constructor call trick. Thus there's no advantage to createdCallback over the constructor, and we can all agree on using the constructor.

Symbol-named properties for lifecycle hooks

The current proposal uses createdCallback, attributeChangedCallback, detachedCallback, and attachedCallback. Mozilla proposed, and everyone pretty much agrees, that we should move to symbol-named properties: [Element.attributeChanged] and friends. However, the exact lifecycle hooks are still contentious in the case of detached/attached.

Calling attributeChanged for all attributes on creation

There is general agreement that, instead of forcing component authors to iterate over their attributes in their constructor, the browser should instead call attributeChanged once for every attribute that is present. (This is applicable to parser-created elements, not those created via document.createElement().)

Contentious bits

The general timing of lifecycle hooks

The proposal currently uses "nanotask" timing. Roughly, lifecycle hook callbacks are run "when transitioning from C++ back to JS," giving an appearance of synchronicity in most cases, without the potential dangers of running author code inside browser algorithms that are unprepared for that.

At the last F2F, WebKit was questioning whether instead the algorithms should all be called synchronously. Blink strongly prefers otherwise, due to their experience with security and stability bugs caused by allowing arbitrary author code to run at unexpected places.

The specific timing of the constructor, and the consistent world view issue

At the last F2F, a major point of contention was consistency of the "world view" that custom elements would have, depending on how they are created.

The current proposal contains one major bug, which is that createdCallback and upgrades could happen at nondeterminstic times, depending on network boundaries; this is clearly bad.

The Constructor-Dmitry proposal removes this nondeterminism, leaving the only worldview inconsistency that:

  • Elements which are upgraded will be able to see all their children and attributes inside their constructor.
  • Elements which are created via algorithms that construct a larger tree, such as cloneNode (possibly innerHTML as well?), will be able to see all their children and attributes inside their constructor.
  • Elements which are created via new CustomXElement() or document.createElement("x-custom-element"), will necessarily always have no children or attributes.
  • Elements which are created via the parser will necessarily always have no children or attributes.
    • This can be specced either direction, but Maciej requested the no children/attributes alternative, so that is what the Constructor-Dmitry proposal currently outlines. Elliott is not terribly comfortable with this.

Constructor-Dmitry still proposes using nanotask timing for the constructor, i.e. never calling it synchronously from inside algorithms like cloneNode or innerHTML DocumentFragment construction.

At the last F2F, there was talk of tricks that would allow the world view to consistently be that of no children/no attributes, even for upgrades; the thinking is that then authors would be more careful, and not write code that breaks document.createElement("x-custom-element")-style usage. Elliott has proposed one potential solution along these lines with his backing swap proposal, and Maciej was given an action item to figure out similar tricks.

Attached/detached vs. inserted/removed hooks

The current proposal contains attached/detached hooks, corresponding to "inserted into the document" and "removed from the document" (for documents with browsing contexts only). This is a fairly high-level API and was chosen based on web author feedback.

WebKit proposes in #222 inserted/removed hooks, corresponding to "a node is inserted" and "a node is removed". This is closer to what browsers implement on the lowest level (and what the DOM Standard specifies). There is some concern that it would be noisy, and that common use cases would take the performance hit of being called more often than necessary, compared to attached/detached.

One potential resolution is to support both the high- and low-level APIs. Implementations can easily optimize such that when inserted/removed callbacks are not present on an element definition, no performance overhead of frequent calls is incurred. But the capability would still be there for those use cases that need it.

Other things to work out

registerElement or something new?

Should we create a new method, or use the proposal's existing registerElement? Using registerElement while changing the semantics might cause Chrome some pain, and WebKit's proposal uses defineCustomElement, which also has a slightly changed signature. What should the exact name and signature of the registration method be?

A more radical proposal might include a "registry API" such as document.customElements.register(tagName, Constructor) and a few other counterparts; compare e.g. that of the Parser-Created Constructors proposal. However, it is probably simplest to leave this for v2 work; if such an API grows, we can explain document.registerElement (or whatever) as being layered on top of the element API.

A solution for the style="" attribute spamming

As raised in #350, attributes such as style="" do not play well with the current attributeChanged design. There are a few potential solutions:

  • Remove attributeChanged (for now?); judicious use of MutationObservers, and especially their takeRecord()s method, can provide almost the same functionality, including synchronous property/attribute reflection.
  • Provide an attributeFilter option (probably spelled [Element.attributeFilter]) to allow custom element classes to choose which attributes they receive notifications for.
  • Pare down attributeChanged to only contain the attribute name, and not contain a serialization of the attribute. This means that the attributeChanged "spam" will still occur, but it allows implementations to continue lazily serializing attributes like style="".
  • Just don't call attributeChanged for style at all. Optionally provide a different hook for that, perhaps with batched timing like MutationObserver, or perhaps some more general "style change observer" that works for more than just inline style="".

Other issues worth mentioning

These haven't yet been analyzed enough to develop clear positions on them. If you've thought about these and/or have a proposal to offer, please add detail!

  • Should we consider adding a childrenChanged callback? If we call it at creation time, similar to how we're planning to do for attributeChanged, this could encourage component authors to write any children-related logic in the childrenChanged callback and leave it out of their constructor. This helps a decent bit with the consistent world view issue.

  • How does a component know when it is in the document: i.e., how can it do a deep contains()?

  • If a component contains children which are also components, when can the containing component expect to know those children have been upgraded? Is upgrade done in tree-order or bottom-up?

  • How can we resolve name conflicts in element tags? Should we support multiple registries? In designing for ES6, is the expected pattern for a component module to just export a class that can registered by the importer, or is the module expected to register the component itself (that is, as a side effect)?

  • What is the fate of is=""?