Обертывание поля выбора в настраиваемый элемент

Я хочу создать настраиваемый элемент html, который должен вести себя почти так же, как собственный элемент <select>, но, кроме того, он также должен вызывать определенную функцию обновления каждый раз, когда изменяется атрибут или дочерний узел. (предыстория: это необходимый шаг для использования элемента bootstrap-select внутри вяза framework. См. также мой последний вопрос.)

Используя фреймворк LitElement, я смог создать рабочий настраиваемый элемент (названный <lit-select>), аналогичный описанию выше. Но, к сожалению, мне не удалось заставить его принимать элементы html <option> или <optgroup> как дочерние элементы. Вместо этого пользователь должен передать список параметров определенному атрибуту в виде строки в кодировке json.

То есть вместо звонка

<lit-select>
  <option>foo</option>
  <option>bar</option>
</lit-select>

пользователь должен позвонить

<lit-select items='["foo", "bar"]'></lit-select>

Каким образом мне нужно изменить определение <lit-select>, чтобы сделать первый вызов возможным? Мне известен элемент <slot>, но, к сожалению, этот элемент нельзя использовать внутри <select>, поэтому браузер просто удаляет его.

Заранее спасибо!


Обновление 1

На самом деле есть некоторые ограничения, которые делают проблему более сложной, чем я думал сначала:

  • Я должен избегать теневого DOM. Это связано с тем, что мой пользовательский элемент стилизован / улучшен с помощью bootstrap (и bootstrap-select) css / js, которые смотрят только на обычную DOM. Как я только что узнал, это исключает slot элементов, поскольку они специфичны для теневой DOM.
  • Мой настраиваемый элемент должен полностью реагировать на изменения (добавление / удаление дочерних заметок, изменение атрибутов). Это связано с тем, что я планирую использовать элемент внутри виртуальной DOM (в моем случае вяз, но он также должен работать с реакцией).

Приложение

Мое определение <lit-select>:

import { LitElement, html, customElement, property } from 'lit-element';
import * as $ from 'jquery';
import 'bootstrap';
import 'bootstrap-select';

@customElement('lit-select')
export class LitSelect extends LitElement {

    @property({ type : Array }) items = []

    updated() {
        $(this).find(".selectpicker").selectpicker('refresh');
    }

    createRenderRoot() {
        return this;
    }

    private renderItem(item: string) {
        return html`
            <option>
                ${item}
            </option>
        `;
    }

    render() {
        return html`
            <select class="selectpicker" data-live-search = "true">
                ${this.items.map(item => this.renderItem(item))}
            </select>
        `;
    }
}

person Dune    schedule 04.05.2020    source источник


Ответы (2)


Почему бы не начать с собственного стандартного настраиваемого элемента W3C

Все, что вам нужно сделать, это переместить несколько <option> элементов.

<template id=MY-SELECT>
  <h3>My Never Todo List</h3>
  <select multiple></select>
</template>

<my-select name="NTD">
  <option>Grow up</option>
  <option>Learn React</option>
  <option>Use Lit</option>
  <option>Forget W3C Custom Elements API</option>
</my-select>

<script>
  customElements.define("my-select", class extends HTMLElement {
    static get observedAttributes() {
      return ["name"]; //use one to trigger attributeChangedCallback
    }
    connectedCallback() {
      console.log('connected');
      //clone template
      this.append(document.getElementById(this.nodeName).content.cloneNode(true));
      //MOVE options inside SELECT:
      this.querySelector('select').append(...this.querySelectorAll('option'));
    }
    attributeChangedCallback() {
      setTimeout(() => this.updated(...arguments))// execute after Event Loop is done
    }
    updated(name,oldValue,newValue){
      console.log('updated',name,oldValue,newValue);
    }
  })

</script>

person Danny '365CSI' Engelman    schedule 04.05.2020
comment
Большое спасибо за такой быстрый ответ! Одна из причин использования LitElement заключается в том, что он предоставляет обновленный обратный вызов, который является именно тем местом, где можно вызвать нужную мне функцию обновления. К сожалению, connectedCallback вызывается слишком рано (и только один раз!). Кроме того, я не могу использовать теневой корень, потому что мой элемент будет изменен функциями начальной загрузки, которые (просто) не работают внутри теневых корней. - person Dune; 04.05.2020
comment
Я обновил код. Нет shadowDOM и метода обновления. connectedCallback не вызывается слишком рано ... Ваш код хочет что-то сделать до того, как цикл событий завершит создание этих элементов. Так что отложите свой код. Lit также ожидает появления элементов. - person Danny '365CSI' Engelman; 04.05.2020
comment
Еще раз спасибо, ваш подход выглядит очень многообещающим! Я скоро попробую. На самом деле, я уже пробовал setTimeout внутри connectedCallback и отклонил эту идею, потому что это приводит к условиям гонки. Например (на моем оборудовании) ожидания 1 мс недостаточно. Иногда достаточно ожидания 3 мс, а иногда нет. Проблема в том, что мне нужно дождаться завершения рендеринга. Это не то же самое, что ожидание очистки цикла событий. - person Dune; 04.05.2020
comment
Все зависит от того, что вы делаете с элементами DOM. Вы не можете что-то сделать с элементами DOM, если их нет (пока) Settimeout 0 мс должно быть достаточно в connectedCallback ... если вы не выполняете асинхронные операции с этими штуками Bootstrap ... но не включайте элементы тогда. Сначала заставьте его работать со стандартным JS-кодом. Если вы этого не сделаете, вы накапливаете технологии, делая это все труднее и труднее. - person Danny '365CSI' Engelman; 04.05.2020

Как насчет этого решения?

import {LitElement, html } from 'lit-element';


export class GraniteSelect extends LitElement {

  static get properties() {
    return {
      options: {
        type: Object,
      }
    }
  }

  constructor() {
    super();
    this.options =  [];
  }

  connectedCallback() {
    super.connectedCallback();

    this.observer = new MutationObserver((mutations) => {
      mutations.forEach((mutation) => {
        mutation.removedNodes.forEach((node) => {
          if (node.nodeName === 'OPTION' ) {
            this.options = [ ...this.options, node ];
           console.log(`options - ${this.options}`);
           console.dir(this.options);
          }
        });
      });
    });
    this.observer.observe(this, {
        childList: true,
    });
  }

  firstUpdate() {
    this.observer.disconnect();
  }

  createRenderRoot() {
  /**
   * Render template without shadow DOM. Note that shadow DOM features like 
   * encapsulated CSS and slots are unavailable.
   */
    return this;
  }


  render() {
    console.log('Rendering', this.options);
    return html`
      <select class="selectpicker" data-live-search = "true">
          ${this.options}
      </select>
    `;
  }
}

window.customElements.define('granite-select', GraniteSelect);

Он использует light-dom (часть createShadowRoot) и помещает все дочерние параметры вашего элемента в select. Потомки без параметров просто игнорируются, но с ними можно делать что угодно.

Вы можете увидеть полный пример в https://stackblitz.com/edit/js-14dcae.

Что вы думаете об этом подходе?

person LostInBrittany    schedule 04.05.2020
comment
Выглядит интересно! Однако, поскольку вы закрываете наблюдатель после первого обновления, узел не изменится, если кто-то (например, виртуальный дом) добавит или удалит дочерние заметки. Конечно, мы могли бы адаптировать наблюдателя, чтобы он правильно реагировал на все виды операций dom, но мне это кажется сложным ... - person Dune; 04.05.2020
comment
Да, в этом случае вы не должны закрывать наблюдателя и использовать его для синхронизации дочерних элементов со свойствами параметров при каждой мутации. Это действительно сложнее, но я не уверен, что в этом случае можно сделать проще. - person LostInBrittany; 06.05.2020