Web Components 开始

<!-- load webcomponents bundle, which includes all the necessary polyfills -->
<script src="node_modules/@webcomponents/webcomponentsjs/webcomponents-bundle.js"></script>

<!-- load the element -->
<script type="module" src="my-element.js"></script>

<!-- use the element -->
<my-element></my-element>

<user-card  user-id="1"></user-card>

Web Component 的四个核心概念

定制元素(Custom Elements):

HTML 模板(HTML Templates):

Shadow DOM:

HTML 引用(HTML Imports):


组件的命名


拓展组件


组件元素是类的实例


定义私有方法

(function() {})();

冻结类

class  MyComponent  extends  HTMLElement { ... }
const  FrozenMyComponent = Object.freeze(MyComponent);
customElements.define('my-component', FrozenMyComponent);

服务器渲染 项目 注意事项

<script type="module" async>
    import 'https://xxx/button.js';
</script>

定义定制元素

定制元素的生命周期方法

static get observedAttributes() { return ['disabled','icon','loading'] }

constructor(){}

user-card 元素

class UserCard extends HTMLElement {
  constructor() {
    super();

    this.addEventListener("click", e => {
      this.toggleCard();
    });
  }

  toggleCard() {
    console.log("Element was clicked!");
  }
}

customElements.define("user-card", UserCard);

创建模板

<template id="user-card-template">
    <div>
        <h2>
            <span></span> (
            <span></span>)
        </h2>
        <p>Website: <a></a></p>
        <div>
            <p></p>
        </div>
        <button class="card__details-btn">More Details</button>
    </div>
</template>
<script src="/UserCard/UserCard.js"></script>

编写样式

.card__user-card-container {
  text-align: center;
  display: inline-block;
  border-radius: 5px;
  border: 1px solid grey;
  font-family: Helvetica;
  margin: 3px;
  width: 30%;
}

.card__user-card-container:hover {
  box-shadow: 3px 3px 3px;
}

.card__hidden-content {
  display: none;
}

.card__details-btn {
  background-color: #dedede;
  padding: 6px;
  margin-bottom: 8px;
}
<template id="user-card-template">
<link  rel="stylesheet"  href="/UserCard/UserCard.css">
    <div>
        <h2>
            <span></span> (
            <span></span>)
        </h2>
        <p>Website: <a></a></p>
        <div>
            <p></p>
        </div>
        <button class="card__details-btn">More Details</button>
    </div>
</template>
<script src="/UserCard/UserCard.js"></script>

组件的功能

connectedCallback

const  currentDocument = document.currentScript.ownerDocument;
// 元素插入 DOM 时调用
connectedCallback() {
  const shadowRoot = this.attachShadow({ mode: "open" });
  // 选取模板并且克隆它。最终将克隆后的节点添加到 shadowDOM 的根节点。

  // 当前文档需要被定义从而获取引入 HTML 的 DOM 权限。
  const template = currentDocument.querySelector("#user-card-template");

  const instance = template.content.cloneNode(true);
  shadowRoot.appendChild(instance);

  // 从元素中选取 user-id 属性
  // 注意我们要像这样指定卡片:
  // <user-card user-id="1"></user-card>

  const userId = this.getAttribute("user-id");
  // 根据 user ID 获取数据,并且使用返回的数据渲染

  fetch(`https://jsonplaceholder.typicode.com/users/${userId}`)
    .then(response => response.text())
    .then(responseText => {
      this.render(JSON.parse(responseText));
    })
    .catch(error => {
      console.error(error);
    });
}

渲染用户数据

render(userData) {
  // 使用操作 DOM 的 API 来填充卡片的不同区域
  // 组件的所有元素都存在于 shadow dom 中,所以我们使用了 this.shadowRoot 这个属性来获取 DOM
  // DOM 只可以在这个子树种被查找到
  this.shadowRoot.querySelector(".card__full-name").innerHTML = userData.name;
  this.shadowRoot.querySelector(".card__user-name").innerHTML =
    userData.username;
  this.shadowRoot.querySelector(".card__website").innerHTML = userData.website;
  this.shadowRoot.querySelector(".card__address").innerHTML = `<h4>Address</h4>
    ${userData.address.suite}, <br />
    ${userData.address.street},<br />
    ${userData.address.city},<br />
    Zipcode: ${userData.address.zipcode}`;
}

toggleCard() {
  let elem = this.shadowRoot.querySelector(".card__hidden-content");
  let btn = this.shadowRoot.querySelector(".card__details-btn");
  btn.innerHTML =
    elem.style.display == "none" ? "Less Details" : "More Details";

  elem.style.display = elem.style.display == "none" ? "block" : "none";
}

在任意项目中使用组件

<html>

<head>
    <title>Web Component</title>
</head>

<body>
    <user-card user-id="1"></user-card>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/webcomponentsjs/1.0.14/webcomponents-hi.js"></script>
    <link rel="import" href="./UserCard/UserCard.html">
</body>

</html>

组件示例


代码组织

src/
  index.html
  components/
    PeopleController/
      PeopleController.js
      PeopleController.html
      PeopleController.css
      PeopleList/
        PeopleList.js
        PeopleList.html
        PeopleList.css
      PersonDetail/
        PersonDetail.js
        PersonDetail.html
        PersonDetail.css

人员列表组件 PeopleList

<template id="people-list-template">
  <style>
  .people-list__container {
    border: 1px solid black;
  }
  .people-list__list {
    list-style: none
  }

  .people-list__list > li {
    font-size: 20px;
    font-family: Helvetica;
    color: #000000;
    text-decoration: none;
  }
  </style>
  <div class="people-list__container">
    <ul class="people-list__list"></ul>
  </div>
</template>
<script src="/components/PeopleController/PeopleList/PeopleList.js"></script>
(function () {
  const currentDocument = document.currentScript.ownerDocument;

  function _createPersonListElement(self, person) {
    let li = currentDocument.createElement('LI');
    li.innerHTML = person.name;
    li.className = 'people-list__name'
    li.onclick = () => {
      let event = new CustomEvent("PersonClicked", {
        detail: {
          personId: person.id
        },
        bubbles: true
      });
      self.dispatchEvent(event);
    }
    return li;
  }

  class PeopleList extends HTMLElement {
    constructor() {
      // If you define a constructor, always call super() first as it is required by the CE spec.
      super();

      // A private property that we'll use to keep track of list
      let _list = [];

      //使用defineProperty定义此对象的prop,即组件。
      //每当设置列表时,调用render。 这种方式当父组件设置一些数据时
      //在子对象上,我们可以自动更新子对象。
      Object.defineProperty(this, 'list', {
        get: () => _list,
        set: (list) => {
          _list = list;
          this.render();
        }
      });
    }

    connectedCallback() {
      // Create a Shadow DOM using our template
      const shadowRoot = this.attachShadow({ mode: 'open' });
      const template = currentDocument.querySelector('#people-list-template');
      const instance = template.content.cloneNode(true);
      shadowRoot.appendChild(instance);
    }

    render() {
      let ulElement = this.shadowRoot.querySelector('.people-list__list');
      ulElement.innerHTML = '';

      this.list.forEach(person => {
        let li = _createPersonListElement(this, person);
        ulElement.appendChild(li);
      });
    }
  }

  customElements.define('people-list', PeopleList);
})();

PersonDetail组件

<template id="person-detail-template">
  <link rel="stylesheet" href="/components/PeopleController/PersonDetail/PersonDetail.css">
  <div class="card__user-card-container">
    <h2 class="card__name">
      <span class="card__full-name"></span> (
      <span class="card__user-name"></span>)
    </h2>
    <p>Website: <a class="card__website"></a></p>
    <div class="card__hidden-content">
      <p class="card__address"></p>
    </div>
    <button class="card__details-btn">More Details</button>
  </div>
</template>
<script src="/components/PeopleController/PersonDetail/PersonDetail.js"></script>
.card__user-card-container {
  text-align: center;
  border-radius: 5px;
  border: 1px solid grey;
  font-family: Helvetica;
  margin: 3px;
}

.card__user-card-container:hover {
  box-shadow: 3px 3px 3px;
}

.card__hidden-content {
  display: none;
}

.card__details-btn {
  background-color: #dedede;
  padding: 6px;
  margin-bottom: 8px;
}
(function () {
  const currentDocument = document.currentScript.ownerDocument;

  class PersonDetail extends HTMLElement {
    constructor() {
      // If you define a constructor, always call super() first as it is required by the CE spec.
      super();

      // Setup a click listener on <user-card>
      this.addEventListener('click', e => {
        this.toggleCard();
      });
    }

    // Called when element is inserted in DOM
    connectedCallback() {
      const shadowRoot = this.attachShadow({ mode: 'open' });
      const template = currentDocument.querySelector('#person-detail-template');
      const instance = template.content.cloneNode(true);
      shadowRoot.appendChild(instance);
    }

    // 创建API函数,以便其他组件可以使用它来填充此组件
    // Creating an API function so that other components can use this to populate this component
    updatePersonDetails(userData) {
      this.render(userData);
    }

    /// 填充卡的功能(可以设为私有)
    // Function to populate the card(Can be made private)
    render(userData) {
      this.shadowRoot.querySelector('.card__full-name').innerHTML = userData.name;
      this.shadowRoot.querySelector('.card__user-name').innerHTML = userData.username;
      this.shadowRoot.querySelector('.card__website').innerHTML = userData.website;
      this.shadowRoot.querySelector('.card__address').innerHTML = `<h4>Address</h4>
      ${userData.address.suite}, <br />
      ${userData.address.street},<br />
      ${userData.address.city},<br />
      Zipcode: ${userData.address.zipcode}`
    }

    toggleCard() {
      let elem = this.shadowRoot.querySelector('.card__hidden-content');
      let btn = this.shadowRoot.querySelector('.card__details-btn');
      btn.innerHTML = elem.style.display == 'none' ? 'Less Details' : 'More Details';
      elem.style.display = elem.style.display == 'none' ? 'block' : 'none';
    }
  }

  customElements.define('person-detail', PersonDetail);
})()

父组件 PeopleController

<template id="people-controller-template">
  <link rel="stylesheet" href="/components/PeopleController/PeopleController.css">
  <people-list id="people-list"></people-list>
  <person-detail id="person-detail"></person-detail>
</template>
<link rel="import" href="/components/PeopleController/PeopleList/PeopleList.html">
<link rel="import" href="/components/PeopleController/PersonDetail/PersonDetail.html">
<script src="/components/PeopleController/PeopleController.js"></script>
#people-list {
  width: 45%;
  display: inline-block;
}
#person-detail {
  width: 45%;
  display: inline-block;
}
(function () {
  const currentDocument = document.currentScript.ownerDocument;

  function _fetchAndPopulateData(self) {
    let peopleList = self.shadowRoot.querySelector('#people-list');
    fetch(`https://jsonplaceholder.typicode.com/users`)
      .then((response) => response.text())
      .then((responseText) => {
        const list = JSON.parse(responseText);
        self.peopleList = list;
        peopleList.list = list;

        _attachEventListener(self);
      })
      .catch((error) => {
        console.error(error);
      });
  }
  function _attachEventListener(self) {
    let personDetail = self.shadowRoot.querySelector('#person-detail');

    //Initialize with person with id 1:
    personDetail.updatePersonDetails(self.peopleList[0]);

    self.shadowRoot.addEventListener('PersonClicked', (e) => {
      // e contains the id of person that was clicked.
      // We'll find him using this id in the self.people list:
      self.peopleList.forEach(person => {
        if (person.id == e.detail.personId) {
          // Update the personDetail component to reflect the click
          personDetail.updatePersonDetails(person);
        }
      })
    })
  }

  class PeopleController extends HTMLElement {
    constructor() {
      super();
      this.peopleList = [];
    }

    connectedCallback() {
      const shadowRoot = this.attachShadow({ mode: 'open' });
      const template = currentDocument.querySelector('#people-controller-template');
      const instance = template.content.cloneNode(true);
      shadowRoot.appendChild(instance);

      _fetchAndPopulateData(this);
    }
  }

  customElements.define('people-controller', PeopleController);
})()

使用组件

<html>

<head>
  <title>Web Component Part 2</title>
</head>

<body>
  <people-controller></people-controller>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/webcomponentsjs/1.0.14/webcomponents-hi.js"></script>
  <link rel="import" href="./components/PeopleController/PeopleController.html">
</body>

</html>

attributes 组件属性 观察/更新


<user-card username="Ayush" address="Indore, India" is-admin></user-card>
let myUserCard = document.querySelector('user-card')

myUserCard.getAttribute('username') // Ayush

myUserCard.setAttribute('username', 'Ayush Gupta')
myUserCard.getAttribute('username') // Ayush Gupta

观察属性更改

创建UserCard组件

(async () => {
  const res = await fetch('/UserCard/UserCard.html');
  const textTemplate = await res.text();
  const HTMLTemplate = new DOMParser().parseFromString(textTemplate, 'text/html')
                           .querySelector('template');

  class UserCard extends HTMLElement {
    constructor() { ... }
    connectedCallback() { ... }

    // Getter to let component know what attributes
    // to watch for mutation
    static get observedAttributes() {
      return ['username', 'address', 'is-admin'];
    }

    attributeChangedCallback(attr, oldValue, newValue) {
      console.log(`${attr} was changed from ${oldValue} to ${newValue}!`)
    }
  }

  customElements.define('user-card', UserCard);
})();

使用属性初始化

<user-card username="Ayush" address="Indore, India" is-admin="true"></user-card>
connectedCallback() {
  const shadowRoot = this.attachShadow({ mode: 'open' });
  const instance = HTMLTemplate.content.cloneNode(true);
  shadowRoot.appendChild(instance);

  // You can also put checks to see if attr is present or not
  // and throw errors to make some attributes mandatory
  // Also default values for these variables can be defined here
  this.username = this.getAttribute('username');
  this.address = this.getAttribute('address');
  this.isAdmin = this.getAttribute('is-admin');
}

// Define setters to update the DOM whenever these values are set
set username(value) {
  this._username = value;
  if (this.shadowRoot)
    this.shadowRoot.querySelector('#card__username').innerHTML = value;
}

get username() {
  return this._username;
}

set address(value) {
  this._address = value;
  if (this.shadowRoot)
    this.shadowRoot.querySelector('#card__address').innerHTML = value;
}

get address() {
  return this._address;
}

set isAdmin(value) {
  this._isAdmin = value;
  if (this.shadowRoot)
    this.shadowRoot.querySelector('#card__admin-flag').style.display = value == true ? "block" : "none";
}

get isAdmin() {
  return this._isAdmin;
}

观察属性更改

attributeChangedCallback(attr, oldVal, newVal) {
  const attribute = attr.toLowerCase()
  console.log(newVal)
  if (attribute === 'username') {
    this.username = newVal != '' ? newVal : "Not Provided!"
  } else if (attribute === 'address') {
    this.address = newVal !== '' ? newVal : "Not Provided!"
  } else if (attribute === 'is-admin') {
    this.isAdmin = newVal == 'true';
  }
}

创建组件

<template id="user-card-template">
  <h3 id="card__username"></h3>
  <p id="card__address"></p>
  <p id="card__admin-flag">I'm an admin</p>
</template>

使用组件

<html>

<head>
  <title>Web Component</title>
</head>

<body>
  <input type="text" onchange="updateName(this)" placeholder="Name">
  <input type="text" onchange="updateAddress(this)" placeholder="Address">
  <input type="checkbox" onchange="toggleAdminStatus(this)" placeholder="Name">
  <user-card username="Ayush" address="Indore, India" is-admin></user-card>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/webcomponentsjs/1.0.14/webcomponents-hi.js"></script>
  <script src="/UserCard/UserCard.js"></script>
  <script>
    function updateAddress(elem) {
      document.querySelector('user-card').setAttribute('address', elem.value);
    }

    function updateName(elem) {
      document.querySelector('user-card').setAttribute('username', elem.value);
    }

    function toggleAdminStatus(elem) {
      document.querySelector('user-card').setAttribute('is-admin', elem.checked);
    }
  </script>
</body>

</html>

何时使用属性

我们有3种方法可以创建交互式Web组件:


Web Components modal 模态弹窗

定义模态组件
class Modal extends HTMLElement {
    constructor() {
        super();
        this._modalVisible = false;
        this._modal;
        this.attachShadow({ mode: 'open' });
        this.shadowRoot.innerHTML = `
        <style>
            /* The Modal (background) */
            .modal {
                display: none;
                position: fixed;
                z-index: 1;
                padding-top: 100px;
                left: 0;
                top: 0;
                width: 100%;
                height: 100%;
                overflow: auto;
                background-color: rgba(0,0,0,0.4);
            }

            /* Modal Content */
            .modal-content {
                position: relative;
                background-color: #fefefe;
                margin: auto;
                padding: 0;
                border: 1px solid #888;
                width: 80%;
                box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19);
                -webkit-animation-name: animatetop;
                -webkit-animation-duration: 0.4s;
                animation-name: animatetop;
                animation-duration: 0.4s
            }

            /* Add Animation */
            @-webkit-keyframes animatetop {
                from {top:-300px; opacity:0}
                to {top:0; opacity:1}
            }

            @keyframes animatetop {
                from {top:-300px; opacity:0}
                to {top:0; opacity:1}
            }

            /* The Close Button */
            .close {
                color: white;
                float: right;
                font-size: 28px;
                font-weight: bold;
            }

            .close:hover,
            .close:focus {
            color: #000;
            text-decoration: none;
            cursor: pointer;
            }

            .modal-header {
            padding: 2px 16px;
            background-color: #000066;
            color: white;
            }

            .modal-body {padding: 2px 16px; margin: 20px 2px}

        </style>
        <button>Open Modal</button>
        <div class="modal">
            <div class="modal-content">
                <div class="modal-header">
                    <span class="close">&times;</span>
                    <slot name="header"><h1>Default text</h1></slot>
                </div>
                <div class="modal-body">
                    <slot><slot>
                </div>
            </div>
        </div>
        `
    }
    connectedCallback() {
        this._modal = this.shadowRoot.querySelector(".modal");
        this.shadowRoot.querySelector("button").addEventListener('click', this._showModal.bind(this));
        this.shadowRoot.querySelector(".close").addEventListener('click', this._hideModal.bind(this));
    }
    disconnectedCallback() {
        this.shadowRoot.querySelector("button").removeEventListener('click', this._showModal);
        this.shadowRoot.querySelector(".close").removeEventListener('click', this._hideModal);
    }
    _showModal() {
        this._modalVisible = true;
        this._modal.style.display = 'block';
    }
    _hideModal() {
        this._modalVisible = false;
        this._modal.style.display = 'none';
    }
}
customElements.define('pp-modal',Modal);

使用 模态组件
<!DOCTYPE html>
<html>

<head>
  <meta name="viewport" content="width=device-width, initial-scale=1">

  <script src="./modal.js"></script>
</head>

<body>

  <h2>Modal web component with vanilla JS.</h2>

  <pp-modal>
    <h1 slot="header">Information Box</h1>
    Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna
    aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
  </pp-modal>

</body>

</html>

template

(function () {
    class MidociLayOut extends HTMLElement {
      static get observedAttributes() {
        return ['acitve-title', 'active-sub-title']
      }

      constructor() {
        super()
        this.attachShadow({mode: 'open'})
        this.shadowRoot.innerHTML = `
          <style>

          </style>

          <div class="wrapper">


          </div>
        `

        this._a = ''
      }

      connectedCallback() {
      }

      disconnectedCallback() {

      }

      attributeChangedCallback(attr, oldVal, newVal) {
        // const attribute = attr.toLowerCase()
        // if (attribute === 'descriptions') {
        //   console.log(1)
        //   this.render(newVal)
        // }
      }

    }

    const FrozenMidociLayOut = Object.freeze(MidociLayOut);
    customElements.define('midoci-lay-out', FrozenMidociLayOut);
  })()
  

使用 web component 构建一个通用无依赖 html 单文件 select 组件

web component select

(function () {
  const selectListDemo = [
    {name: 'test1', value: 1},
    {name: 'test2', value: 2},
    {name: 'test3', value: 3}
  ]

  class MidociSelect extends HTMLElement {
    static get observedAttributes() {
      return ['acitve-title', 'active-sub-title']
    }

    constructor() {
      super()
      this.attachShadow({mode: 'open'})
      this.shadowRoot.innerHTML = `
        <style>
          :host{
            --themeColor:rgb(24,144,255);
            box-sizing: border-box;
            font-size: 14px;
            --borderColor:#eee;
          }

          .wrapper{
            position: relative;
            display: inline-flex;
            align-items: center;
            padding-left: 10px;
            width: 95px;
            height: 36px;
            border: 1px solid var(--borderColor);
            color: #333;
            border-radius: 2px;
            user-select: none;
            transition: .3s cubic-bezier(.12, .4, .29, 1.46);
            outline:none
          }

          .wrapper:hover{
            border: 1px solid var(--themeColor);
            transition: .3s cubic-bezier(.12, .4, .29, 1.46);
          }

          .title{

          }

          .arrow-out{
            position: absolute;
            right: 12px;
            top: 50%;
            transform: translateY(0px) rotateX(0deg);
            transition: .3s cubic-bezier(.12, .4, .29, 1.46);
          }

          .wrapper.flip>.arrow-out{
            transform: translateY(-3px) rotateX(180deg);
            transition: .3s cubic-bezier(.12, .4, .29, 1.46);
          }

          .arrow{
            display: flex;
            width: 6px;
            height:6px;
            border: none;
            border-left: 1px solid #333;
            border-bottom: 1px solid #333;
            transform: translateY(-50%) rotateZ(-45deg);
            transition: .3s cubic-bezier(.12, .4, .29, 1.46);
          }

          .wrapper:hover .arrow{
            border-left: 1px solid var(--themeColor);
            border-bottom: 1px solid var(--themeColor);
            transition: .3s cubic-bezier(.12, .4, .29, 1.46);
          }



          .list{
            z-index: 100;
            position: absolute;
            top: 130%;
            left: 0;
            background-color: #fff;
            box-shadow: 0 0 10px rgba(0,0,0,0.1);
            visibility: hidden;
            min-width: 100%;
            border-radius: 3px;
            transform: scale(0);
            transform-origin: top;
            transition: .3s cubic-bezier(.12, .4, .29, 1.46);
          }

          .wrapper.flip>.list{
          visibility: visible;
            transform: scale(1);
            transition: .3s cubic-bezier(.12, .4, .29, 1.46);
          }

          .item{
            display: flex;
            align-items: center;
            padding-left: 10px;
            width: 95px;
            height: 36px;
            color: #333;
            border-radius: 2px;
            user-select: none;
            background-color: #fff;
            transition: background-color .3s ease-in-out;
          }

          .item:hover{
            background-color: rgba(24,144,255,0.1);
            transition: background-color .3s ease-in-out;
          }
        </style>

        <div class="wrapper" tabindex="1">
          <span class="title">1</span>
          <span class="arrow-out">
            <span class="arrow"></span>
          </span>
          <div class="list" >
            <div class="item">1</div>
            <div class="item">2</div>
            <div class="item">3</div>
            <div class="item">4</div>
          </div>
        </div>
      `
      this._wrapperDom = null
      this._listDom = null
      this._titleDom = null
      this._list = []
      this._arrowFlip = false
      this._value = null
      this._name = null
    }

    connectedCallback() {
      this._wrapperDom = this.shadowRoot.querySelector('.wrapper')
      this._listDom = this.shadowRoot.querySelector('.list')
      this._titleDom = this.shadowRoot.querySelector('.title')
      this.initEvent()
      this.list = selectListDemo
    }

    disconnectedCallback() {
      this._wrapperDom.removeEventListener('click', this.flipArrow.bind(this))
      this._wrapperDom.removeEventListener('blur', this.blurWrapper.bind(this))

      this.shadowRoot.querySelectorAll('.item')
        .forEach((item, index) => {
          item.removeEventListener('click', this.change.bind(this, index))
        })
    }

    attributeChangedCallback(attr, oldVal, newVal) {
      // const attribute = attr.toLowerCase()
      // if (attribute === 'descriptions') {
      //   console.log(1)
      //   this.render(newVal)
      // }
    }

    set list(list) {
      if (!this.shadowRoot) return
      this._list = list
      this.render(list)
    }

    get list() {
      return this._list
    }

    set value(value) {
      this._value = value
    }

    get value() {
      return this._value
    }

    set name(name) {
      this._name = name
    }

    get name() {
      return this._name
    }

    initEvent() {
      this.initArrowEvent()
      this.blurWrapper()
    }

    initArrowEvent() {
      this._wrapperDom.addEventListener('click', this.flipArrow.bind(this))
    }

    initChangeEvent() {
      this.shadowRoot.querySelectorAll('.item')
        .forEach((item, index) => {
          item.addEventListener('click', this.change.bind(this, index))
        })
    }

    change(index) {
      this.changeTitle(this._list, index)

      let changeInfo = {
        detail: {
          value: this._value,
          name: this._name
        },
        bubbles: true
      }
      let changeEvent = new CustomEvent('change', changeInfo)
      this.dispatchEvent(changeEvent)
    }

    changeTitle(list, index) {
      this._value = list[index].value
      this._name = list[index].name
      this._titleDom.innerText = this._name
    }

    flipArrow() {
      if (!this._arrowFlip) {
        this.showList()
      } else {
        this.hideList()
      }
    }

    showList() {
      this._arrowFlip = true
      this._wrapperDom.classList = 'wrapper flip'
    }

    hideList() {
      this._arrowFlip = false
      this._wrapperDom.classList = 'wrapper'
    }

    blurWrapper() {
      this._wrapperDom.addEventListener('blur', (event) => {
        event.stopPropagation()
        this.hideList()
      })
    }

    render(list) {
      if (!list instanceof Array) return
      let listString = ''
      list.forEach((item) => {
        listString += `
          <div class="item" data-value="${item.value}">${item.name}</div>
        `
      })
      this._listDom.innerHTML = listString
      this.changeTitle(list, 0)
      this.initChangeEvent()
    }
  }

  const FrozenMidociSelect = Object.freeze(MidociSelect);
  customElements.define('midoci-select', FrozenMidociSelect);
})()

使用

<script type="module" async>
    import './MidociSelect.js'
</script>

<midoci-select></midoci-select>

<script>
    const list = [
        {name: '全平台', value: 1},
        {name: '东券', value: 2},
        {name: '京券', value: 3}
      ]

    window.onload=function(){
        document.querySelector('midoci-select').list=list

        console.log(document.querySelector('midoci-select').value)
        console.log(document.querySelector('midoci-select').name)

        document.querySelector('midoci-select').addEventListener('change', (event) => {
        console.log('选中的 value:', event.detail.value)
        console.log('选中的 name:', event.detail.name)
      })
    }
</script>

03-05 14:05