Creating new instances of custom HTML elements

Viewed 2913

I am trying to understand the newish HTML custom elements.

My goal is, given some array of data, create n instances of the custom element. For example, given a list of 10 users, create 10 user html objects.

Ok - so I define a custom element in the html

HTML

<template-user>
   <div class="user-name"></div>
</template-user>

Then I create my controller

JS

class UserTemplate extends HTMLElement {
   constructor(){
      super();
      this.username = this.querySelectorAll('[class="user-name"]')[0];
   }
   setName(name){
      this.username.innerHtml = name;
   }
}
customElements.define('template-user', UserTemplate);

The page loads fine, but now I am confused on how to reuse that element. If I was doing normal old school stuff, I'd have a for-loop pumping out some HTML strings and setting the innerHTML of something. But now I'd rather do something like

for(let i = 0; i < users.length; i++){
   let userTemplate = new UserTemplate();
   userTemplate.setName(user.name);
   // append to user list, etc..
}

When I try to do this, it seems to almost work. But it cannot find username, ie this.querySelectorAll will return null. That's only when I try to construct a new instance of this element. How then, am I supposed to create new custom element DOM objects?

4 Answers

Make sure you understand the requirements and limitations of constructors for Web Components:


https://html.spec.whatwg.org/multipage/custom-elements.html#custom-element-conformance

4.13.2 Requirements for custom element constructors

When authoring custom element constructors, authors are bound by the following conformance requirements:

  • A parameter-less call to super() must be the first statement in the constructor body, to establish the correct prototype chain and this value before any further code is run.
  • A return statement must not appear anywhere inside the constructor body, unless it is a simple early-return (return or return this).
  • The constructor must not use the document.write() or document.open() methods.
  • The element's attributes and children must not be inspected, as in the non-upgrade case none will be present, and relying on upgrades makes the element less usable.
  • The element must not gain any attributes or children, as this violates the expectations of consumers who use the createElement or createElementNS methods.
  • In general, work should be deferred to connectedCallback as much as possible—especially work involving fetching resources or rendering. However, note that connectedCallback can be called more than once, so any initialization work that is truly one-time will need a guard to prevent it from running twice.
  • In general, the constructor should be used to set up initial state and default values, and to set up event listeners and possibly a shadow root.

Several of these requirements are checked during element creation, either directly or indirectly, and failing to follow them will result in a custom element that cannot be instantiated by the parser or DOM APIs. This is true even if the work is done inside a constructor-initiated microtask, as a microtask checkpoint can occur immediately after construction.


You could make changes similar to this:

class TemplateUser extends HTMLElement {
  static get observedAttributes() {
    return ['user-name'];
  }

  constructor(){
    super();
    this.attachShadow({mode:'open'});
    this.shadowRoot.innerHTML = '<div></div>';
  }
   
  attributeChangedCallback(attrName, oldVal, newVal) {
    if (oldVal !== newVal) {
      this.shadowRoot.firstChild.innerHTML = newVal;
    }
  }
 
  get userName() {
    return this.getAttribute('user-name');
  }
  
  set userName(name) {
    this.setAttribute('user-name', name);
  }
}

customElements.define('template-user', TemplateUser);


setTimeout( function () {
  var el = document.querySelector('[user-name="Mummy"]');
  el.userName = "Creature from the Black Lagoon";
}, 2000);
<template-user user-name="Frank N Stein"></template-user>
<template-user user-name="Dracula"></template-user>
<template-user user-name="Mummy"></template-user>

This uses shadowDOM to store a <div>, then you set the value through the user-name attribute or through the userName property.

But it cannot find username, ie this.querySelectorAll will return null.

When you make a new instance, the new element has no children so querySelectorAll will return an empty NodeList. If you query the DOM and select the template-user which has been defined in your markup then the username property will refer to your div element.

If you want a dynamically generated template-user element to have a <div class="user-name"></div> child by default, you should create and append an element in your constructor.

Also for selecting the first matching element you can use .querySelector(...) instead of .querySelectorAll(...)[0].

When you create a custom element via Javascript with new, you can set some variables that will be passed as parameters in the constrcutor() method:

for (let user of users) {
   document.body.appendChild( new UserTemplate(user.name) )
}

You can get it and save it in an object variable, or use in as a variable in a template literal string in a Shadow DOM.

class UserTemplate extends HTMLElement {
    constructor(username){
        super()
        //this.username = username
        this.attachShadow({ mode:'open' })
            .innerHTML = `<div class="user-name"> ${username} </div>` 
    }
}
customElements.define('template-user', UserTemplate);

you can create your components and assign properties right in JS

let templateUser = document.createElement('template-user');
templateUser.userName= 'Your name here';
document.body.appendChild(templateUser);

or something similar to that based on your needs. Google has some docs here, about 2/3 the way down they describe how to "create an instance in JavaScript". Which can be pretty powerful, especially in a for-loop like your example

Related