Initialisation of Custom Elements Inside Document Fragment
Asked Answered
F

3

7

Consider this HTML template with two flat x-elements and one nested.

<template id="fooTemplate">
  <x-element>Enter your text node here.</x-element>
  <x-element>
    <x-element>Hello, World?</x-element>
  </x-element>
</template>

How to initialise (fire constructor) all custom elements in cloned from fooTemplate document fragment without appending it to DOM, neither by extending built-in elements with is="x-element"; either entire fragment.

class XElement extends HTMLElement {
  constructor() { super(); }
  foo() { console.log( this ); }
} customElements.define( 'x-element', XElement );

const uselessf = function( temp ) {
  const frag = window[ temp ].content.cloneNode( true );

  /* Your magic code goes here:
  */ do_black_magic( frag );

  for (const e of  frag.querySelectorAll('x-element') )
    e.foo(); // This should work.

  return frag;
};

window['someNode'].appendChild( uselessf('fooTemplate') );

Note that script executes with defer attribute.

Flanker answered 22/4, 2019 at 8:7 Comment(1)
Maybe I should somehow upcast HTMLElement to XElement?Flanker
F
3

We can initialise template with this arrow function:

const initTemplate = temp =>
  document.createRange().createContextualFragment( temp.innerHTML );

const frag = initTemplate( window['someTemplate'] );

Or with this method defined on template prototype (I prefer this way):

Object.defineProperty(HTMLTemplateElement.prototype, 'initialise', {
  enumerable: false,
  value() {
    return document.createRange().createContextualFragment( this.innerHTML );
  }
});

const frag = window['someTemplate'].initialise();

In any case in result this code will work fine:

for (const elem of  frag.querySelectorAll('x-element') )
  elem.foo();

window['someNode'].appendChild( frag );

I'm not sure if these methods are the most effective way to initialise custom elements in template.

Also note that there is no need for cloning template.

Flanker answered 22/4, 2019 at 9:49 Comment(3)
@LogicalBranch in first link Eric uses document.registerElement which is deprecated and in last link nothing about custom elements in templates.Flanker
What I was about: first link, second linkFlanker
document.createRange().createContextualFragment( temp.innerHTML ); will actually create a copy of the template contents/html. The reason this works is because the copy uses the global document as the context/ownerDocument.Scoreboard
S
3

TLDR:

Use document.importNode(template.content, true); instead of template.content.cloneNode(true); Read more about document.importNode() here.

Explanation:

Since the custom element is created in a different document/context (the DocumentFragment of the template) it doesn't know about the custom elements definition in the root/global document. You can get the document an element belongs to by reading the Node.ownerDocument property (MDN) which in this case will be different to the window.document element.

This being said you need to create the custom element in the context of the global document in order to "apply" the custom element. This can be done by calling document.importNode(node, [true]) (MDN) which works like node.cloneNode([true]), but creates a copy of the element in the global document context.

Alternatively you can also use document.adoptNode(node) (MDN) to first adopt the DocumentFragment to the global document and then create copies of it via node.cloneNode([true]). Note though if you use adoptNode() on an HTML element it will be removed from its original document.

Illustrative Example Code:

class XElement extends HTMLElement {
  constructor() { super(); console.log("Custom Element Constructed") }
}
customElements.define( 'x-element', XElement );

const externalFragment = fooTemplate.content;

console.log(
  "Is ownerDocument equal?",
  externalFragment.ownerDocument === document
);

console.log("import start");

const importedFragment = document.importNode(externalFragment, true);

console.log("import end");

console.log(
  "Is ownerDocument equal?",
  importedFragment.ownerDocument === document
);
<template id="fooTemplate">
  <x-element>Hello, World?</x-element>
</template>

Note: Appending an element from one document to another document forces an implicit adoption of the node. That's why appending the element to the global DOM works in this case.

Scoreboard answered 25/4, 2021 at 11:15 Comment(2)
This is the correct answer!!! Something you do not want to add the document fragment to the DOM prior to manipulation or query. - You want to be able to manipulate without bubbling up events. - You want to query the document fragment in a silo rather than querying in the context of the whole doc. document.importNode(MY_COMPONENT_TEMPLATE, true) allows to have light DocumentFragment templates, and "activate" their content on clone. Best of both world! Thanks a lot!Whorled
Impressive, thank you! how did you learn all this? I could not garner this much understanding from reading the MDN articles about all of these functions.Seignior
M
0

You can avoid the "createContextualFragment" hack from the previous answer by simply adding the template clone to the document immediately before processing it.

Assuming we have these two variables defined...

const containerEl = document.querySelector('div.my-container')
const templateEl = document.querySelector('#fooTemplate')

...instead of doing this (where frag contains uninitialised custom elements)...

const frag = templateEl.content.cloneNode(true)
manipulateTemplateContent(frag)
containerEl.appendChild(frag)

...append the template clone to the document first, then manipulate it. The user won't notice any difference - it's all synchronous code executed within the same frame.

const frag = templateEl.content.cloneNode(true)
containerEl.appendChild(frag)
manipulateTemplateContent(containerEl)
Mcadoo answered 16/8, 2020 at 8:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.