-
Notifications
You must be signed in to change notification settings - Fork 375
Custom Elements: Contentious Bits
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.
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).
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.
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.
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()
.)
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.
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
(possiblyinnerHTML
as well?), will be able to see all their children and attributes inside their constructor. - Elements which are created via
new CustomXElement()
ordocument.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.
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.
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.
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 ofMutationObserver
s, and especially theirtakeRecord()
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 theattributeChanged
"spam" will still occur, but it allows implementations to continue lazily serializing attributes likestyle=""
. - Just don't call
attributeChanged
for style at all. Optionally provide a different hook for that, perhaps with batched timing likeMutationObserver
, or perhaps some more general "style change observer" that works for more than just inlinestyle=""
.
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 forattributeChanged
, this could encourage component authors to write any children-related logic in thechildrenChanged
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=""
?