See whatwg/html#4925 for discussion.
- If you can get a reference to an element, you can technically set it as a reference, and that reference will persist until it is un-set.
- However, it may not have any effect, and may be an authoring error.
let el = document.body.appendChild(document.createElement('div'));
let descendant = document.createElement('div');
el.ariaActiveDescendantElement = descendant; // this is valid
console.log(el.ariaActiveDescendantElement); // logs descendant
el.parentNode.appendChild(el.ariaActiveDescendantElement); // still valid
console.log(el.ariaActiveDescendantElement); // logs descendant
descendant.remove(); // still valid, descendant is not garbage collected
console.log(el.ariaActiveDescendantElement); // logs descendant
let shadowRoot = el.attachShadow({mode: 'closed'});
shadowRoot.appendChild(descendant); // still valid
console.log(el.ariaActiveDescendantElement); // logs descendant
el.ariaActiveDescendantElement = null;
descendant.ariaActiveDescendantElement = el; // works the other way around too
console.log(descendant.ariaActiveDescendantElement); // logs el
Pros
- Easy to understand (consistent with expando properties).
- Easy to implement (with a caveat that there is some complexity added for checking validity when attr-associated elements may have effects).
- Allows relationships to persist when removing and re-adding elements from/to the tree.
- Allows relationships to be created within a sub-tree which is created before being added to the document/shadow tree.
Cons
- May be construed as weakening Shadow DOM encapsulation. Specific concerns:
- May cause problems if scripts "accidentally" walk into deeper shadow content via known properties
e.g.
// lightEl.ariaLabelledByElement was set by the component to be // an element within the component's shadow DOM lightEl.ariaLabelledByElement.textContent = "new label"; lightEl.ariaLabelledByElement.parent.appendChild(document.createTextNode("🆕"));
- (Note that extension scripts often already have access to Shadow DOM in any case; the concern here would be page scripts.)
- Provides a surface for developers to "hack" their way into depending on implementation details of components, if components expose elements within shadow DOM, e.g.
// In this method, component sets `for` on lightDOMElement to an <input> inside Shadow DOM. // The author uses this to get access to elements inside Shadow DOM, // creating an implicit dependency on Shadow DOM internals. component.setLabel(lightDOMLabelElement); lightDOMLabelElement.for.style.backgroundColor = "papayawhip"; lightDOMLabelElement.for = null;
- May be cited as a precedent for other APIs which may also be construed as weakening Shadow DOM encapsulation.
- May cause problems if scripts "accidentally" walk into deeper shadow content via known properties
e.g.
- May cause a "memory leak" (i.e. surprising retention of a DOM object in memory) if authors do not remove references to DOM elements which are removed from the tree.
- Each API which refers to an attr-associated element needs to specify what the conditions are for an association to be valid (for example, an API may or may not require an element to be in the tree for the association to be valid, depending on the purpose of the association).
- May allow specifying a cross-document association without using
adoptNode()
orimportNode()
. - Cross-document references may cause memory leaks due to a Node effectively having two owner Documents (its actual owner Document, and the owner Document of the node which has the reference)
let el = document.body.appendChild(document.createElement('div'));
let descendant = document.createElement('div');
el.ariaActiveDescendantElement = descendant;
console.log(el.ariaActiveDescendantElement); // logs descendant
var iframe = document.createElement('iframe');
var html = '<body></body>';
iframe.src = 'data:text/html;charset=utf-8,' + encodeURI(html);
document.body.appendChild(iframe);
iframe.contentDocument.adoptNode(descendant); // Reference is dropped
console.log(el.ariaActiveDescendantElement); // logs null
And/or: use WeakReference
s
let el = document.body.appendChild(document.createElement('div'));
// not keeping a JS reference to the newly created element
el.ariaActiveDescendantElement = document.body.appendChild(document.createElement('div'));
console.log(el.ariaActiveDescendantElement); // logs newly created element from above
var iframe = document.createElement('iframe');
var html = '<body></body>';
iframe.src = 'data:text/html;charset=utf-8,' + encodeURI(html);
document.body.appendChild(iframe);
iframe.contentDocument.adoptNode(el.ariaActiveDescendantElement);
console.log(el.ariaActiveDescendantElement); // logs null - no current reference to the ariaActiveDescendantElement
- Dependent on WeakRefs proposal being implemented
- Unclear how to handle array types
- Would probably still need to drop references at
adoptNode()
time?
- If an element is in a tree, the attr-associated element must be in the same tree. If an element is removed from a tree, any relationship it is participating in is invalid and removed.
let el = document.body.appendChild(document.createElement('div'));
let descendant = document.createElement('div');
el.ariaActiveDescendant = descendant; // this is not valid
console.log(el.ariaActiveDescendant); // logs null
el.parentNode.appendChild(el.ariaActiveDescendant);
console.log(el.ariaActiveDescendant); // logs null, the association was never created
el.ariaActiveDescendant = descendant; // this is valid
console.log(el.ariaActiveDescendant); // logs descendant
descendant.remove(); // relationship is annulled,
// descendant would be garbage collected if we hadn't kept a reference to it
console.log(el.ariaActiveDescendant); // logs null
let shadowRoot = el.attachShadow({mode: 'closed'});
shadowRoot.appendChild(descendant);
el.ariaActiveDescendant = descendant; // this is not valid
console.log(el.ariaActiveDescendant); // logs null
el.ariaActiveDescendant = null;
descendant.ariaActiveDescendant = el; // not valid
console.log(descendant.ariaActiveDescendant); // logs null
Pros
- Stricter about Shadow DOM encapsulation than Option 1.
- Does not require each API to determine validity.
- Does not constitute a dangling reference to nodes which are removed from the tree.
- Does not allow cross-document references.
- Reasonably simple to understand.
Cons
- Some implementation complexity to remove element relationships when elements are removed from the tree (for any element being removed, need to look up whether it is the subject of a relationship and remove it).
- Possibly more difficult for developers to reason about:
- "action at a distance" when removing an element from the DOM affects the properties of another element
- may cause surprising behaviour when relationships do not persist across element remove/re-add.
- Requires more bookkeeping from developers as relationships may not persist across mutations.
- Does not allow authors to refer to elements across any shadow boundaries.
- A relationship between two elements may be created if one or both is not yet in the tree, but is validated and may be removed after both nodes have been inserted in the tree.
- The validation check may occur on setting, and/or when elements are added to/removed from a tree.
- Elements in different shadow scopes are still not valid.
let el = document.body.appendChild(document.createElement('div'));
let descendant = document.createElement('div');
el.ariaActiveDescendant = descendant; // this is valid
console.log(el.ariaActiveDescendant); // logs descendant
el.parentNode.appendChild(el.ariaActiveDescendant); // relationship may be validated here...
console.log(el.ariaActiveDescendant); // ...or here.
// Logs descendant either way, unless it was annulled in the earlier get.
descendant.remove(); // no longer valid.
console.log(el.ariaActiveDescendant); // logs null.
// descendant would be garbage collected if we hadn't kept a reference to it.
let shadowRoot = el.attachShadow({mode: 'closed'});
shadowRoot.appendChild(descendant);
el.ariaActiveDescendant = descendant; // this is not valid, validated on set
console.log(el.ariaActiveDescendant); // logs null
el.ariaActiveDescendant = null;
descendant.ariaActiveDescendant = el; // not valid, validated on set
console.log(descendant.ariaActiveDescendant); // logs null
Pros
- Allows developers to build up subtrees of nodes with inter-relationships before inserting the subtree into the tree (for example, when building a component).
Cons
- Implementation/runtime complexity to continually validate elements being added to/removed from the DOM may be significant (would require a mechanism to determine whether a given element is the subject of an element relationship, and a lookup of this mechanism any time an element is added to or removed from the tree)
- May still require author bookkeeping as relationships do not persist across removing and re-adding elements.
- Does not allow authors to refer to elements across any shadow boundaries.
Elements in "lighter" shadow trees may be allowed to be set as associated elements, while excluding references to "darker" shadow trees.
// mostly the same as above, except:
el.ariaActiveDescendant = null;
descendant.ariaActiveDescendant = el; // this is valid, validated on set
console.log(descendant.ariaActiveDescendant); // logs el
// and as a reminder...
el.remove(); // no longer valid
console.log(descendant.ariaActiveDescendant); // logs null
Pros
- As above, but also allows authors to create references across shadow boundaries in limited cases.
Cons
- Largely as above, plus potentially more difficult to understand the cases in which relationships are and are not valid.
- For example, sprout a GUID if an element doesn't already have one.
Pros
- Avoids dealing with the trade-offs of either of the above options.
Cons
- GUIDs :(
- Can't refer to lighter Shadow DOM without an extra mechanism (e.g. a space-separated token list indicating how many levels "up" to go before searching for the given ID)
This was the original idea in AOM: each element would have an associated accessibleNode
which could be associated with any other accessibleNode
regardless of Shadow DOM (and which does not allow retrieving the associated element).