Monsieur Pierre Doune
Monsieur Pierre Doune

Reputation: 737

Initialisation of Custom Elements Inside Document Fragment

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.

Upvotes: 7

Views: 826

Answers (3)

Robbendebiene
Robbendebiene

Reputation: 4879

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.

Upvotes: 4

Lloyd
Lloyd

Reputation: 8406

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)

Upvotes: 0

Monsieur Pierre Doune
Monsieur Pierre Doune

Reputation: 737

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.

Upvotes: 3

Related Questions