组件编程四

内容纲要

组件封装好后,与常用的框架或库类似。

当全局样式和组件内样式能够很好的控制后,下面对之前的自定义组件进行完善:

<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

链接:https://indeex.club

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


发表评论

您的电子邮箱地址不会被公开。