Passing values to constructor of custom HTMLElement
Asked Answered
V

3

15

I have a custom HTMLElement that's working as intended, but I cannot find a way to pass arguments to the constructor; instead I'm accessing passing the values in my custom tag, but then I have to set the values in connectedCallback(), which I'm not satisfied with.

Here's a basic version of what I'm doing now:

class TrackingPreview extends HTMLElement {
  constructor() {
    super();

    const video = document.createElement('video');
    video.setAttribute('controls', '');
    video.setAttribute('width', '1920');
    video.setAttribute('height', '1080');
    shadow.appendChild(video);

    this.video = video;
  }

  connectedCallback() {
    const videoId = this.getAttribute('video-id');
    this.video.id = videoId;
  }
}

I'd rather pass the videoId directly to the constructor, something like this (which is NOT working):

JS:

 class TrackingPreview extends HTMLElement {
  constructor(videoId) {
    super();

    const video = document.createElement('video');
    video.setAttribute('id', videoId);
    video.setAttribute('controls', '');
    video.setAttribute('width', '1920');
    video.setAttribute('height', '1080');
    shadow.appendChild(video);
  }

  connectedCallback() {
  }
}

JS Script on HTML Page:

  $(document).ready(function(){
    const tracking_preview = document.createElement('tracking-preview','{{video_id}}');
    tracking_preview.videoId = '{{video_id}}';
    document.body.append(tracking_preview);
  });

Is there a way to pass values to a custom constructor? The docs imply it is possible but aren't very helpful for how to do it.

Volans answered 8/4, 2019 at 21:14 Comment(1)
The key is to use the customElements.define() method instead of creating the element directly.Salado
R
12

Remember that none of the regular HTML elements come with constructor arguments either: don't rely on them. Instead, rely on attributes, with JS property backing, and hook those up according to the custom elements spec.

So, in the same way that you'd have to the following code for an Image:

let img = new Image();
img.src = "https://example.com/someimage.jpg";
img.width = ...
...

or a script element:

let script = document.createElement("script");
script.src = ...
// or script.textContent = ...

your component should not need constructor properties. You'd use it like this:

let preview = new TrackingPreview();
preview.width = 1920;
preview.height = 1080;

or in HTML form,

<track-preview width="100" height="200" ...></track-preview>

And have your custom element react to those properties getting set:

class TrackingPreview extends HTMLElement {

  static get observedAttributes() {
    return [`width`, `height`];
  }
  
  constructor() {
    super();
    const shadow = this.attachShadow({mode: 'open'});
    const video = this.video = document.createElement('video');
    video.setAttribute(`controls`, `controls`);
    video.setAttribute(`width`, this.getAttribute(`width`) || 1920);
    video.setAttribute(`height`, this.getAttribute(`height`) || 1080);
    shadow.appendChild(video);
  }

  get width() {
    return this.video.width;
  }

  set width(val) {
    this.setAttribute(`width`, val);
  }

  get height() {
    return this.video.height;
  }

  set height(val) {
    this.setAttribuite(`height`, val);
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (name === `width`) {
      this.video.width = newValue;
    }
    if (name === `height`) {
      this.video.height = newValue;
    }
    // ...
  }

  // ...
}
Redstart answered 21/7, 2020 at 23:29 Comment(0)
L
7

The rules of a constructor for a Web Component are pretty strict. One rule is that you are not allowed to pass in any arguments into the constructor.

https://w3c.github.io/webcomponents/spec/custom/#custom-element-conformance

Instead use both a property and an attribute

class TrackingPreview extends HTMLElement {
  static get observedAttributes() {
    return ['videoid'];
  }

  constructor() {
    super();

    const video = document.createElement('video');
    video.setAttribute('controls', '');
    video.width = 192;
    video.height = 108;
    this.video = video;
    this.attachShadow({mode: 'open'}).appendChild(video);
  }

  attributeChangedCallback(attrName, oldVal, newVal) {  
    if (oldVal !== newVal) {
      // Since we only watch videoid there is no need to check attrName
      this.video.setAttribute('id', newVal);
    }
  }
  
  get videoId() {
    return this.video.getAttribute('id');
  }

  set videoId(val) {
    if (val == null) { // check for null and undefined
      this.removeAttribute('videoid');
    }
    else {
      this.setAttribute('videoid', val);
    }
  }
}

customElements.define('tracking-preview', TrackingPreview);

setTimeout(function() {
  let el = document.querySelector('tracking-preview');
  if (el) {
    console.log(el.videoId);
    el.videoId = 99;
    console.log(el.videoId);
  }
}, 500);
<tracking-preview videoid="10"></tracking-preview>

When you attribute videoid is set then the attributeChangedCallback function is called and you pass the value down to your video element.

In my timeout function I both read and write the videoId property which read and writes the videoid attribute.

Legaspi answered 2/8, 2019 at 22:30 Comment(1)
The latest spec does not say that you are not allowed to pass any arguments into the constructor, it says that "When upgraded, its constructor is run, with no arguments." So when you write the element in HTML, it is created by calling the constructor with no arguments. It doesn't say anywhere that you can't define parameters, your parameters will just not be used when the element is instantiated from HTML being parsed.Drusy
P
-3

You can do it with the new keyword:

const tacking_preview = new TrackingPreview( id )
Prewar answered 10/6, 2019 at 1:33 Comment(6)
note that you can't set attributes in the custom element constructorEbb
You cannot instantiate custom elements with the new keyword, you always need to call document.createElement('node-name-for-element') insteadChiang
@Chiang you're wrong. Of course you can instantiate a custom element with "new".Prewar
@Ebb Question is not about setting attributes but passing values to "constructor". It's possible of course.Prewar
This isn't a good pattern for redistributed web components, because consumers may run into issues trying to use it via createElement or html markup. However, this method appears to work and answer the question as long as the element is always being instantiated and attached to the document using JavaScript.Facial
I feel like this is the most correct answer. Although it might not be the best practice to define parameters on custom element constructors, because they can't really be used in html, it does answer your question: "Is there a way to pass values to a custom constructor?" Just because something is "bad practice" doesn't mean it's wrong, so I up-voted this.Drusy

© 2022 - 2024 — McMap. All rights reserved.