【问题标题】:Wrapping a select box into a custom element将选择框包装到自定义元素中
【发布时间】:2020-08-18 17:51:11
【问题描述】:

我想构建一个 html 自定义元素,它的行为应该与原生 <select> 元素几乎相同,但此外,它还必须在每次属性或子节点更改时调用某个更新函数。 (背景故事:这是在 elm 框架内使用 bootstrap-select 元素的必要步骤。另见 my last question。)

使用 LitElement 框架,我能够构建一个与上述描述类似的工作自定义元素(称为<lit-select>)。但不幸的是,我无法让它接受 html <option><optgroup> 元素作为子元素。相反,用户必须将选项列表作为 json 编码字符串传递给某个属性。

也就是说,而不是调用

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

用户必须调用

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

我必须以哪种方式更改&lt;lit-select&gt; 的定义才能使第一次通话成为可能?我知道&lt;slot&gt; 元素,但不幸的是,&lt;select&gt; 中不允许使用此元素,因此浏览器将其删除。

提前谢谢你!


更新 1

实际上有一些限制使得这个问题比我最初想象的更具挑战性:

  • 我必须避免影子 DOM。这是因为我的自定义元素正在被引导(和引导选择)css/js 设置样式/增强,它只查看普通 DOM。正如我刚刚了解到的,这排除了 slot 元素,因为这些是影子 DOM 特定的。
  • 我的自定义元素必须完全响应更改(添加/删除子注释、更改属性)。这是因为我计划在虚拟 DOM 中使用该元素(在我的例子中是 elm,但它也应该与 react 一起使用)。

附录

我对@9​​87654333@的定义:

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>
        `;
    }
}

【问题讨论】:

    标签: javascript typescript polymer custom-element lit-element


    【解决方案1】:

    为什么不从原生 W3C 标准自定义元素开始

    您所要做的就是移动一些&lt;option&gt;元素。

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

    【讨论】:

    • 非常感谢您的快速回复!使用 LitElement 的一个原因是它提供了“更新的”回调,这正是调用我需要的刷新函数的正确位置。不幸的是,“connectedCallback”被调用得太早了(而且只有一次!)。另外,我不能使用影子根,因为我的元素将被引导函数更改,这些函数不能(简单地)在影子根中工作。
    • 我已经更新了代码。没有 shadowDOM 和更新方法。 connectedCallback 不会太早被调用...您的代码想要在事件循环完成创建这些元素之前做一些事情。所以延迟你的代码。 Lit 还等待元素实际存在。
    • 再次感谢,您的方法看起来很有前途!我会尽快尝试的。实际上,我已经在“connectedCallback”中尝试过“setTimeout”并拒绝了这个想法,因为这会导致竞争条件。例如(在我的硬件上),等待 1ms 是不够的。等待 3ms 有时就足够了,有时则不行。问题是,我必须等待渲染完成。这与等待清除事件循环不同。
    • 这完全取决于你对 DOM 元素做了什么。如果 DOM 元素不存在,你就不能对它们做一些事情(还) A 0 ms Settimeout 在 connectedCallback 中应该足够了......除非你正在用那些 Bootstrap 东西做异步的东西......但是不要合并元素。 .首先让它与标准的JS代码一起工作。如果你不这样做,你就是在堆叠技术,让它变得越来越困难。
    【解决方案2】:

    这个解决方案怎么样?

    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看到一个完整的例子

    您如何看待这种方法?

    【讨论】:

    • 看起来很有趣!但是,由于您在第一次更新后关闭观察者,因此如果有人(例如虚拟 dom)添加或删除子注释,节点不会更改。当然,我们可以让观察者对所有类型的 dom 操作做出适当的反应,但这对我来说似乎有点复杂......
    • 是的,在这种情况下,您不应该关闭观察者,并使用它在每次突变时将子项与选项属性同步。它确实更复杂,但我不确定在这种情况下你可以做得更简单
    猜你喜欢
    • 1970-01-01
    • 2015-10-07
    • 1970-01-01
    • 2023-03-06
    • 2016-02-28
    • 2014-11-21
    • 1970-01-01
    • 2021-10-09
    • 1970-01-01
    相关资源
    最近更新 更多