内容简介:在接下来,我们将继续创建对话框组件的自定义元素版本,该自定义元素版本目前仅使用请在CodePen 上查看由 Caleb Williams (@calebdwilliams) 创建的带有脚本的模板对话框 Demo。
在 上一篇文章 ,我们在文档中创建了 HTML 模板,希望它们在需要时才呈现,这让我们开始接触 Web 组件。
接下来,我们将继续创建对话框组件的自定义元素版本,该自定义元素版本目前仅使用 HTMLTemplateElement
。
请在CodePen 上查看由 Caleb Williams (@calebdwilliams) 创建的带有脚本的模板对话框 Demo。
因此,下一步我们将创建一个自定义元素,该元素实时使用我们的 template#dialog-template
元素。
系列文章:
添加一个自定义元素
Web 组件的基础元素是 自定义元素
。该 customElements
的 API 为我们提供了创建自定义 HTML 标签的途径,这些标签可以在包含定义类的任何文档中使用。
可以把它想象成 React 或 Angular 组件(例如 <MyCard />
),但实际上它不依赖于 React 或 Angular。原生自定义组件是这样的: <my-card></my-card>
。更重要的是,将它视为一个标准元素,可以在你的 React、Angular、Vue、[insert-framework-you’re-interested-in-this-week] 应用中使用,而不必大惊小怪。
从本质上讲,一个自定义元素分为两个部分组成:一个 标签名称
和一个 Class
类扩展内置 HTMLElement
类。我们自定义元素的简易 demo 版本如下所示:
class OneDialog extends HTMLElement { connectedCallback() { this.innerHTML = `<h1>Hello, World!</h1>`; } } customElements.define('one-dialog', OneDialog); 复制代码
注意:在整个自定义元素中,this 值是对自身自定义元素实例的引用。
在上面的示例中,我们定义了一个符合标准的新 HTML 元素, <one-dialog></one-dialog>
。它现在暂时还做不了什么...,在任何 HTML 文档中使用 <one-dialog>
标签将会创建一个带着 <h1>
标签显示 “Hello, World!” 的新元素。
我们肯定想把它做的更 NB,很幸运。在上一篇文章中,我们为弹出框创建模板,并且能够拿到模板,让我们在自定义元素中使用它。我们在该示例中添加了一个 script 标签来执行一些对话框魔术。我们暂时删除它,因为我们将把逻辑从 HTML 模板移到自定义元素类中。
class OneDialog extends HTMLElement { connectedCallback() { const template = document.getElementById('one-dialog'); const node = document.importNode(template.content, true); this.appendChild(node); } } 复制代码
现在,定义了自定义元素( <one-dialog>
)并指示浏览器呈现包含在调用自定义元素的 HTML 模板中的内容。
下一步是将我们的逻辑转移到组件类中。
自定义元素生命周期方法
与 React 或 Angular 一样,自定义元素具有 生命周期方法
。笔者已经向各位介绍过 connectedCallback
,当我们的元素被添加到 DOM 的时候调用它。
connectedCallback
与元素的 constructor
是分开的。函数用于设置元素的基本骨架,而 connectedCallback
通常用于向元素添加内容、设置事件监听器或以其他方式初始化组件。
实际上,构造函数不能用于设计或修改或操作元素的属性,如果我们要使用对话框创建新实例, document.createElement
则会调用构造函数。元素的使用者需要一个没有插入属性或内容的简单节点。
该 createElement 函数没有可以用于配置将返回的元素的选项。这是符合情理的,那么话说回来了,既然这个函数没有选项可以配置会返回的元素,那我们唯一的选择就是 connectedCallback
。
在标准内置元素中,元素的状态通常通过元素上存在的属性和这些属性的值来反映。对于我们的示例,我们将仅查看一个属性: [open]
。为此,我们需要观察该属性的更改,我们需要 attributeChangedCallback
来做到这一点。只要其中一个元素构造函数 observedAttributes
之一的属性发生变化就会触发第二个生命周期方法。
这可能听起来难以实现,但语法非常简单:
class OneDialog extends HTMLElement { static get observedAttributes() { return ['open']; } attributeChangedCallback(attrName, oldValue, newValue) { if (newValue !== oldValue) { this[attrName] = this.hasAttribute(attrName); } } connectedCallback() { const template = document.getElementById('one-dialog'); const node = document.importNode(template.content, true); this.appendChild(node); } } 复制代码
在上面的例子中,我们只关心属性是否设置,我们不关心具体的值(这类似于 HTML5 input 输入框上的 required
属性)。更新此属性时,我们更新元素的 open
属性。属性(property)存在于 JavaScript 对象上,HTML Elements 也具有属性(attribute);这个生命周期方法可以帮助我们让两种属性保持同步。
我们将 updater 包含在 attributeChangedCallback
内部的条件检查中,以查看新值和旧值是否相等。我们这样做是为了防止程序中出现无限循环,因为稍后我们将创建一个 getter 和 setter 属性,它将通过在元素的属性(property)更新时设置元素的属性(attribute)来保持属性(attribute)和属性(property)的同步。 attributeChangedCallback
反向执行:当属性更改时更新属性。
现在,开发者可以使用我们的组件,并且利用 open
属性决定对话框是否默认打开。为了使它更具动态性,我们可以在元素的 open
属性中添加自定义 getter 和 setter:
class OneDialog extends HTMLElement { static get boundAttributes() { return ['open']; } attributeChangedCallback(attrName, oldValue, newValue) { this[attrName] = this.hasAttribute(attrName); } connectedCallback() { const template = document.getElementById('one-dialog'); const node = document.importNode(template.content, true); this.appendChild(node); } get open() { return this.hasAttribute('open'); } set open(isOpen) { if (isOpen) { this.setAttribute('open', true); } else { this.removeAttribute('open'); } } } 复制代码
getter 和 setter 将保证(HTML 元素节点上)的 open
特性和属性(在 DOM 对象上)的值同步。添加 open
特性会将 element.open
设置为 true
,同理,将 element.open
设置为 true
会添加 open
属性。我们这样做是为了确保元素的状态由其属性反映出来。虽然在技术层面上不一定需要,但被认为是创建自定义元素的最优办法。
虽然这难免引入一些样板文件,但是通过循环观察到的属性列表并使用 Object.defineProperty
创建一个保持这些属性同步的抽象类是一项相当简单的任务。
class AbstractClass extends HTMLElement { constructor() { super(); // 检查观察到的属性是否已定义并具有长度 if (this.constructor.observedAttributes && this.constructor.observedAttributes.length) { // 通过观察到的属性进行循环 this.constructor.observedAttributes.forEach(attribute => { // 动态定义 getter/setter 原型 Object.defineProperty(this, attribute, { get() { return this.getAttribute(attribute); }, set(attrValue) { if (attrValue) { this.setAttribute(attribute, attrValue); } else { this.removeAttribute(attribute); } } } }); } } } // 我们可以扩展抽象类,而不是直接扩展 HTMLElement class SomeElement extends AbstractClass { /** 省略 **/ } customElements.define('some-element', SomeElement); 复制代码
上面的例子并不完美,它没有考虑实现像 open
这样的属性的可能性,这些属性没有被赋值,而仅仅依赖于属性的存在。做一个完美的版本将超出本文的范围。
现在我们已经知道我们的对话框是否打开了,让我们添加一些逻辑来实际地进行显示和隐藏:
class OneDialog extends HTMLElement { /** 省略 */ constructor() { super(); this.close = this.close.bind(this); } set open(isOpen) { this.querySelector('.wrapper').classList.toggle('open', isOpen); this.querySelector('.wrapper').setAttribute('aria-hidden', !isOpen); if (isOpen) { this._wasFocused = document.activeElement; this.setAttribute('open', ''); document.addEventListener('keydown', this._watchEscape); this.focus(); this.querySelector('button').focus(); } else { this._wasFocused && this._wasFocused.focus && this._wasFocused.focus(); this.removeAttribute('open'); document.removeEventListener('keydown', this._watchEscape); this.close(); } } close() { if (this.open !== false) { this.open = false; } const closeEvent = new CustomEvent('dialog-closed'); this.dispatchEvent(closeEvent); } _watchEscape(event) { if (event.key === 'Escape') { this.close(); } } } 复制代码
这里发生了很多事情,让我们来梳理一下。我们要做的第一件事就是获取我们的容器,在 isOpen
的基础上切换 .open
类。为了使我们的元素可以访问,我们还需要切换 aria-hidden
属性。
如果对话框已经打开了,那么我们希望保存对先前聚焦元素的引用。这是为了考虑可访问性标准。我们还将一个 keydown 监听器添加到名为 WatEscape
的文档中,该文档在构造函数中绑定元素的 this
,其模式类似于 React 处理类组件中的方法调用的方式。
我们这样做不仅是为了确保正确绑定 this.close
,还因为 Function.prototype.bind
返回带绑定调用栈的函数的实例。通过在构造函数中保存对新绑定方法的引用,我们可以在对话框断开时删除事件(稍后将详细介绍)。最后,我们将注意力集中在元素上,并将焦点设置在 shadow root 中的适当元素上。
我们还创建了一个很好的小实用 工具 方法来关闭我们的对话框,它分派一个自定义事件来通知某个监听器对话框已经关闭。
如果元素是关闭的(即 !open
),我们检查以确保 this._wasFocused
属性已定义并具有 focus
方法并调用该方法以将用户的焦点返回到常规 DOM。然后我们删除我们的事件监听器以避免任何内存泄漏。
说到为自己的代码做好清理善后,就自然也要说下我们采用了另一种生命周期方法: disconnectedCallback
。 disconnectedCallback
与 connectedCallback
相反,因为一旦从 DOM 中删除了元素,该方法就会被调用,它允许我们清理附加到元素的任何事件监听器或 MutationObservers
。
碰巧的是,我们还有几个事件侦听器要连接起来:
class OneDialog extends HTMLElement { /** Omitted */ connectedCallback() { this.querySelector('button').addEventListener('click', this.close); this.querySelector('.overlay').addEventListener('click', this.close); } disconnectedCallback() { this.querySelector('button').removeEventListener('click', this.close); this.querySelector('.overlay').removeEventListener('click', this.close); } } 复制代码
现在我们有一个运行良好,大部分可访问的对话框元素。我们可以做一些修饰,比如将焦点集中在元素上,但这超出了我们在本文学习的范围。
还有一个生命周期方法 adoptedCallback
。它不适用于我们的元素,其作用是元素被采用(插入)到 DOM 的另一部分时触发。
在下面的示例中,您将看到我们的模板元素正被一个标准元素 <one-dialog>
所使用。
请在CodePen 上查看由 Caleb Williams (@calebdwilliams) 创建的对话框组件使用模板 Demo。
另一个概念:非演示组件
到目前为止,我们创建的 <one-template>
是一个典型的自定义元素,它包含了当元素包含在文档中时被插入到文档中的标记和行为。然而,并不是所有的元素都需要直观地呈现。在 React 生态系统中,组件通常用于管理应用程序状态或其他一些主要功能,像react-redux 里的 <Provider />
。
让我们想象一下,我们的组件是工作流中一系列对话框的一部分。当一个对话框关闭时,下一个对话框应该打开。我们可以创建一个容器组件来监听我们的 dialog-closed
事件并在整个工作流程中进行:
class DialogWorkflow extends HTMLElement { connectedCallback() { this._onDialogClosed = this._onDialogClosed.bind(this); this.addEventListener('dialog-closed', this._onDialogClosed); } get dialogs() { return Array.from(this.querySelectorAll('one-dialog')); } _onDialogClosed(event) { const dialogClosed = event.target; const nextIndex = this.dialogs.indexOf(dialogClosed); if (nextIndex !== -1) { this.dialogs[nextIndex].open = true; } } } 复制代码
这个元素没有任何表示逻辑,但它充当了应用程序状态的控制器。只需稍加努力,我们就可以重新创建类似 Redux 的状态管理系统,只使用一个自定义元素,可以在 React 的 Redux 容器组件所在的同一个应用程序中管理整个应用程序的状态。
这是对自定义元素的深入了解
现在我们对自定义元素有了很好的理解,我们的对话框开始融合在一起。但它仍然存在一些问题。
请注意,我们必须添加一些 CSS 来重新设置对话框按钮,因为元素的样式会干扰页面的其余部分。虽然我们可以利用命名策略(如 BEM)来确保我们的样式不会与其他组件产生冲突,但是有一种更友好的方式来隔离样式。那就是 shadow DOM。本文系列 Web Components 专题的下一篇文章就会谈到它。
我们需要做的另一件事是为每个组件定义一个新模板,或者为我们的对话框找到一些切换模板的方法。就目前而言,每页只能有一个对话框类型,因为它使用的模板必须始终存在。因此,我们要么需要注入动态内容的方法,要么需要替换模板的方法。
在下一篇文章中,我们将研究如何通过使用 shadow DOM 合并样式和内容封装来提高我们刚刚创建的 <one-dialog>
元素的可用性。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- HashSet存储自定义类型元素
- 使用CSS定义页面元素的外观样式
- xml – 如何定义XSD以允许任何元素
- [译] 写给 React 开发者的自定义元素指南
- CSS进阶——绝对定位元素的宽高是如何定义的
- java – 使用JAXB编译包含相同元素的重复定义的几个XSD
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
The Mechanics of Web Handling
David R. Roisum
This unique book covers many aspects of web handling for manufacturing, converting, and printing. The book is applicable to any web including paper, film, foil, nonwovens, and textiles. The Mech......一起来看看 《The Mechanics of Web Handling》 这本书的介绍吧!