Обвиване на поле за избор в потребителски елемент

Искам да създам персонализиран html елемент, който трябва да се държи почти идентично с основния елемент <select>, но освен това трябва да извиква определена функция за актуализиране всеки път, когато се промени атрибут или дъщерен възел. (история: Това е необходима стъпка за използване на елемента bootstrap-select вътре в elm рамка. Вижте също последния ми въпрос.)

Използвайки рамката 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

Всъщност има някои ограничения, които правят проблема по-предизвикателен, отколкото си мислех в началото:

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

Приложение

Моето определение за <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 се извиква твърде рано (и само веднъж!). Също така, не мога да използвам shadow root, защото моят елемент ще бъде променен от функции за стартиране, които не работят (просто) в shadow roots. - person Dune; 04.05.2020
comment
Актуализирах кода. Без shadowDOM и метод за актуализиране. connectedCallback не се извиква твърде рано... Вашият код иска да направи нещо, преди цикълът на събитията да е приключил със създаването на тези елементи. Така че забавете кода си. Lit също чака елементите да бъдат действително там. - person Danny '365CSI' Engelman; 04.05.2020
comment
Благодаря отново, вашият подход изглежда много обещаващ! Скоро ще пробвам. Всъщност вече опитах setTimeout вътре вconnectedCallback и отказах тази идея, защото това води до условия на състезание. Например (на моя хардуер), изчакването от 1 ms не е достатъчно. Изчакването от 3 ms понякога е достатъчно, а понякога не. Проблемът е, че трябва да изчакам завършването на рендирането. Това не е същото нещо като изчакване цикълът на събитието да бъде изчистен. - person Dune; 04.05.2020
comment
Всичко зависи от това какво правите с DOM елементи. Не можете да направите нещо с DOM елементи, ако те не са там (все още) 0 ms Settimeout трябва да е достатъчно в ConnectedCallback... освен ако не правите асинхронни неща с онези неща на Bootstrap.. но тогава не включвайте Elements. .първо го накарайте да работи със стандартен 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
Изглежда интересно! Въпреки това, тъй като затваряте наблюдателя след първата актуализация, възелът няма да се промени, ако някой (напр. виртуалният дом) добави или изтрие дъщерни бележки. Разбира се, бихме могли да адаптираме наблюдателя да реагира правилно на всички видове операции на домовете, но това изглежда много сложно за мен... - person Dune; 04.05.2020
comment
Да, в този случай не трябва да затваряте наблюдателя и да го използвате, за да синхронизирате децата със свойствата на опциите при всяка мутация. Наистина е по-сложно, но не съм сигурен, че можете да направите по-просто в този случай - person LostInBrittany; 06.05.2020