There is an article “Web Components” recorded in “The Definitive Guide to JavaScript”. It says in it
The example implements the
component with custom elements, uses the
tag to improve efficiency, and uses the shadow root node to achieve this Encapsulation.
There are three key points here: custom tags, shadow DOM, and slots.
First, let’s introduce the shadow node.
What is shadow DOM
Shadow DOM (Shadow DOM) is a feature in Web component technology that allows developers to create encapsulated components that isolate their style and behavior from other parts of the page. The shadow DOM can be regarded as an independent DOM subtree. It is independent of the main DOM tree and will not be affected by external styles and scripts.
By using the shadow DOM, developers can create custom HTML elements that have their own styles and behaviors, isolated from the rest of the page. This avoids the problems of style conflicts and namespace pollution, and also provides better encapsulation and maintainability.
Shadow DOM is implemented by using the ShadowRoot object, which developers can attach to an ordinary DOM element to create a shadow DOM with an independent DOM tree. Elements in the shadow DOM can be manipulated and controlled through JavaScript, and can also be styled using CSS styles.
In summary, shadow DOM is a technology for creating encapsulated, independent DOM subtrees, which can help developers build more reliable, maintainable and reusable Web components.
For example, the commonly used tag is encapsulated in the shadow DOM, as shown in the following figure:
You can see that there is a #shadow-root under the video tag. Under normal circumstances, it is hidden. If you want to see #shadow-root, you can set the preferences in the browser, as shown in the figure below:
Encapsulate searchBox component
Define a SearchBox class, inherit HTMLElement, and then just reference it in the body.
<body> <search-box></search-box> </body>
class SearchBox extends HTMLElement{<!-- --> constructor() {<!-- --> super(); this.attachShadow({<!-- --> mode:'open'}); this.shadowRoot.append(SearchBox.template.content.cloneNode(true)); this.input = this.shadowRoot.querySelector("#input"); let leftSlot = this.shadowRoot.querySelector('slot[name="left"]'); let rightSlot = this.shadowRoot.querySelector('slot[name="right"]'); this.input.onfocus = () => {<!-- -->this.setAttribute("focused",'');}; this.input.onblur = () => {<!-- -->this.removeAttribute("focused");}; leftSlot.onclick = this.input.onchange = (event) =>{<!-- --> event.stopPropagation(); if (this.disabled) return; this.dispatchEvent(new CustomEvent('search',{<!-- --> detail:this.input.value })); }; rightSlot.onclick = (event) =>{<!-- --> event.stopPropagation(); if (this.disabled) return; let e = new CustomEvent('clear',{<!-- --> cancelable: true}); this.dispatchEvent(e); if (!e.defaultPrevented){<!-- --> this.input.value = ''; } }; } attributeChangedCallback(name,oldValue,newValue){<!-- --> if (name === 'disabled'){<!-- --> this.input.disabled = newValue !== null; }else if (name === 'placeholder'){<!-- --> this.input.placeholder = newValue; }else if (name === 'size'){<!-- --> this.input.size = newValue; }else if (name === 'value'){<!-- --> this.input.value = newValue; } } get placeholder(){<!-- --> return this.getAttribute('placeholder') } get size(){<!-- --> return this.getAttribute('size') } get value(){<!-- --> return this.getAttribute('value') } get disabled(){<!-- --> return this.getAttribute('disabled') } get hidden(){<!-- --> return this.getAttribute('hidden') } set placeholder(val){<!-- --> this.setAttribute('placeholder',val)} set size(val){<!-- --> this.setAttribute('size',val)} set value(text){<!-- --> this.setAttribute('value',text)} set disabled(val){<!-- --> if (val) this.setAttribute('disabled',''); else this.removeAttribute('disabled') } set hidden(val){<!-- --> if (val) this.setAttribute('hidden',''); else this.removeAttribute('hidden') } } SearchBox.observedAttributes = ['disabled','placeholder','value','size']; SearchBox.template = document.createElement('template'); SearchBox.template.innerHTML = ` <style> :host{ display: inline-block; border: solid black 1px; border-radius: 5px; padding: 4px 6px; } :host[disabled]{ opacity: 0.5; } :host[hidden]{ display: none; } :host[focused]{ box-shadow: 0 0 2px 2px #6ae; } input{ border-width: 0; outline: none; font:inherit; background: inherit; } slot{ cursor: default; user-select:none; } slot[name='left']{ /*font-size: 60px;*/ } </style> <div> <slot name="left">\u{1f50d}</slot> <input type="text" id="input" /> <slot name="right">\u{2573}</slot> </div> ` customElements.define('search-box',SearchBox); </script>
Using component slots
During the packaging process, the slot is used
<div> <slot name="left">\u{<!-- -->1f50d}</slot> <input type="text" id="input" /> <slot name="right">\u{<!-- -->2573}</slot> </div>
Use directly on the page
<search-box> <span slot="right">Cancel</span> </search-box>
Think of Vue
When I read the book article here, I had to think of Vue’s knowledge of custom components and slots. Strike while the iron is hot, go to Vue’s components and look at the code. Vue can define components like this.
Vue.component('router-link', {<!-- --> props: {<!-- --> to: String }, render(){<!-- --> let path = this.to; if(this._self.$router.mode === 'hash'){<!-- --> path = '#' + path; } return <a href={<!-- -->path}>{<!-- -->this.$slots.default}</a> } });
Vue slot
I’m looking at the Vue official documentation slot document, which contains a lot of knowledge points.
https://v2.cn.vuejs.org/v2/guide/components-slots.html
In fact, at the beginning, I thought that slots were unique to Vue. It wasn’t until I read the implementation of Web components in “The Definitive Guide to JavaScript” that I realized that the concept of slots already exists in JavaScript.
With curiosity about slots (actually, I always forget about the several slot types of Vue), I want to take a deeper look at how Vue implements slots.
Slot usage
The first is to define common slot types in subcomponents, such as the following types: default slots (anonymous slots), named slots, and scope slots.
<template id="son"> <div> <div>I am the head</div> <slot>Default</slot> <slot name="named">Named</slot> <slot name="scope" item="Hello" ></slot> <button @click="GetSlots">Get slots</button> </div> </template>
Then use it in parent component.
<template id="father"> <div> <son> <div>Default slot</div> <template #named>Named slot</template> <template #scope="{item}"> <div>{<!-- -->{item}}, scope slot</div> </template> </son> </div> </template>
Define a button GetSlotsGetSlots(){ console.log(this.$scopedSlots);}
in the subcomponent and take a look at how Vue handles slots when clicked. As you can see in the figure below, an object is returned. The key of the object is the type of the slot, and the value is a function, that is, the node.
Since .$scopedSlots
directly returns the slot node, you can use the render
function to directly execute it in the subcomponent to see the effect of the execution, that is to say, the slot , in fact, it is processed in the form of functions, so the processing of functions can be very flexible.
render(createElement){<!-- --> return createElement('div',null,[ ...this.$scopedSlots.default(), // Anonymous slots ...this.$scopedSlots.named(), // Named slots ...this.$scopedSlots.scope({<!-- -->item:'Hello'}) // Scope slots ] ) }
Vue2 slot complete sample code
<template id="father"> <div> <son> <div>Default slot</div> <template #named>Named slot</template> <template #scope="{item}"> <div>{<!-- -->{item}}, scope slot</div> </template> </son> </div> </template>
Vue3 sample code
<template> <Son> <div>Default slot</div> <template #name>Named slot</template> <template #scope="{item}"> <div>{<!-- -->{<!-- -->item}}, scope slot</div> </template> </Son> </template> <script setup> import Son from './slots.js' </script>
slots.js
file, you can see the same effect as vue2.
import {<!-- --> createElementVNode } from 'vue' export default {<!-- --> setup(props,{<!-- -->slots}){<!-- --> const slotDefault = slots.default() const slotName = slots.name() const scopeNamed = slots.scope({<!-- -->item:'Hello'}) return ()=>{<!-- --> return createElementVNode('div',null,[ ...slotDefault, ...slotName, ...scopeNamed ]) } } }
From Web components to shadow DOM, to Vue custom components, and finally to the implementation of Vue slots, this exploration process can not only learn new knowledge, but also consolidate the knowledge of Vue slots. Really interesting.