创建一个多选组件作为Web组件

2016年12月5日更新:在评论中进行了一些讨论之后,撰写了第二篇文章以解决该文章的缺点- 如何使可访问的Web组件 。 请确保也阅读此内容。

本文由Ryan Lewis同行评审。 感谢所有SitePoint的同行评审人员使SitePoint内容达到最佳状态!

Web应用程序变得越来越复杂,并且需要大量的标记,脚本和样式。 为了管理和维护数百KB的HTML,JS和CSS,我们尝试将应用程序拆分为可重用的组件。 我们努力封装组件,防止样式冲突和脚本干扰。

最后,组件源代码分布在多个文件之间:标记文件,脚本文件和样式表。 我们可能会遇到的另一个问题是,长标记因divspan混乱。 这种代码表现力很弱,也很难维护。 为了解决并尝试解决所有这些问题,W3C引入了Web组件。

在本文中,我将解释什么是Web组件以及如何自己构建Web组件。

认识Web组件

Web组件解决了引言中讨论的所有这些问题。 使用Web组件,我们可以链接包含组件实现的单个HTML文件,并将其与自定义HTML元素一起在页面上使用。 它们简化了组件的创建,增强了封装,并使标记更具表现力。

Web组件通过一系列规范进行定义:

  • 自定义元素 :允许为组件注册自定义的有意义的HTML元素
  • HTML模板 :定义组件的标记
  • Shadow DOM :封装组件的内部并将其从使用它的页面隐藏
  • HTML Imports :提供将组件包含到目标页面的功能。

描述了什么是Web组件之后,让我们看看它们的实际作用。

如何构建可用于生产的Web组件

在本节中,我们将构建一个有用的多选小部件,该小部件可以在生产中使用。 结果可以在此演示页面上找到,完整的源代码可以在GitHub找到

要求

首先,让我们为多选小部件定义一些要求。

标记应具有以下结构:

<x-multiselect placeholder="Select Item">
    <li value="1" selected>Item 1</li>
    <li value="2">Item 2</li>
    <li value="3" selected>Item 3</li>
</x-multiselect>

定制元素<x-multiselect>具有一个placeholder属性,用于定义空的multiselect的占位符。 使用支持valueselected属性的<li>元素定义项目。

多重selectedItems应该具有selectedItems API方法,该方法返回所选项目的数组。

// returns an array of values, e.g. [1, 3]
var selectedItems = multiselect.selectedItems();

此外,小部件应在每次change所选项目时触发事件change

multiselect.addEventListener('change', function() {
    // print selected items to console
    console.log('Selected items:', this.selectedItems()); 
});

最后,该小部件应在所有现代浏览器中都可以使用。

模板

我们开始创建multiselect.html文件,该文件将包含组件的所有源代码:HTML标记,CSS样式和JS代码。

HTML模板允许我们在特殊的HTML元素<template>定义组件的<template> 。 这是我们的多选模板:

<template id="multiselectTemplate">
    <style>
      /* component styles */
    </style>

    <!-- component markup -->
    <div class="multiselect">
        <div class="multiselect-field"></div>
        <div class="multiselect-popup">
            <ul class="multiselect-list">
                <content select="li"></content>
            </ul>
        </div>
    </div>
</template>

组件标记包含多重选择的字段和带有项目列表的弹出窗口。 我们希望多选从用户标记中获取项目。 我们可以使用新的HTML元素<content>来做到这一点( 您可以在MDN上找到有关content元素的更多信息 )。 它定义了从影子主机(用户标记中的组件声明)到影子DOM(封装的组件标记)的标记的插入点。

select属性接受CSS选择器并定义要从影子主机中选择的元素。 在我们的例子中,我们要采用所有<li>元素并设置select="li"

创建组件

现在,让我们创建一个组件并注册一个自定义HTML元素。 将以下创建脚本添加到multiselect.html文件:

<script>
    // 1. find template
    var ownerDocument = document.currentScript.ownerDocument;
    var template = ownerDocument.querySelector('#multiselectTemplate');

    // 2. create component object with the specified prototype 
    var multiselectPrototype = Object.create(HTMLElement.prototype);

    // 3. define createdCallback
    multiselectPrototype.createdCallback = function() {
        var root = this.createShadowRoot();
        var content = document.importNode(template.content, true);
        root.appendChild(content);
    };

    // 4. register custom element
    document.registerElement('x-multiselect', {
        prototype: multiselectPrototype
    });
</script>

Web组件的创建包括四个步骤:

  1. 在所有者文档中找到一个模板。
  2. 使用指定的原型对象创建一个新对象。 在这种情况下,我们将从现有的HTML元素继承,但是可以扩展任何可用的元素。
  3. 定义创建组件时调用的createdCallback 。 在这里,我们为组件创建一个影子根,并在其中附加模板的内容。
  4. 使用document.registerElement方法注册组件的自定义元素。

要了解有关创建自定义元素的更多信息,建议您阅读Eric Bidelman的指南

渲染多选字段

下一步是根据选定的项目渲染多选字段。

入口点是createdCallback方法。 让我们定义两个方法, initrender

multiselectPrototype.createdCallback = function() {
    this.init();
    this.render();
};

init方法创建一个影子根,并找到所有内部组成部分(字段,弹出窗口和列表):

multiselectPrototype.init = function() {
    // create shadow root
    this._root = this.createRootElement();

    // init component parts
    this._field = this._root.querySelector('.multiselect-field');
    this._popup = this._root.querySelector('.multiselect-popup');
    this._list = this._root.querySelector('.multiselect-list');
};

multiselectPrototype.createRootElement = function() {
    var root = this.createShadowRoot();
    var content = document.importNode(template.content, true);
    root.appendChild(content);
    return root;
};

render方法执行实际的渲染。 因此,它将调用refreshField方法,该方法遍历所选项目并为每个所选项目创建标签:

multiselectPrototype.render = function() {
    this.refreshField();
};

multiselectPrototype.refreshField = function() {
    // clear content of the field
    this._field.innerHTML = '';

    // find selected items
    var selectedItems = this.querySelectorAll('li[selected]');

    // create tags for selected items
    for(var i = 0; i < selectedItems.length; i++) {
        this._field.appendChild(this.createTag(selectedItems[i]));
    }
};

multiselectPrototype.createTag = function(item) {
    // create tag text element
    var content = document.createElement('div');
    content.className = 'multiselect-tag-text';
    content.textContent = item.textContent;

    // create item remove button
    var removeButton = document.createElement('div');
    removeButton.className = 'multiselect-tag-remove-button';
    removeButton.addEventListener('click', this.removeTag.bind(this, tag, item));

    // create tag element
    var tag = document.createElement('div');
    tag.className = 'multiselect-tag';
    tag.appendChild(content);
    tag.appendChild(removeButton);

    return tag;
};

每个标签都有一个删除按钮。 删除按钮单击处理程序将从项目中删除选择并刷新multiselect字段:

multiselectPrototype.removeTag = function(tag, item, event) {
    // unselect item
    item.removeAttribute('selected');

    // prevent event bubbling to avoid side-effects
    event.stopPropagation();

    // refresh multiselect field
    this.refreshField();
};

打开弹出窗口并选择项目

当用户单击该字段时,我们应该显示弹出窗口。 当他/她单击列表项时,应将其标记为已选择,并且应隐藏弹出窗口。

为此,我们处理字段和项目列表上的单击。 让我们将attachHandlers方法添加到render

multiselectPrototype.render = function() {
    this.attachHandlers();
    this.refreshField();
};

multiselectPrototype.attachHandlers = function() {
    // attach click handlers to field and list
    this._field.addEventListener('click', this.fieldClickHandler.bind(this));
    this._list.addEventListener('click', this.listClickHandler.bind(this));
};

在字段单击处理程序中,我们切换弹出窗口的可见性:

multiselectPrototype.fieldClickHandler = function() {
    this.togglePopup();
};

multiselectPrototype.togglePopup = function(show) {
    show = (show !== undefined) ? show : !this._isOpened;
    this._isOpened = show;
    this._popup.style.display = this._isOpened ? 'block' : 'none';
};

在列表单击处理程序中,我们找到被单击的项目并将其标记为选中状态。 然后,我们隐藏弹出窗口并刷新多选字段:

multiselectPrototype.listClickHandler = function(event) {
    // find clicked list item
    var item = event.target;
    while(item && item.tagName !== 'LI') {
        item = item.parentNode;
    }
    
    // set selected state of clicked item
    item.setAttribute('selected', 'selected');

    // hide popup
    this.togglePopup(false);

    // refresh multiselect field
    this.refreshField();
};

添加占位符属性

另一个多选功能是placeholder属性。 当未选择任何项目时,用户可以指定要在字段中显示的文本。 为了完成此任务,让我们读取组件初始化时的属性值(在init方法中):

multiselectPrototype.init = function() {
    this.initOptions();
    ...
};

multiselectPrototype.initOptions = function() {
    // save placeholder attribute value
    this._options = {
        placeholder: this.getAttribute("placeholder") || 'Select'
    };
};

当未选择任何项目时, refreshField方法将显示占位符:

multiselectPrototype.refreshField = function() {
    this._field.innerHTML = '';

    var selectedItems = this.querySelectorAll('li[selected]');

    // show placeholder when no item selected
    if(!selectedItems.length) {
        this._field.appendChild(this.createPlaceholder());
        return;
    }

    ...
};

multiselectPrototype.createPlaceholder = function() {
    // create placeholder element
    var placeholder = document.createElement('div');
    placeholder.className = 'multiselect-field-placeholder';
    placeholder.textContent = this._options.placeholder;
    return placeholder;
};

但这还不是故事的结局。 如果占位符属性值更改了怎么办? 我们需要处理并更新该字段。 在这里, attributeChangedCallback回调很方便。 每次更改属性值时都会调用此回调。 在我们的例子中,我们保存一个新的占位符值并刷新多选字段:

multiselectPrototype.attributeChangedCallback = function(optionName, oldValue, newValue) {
    this._options[optionName] = newValue;
    this.refreshField();
};

添加selectedItems方法

我们需要做的就是向组件原型添加一个方法。 selectedItems方法的实现很简单–循环选中的项目并读取值。 如果项目没有值,则返回项目文本:

multiselectPrototype.selectedItems = function() {
    var result = [];

    // find selected items
    var selectedItems = this.querySelectorAll('li[selected]');

    // loop over selected items and read values or text content
    for(var i = 0; i < selectedItems.length; i++) {
        var selectedItem = selectedItems[i];

        result.push(selectedItem.hasAttribute('value')
                ? selectedItem.getAttribute('value')
                : selectedItem.textContent);
    }

    return result;
};

添加自定义事件

现在,让我们添加一次change事件,该事件将在用户每次更改选择时触发。 要触发事件,我们需要创建一个CustomEvent实例并调度它:

multiselectPrototype.fireChangeEvent = function() {
    // create custom event instance
    var event = new CustomEvent("change");

    // dispatch event
    this.dispatchEvent(event);
};

此时,我们需要在用户选择或取消选择项目时触发事件。 在列表单击处理程序中,我们仅在实际选择一个项目时触发该事件:

multiselectPrototype.listClickHandler = function(event) {
    ...
    
    if(!item.hasAttribute('selected')) {
        item.setAttribute('selected', 'selected');
        this.fireChangeEvent();
        this.refreshField();
    }
    
    ...
};

在移除标签按钮处理程序中,由于未选择项目,我们还需要触发change事件:

multiselectPrototype.removeTag = function(tag, item, event) {
    ...
    
    this.fireChangeEvent();
    this.refreshField();
};

造型

设置Shadow DOM的内部元素非常简单。 我们附加一些特殊的类,例如multiselect-fieldmultiselect-popup并为它们添加必要的CSS规则。

但是如何为列表项设置样式? 问题在于它们来自影子主机,不属于影子DOM。 特殊选择器::content可以帮助我们。

以下是我们列表项的样式:

::content li {
    padding: .5em 1em;
    min-height: 1em;
    list-style: none;
    cursor: pointer;
}

::content li[selected] {
    background: #f9f9f9;
}

Web Components引入了一些特殊的选择器, 您可以在此处找到有关它们的更多信息

用法

大! 我们的多选功能已完成,因此可以使用它了。 我们需要做的就是导入多选HTML文件,并将自定义元素添加到标记中:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <link rel="import" href="multiselect.html">
</head>
<body>
    <x-multiselect placeholder="Select Value">
        <li value="1" selected>Item 1</li>
        <li value="2">Item 2</li>
        <li value="3" selected>Item 3</li>
        <li value="4">Item 4</li>
    </x-multiselect>
</body>
</html>

让我们订阅change事件,并在用户每次更改选择时将选择的项目打印到控制台:

<script>
    var multiselect = document.querySelector('x-multiselect');
    multiselect.addEventListener('change', function() {
        console.log('Selected items:', this.selectedItems());
    });
</script>

每次更改选择后, 转到演示页面并打开浏览器控制台以查看选定的项目。

浏览器支持

如果我们看一下浏览器的支持 ,就会发现Web组件仅受Chrome和Opera的完全支持。 尽管如此,我们仍然可以将Web组件与polyfills webcomponentjs套件一起使用,从而可以在所有浏览器的最新版本中使用Web组件。

让我们应用此polyfill以便能够在所有浏览器中使用我们的多重选择。 它可以与Bower一起安装,然后包含在您的网页中。

bower install webcomponentsjs

如果在Safari中打开演示页面,则会在控制台中看到错误“ null不是对象” 。 问题是document.currentScript不存在。 要解决此问题,我们需要从polyfilled环境中获取ownerDocument (使用document._currentScript而不是document.currentScript )。

var ownerDocument = (document._currentScript || document.currentScript).ownerDocument;

有用! 但是,如果您在Safari中打开多选,则会看到列表样式未设置样式。 要解决此其他问题,我们需要填充模板内容的样式。 可以使用WebComponents.ShadowCSS.shimStyling方法来完成。 我们应该在附加阴影根目录内容之前调用它:

multiselectPrototype.createRootElement = function() {
    var root = this.createShadowRoot();
    var content = document.importNode(template.content, true);

    if (window.ShadowDOMPolyfill) {
        WebComponents.ShadowCSS.shimStyling(content, 'x-multiselect');
    }

    root.appendChild(content);
    return root;
};

恭喜你! 现在,我们的多选组件可以正常工作,并且在所有现代浏览器中都可以正常显示。

Web组件polyfill很棒! 为了使这些规范在所有现代浏览器上都能正常工作,显然需要付出巨大的努力。 polyfill源脚本的大小为258Kb。 尽管缩小和压缩的版本为38Kb,但我们可以想象幕后隐藏了多少逻辑。 它不可避免地影响性能。 尽管作者使垫片越来越好地强调了性能。

聚合物和X标签

关于Web组件,我应该提到Polymer 。 Polymer是在Web组件之上构建的库,可简化组件的创建并提供大量现成的元素。 webcomponents.js是Polymer的一部分,被称为platform.js 。 后来,它被提取并重命名

使用Polymer创建Web组件更加容易。 Pankaj Parashar的这篇文章展示了如何使用Polymer来创建Web组件。
如果您想加深话题,这是一些有用的文章列表:

还有另一个库可以简化Web组件的使用,这就是X-Tag 。 它由Mozilla开发,现在得到Microsoft的支持。

结论

Web组件是Web开发领域的一大进步。 它们有助于简化组件的提取,增强封装和使标记更具表现力。

在本教程中,我们已经看到了如何使用Web组件构建可用于生产的多选窗口小部件。 尽管缺少浏览器支持,但由于高质量的polyfill webcomponentsjs,我们今天仍可以使用Web Components。 诸如Polymer和X-Tag之类的库提供了以更简单的方式创建Web组件的机会。

现在,请务必查看后续文章: 如何制作可访问的Web组件

您是否已在Web应用程序中使用过Web组件? 请在下面的部分中随意分享您的经验和想法。

From: https://www.sitepoint.com/creating-a-multiselect-component-as-a-web-component/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值