原文:https://www.robinwieruch.de/web-components-tutorial/

原题目:Web Components Tutorial for Beginners

作者:ROBIN WIERUCH

本教程将教你如何构建你的第一个Web Components以及如何在应用程序中应用它们。在咱们开始之前,让咱们花点工夫理解一下Web Components的个别状况:近年来,Web Components(也称为自定义元素)已成为几个浏览器的规范API,容许开发人员只应用HTML、CSS和JavaScript来实现可重用的组件。这里不须要React、Angular或Vue。相同,自定义元素为你提供了将所有构造(HTML)、款式(CSS)和行为(JavaScript)封装在一个自定义HTML元素中的性能。例如,设想一下,你能够领有一个像以下代码片段中的HTML下拉组件:

<my-dropdown  label="Dropdown"  option="option2"  options='{ "option1": { "label": "Option 1" }, "option2": { "label": "Option 2" } }'></my-dropdown>

在本教程中,咱们将应用Web Components从头开始逐渐实现此下拉组件。之后,你能够在整个应用程序中持续应用它,将其作为开源Web Components装置在其余中央,或者应用像React这样的框架中,在Web Components的坚实基础上构建React应用程序。

为什么选 Web Components

一个集体故事,用来阐明如何从Web Components中受害:当我有一个客户,他有很多跨职能团队,想要基于一个款式指南创立一个UI库时,我开始接触Web Components。两个团队开始依据款式指南实现组件,但每个团队应用了不同的框架:React和Angular。只管这两个实现在构造(HTML)和款式(CSS)上与款式指南有些类似,但行为的实现(例如关上/敞开下拉菜单,在下拉菜单中抉择我的项目)由各个团队依据他们所抉择的框架来实现。此外,如果款式指南在组件的款式或构造上呈现谬误,每个团队都会独自修复这些谬误,而不会随后调整款式指南。不久之后,这两个UI库在外观和行为上开始一致。

留神:与Web Components无关,这是款式指南的一个常见缺点,如果它们不被踊跃应用(例如作为流动款式指南)在代码中,而只是作为逐步过期的文档。

最终,两个团队走到了一起,探讨如何解决问题。他们请我钻研Web Components,看看它们是否能解决他们的问题。事实上,Web Components 提供了一个令人信服的解决方案:两个团队能够依据款式指南实现独特的Web Components。像下拉菜单、按钮和表格这样的组件只需应用HTML、CSS和JavaScript来实现。此外,他们不用强制应用Web Components来实现他们后续的个别应用程序,而是能够在他们的React或Angular应用程序中应用这些组件。如果款式指南的要求发生变化,或者须要修复一个组件,两个团队能够在他们共享的Web ComponentsUI库上进行单干。

开始应用Web Components

如果你须要以下教程的入门我的项目,能够从 GitHub 克隆此我的项目。你应该查看dist/src/文件夹,以便依据教程进行调整。本教程的实现我的项目能够在 GitHub 上找到。

让咱们开始应用咱们的第一个 Web 组件。咱们不会从一开始就实现下拉组件,而是一个简略的按钮组件,稍后会在下拉组件中应用。应用 Web 组件实现一个简略的按钮组件没有多大意义,因为你能够应用<button>带有一些 CSS 的元素,然而,为了学习 Web 组件,咱们将从这个按钮组件开始。因而,以下代码块足以为具备自定义构造和款式的单个按钮创立 Web 组件:

const template = document.createElement('template');template.innerHTML = `  <style>    .container {      padding: 8px;    }    button {      display: block;      overflow: hidden;      position: relative;      padding: 0 16px;      font-size: 16px;      font-weight: bold;      text-overflow: ellipsis;      white-space: nowrap;      cursor: pointer;      outline: none;      width: 100%;      height: 40px;      box-sizing: border-box;      border: 1px solid #a1a1a1;      background: #ffffff;      box-shadow: 0 2px 4px 0 rgba(0,0,0, 0.05), 0 2px 8px 0 rgba(161,161,161, 0.4);      color: #363636;    }  </style>  <div class="container">    <button>Label</button>  </div>`;class Button extends HTMLElement {    constructor() {        super();        this._shadowRoot = this.attachShadow({ mode: 'open' });        this._shadowRoot.appendChild(template.content.cloneNode(true));    }}window.customElements.define('my-button', Button);

让咱们逐渐进行所有步骤。自定义元素(Web Components)的定义是通过一个继承自HTMLElement的JavaScript类来实现的,该类能够帮忙你实现任何自定义HTML元素。通过继承自它,你将能够拜访各种类办法,例如组件的生命周期回调(生命周期办法),这些办法有助于你实现Web Components。稍后你将看到咱们如何利用这些类办法。

此外,Web Components应用Shadow DOM,不应将其与虚构DOM(性能优化)混同。Shadow DOM用于封装CSS、HTML和JavaScript,以便对应用Web Components的内部组件/HTML进行暗藏。你能够为Shadow DOM设置模式,在咱们的状况下设置为true,以使Shadow DOM在某种程度上对外界可拜访。无论如何,你能够将Shadow DOM视为自定义元素外部的本人的子树,该子树封装了构造和款式。

构造函数中的另一个语句通过克隆上述申明的模板来将一个子元素附加到咱们的Shadow DOM上。模板通常用于使HTML可重用。然而,在Web Components中,模板也在定义其构造和款式方面起着关键作用。在咱们的自定义元素顶部,咱们应用这样的模板来定义构造和款式,该模板在自定义元素的构造函数中应用。

咱们代码片段的最初一行通过在window上定义自定义元素将其定义为咱们的HTML的无效元素。第一个参数是咱们可重用自定义元素的名称作为HTML,必须有一个连字符,第二个参数是咱们自定义元素的定义,包含出现的模板。之后,咱们能够在咱们的HTML中的某个中央应用咱们的新自定义元素<my-button></my-button>。请留神,自定义元素不能/不利用作自闭合标签。

如何将属性传递给 Web Components?

到目前为止,咱们的自定义元素除了领有本人的构造和款式之外并没有做太多事件。咱们能够通过应用带有一些 CSS 的按钮元素来实现同样的成果。不过,为了理解 Web 组件,咱们持续探讨自定义按钮元素。就目前而言,咱们无奈扭转它显示的内容。例如,将标签作为 HTML 属性传递给它怎么样:

<my-button label="Click Me"></my-button>

渲染的输入仍将显示应用字符串的外部自定义元素的模板Label。为了使自定义元素对这个新属性做出反馈,你能够监听它,并应用来自扩大 HTMLElement 类的类办法对其执行某些操作:

class Button extends HTMLElement {    constructor() {        super();        this._shadowRoot = this.attachShadow({ mode: 'open' });        this._shadowRoot.appendChild(template.content.cloneNode(true));    }    static get observedAttributes() {        return ['label'];    }    attributeChangedCallback(name, oldVal, newVal) {        this[name] = newVal;    }}

每次标签属性发生变化时,都会调用attributeChangedCallback()函数,因为咱们在observedAttributes()函数中将标签定义为可察看的属性。在咱们的状况下,回调函数除了在咱们的Web Components类实例上设置标签(这里是:this.label = 'Click Me')之外,没有太多的操作。然而,自定义元素依然没有渲染这个标签。为了调整渲染的输入,你必须获取理论的HTML按钮并设置其HTML内容:

class Button extends HTMLElement {    constructor() {        super();        this._shadowRoot = this.attachShadow({ mode: 'open' });        this._shadowRoot.appendChild(template.content.cloneNode(true));        this.$button = this._shadowRoot.querySelector('button');    }    static get observedAttributes() {        return ['label'];    }    attributeChangedCallback(name, oldVal, newVal) {        this[name] = newVal;        this.render();    }    render() {        this.$button.innerHTML = this.label;    }}

当初,初始标签属性已在按钮中设置。此外,自定义元素也将对属性的更改做出反馈。你能够以雷同的形式实现其余属性。然而,你会留神到非JavaScript原始类型(如对象和数组)须要以JSON格局的字符串模式传递。在实现下拉组件时,咱们将在稍后看到这一点。

将属性映射到属性

到目前为止,咱们曾经应用属性将信息传递给咱们的自定义元素。每当属性发生变化时,咱们在回调函数中将该属性设置为咱们Web Components实例上的属性。而后,咱们以命令形式进行所有必要的更改以进行渲染。然而,咱们也能够应用一个获取办法将属性反映到属性。通过这种形式,咱们确保始终取得最新的值,而无需在回调函数中本人进行调配。而后,this.label总是从咱们的getter函数返回最新的属性:

class Button extends HTMLElement {  constructor() {    super();    this._shadowRoot = this.attachShadow({ mode: 'open' });    this._shadowRoot.appendChild(template.content.cloneNode(true));    this.$button = this._shadowRoot.querySelector('button');  }  get label() {    return this.getAttribute('label');  }  static get observedAttributes() {    return ['label'];  }  attributeChangedCallback(name, oldVal, newVal) {    this.render();  }  render() {    this.$button.innerHTML = this.label;  }}

这就是将属性映射到属性的全部内容。然而,反过来,你也能够应用属性将信息传递给自定义元素。例如,咱们能够将信息设置为元素的属性,而不是应用属性<my-button label="Click Me"></my-button>来渲染咱们的按钮。通常,当将对象和数组等信息调配给元素时,会应用这种形式:

<my-button></my-button><script>  const element = document.querySelector('my-button');  element.label = 'Click Me';</script>

很可怜,当应用属性而不是属性时,咱们用于更改属性的回调函数不再被调用,因为它只对属性更改做出反馈,而不解决属性赋值。这就是咱们类中的set办法派上用场的中央:

class Button extends HTMLElement {  constructor() {    super();    this._shadowRoot = this.attachShadow({ mode: 'open' });    this._shadowRoot.appendChild(template.content.cloneNode(true));    this.$button = this._shadowRoot.querySelector('button');  }  get label() {    return this.getAttribute('label');  }  set label(value) {    this.setAttribute('label', value);  }  static get observedAttributes() {    return ['label'];  }  attributeChangedCallback(name, oldVal, newVal) {    this.render();  }  render() {    this.$button.innerHTML = this.label;  }}

当初,因为咱们从元素的内部设置属性,因而咱们的自定义元素的 setter 办法通过将元素的属性设置为反射的属性值,确保将属性反映到属性。之后,咱们的属性回调再次运行,因为属性已更改,因而咱们复原了渲染机制。

你能够为此类的每个办法增加控制台日志,以理解每个办法产生的程序。通过关上浏览器的开发工具,也能够在 DOM 中见证整个反射:即便属性被设置为属性,属性也应该呈现在元素上。

最初,在为咱们的信息筹备好 getter 和 setter 办法之后,咱们能够将信息作为属性和个性传递给咱们的自定义元素。整个过程称为将属性反映到属性,反之亦然。

如何将函数传递给 Web Components?

最初但并非最不重要的是,咱们须要在点击时使咱们的自定义元素起作用。首先,自定义元素能够注册一个事件监听器来响应用户的交互。例如,咱们能够抉择按钮并为其增加一个事件监听器:

class Button extends HTMLElement {  constructor() {    super();    this._shadowRoot = this.attachShadow({ mode: 'open' });    this._shadowRoot.appendChild(template.content.cloneNode(true));    this.$button = this._shadowRoot.querySelector('button');    this.$button.addEventListener('click', () => {      // do something    });  }  get label() {    return this.getAttribute('label');  }  set label(value) {    this.setAttribute('label', value);  }  static get observedAttributes() {    return ['label'];  }  attributeChangedCallback(name, oldVal, newVal) {    this.render();  }  render() {    this.$button.innerHTML = this.label;  }}

留神:能够简略地在元素的内部增加此监听器,而不须要在自定义元素中懊恼 -- 然而,将其定义在自定义元素外部能够更好地管制应该传递给内部注册的监听器的内容。

短少的是从内部传递的回调函数,能够在此监听器中调用。有多种办法能够解决这个工作。首先,咱们能够将函数作为属性传递。然而,因为咱们曾经理解到将非原始类型传递给HTML元素是麻烦的,咱们心愿防止这种状况。其次,咱们能够将函数作为属性传递。让咱们看看当应用咱们的自定义元素时,这将是什么样子:

<my-button label="Click Me"></my-button><script>  document.querySelector('my-button').onClick = value =>    console.log(value);</script>

咱们刚刚将一个onClick处理程序定义为咱们元素的函数。接下来,在咱们自定义元素的监听器中,咱们能够调用这个函数属性:

class Button extends HTMLElement {  constructor() {    super();    this._shadowRoot = this.attachShadow({ mode: 'open' });    this._shadowRoot.appendChild(template.content.cloneNode(true));    this.$button = this._shadowRoot.querySelector('button');    this.$button.addEventListener('click', () => {      this.onClick('Hello from within the Custom Element');    });  }  ...}

如果你没有在自定义元素外部应用监听器,你只会接管到事件。你能够本人尝试一下。当初,即便这样按预期工作,我宁愿应用DOM API提供的内置事件零碎。因而,让咱们从内部注册一个事件监听器,而不将函数调配为元素的属性:

<my-button label="Click Me"></my-button><script>  document    .querySelector('my-button')    .addEventListener('click', value => console.log(value));</script>

点击按钮时的输入与之前的雷同,但这次应用了点击交互的事件监听器。这样,自定义元素依然可能通过应用点击事件向外界发送信息,因为咱们从自定义元素的外部工作中发送的音讯依然会被发送并能够在浏览器的日志中看到。通过这种形式,如果不须要非凡的行为,也能够省略在自定义元素中定义事件监听器的步骤,如前所述。

然而,通过这种形式留下的一个注意事项是:咱们只能应用内置事件来解决自定义元素。然而,如果当前在不同的环境中应用你的Web Components(例如React),你可能还心愿为组件提供自定义事件(例如onClick)作为API。当然,咱们也能够手动将自定义元素的点击事件映射到咱们框架的onClick函数,但如果咱们能够在那里简略地应用雷同的命名约定,那将更加不便。让咱们看看如何进一步改良咱们之前的实现,以反对自定义事件:

Web Components 的生命周期回调函数

咱们简直实现了自定义按钮。在咱们持续自定义下拉元素(将应用咱们的自定义按钮元素)之前,让咱们增加最初一个点睛之笔。目前,该按钮定义了一个带有填充的外部容器元素。这对于并排应用这些自定义按钮并具备彼此的天然边距时十分有用。然而,在另一个上下文(例如下拉列表组件)中应用该按钮时,你可能心愿从容器中删除此填充。因而,你能够应用名为 connectedCallback 的 Web 组件的生命周期回调之一:

class Button extends HTMLElement {  constructor() {    super();    this._shadowRoot = this.attachShadow({ mode: 'open' });    this._shadowRoot.appendChild(template.content.cloneNode(true));    this.$container = this._shadowRoot.querySelector('.container');    this.$button = this._shadowRoot.querySelector('button');    ...  }  connectedCallback() {    if (this.hasAttribute('as-atom')) {      this.$container.style.padding = '0px';    }  }  ...}

在此状况下,如果元素上存在一个名为as-atom的属性,它将将咱们的按钮容器的填充重置为零。顺便说一下,这就是你能够依照原子设计准则创立一个杰出的UI库的形式,其中自定义按钮元素是一个原子,自定义下拉菜单元素是一个分子。兴许两者最终会与更大的有机体中的另一个元素联合在一起。当初咱们的按钮能够在下拉菜单元素中无填充地应用,如下所示:<my-button as-atom></my-button>。按钮的标签将稍后通过应用属性来设置。

那么生命周期回调呢?connectedCallback在Web Components附加到DOM时运行一次。这就是为什么能够在组件渲染后执行所有须要实现的工作的起因。当组件被移除时,存在一个等效的生命周期回调函数,称为disconnectedCallback。此外,之前曾经在自定义元素中应用了一个生命周期办法,称为attributeChangedCallback,以对属性更改做出反馈。Web Components提供了各种生命周期回调函数,请务必具体理解它们。

Web Components 中的 Web Components

Last but not least, 咱们心愿在另一个 Web 组件中应用实现的按钮 Web 组件。因而,咱们将实现一个自定义下拉元素,应按以下形式应用:

<my-dropdown  label="Dropdown"  option="option2"  options='{ "option1": { "label": "Option 1" }, "option2": { "label": "Option 2" } }'></my-dropdown>

请留神,options(对象)作为 JSON 格局的属性传递给自定义元素,这种将对象和数组作为属性传递会更不便:

<my-dropdown  label="Dropdown"  option="option2"></my-dropdown><script>  document.querySelector('my-dropdown').options = {    option1: { label: 'Option 1' },    option2: { label: 'Option 2' },  };</script>

让咱们深刻理解自定义下拉元素的实现。咱们将从一个简略的根底开始,该根底为定义 Web 组件的类定义咱们的构造、款式和样板代码。后者用于设置 Shadow DOM 的模式,将模板附加到咱们的自定义元素,为咱们的属性/属性定义 getter 和 setter 办法,察看咱们的属性变动并对其做出响应:

const template = document.createElement('template');template.innerHTML = `  <style>    :host {      font-family: sans-serif;    }    .dropdown {      padding: 3px 8px 8px;    }    .label {      display: block;      margin-bottom: 5px;      color: #000000;      font-size: 16px;      font-weight: normal;      line-height: 16px;    }    .dropdown-list-container {      position: relative;    }    .dropdown-list {      position: absolute;      width: 100%;      display: none;      max-height: 192px;      overflow-y: auto;      margin: 4px 0 0;      padding: 0;      background-color: #ffffff;      border: 1px solid #a1a1a1;      box-shadow: 0 2px 4px 0 rgba(0,0,0, 0.05), 0 2px 8px 0 rgba(161,161,161, 0.4);      list-style: none;    }    .dropdown-list li {      display: flex;      align-items: center;      margin: 4px 0;      padding: 0 7px;      font-size: 16px;      height: 40px;      cursor: pointer;    }  </style>  <div class="dropdown">    <span class="label">Label</span>    <my-button as-atom>Content</my-button>    <div class="dropdown-list-container">      <ul class="dropdown-list"></ul>    </div>  </div>`;class Dropdown extends HTMLElement {  constructor() {    super();    this._sR = this.attachShadow({ mode: 'open' });    this._sR.appendChild(template.content.cloneNode(true));  }  static get observedAttributes() {    return ['label', 'option', 'options'];  }  get label() {    return this.getAttribute('label');  }  set label(value) {    this.setAttribute('label', value);  }  get option() {    return this.getAttribute('option');  }  set option(value) {    this.setAttribute('option', value);  }  get options() {    return JSON.parse(this.getAttribute('options'));  }  set options(value) {    this.setAttribute('options', JSON.stringify(value));  }  static get observedAttributes() {    return ['label', 'option', 'options'];  }  attributeChangedCallback(name, oldVal, newVal) {    this.render();  }  render() {  }}window.customElements.define('my-dropdown', Dropdown);

这里有几点须要留神:首先,在咱们的格调中,咱们能够应用 :host 选择器为咱们的自定义元素设置全局款式。其次,模板应用咱们的自定义按钮元素,但尚未为其提供标签属性。第三,每个属性/属性都有 getter 和 setter,然而,属性/ options 属性反射的 getter 和 setter 正在解析对象从or到 JSON格局。

留神:除了所有提到的内容之外,你可能还会留神到所有用于属性/属性反射的 getter 和 setter 办法的样板文件。此外,属性的生命周期回调看起来是反复的,构造函数与自定义按钮元素中的构造函数雷同。稍后可能会理解到,存在各种轻量级库(例如带有 LitHTML 的 LitElement)用于 Web 组件之上,以打消咱们的这种重复性。

到目前为止,尚未应用所有传递的属性和属性。咱们只是用一个空的渲染办法对它们做出响应。让咱们通过将它们调配给下拉列表和按钮元素以此来利用它们:

class Dropdown extends HTMLElement {  constructor() {    super();    this._sR = this.attachShadow({ mode: 'open' });    this._sR.appendChild(template.content.cloneNode(true));    this.$label = this._sR.querySelector('.label');    this.$button = this._sR.querySelector('my-button');  }  ...  static get observedAttributes() {    return ['label', 'option', 'options'];  }  attributeChangedCallback(name, oldVal, newVal) {    this.render();  }  render() {    this.$label.innerHTML = this.label;    this.$button.setAttribute('label', 'Select Option');  }}window.customElements.define('my-dropdown', Dropdown);

下拉列表从内部获取其标签作为要设置为外部 HTML 的属性,而按钮当初将任意标签设置为属性。稍后咱们将依据下拉列表中的选定选项设置此标签。此外,咱们能够利用这些选项为咱们的下拉列表出现理论的可选我的项目:

class Dropdown extends HTMLElement {  constructor() {    super();    this._sR = this.attachShadow({ mode: 'open' });    this._sR.appendChild(template.content.cloneNode(true));    this.$label = this._sR.querySelector('.label');    this.$button = this._sR.querySelector('my-button');    this.$dropdownList = this._sR.querySelector('.dropdown-list');  }  ...  render() {    this.$label.innerHTML = this.label;    this.$button.setAttribute('label', 'Select Option');    this.$dropdownList.innerHTML = '';    Object.keys(this.options || {}).forEach(key => {      let option = this.options[key];      let $option = document.createElement('li');      $option.innerHTML = option.label;      this.$dropdownList.appendChild($option);    });  }}window.customElements.define('my-dropdown', Dropdown);

在这种状况下,在每次渲染时,咱们都会擦除下拉列表的外部 HTML,因为选项可能已更改。而后,咱们为对象 option 元素动态创建一个列表元素,并将其附加到咱们的列表元素中,其中蕴含 option 属性的 label . options 如果未定义, properties 咱们应用默认的空对象来防止在此处遇到异样,因为传入属性和属性之间存在争用条件。然而,即便列表被出现,咱们的款式也将 CSS display 属性定义为 none 。这就是为什么咱们还看不到列表的起因,然而在咱们为自定义元素的行为增加更多JavaScript之后,咱们将在下一步中看到它。

Web Components 与 JavaScript

到目前为止,咱们次要构建和款式化了自定义元素。咱们也对更改的属性做出了反馈,但在渲染步骤中还没有做太多事件。当初咱们将向 Web 组件增加更多 JavaScript 的行为。只有这样,它才真正不同于应用 CSS 款式的简略 HTML 元素。你将看到如何将所有行为封装在自定义下拉元素中,而无需从内部执行任何操作。

让咱们首先应用咱们的按钮元素关上和敞开下拉列表,这应该使咱们的下拉列表可见。首先,定义一个新款式,用于应用 open 类出现下拉列表。请记住,咱们之前曾将 display: none; 下拉列表用作默认款式。

const template = document.createElement('template');template.innerHTML = `  <style>    :host {      font-family: sans-serif;    }    ...    .dropdown.open .dropdown-list {      display: flex;      flex-direction: column;    }    ...  </style>  ...`;

在下一步中,咱们定义一个类办法,用于切换自定义元素的外部状态。此外,当调用此类办法时,将依据新状态将新 open 类增加或删除到咱们的下拉元素中。

class Dropdown extends HTMLElement {  constructor() {    super();    this._sR = this.attachShadow({ mode: 'open' });    this._sR.appendChild(template.content.cloneNode(true));    this.open = false;    this.$label = this._sR.querySelector('.label');    this.$button = this._sR.querySelector('my-button');    this.$dropdown = this._sR.querySelector('.dropdown');    this.$dropdownList = this._sR.querySelector('.dropdown-list');  }  toggleOpen(event) {    this.open = !this.open;    this.open      ? this.$dropdown.classList.add('open')      : this.$dropdown.classList.remove('open');  }  ...}

咱们须要为自定义按钮元素的事件增加一个事件侦听器,以将下拉列表的外部状态从关上切换到敞开,反之亦然。应用它时不要遗记绑定 this 到咱们的新类办法,否则它无法访问 this 设置新的外部状态或拜访调配 $dropdown 的元素。

class Dropdown extends HTMLElement {  constructor() {    super();    this._sR = this.attachShadow({ mode: 'open' });    this._sR.appendChild(template.content.cloneNode(true));    this.open = false;    this.$label = this._sR.querySelector('.label');    this.$button = this._sR.querySelector('my-button');    this.$dropdown = this._sR.querySelector('.dropdown');    this.$dropdownList = this._sR.querySelector('.dropdown-list');    this.$button.addEventListener(      'onClick',      this.toggleOpen.bind(this)    );  }  toggleOpen(event) {    this.open = !this.open;    this.open      ? this.$dropdown.classList.add('open')      : this.$dropdown.classList.remove('open');  }  ...}

立刻亲自试用你的 Web 组件。应该能够通过单击咱们的自定义按钮来关上和敞开自定义下拉元素。这是咱们自定义元素的第一个真正的外部行为,否则它将在 React 或 Angular 等框架中实现。当初,你的框架能够简略地应用此 Web 组件,并冀望它提供此行为。让咱们持续在单击时从关上的列表中抉择一个我的项目:

class Dropdown extends HTMLElement {  ...  render() {    ...    Object.keys(this.options || {}).forEach(key => {      let option = this.options[key];      let $option = document.createElement('li');      $option.innerHTML = option.label;      $option.addEventListener('click', () => {        this.option = key;        this.toggleOpen();        this.render();      });      this.$dropdownList.appendChild($option);    });  }}

列表中的每个出现选项都会获取单击事件的事件侦听器。单击该选项时,该选项将设置为属性,下拉列表切换到 close ,组件将再次出现。然而,为了查看产生了什么,让咱们可视化下拉列表中的选定选项项:

const template = document.createElement('template');template.innerHTML = `  <style>    ...    .dropdown-list li.selected {      font-weight: 600;    }  </style>  <div class="dropdown">    <span class="label">Label</span>    <my-button as-atom>Content</my-button>    <div class="dropdown-list-container">      <ul class="dropdown-list"></ul>    </div>  </div>`;

接下来,只有选项属性与列表中的选项匹配,咱们就能够在 render 办法中设置这个新类。有了这个新款式,并在下拉列表中的一个选项上动静设置款式,咱们能够看到该性能实际上无效:

class Dropdown extends HTMLElement {  ...  render() {    ...    Object.keys(this.options || {}).forEach(key => {      let option = this.options[key];      let $option = document.createElement('li');      $option.innerHTML = option.label;      if (this.option && this.option === key) {        $option.classList.add('selected');      }      $option.addEventListener('click', () => {        this.option = key;        this.toggleOpen();        this.render();      });      this.$dropdownList.appendChild($option);    });  }}

让咱们在自定义按钮元素中显示以后选定的选项,而不是设置任意值:

class Dropdown extends HTMLElement {  ...  render() {    this.$label.innerHTML = this.label;    if (this.options) {      this.$button.setAttribute(        'label',        this.options[this.option].label      );    }    this.$dropdownList.innerHTML = '';    Object.keys(this.options || {}).forEach(key => {      ...    });  }}

自定义下拉元素的外部行为无效。咱们可能关上和敞开它,并且能够通过从下拉列表中抉择一个选项来设置一个新选项。短少一件要害的事件:咱们须要再次向外界提供 API(例如自定义事件),以告诉他们更改的选项。因而,请为每个列表项单击调度一个自定义事件,但为每个自定义事件提供一个键,以标识单击了哪个项:

class Dropdown extends HTMLElement {  ...  render() {    ...    Object.keys(this.options || {}).forEach(key => {      let option = this.options[key];      let $option = document.createElement('li');      $option.innerHTML = option.label;      if (this.option && this.option === key) {        $option.classList.add('selected');      }      $option.addEventListener('click', () => {        this.option = key;        this.toggleOpen();        this.dispatchEvent(          new CustomEvent('onChange', { detail: key })        );        this.render();      });      this.$dropdownList.appendChild($option);    });  }}

最初,将下拉列表用作 Web 组件时,能够为自定义事件增加事件侦听器,以获取无关更改的告诉:

<my-dropdown label="Dropdown" option="option2"></my-dropdown><script>  document.querySelector('my-dropdown').options = {    option1: { label: 'Option 1' },    option2: { label: 'Option 2' },  };  document    .querySelector('my-dropdown')    .addEventListener('onChange', event => console.log(event.detail));</script>

就是这样。你曾经创立了一个齐全封装的下拉组件作为 Web 组件,具备本人的构造、款式和行为。后者是Web Components的要害局部,因为否则你能够简略地应用带有一些CSS的HTML元素作为款式。当初,你还将 behvaior 封装在新的自定义 HTML 元素中。恭喜!


下拉列表和按钮元素作为 Web 组件的实现能够在此 GitHub 我的项目中找到,其中蕴含一些有用的扩大。正如我之前所说,自定义按钮元素对于下拉组件来说有点不重要,因为它没有实现任何非凡行为。你能够应用带有CSS款式的一般HTML按钮元素。然而,自定义按钮元素通过一个简略的示例帮忙咱们把握了 Web 组件的概念。这就是为什么我认为从按钮组件开始是一个很好的想法,该组件稍后将在下拉组件中应用。如果你想持续在 React 中应用你的 Web 组件,请查看这个简洁的 React 钩子或这个 Web Components for React 教程。最初,我心愿你从这个 Web 组件教程中学到了很多货色。如果你有反馈或只是喜爱它,请发表评论:-)

参考:

rwieruch/web-components-starter-kit: Starter Kit for Web Components with Webpack as application bundler.

十分感谢您看到这里~来自译者 - 泯泷