Как заполнить слоты при наследовании от веб-компонента?

Скажем, у меня есть диалоговый компонент, например

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)

Теперь предположим, что я хочу создать специальные диалоги... например. тот, который позволяет пользователю выбрать изображение.

Я мог бы сделать это так

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)

но мне не нравятся методы show и hide.

Другой вариант - наследовать от диалога, а не от HTMLElement...

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

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

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

но если я это сделаю, как мне на самом деле заполнить слоты?

Наивный подход, конечно, состоял бы в том, чтобы просто использовать _shadow и поместить его во внутренний html слотов, но у меня такое чувство, что это не тот путь.


person User1291    schedule 07.03.2021    source источник


Ответы (2)


TLDR; Одновременное использование наследования и композиции невозможно.

Длинный ответ:

На самом деле вы смешиваете две разные, но альтернативные концепции:

  1. Наследование — используйте при переопределении/перегрузке существующего поведения
  2. Композиция — используйте при использовании поведения какой-либо другой сущности и добавляйте к ней дополнительное поведение.

Вы можете заменить одно на другое, и, как правило, в программировании веб-интерфейса Композиция всегда предпочтительнее Наследование из-за слабой связанности, которую она обеспечивает.

В вашем случае вы действительно хотите использовать шаблон, а не переопределять его. Таким образом, композиция является лучшим выбором здесь. Но это также означает, что вам действительно придется написать еще немного шаблонного кода, т. е. реализацию методов-оболочек show и hide.

Теоретически наследование было изобретено для повторного использования кода и предотвращения его повторения, но это требует затрат по сравнению с композицией.

person Harshal Patil    schedule 08.03.2021
comment
вы действительно хотите использовать шаблон, а не переопределять его. Спорный. Я бы сказал, что пытаюсь переопределить части родительского шаблона. ;) Но я понимаю вашу точку зрения. - person User1291; 09.03.2021

Хорошая информация из ответа Харшалса; вот код пользователя

Если вы не выполняете SSR, самым первым прочитанным файлом будет файл HTML.

Поэтому поместите содержимое шаблона туда и позвольте одному универсальному модальному веб-компоненту прочитать шаблоны.

<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>

Примечания

  • Конечно, вы можете обернуть <TEMPLATES> в строку JS.

  • Вы не можете добавить <script> в шаблон
    ну, вы можете... но он работает в глобальной области видимости, а не в области компонента

  • Для более сложных диалогов и <SLOT> вам, вероятно, придется удалить ненужные слоты из базового шаблона с помощью кода.

  • Не усложняйте:

    • Good Components do a handful of things very good.
    • Плохие компоненты пытаются сделать все... создать еще один компонент
person Danny '365CSI' Engelman    schedule 08.03.2021
comment
Я ценю ваш фрагмент, но для кого-то вроде меня, кто еще не знаком с ним, это выглядит как подход, который не поддерживает какие-либо диалоги с отслеживанием состояния со сложными операциями. По крайней мере, без нарушения инкапсуляции. Я ошибаюсь в этом? - person User1291; 09.03.2021
comment
В отличие от фреймворков, которые заставляют вас разрабатывать по определенному шаблону, веб-компоненты — это просто кирпичи и раствор; решать вам, что с ними делать. Вы можете разрабатывать все, что захотите... это всего лишь JavaScript... который, строго говоря, не имеет ООП-инкапсуляции. См. itnext.io/ и css-tricks.com/ - person Danny '365CSI' Engelman; 09.03.2021