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">×</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
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>