0
votes

Let's say I have a dialog component like

class ModalDialog extends HTMLElement {
    constructor(){
        super()
        this._shadow = this.attachShadow({mode: 'closed'})
    }

    connectedCallback(){
        const template = `
            <style>
            ... lots of style that doesn't matter to this question ...
            </style>
            <div class="dialog">
                <div class="dialog-content">
                    <div class="dialog-header">
                        <slot name="header"></slot>
                        <span class="close">&times;</span>
                    </div>
                    <div class="dialog-body"><slot name="body"></slot></div>
                    <div class="dialog-footer"><slot name="footer"></slot></div>
                </div>
            </div>
        `

        this._shadow.innerHTML = template
        this._shadow.querySelector('.close').onclick = () => this.hide()
        const dialog = this._shadow.querySelector('.dialog')
        dialog.onclick = evt => {
            if(evt.target == dialog){ //only if clicking on the overlay
                this.hide()
            }
        }
        this.hide()
    }

    show() {
        this.style.display = 'block'
    }

    hide(){
        this.style.display = 'none'
    }
}

window.customElements.define('modal-dialog', ModalDialog)

Now let's say I want to create dedicated dialogs ... e.g. one that allows a user to pick an image.

I could do it like this

import {} from './modal-dialog.js'

class ImageSelector extends HTMLElement {
    constructor(){
        super()
        this._shadow = this.attachShadow({mode: 'closed'})
    }

    connectedCallback(){
        const template = `
            <style>
                ... more style that doesn't matter ...
            </style>
            <modal-dialog>
                <div slot="header"><h3>Select Image</h3></div>
                <div slot="body">
                     ... pretend there's some fancy image selection stuff here ...
                </div>
            </modal-dialog>
        `
        this._shadow.innerHTML = template
    }

    show(){
        this._shadow.querySelector('modal-dialog').show()
    }

    hide(){
        this._shadow.querySelector('modal-dialog').hide()
    }
}
window.customElements.define('image-selector', ImageSelector)

but I don't like the show and hide methods, there.

Another option would be to inherit from the dialog rather than from HTMLElement...

import {} from './modal-dialog.js'

class ImageSelector extends customElements.get('modal-dialog'){
    constructor(){
        super()
    }

    connectedCallback(){
        ... now what? ...
    }
}
window.customElements.define('image-selector', ImageSelector)

but if I do that, how do I actually fill the slots?

Naive approach would of course be to just use _shadow and put it into the slots' inner html, but I have a feeling that's not the way to go.

2

2 Answers

1
votes

TLDR; It is impossible to use both inheritance and composition at same time.

Long answer:

You are actually mixing two distinct but alternative concepts:

  1. Inheritance - Use when overriding/overloading the existing behavior
  2. Composition - Use when using the behavior of some other entity and add some more behavior around it.

You can substitute one for another and in general, in Web UI programming, Composition is always preferred over the Inheritance for the loose coupling it provides.

In you case, you actually want to use the template and not actually override it. So, composition is a better choice here. But that also means that you will actually have to write some more boilerplate code i.e. wrapper implementations of show and hide method.

In theory, inheritance was invented to promote code re-use and avoid repetitive code but that comes as a cost as compared to composition.

0
votes

Good info from Harshals' answer; here is the userland code

Unless you are doing SSR, the very first file read is the HTML file.

So put the Template content there and let one generic Modal Web Component read the templates

<template id="MODAL-DIALOG">
  <style>
    :host { display: block; background: lightgreen; padding:.5em }
    [choice]{ cursor: pointer }
  </style>
  <slot></slot>
  <button choice="yes">Yes</button>
  <button choice="no" >No</button>
</template>
<template id="MODAL-DIALOG-IMAGES">
  <style>
    :host { background: lightblue; } /* overrule base template CSS */
    img { height: 60px; }
    button { display: none; } /* hide stuff from base template */
  </style>
  <h3>Select the Framework that is not Web Components friendly</h3>
  <img choice="React" src="https://image.pngaaa.com/896/2507896-middle.png">
  <img choice="Vue" src="https://upload.wikimedia.org/wikipedia/commons/thumb/9/95/Vue.js_Logo_2.svg/1184px-Vue.js_Logo_2.svg.png">
  <img choice="Svelte" src="https://upload.wikimedia.org/wikipedia/commons/thumb/1/1b/Svelte_Logo.svg/1200px-Svelte_Logo.svg.png">
   <slot><!-- remove slot from BaseTemplate, then this slot works --></slot>
</template>

<modal-dialog>Standard Modal</modal-dialog>
<modal-dialog template="-IMAGES">Images Modal</modal-dialog>

<script>
  document.addEventListener("modal-dialog", (evt) => {
    alert(`You selected: ${evt.detail.getAttribute("choice")}`)
  });
  customElements.define('modal-dialog', class extends HTMLElement {
    constructor() {
      let template = (id="") => {// if docs say "use super() first" then docs are wrong
        let templ = document.getElementById(this.nodeName + id);
        if (templ) return templ.content.cloneNode(true);
        else return []; // empty content for .append
      }
      super().attachShadow({mode:"open"})
             .append( template(),
                      template( this.getAttribute("template") ));
      this.onclick = (evt) => {
        let choice = evt.composedPath()[0].hasAttribute("choice");
        if (choice) 
          this.dispatchEvent(
            new CustomEvent("modal-dialog", {
              bubbles: true,
              composed: true,
              detail: evt.composedPath()[0]
            })
          );
       // else this.remove();   
      }
    }
    connectedCallback() {}
  });
</script>

Notes

  • Ofcourse you can wrap the <TEMPLATES> in a JS String

  • You can't add <script> in a Template
    well, you can.. but it runs in Global Scope, not Component Scope

  • For more complex Dialogs and <SLOT> you will probably have to remove the unwanted slots from a Basetemplate with code

  • Don't make it too complex:

    • Good Components do a handful of things very good.
    • Bad Components try to do everything... create another Component