Web Components 系列教程

Web Components 開始

不添加任何依賴來構建自己的定製組件

帶有樣式,擁有交互功能並且在各自文件中優雅組織的 HTML 標籤

https://developer.mozilla.org...

Web Components是一套不同的技術,允許您創建可重用的定製元素(它們的功能封裝在您的代碼之外)並且在您的web應用中使用它們。

示例

https://github.com/mdn/web-co...

polyfill

https://www.webcomponents.org...

https://github.com/webcompone...

https://unpkg.com/browse/@web...

npm install @webcomponents/webcomponentsjs

<!-- 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>

Web Component 是一系列 web 平臺的 API,它們可以允許你創建全新可定製、可重用並且封裝的 HTML 標籤

定製的組件基於 Web Component 標準構建,可以在現在瀏覽器上使用,也可以和任意與 HTML 交互的 JavaScript 庫和框架配合使用。

它賦予了僅僅使用純粹的JS/HTML/CSS就可以創建可重用組件的能力。如果 HTML 不能滿足需求,我們可以創建一個可以滿足需求的 Web Component。

舉個例子,你的用戶數據和一個 ID 有關,你希望有一個可以填入用戶 ID 並且可以獲取相應數據的組件。HTML 可能是下面這個樣子:

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

Web Component 的四個核心概念

HTML 和 DOM 標準定義了四種新的標準來幫助定義 Web Component。這些標準如下:

定製元素(Custom Elements):

web 開發者可以通過定製元素創建新的 HTML 標籤、增強已有的 HTML 標籤或是二次開發其它開發者已經完成的組件。這個 API 是 Web Component 的基石。

HTML 模板(HTML Templates):

HTML 模板定義了新的元素,描述一個基於 DOM 標準用於客戶端模板的途徑。模板允許你聲明標記片段,它們可以被解析爲 HTML。這些片段在頁面開始加載時不會被用到,之後運行時會被實例化。

Shadow DOM:

Shadow DOM 被設計爲構建基於組件的應用的一個工具。它可以解決 web 開發的一些常見問題,比如允許你把組件的 DOM 和作用域隔離開,並且簡化 CSS 等等。

HTML 引用(HTML Imports):

HTML 模板(HTML Templates)允許你創建新的模板,同樣的,HTML 引用(HTML imports)允許你從不同的文件中引入這些模板。通過獨立的HTML文件管理組件,可以幫助你更好的組織代碼。

組件的命名

定製元素的名稱必須包含一個短橫線。所以 <my-tabs> 和 <my-amazing-website> 是合法的名稱, 而 <foo> 和 <foo_bar> 不行。

在 HTML 添加新標籤時需要確保向前兼容,不能重複註冊同一個標籤。

定製元素標籤不能是自閉合的,因爲 HTML 只允許一部分元素可以自閉合。需要寫成像 <app-drawer></app-drawer> 這樣的閉合標籤形式。


拓展組件

創建組件時可以使用繼承的方式。

舉個例子,如果想要爲兩種不同的用戶創建一個 UserCard,

你可以先創建一個基本的 UserCard 然後將它拓展爲兩種特定的用戶卡片。

Google web developers’ article https://developers.google.com...


組件元素是類的實例

組件元素是類的實例,就可以在這些類中定義公用方法。

這些公用方法可以用來允許其它定製組件/腳本來和這些組件產生交互,而不是隻能改變這些組件的屬性。


定義私有方法

可以通過多種方式定義私有方法。我傾向於使用(立即執行函數),因爲它們易寫和易理解。
(function() {})();

凍結類

爲了防止新的屬性被添加,需要凍結你的類。

這樣可以防止類的已有屬性被移除,或者已有屬性的可枚舉、可配置或可寫屬性被改變,同樣也可以防止原型被修改。

class  MyComponent  extends  HTMLElement { ... }
const  FrozenMyComponent = Object.freeze(MyComponent);
customElements.define('my-component', FrozenMyComponent);
凍結類會阻止你在運行時添加補丁並且會讓你的代碼難以調試。

服務器渲染 項目 注意事項

鑑於 服務器的根路徑的配置不統一

import 可以使用絕對路徑

import 的 js 內部不可以再次 import ,會出現路徑錯誤

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

定義定製元素

聲明一個類,定義元素如何表現。這個類需要繼承 HTMLElement 類

定製元素的生命週期方法

connectedCallback — 每當元素插入 DOM 時被觸發。

disconnectedCallback — 每當元素從 DOM 中移除時被觸發。

attributeChangedCallback — 當元素上的屬性被添加、移除、更新或取代時被觸發。

如果需要在元素屬性變化後,觸發 attributeChangedCallback()回調函數,你必須監聽這個屬性。

這可以通過定義observedAttributes() get函數來實現

observedAttributes()函數體內包含一個 return語句,返回一個數組,包含了需要監聽的屬性名稱:

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

constructor(){}
該段代碼處於構造函數的上方。

user-card 元素

在 UserCard 文件夾下創建 UserCard.js:
class UserCard extends HTMLElement {
  constructor() {
    super();

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

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

customElements.define("user-card", UserCard);
customElements.define('user-card', UserCard) 函數調用告知 DOM 我們已經創建了一個新的定製元素叫 user-card

它的行爲被 UserCard 類定義。

現在可以在我們的 HTML 裏使用 user-card 元素了。


創建模板

UserCard.html
<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__ 前綴,避免意外的樣式覆蓋

在較早版本的瀏覽器中,我們不能使用 shadow DOM 來隔離組件 DOM


編寫樣式

UserCard.css
.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;
}
UserCard.html 文件的最前面引入這個 CSS 文件:
<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

constructor 方法是元素被實例化時調用

connectedCallback 方法是每次元素插入 DOM 時被調用。

connectedCallback 方法在執行初始化代碼時是很有用的,比如獲取數據或渲染。

在 UserCard.js 的頂部,定義一個常量 currentDocument。它在被引入的 HTML 腳本中是必要的,允許這些腳本有途徑操作引入模板的 DOM。像下面這樣定義:

const  currentDocument = document.currentScript.ownerDocument;
定義 connectedCallback 方法

把克隆好的模板綁定到 shadow root 上

// 元素插入 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";
}

在任意項目中使用組件

既然組件已經完成,我們就可以把它用在任意項目中了。爲了繼續教程,我們需要創建一個 index.html 文件
<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>

組件示例

image

構建3個組件。

第一個組件是人員列表。

第二個組件將顯示我們從第一個組件中選擇的人的信息。

父組件將協調這些組件,並允許我們獨立開發子組件並將它們連接在一起。


代碼組織

創建一個components包含所有組件的目錄。

每個組件都有自己的目錄,其中包含組件的HTML模板,JS和樣式表。

僅用於創建其他組件且未重用的組件將放置在該組件目錄中

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

PeopleList.html
<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>
PeopleList.js
(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);
})();
在該render方法中,我們需要使用創建人名列表/<li/>。

我們還將CustomEvent爲每個元素創建一個。每當單擊該元素時,其id將在DOM樹中向上傳播事件。


PersonDetail組件

我們創建了PeopleList一個按名稱列出人員的組件。我們還想創建一個組件,當在該組件中單擊人名時,該組件將顯示人員詳細信息

PersonDetail.html

<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>
PersonDetail.css
.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;
}
/components/PeopleController/PersonDetail/PersonDetail.js
(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);
})()
updatePersonDetails(userData)以便在單擊Person組件時可以使用此函數更新此PeopleList組件。我們也可以使用屬性完成此操作

父組件 PeopleController

HTML導入已從標準中刪除,預計將被模塊導入替換

PeopleController.html

<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>
PeopleController.css
#people-list {
  width: 45%;
  display: inline-block;
}
#person-detail {
  width: 45%;
  display: inline-block;
}
PeopleController.js
(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);
})()
調用API來獲取用戶的數據。 這將採用我們之前定義的2個組件,填充PeopleList組件,並將此數據的第一個用戶提供爲PeopleDetail組件的初始數據。

在父組件中監視PersonClicked事件,以便我們可以相應地更新PersonDetail對象。 因此,在上面的文件中創建2個私有函數


使用組件

創建一個名爲index.html的新HTML文件
<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 組件屬性 觀察/更新

HTML中的元素具有屬性; 這些是配置元素或以各種方式調整其行爲以滿足用戶所需條件的其他值。

使用以下屬性創建一個組件UserCard:username,address和is-admin(布爾值告訴我們用戶是否爲admin)。

觀察這些屬性以進行更改並相應地更新組件。


定義屬性
<user-card username="Ayush" address="Indore, India" is-admin></user-card>
使用JavaScript中的DOM API來使用getAttribute(attrName)和setAttribute(attrName,newVal)方法來獲取和設置屬性。
let myUserCard = document.querySelector('user-card')

myUserCard.getAttribute('username') // Ayush

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

觀察屬性更改

自定義元素規範v1定義了一種觀察屬性更改並對這些更改採取操作的簡便方法。 在創建我們的組件時,我們需要定義兩件事:

觀察到的屬性:要在屬性更改時得到通知,必須在初始化元素時定義觀察到的屬性列表,方法是在返回屬性名稱數組的元素類上放置一個靜態的observeAttributes getter。

attributeChangedCallback(attributeName,oldValue,newValue,namespace):在元素上更改,追加,刪除或替換屬性時調用的生命週期方法。 它僅用於觀察屬性。

創建UserCard組件

構建UserCard組件,它將使用屬性進行初始化,並且我們的組件將觀察對其屬性所做的任何更改。

在項目目錄中創建index.html文件。

還可以使用以下文件創建UserCard目錄:UserCard.html,UserCard.css和UserCard.js。

UserCard.js

(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中,我們將使用這些屬性並定義與每個屬性相對應的變量。
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。 所以我們需要定義當這些屬性發生變化時會發生什麼。 重寫函數以包含以下內容:
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>

使用組件

使用2個輸入元素和一個複選框創建index.html文件,併爲所有這些元素定義onchange方法以更新組件的屬性。 一旦屬性更新,更改也將反映在DOM中。
<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>

何時使用屬性

在上一篇文章中,我們爲子組件創建了一個API,以便父組件可以使用此API初始化並與它們交互。在這種情況下,如果我們已經有一些配置,希望直接提供而不使用父/其他函數調用,將無法做到。

使用屬性,我們可以非常輕鬆地提供初始配置。然後可以在構造函數或connectedCallback中提取此配置以初始化組件。

更改屬性以與組件交互可能會有點單調乏味。假設您要將大量json數據傳遞給組件。這樣做需要將json表示爲字符串屬性,並在組件使用時進行解析。

我們有3種方法可以創建交互式Web組件:

僅使用屬性:這是我們在本文中看到的方法。我們使用屬性來初始化組件以及與外部世界進行交互。

僅使用已創建的函數:這​​是我們在本系列的第2部分中看到的方法,我們使用我們爲它們創建的函數初始化並與組件交互。

使用混合方法:應該使用IMO。在這種方法中,我們使用屬性初始化組件,並且對於所有後續交互,只需使用對其API的調用。


Web Components modal 模態彈窗

peek

定義模態組件
modal.js
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);

使用 模態組件
index.html
<!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 組件

效果

WeChat1e523a42cfe99bd2599505b2e1f6edf0

體驗

web component select

web components polyfill 兼容舊版本瀏覽器的支持插件

https://www.webcomponents.org...

源碼

(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);
})()
注意:如果父元素高度太低,需要關閉父元素的 overflow 屬性,否則會遮蓋 下拉列表

使用

<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>

https://github.com/WangShuXia...
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章