组件编程四
内容纲要
组件封装好后,与常用的框架或库类似。
当全局样式和组件内样式能够很好的控制后,下面对之前的自定义组件进行完善:
<template id="new-template">
<div class="wrapper">
<div class="overlay"></div>
<div class="dialog" role="dialog" aria-labelledby="title" aria-describedby="content">
<button class="close" aria-label="Close">×️</button>
<h1 id="title">
<slot name="heading"></slot>
</h1>
<div id="content" class="content">
<slot></slot>
</div>
</div>
</div>
</template>
<i-dialog template="new-template">
<span slot="heading">我的名字对你有何意义?</span>
<div>
<p>想想小时候,经常给小伙伴取“外号”和“绰号”可谓一大快事,并且大部分的外号都不是那么文雅,畜生的席位可谓占了绝大多数。可是这些渐渐地在我的生活中慢慢消失,一点一点的,不留痕迹。
你的名字对我有何意义?而我的名字对你又有何意义?</p>
<p>可悲的是:人与人之间的纯粹的忠胆侠义越来越多地变成了现代人的文化符号,也许,有人讲,时代在进步,人与人的关系也随之在慢慢发展和变化,并非要抱守着一种自以为高尚的精神状态,而是不断地去适应社会给人带来的新的东西。</p>
<p><a href="https://www.indeex.org/cn/2014/02/my-meaningful-name/" target="_blank"> indeex--《风聆集》</a></p>
</div>
</i-dialog>
<button id="open-dialog">打开弹窗</button>
class IndeexDialog extends HTMLElement {
static get observedAttributes() {
return ['open', 'template'];
}
constructor() {
super();
this.attachShadow({
mode: 'open'
});
this.close = this.close.bind(this);
}
attributeChangedCallback(attrName, oldValue, newValue) {
if (newValue !== oldValue) {
switch (attrName) {
case 'open':
this[attrName] = this.hasAttribute(attrName);
break;
case 'template':
this[attrName] = newValue;
break;
}
}
}
connectedCallback() {
this.render();
}
disconnectedCallback() {
this.shadowRoot.querySelector('button').removeEventListener('click', this.close);
this.shadowRoot.querySelector('.overlay').removeEventListener('click', this.close);
}
render() {
const {
shadowRoot,
template
} = this;
const templateNode = document.getElementById(template);
if (templateNode) {
const content = document.importNode(templateNode.content, true);
shadowRoot.appendChild(content);
} else {
shadowRoot.innerHTML =
`<style>
.wrapper {
opacity: 0;
transition: visibility 0s, opacity 0.25s ease-in;
}
.wrapper:not(.open) {
visibility: hidden;
}
.wrapper.open {
align-items: center;
display: flex;
justify-content: center;
height: 100vh;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
opacity: 1;
visibility: visible;
}
.overlay {
background: rgba(0, 0, 0, 0.8);
height: 100%;
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
width: 100%;
}
.dialog {
background: #ffffff;
max-width: 600px;
padding: 1rem;
position: fixed;
}
button {
all: unset;
cursor: pointer;
font-size: 1.25rem;
position: absolute;
top: 1rem;
right: 1rem;
}
button:hover {
color: #ca5335;
}
</style>
<div class="wrapper">
<div class="overlay"></div>
<div class="dialog" role="dialog" aria-labelledby="title" aria-describedby="content">
<button class="close" aria-label="Close">×️</button>
<h1 id="title"><slot name="heading"></slot></h1>
<div id="content" class="content">
<slot></slot>
</div>
</div>
</div>`;
}
shadowRoot.querySelector('button').addEventListener('click', this.close);
shadowRoot.querySelector('.overlay').addEventListener('click', this.close);
this.open = this.open;
}
get open() {
return this.hasAttribute('open');
}
get template() {
return this.getAttribute('template');
}
set template(template) {
if (template) {
this.setAttribute('template', template);
} else {
this.removeAttribute('template');
}
this.render();
}
set open(isOpen) {
const {
shadowRoot
} = this;
shadowRoot.querySelector('.wrapper').classList.toggle('open', isOpen);
shadowRoot.querySelector('.wrapper').setAttribute('aria-hidden', !isOpen);
if (isOpen) {
this._wasFocused = document.activeElement;
this.setAttribute('open', '');
document.addEventListener('keydown', this._watchEscape);
this.focus();
shadowRoot.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();
}
}
}
customElements.define('i-dialog', IndeexDialog);
const button = document.getElementById('open-dialog');
button.addEventListener('click', () => {
document.querySelector('i-dialog').open = true;
})
组件内有内部样式,全局样式可以覆盖内部样式预览
当然,依然可以使用自定义样式。
对比一下当下流行框架。
Angular
Angular在处理自定义组件时,碰到预置或无法解析的组件直接抛错,并且需要遵循相应规则,详见文档:
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
@NgModule({
schemas: [ CUSTOM_ELEMENTS_SCHEMA ]
})
export class MyModuleAllowsCustomElements {}
//...
<indeex-dialog [open]="isDialogOpen" (dialog-closed)="dialogClosed($event)">
<span slot="heading">A Header Text.....</span>
<div>
<p>Wahahaaaaa.....</p>
</div>
</indeex-dialog>
Vue
Vue库更遵循标准,所以更容易配合使用:
<indeex-dialog v-bind:open="isDialogOpen" v-on:dialog-closed="dialogClosed">
<span slot="heading">A Header Text.....</span>
<div>
<p>Wahahaaaaa.....</p>
</div>
</indeex-dialog>
但是,不管是Angular还是Vue,对于表单控件会复杂很多,不在讨论范围内。
React
React库比较复杂,因为React采用all in js的方案,由于事件被重写,需要使用refs,以下是模板组件:
import React, { Component, createRef } from 'react';
import PropTypes from 'prop-types';
export default class IndeexDialog extends Component {
constructor(props) {
super(props);
this.dialog = createRef();
this.onDialogClosed = this.onDialogClosed.bind(this);
}
componentDidMount() {
this.dialog.current.addEventListener('dialog-closed', this.onDialogClosed);
}
componentWillUnmount() {
this.dialog.current.removeEventListener('dialog-closed', this.onDialogClosed);
}
onDialogClosed(event) {
if (this.props.onDialogClosed) {
this.props.onDialogClosed(event);
}
}
render() {
const { children, onDialogClosed, ...props } = this.props;
return <indeex-dialog {...props} ref={this.dialog}>
{children}
</indeex-dialog>
}
}
IndeexDialog.propTypes = {
children: children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
]).isRequired,
onDialogClosed: PropTypes.func
};
以下是Hooks组件:
import React, { useRef, useEffect } from 'react';
import PropTypes from 'prop-types';
export default function IndeexDialog(props) {
const { children, onDialogClosed, ...restProps } = props;
const indeexDialog = useRef(null);
useEffect(() => {
onDialogClosed ? indeexDialog.current.addEventListener('dialog-closed', onDialogClosed) : null;
return () => {
onDialogClosed ? indeexDialog.current.removeEventListener('dialog-closed', onDialogClosed) : null;
};
});
return <indeex-dialog ref={indeexDialog} {...restProps}>{children}</indeex-dialog>
}
然后就是正常的使用:
import React, { useState } from 'react';
import IndeexDialog from './IndeexDialog';
export default function MyComponent(props) {
const [open, setOpen] = useState(false);
return <div>
<button onClick={() => setOpen(true)}>Open dialog</button>
<IndeexDialog open={open} onDialogClosed={() => setOpen(false)}>
<span slot="heading">A Header Text.....</span>
<div>
<p>Wahahaaaaa.....</p>
</div>
</IndeexDialog>
</div>
}
Web工具和生态
使用Web相关工具、比如Npm等可以快速构建属于自己的组件并发布供他人使用。
当然也有很多成熟的UI框架可以挑选,比如Element、Ant design等,但必须有自我创建和完善组件的能力,而且使用原生开发自己的组件才是最好的选择。
code enjoy!😜😜😜
作者:indeex
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。