Web Components Series Tutorials

Posted by kiran_ravi on Wed, 21 Aug 2019 04:02:57 +0200

Web Components Start

Build your own custom components without adding any dependencies

HTML tags with styles, interactive features, and elegant organization in their files

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

Web Components are a different set of technologies that allow you to create reusable custom elements whose functions are encapsulated outside your code and use them in your web applications.

Example

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 s are API s for a range of web platforms that allow you to create new customizable, reusable, and encapsulated HTML Tags

Customized components are built based on the Web Component standard and can be used in browsers today or in conjunction with any JavaScript library and framework that interacts with HTML.

It gives you the ability to create reusable components using pure JS/HTML/CSS only.If HTML doesn't meet your needs, we can create a Web Component that meets your needs.

For example, your user data is related to an ID, and you want a component that can fill in the user ID and get the corresponding data.HTML might look like this:

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

Four core concepts of Web Component

The HTML and DOM standards define four new standards to help define Web Component s.These criteria are as follows:

Custom Elements:

web developers can create new HTML tags from custom elements, enhance existing HTML tags, or redevelop components that other developers have already completed.This API is the cornerstone of the Web Component.

HTML Templates:

HTML templates define new elements that describe a way to use DOM-based standards for client-side templates.Templates allow you to declare tag fragments that can be parsed into HTML.These fragments are not used when the page starts loading and are instantiated at run time.

Shadow DOM:

Shadow DOM is designed as a tool for building component-based applications.It solves common problems with web development, such as allowing you to isolate the DOM and scope of components, simplify CSS, and so on.

HTML References (HTML Imports):

HTML Templates allow you to create new templates. Similarly, HTML imports allow you to import templates from different files.With separate HTML file management components, you can help better organize your code.

Naming of components

The name of the custom element must contain a dash.So <my-tabs>and <my-amazing-website>are legal names, whereas <foo>and <foo_bar>are not.

When adding new tags to HTML, you need to ensure forward compatibility and that you cannot register the same tag repeatedly.

Custom element tags cannot be self-closing because HTML allows only a few elements to be self-closing.Closed label formats like <app-drawer></app-drawer> are required for writing images.

Expanding Components

You can use inheritance when creating components.

For example, if you want to create a UserCard for two different users,

You can create a basic UserCard and then expand it to two specific user cards.

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

Component elements are instances of classes

Component elements are instances of classes in which common methods can be defined.

These common methods can be used to allow other custom components/scripts to interact with these components, rather than just changing their properties.

Define Private Methods

Private methods can be defined in several ways.I prefer to use (execute functions immediately) because they are easy to write and understand.

(function() {})();

Freeze Class

To prevent new attributes from being added, you need to freeze your class.

This prevents existing attributes of the class from being removed, enumerable, configurable, or writable attributes of existing attributes from being changed, as well as prototypes from being modified.

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

Freezing classes prevents you from adding patches at runtime and makes your code difficult to debug.

Server Rendering Project Notes

Given the inconsistent configuration of the server's root path

import can use absolute paths

import js cannot be imported again, path error will occur

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

Define custom elements

Declare a class that defines how elements behave.This class needs to inherit the HTMLElement class

Lifecycle approach for custom elements

ConneedCallback - Triggered whenever an element is inserted into the DOM.

disconnectedCallback - Triggered whenever an element is removed from the DOM.

attributeChangedCallback - Triggered when an attribute on an element is added, removed, updated, or superseded.

If you need to trigger the attributeChangedCallback() callback function after an element attribute changes, you must listen for this attribute.

This can be done by defining the observedAttributes() get function

The observedAttributes() function body contains a return statement that returns an array containing the name of the attribute to listen on:

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

constructor(){}

The code is above the constructor.

user-card element

Create UserCard.js under the UserCard folder:

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

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

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

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

The customElements.define('user-card', UserCard) function call tells DOM that we have created a new custom element called user-card

Its behavior is defined by the UserCard class.

Now you can use the user-card element in our HTML.

Create Template

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>

Prefix the class name with a card_u to avoid accidental style overrides

In earlier versions of browsers, we could not use shadow DOM to isolate component DOM

Writing Styles

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

This CSS file was introduced first in the UserCard.html file:

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

Functions of components

connectedCallback

The constructor method is called when an element is instantiated

The connectedCallback method is called every time an element is inserted into the DOM.

The connectedCallback method is useful when executing initialization code, such as getting data or rendering.

At the top of UserCard.js, define a constant currentDocument.It is necessary in the introduced HTML scripts that allow them to manipulate the DOM introduced into the template.Define as follows:

const  currentDocument = document.currentScript.ownerDocument;

Define the connectedCallback method

Bind the cloned template to the shadow root

// Called when an element inserts a DOM
connectedCallback() {
  const shadowRoot = this.attachShadow({ mode: "open" });
  // Select the template and clone it.Finally, the cloned node is added to the root node of the shadowDOM.

  // The current document needs to be defined to gain DOM privileges to introduce HTML.
  const template = currentDocument.querySelector("#user-card-template");

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

  // Select user-id attribute from element
  // Note that we need to specify cards like this:
  // <user-card user-id="1"></user-card>

  const userId = this.getAttribute("user-id");
  // Get data based on user ID and render with returned data

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

Rendering User Data

render(userData) {
  // Fill different areas of the card with API s that operate the DOM
  // All elements of the component exist in the shadow dom, so we use this.shadowRoot property to get the DOM
  // DOM can only be found in this subtree
  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";
}

Use components in any project

Now that the component is complete, we can use it in any project.To continue the tutorial, we need to create an index.html file

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

Component example

Build three components.

The first component is the people list.

The second component will display information about the person we selected from the first component.

Parent components will coordinate these components and allow us to develop subcomponents independently and connect them together.

Code organization

Create a directory where components contain all components.

Each component has its own directory, which contains the HTML templates, JS, and stylesheets for the components.

Components that are only used to create other components and are not reused will be placed in that component directory

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

People List Component 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 = [];

      //Use defineProperty to define the prop, or component, of this object.
      //Whenever a list is set, render is called.This way when the parent component sets some data
      //On child objects, we can automatically update child objects.
      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);
})();

In this render method, we need to use the Create Name List /<li/>.

We will also create a CustomEvent for each element.Whenever the element is clicked, its id propagates the event up the DOM tree.

PersonDetail Component

We created a PeopleList component that lists people by name.We also want to create a component that displays people details when a person name is clicked in the component

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);
    }

    // Create an API function so that other components can use it to populate this component
    // Creating an API function so that other components can use this to populate this component
    updatePersonDetails(userData) {
      this.render(userData);
    }

    ///Fill-in card function (can be set private)
    // 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) so that this PeopleList component can be updated when the Person component is clicked.We can also use attributes to do this

Parent Component PeopleController

HTML import has been removed from the standard and is expected to be replaced by module import

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);
})()

Call the API to get the user's data.This will take the two components we defined earlier, populate the PeopleList component, and provide the first user of this data as the initial data for the PeopleDetail component.

Monitor the PersonClicked event in the parent component so that we can update the PersonDetail object accordingly.Therefore, create two private functions in the file above

Using Components

Create a new HTML file named index.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 Component Attribute Observation/Update

Elements in HTML have attributes; these are configuration elements or other values that adjust their behavior in various ways to meet the user's requirements.

Create a component UserCard: username, address, and is-admin using the following properties (booleans tell us if the user is admin).

Observe these properties to make changes and update the components accordingly.

Define Properties

<user-card username="Ayush" address="Indore, India" is-admin></user-card>

Use the DOM API in JavaScript to get and set properties using the getAttribute (attrName) and setAttribute (attrName, newVal) methods.

let myUserCard = document.querySelector('user-card')

myUserCard.getAttribute('username') // Ayush

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

Observing property changes

The custom element specification v1 defines an easy way to observe and take action on property changes.When creating our components, we need to define two things:

Observed Attributes: To be notified when an attribute changes, the list of observed attributes must be defined when the element is initialized by placing a static observeAttributes getter on the element class that returns an array of attribute names.

attributeChangedCallback (attributeName, oldValue, newValue, namespace): Lifecycle method invoked when an attribute is changed, appended, deleted, or replaced on an element.It is only used to observe attributes.

Create UserCard Component

Build the UserCard component, which will initialize using attributes, and our component will watch for any changes to its attributes.

Create an index.html file in the project directory.

You can also create a UserCard directory using the following files: UserCard.html, UserCard.css, and 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);
})();

Initialize using attributes

When you create a component, we provide it with some initial values that will be used to initialize the component.

<user-card username="Ayush" address="Indore, India" is-admin="true"></user-card>

In the connectedCallback, we'll use these properties and define the variables that correspond to each one.

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

Observing property changes

attributeChangedCallback is called when an observed attribute is changed.So we need to define what happens when these attributes change.Rewrite the function to include the following:

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';
  }
}

Create Components

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

Using Components

Create an index.html file with two input elements and a check box, and define the onchange method for all these elements to update the properties of the component.Once the properties are updated, the changes will also be reflected in the 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>

When to use attributes

In the previous article, we created an API for child components so that parent components can use it to initialize and interact with them.In this case, if we already have some configuration that we want to provide directly without using parent/other function calls, we will not be able to do so.

Using attributes, we can easily provide the initial configuration.You can then extract this configuration in the constructor or connectedCallback to initialize the component.

Changing attributes to interact with components can be a bit tedious.Suppose you want to pass a large amount of json data to the component.This requires json to be represented as a string property and parsed when the component is used.

There are three ways to create interactive Web components:

Use attributes only: this is the way we see it in this article.We use properties to initialize components and interact with the outside world.

Use only created functions: This is the method we saw in the second part of this series, where we initialize and interact with components using the functions we created for them.

Use a hybrid approach: IMO should be used.In this approach, we use attributes to initialize components, and for all subsequent interactions, we only need to use calls to their API s.

Web Components modal modal pop-up window

Define modal components

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);
Using modal components

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);
  })()
  

Using web component to build a generic html-independent single-file select component

Effect

experience

web component select

Support plug-in for web components polyfill compatible with older browsers

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

Source code

(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);
})()

Note: If the parent element height is too low, the overflow property of the parent element needs to be turned off, otherwise the drop-down list will be obscured

Use

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

<midoci-select></midoci-select>

<script>
    const list = [
        {name: 'Full Platform', value: 1},
        {name: 'East Voucher', value: 2},
        {name: 'Beijing Voucher', 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('Selected value:', event.detail.value)
        console.log('Selected name:', event.detail.name)
      })
    }
</script>

https://github.com/WangShuXia...

Topics: Front-end Attribute JSON github Javascript