Objective: To implement custom components with native JS, Vue3 bidirectional binding

Pre-school knowledge reserve:

1. Custom Elements

No more nonsense, first on the code:

//html:
<user-card data-open="true"></user-card>

//javascript:
class Learn extends HTMLElement{
    constructor(props) {
        super(props);
        console.log(this.dataset);
        this.innerHTML = 'This is my custom element.';
        this.style.border = '1px solid #899';
        this.style.borderRadius = '3px';
        this.style.padding = '4px'; }}window.customElements.define('user-card',Learn);
Copy the code

Effect:Go throughwindow.customElementsMethod can be used to create custom elements insidedefineMethod is used to specify the name of the custom element and the corresponding class of the custom element.

One detail here is that custom elements must be separated by a line, otherwise they will not work.

At this point, all the contents of the element can be defined in this class, which is similar to the components in Vue. Once we have this foundation, we can extend the components to implement them.

2. Proxy

This guy probably knows that the core of Vue3’s data response is object.defineProperty; Very powerful, very easy to use, start with a simple code:

let obj = {
    a:2938.b:'siduhis'.item:'name'
}

obj = new Proxy(obj,{
    set(target, p, value, receiver) {
        console.log('Listen in',p,'modified from the original:',target[p],'changed to:',value); }});document.onclick = () = >{
    obj.item = 'newValue';
}
Copy the code

Effect:There are many things that can be said about this, such as the set method when modifying a value, the GET method when reading a value, etc. For details, please check the official website documentation.

Prerequisite knowledge 3, Event broker

First, I use the event broker to handle the events in the component, mainly because it is easy to write, easy to expand, first look at the simplest version of the event broker:

//html
<ul class="list">
    <li class="item" data-first="true">This is the first one</li>
    <li class="item">2222</li>
    <li class="item">three</li>
    <li class="item" data-open="true">Open the</li>
    <li class="item">This is the last one</li>
</ul>

//javascript
let list = document.querySelector('.list');

list.onclick = function(ev){
    let target = ev.target;
    console.log('Clicked'+target.innerHTML);
}
Copy the code

Effect:This is the simplest version, inulThe body is bound to click events, using the event bubble principle, click any oneliBoth trigger their parentulClick the event to passulEvents can also be found in reverse order to be precisely clickedliElement, and put the correspondingliPrint out the content, how about, very simple ~

You may have noticed that in the code above, there are twoliHas a data custom property on the body, which will be useful later

Here, we can determine the different attributes of li and execute different functions:

let eventfn = function(ev){
    let target = ev.target;
    let dataset = target.dataset;
    for(b in dataset){
        if(eventfn[b]){
            eventfn[b]({obj:target,parent:this});
        }
    }
}
eventfn.first = function(){
    console.log('Click on the first one and pass in some parameters'.arguments);
}
eventfn.open = function(){
    console.log('Clicked open');
}

list.onclick = eventfn;
Copy the code

In this case, I’m going to get the data property of the element being clicked, and see if it has an event function, and if it does, I’m going to execute it and pass in some parameters that I might need later, so that’s an extension point. At this point, our event handling is pretty much in shape

The first step is to create component content

Analysis of ideas:

  • 1, the content is best to write directly on the page, and then need to fill in the data used{{}}Wrapped up
  • 2. The template tag can be used to wrap templates and not be displayed on the page
  • Copy the contents of the template as the contents of the component, and parse the {{}} inside.
  • 4. It also needs to parse various instructions inside, such asdata-openThis represents an open event

Here’s the code on the rendering:

<template id="userCardTemplate">
    <style>
        .image {
            width: 100px;
        }

        .container {
            background: #eee;
            border-radius: 10px;
            width: 500px;
            padding: 20px;
        }
    </style>
    <img src="img/bg_03.png" class="image">
    <div class="container">
        <p class="name" data-open="true">{{name}}</p>
        <p class="email">{{email}}</p>
        <input type="text" v-model="message">
        <span>{{message}}</span>
        <button class="button">Follow</button>
    </div>
</template>
Copy the code

Second, start writing component classes

The template id is used to retrieve the contents of the component, and then it is dumped directly into the component and the data is defined:

class UserCard extends HTMLElement {
    constructor() {
        super(a);var templateElem = document.getElementById('userCardTemplate');
        var content = templateElem.content.cloneNode(true);
        this.appendChild(content);
        this._data = {
            name:'Username'.email:'[email protected]'.message:'two-way'}}}window.customElements.define('user-card',UserCard);
Copy the code

When you drop the user-card element onto the page, you get something like this:

The third step, parsing

Then the next thing to do is to parse element child elements in it, and see if it contains the {{}} such symbols, take out and put in the middle of the content, and to compare the data, the data when the corresponding, then fill the data into this place is ok, it is simple, it still has the certain difficulty, This will use regular matching, so I wrote this method in class:

compileNode(el){
    let child = el.childNodes;// Get all the child elements
    [...child].forEach((node) = >{// Use the expansion operator to convert directly to an array and then forEach
        if(node.nodeType === 3) {// Determine that it is a text node, so direct re service
            let text = node.textContent;
            let reg = /\{\{\s*([^\s\{\}]+)\s*\}\}/g;
            // match a string of text preceded by two {{and followed by two}}
            if(reg.test(text)){// If such a string can be found
                let $1 = RegExp. $1;// Then take out the contents, such as' name '
                this._data[$1] && (node.textContent = text.replace(reg,this._data[$1]));// Check if there is a name in the data. If there is a name in the data, fill in the value corresponding to the name in the current position.}; }})}Copy the code

I’m going to throw this method intoconstructorRun it inside and get the result:

Step 4, implement data view binding

At this point, we’re just going to render the data onto the page, and what if the data changes again, and we haven’t found a notification mechanism to let the view change? This is where the Proxy comes in. There is also a need for custom events, so let’s look at the Proxy part, which is actually very simple, just add a method:

observe(){
    let _this = this;
    this._data = new Proxy(this._data,{// Listen for data
        set(obj, prop, value){// The set method is triggered when data changes
            // The event notification mechanism allows you to customize the event notification view when changes occur
            let event = new CustomEvent(prop,{
                detail: value// Note that I passed in the detail, so that when you update the view you can get the new data directly
            });
            _this.dispatchEvent(event);
            return Reflect.set(... arguments);// This is to make sure the modification is successful}}); }Copy the code

Event notification is available, but you need to listen for events in the parse function so that the view changes in time:

compileNode(el){
    let child = el.childNodes;// Get all the child elements
    [...child].forEach((node) = >{// Use the expansion operator to convert directly to an array and then forEach
        if(node.nodeType === 3) {// Determine that it is a text node, so direct re service
            let text = node.textContent;
            let reg = /\{\{\s*([^\s\{\}]+)\s*\}\}/g;
            // match a string of text preceded by two {{and followed by two}}
            if(reg.test(text)){// If such a string can be found
                let $1 = RegExp. $1;// Then take out the contents, such as' name '
                this._data[$1] && (node.textContent = text.replace(reg,this._data[$1]));// Check if there is a name in the data. If there is a name in the data, fill in the value corresponding to the name in the current position.

                // Added event listeners to listen for every matched data and update the view again
                // Note that e.dial is a custom event from observe
                this.addEventListener($1.(e) = >{ node.textContent = text.replace(reg,e.detail) }) }; }})}Copy the code

At this point, we can implement that when we modify the data, the view also changes:

let card = document.querySelector('user-card');
document.onclick = function(){
    console.log('Clicked');
    card._data.name = 'New username';
}
Copy the code

Step 5, implement bidirectional binding

As you can see, I wrote an input field in template with a property v-model=”message” so you can guess what I’m going to do. How do I do it? It’s pretty simple: when parsing the content, look at the input element and see if it has a V-Model attribute. If so, listen for its input event and modify the data.

Modify the parse function again:

compileNode(el){
    let child = el.childNodes;
    [...child].forEach((node) = >{
        if(node.nodeType === 3) {let text = node.textContent;
            let reg = /\{\{\s*([^\s\{\}]+)\s*\}\}/g;
            if(reg.test(text)){
                let $1 = RegExp. $1;this._data[$1] && (node.textContent = text.replace(reg,this._data[$1]));

                this.addEventListener($1.(e) = >{
                    node.textContent = text.replace(reg,e.detail)
                })
            };
        }else if(node.nodeType === 1) {let attrs = node.attributes;
            if(attrs.hasOwnProperty('v-model')) {// Check whether this attribute exists
                let keyname = attrs['v-model'].nodeValue;
                node.value = this._data[keyname];
                node.addEventListener('input'.e= >{// If yes, listen for events and modify data
                    this._data[keyname] = node.value;// Modify the data
                });
            }

            if(node.childNodes.length > 0) {this.compileNode(node);// Implement deep parsing recursively}}})}Copy the code

Step 6: Handle the event

Let’s start with the complete component code:

class UserCard extends HTMLElement {
    constructor() {
        super(a);var templateElem = document.getElementById('userCardTemplate');
        var content = templateElem.content.cloneNode(true);
        this.appendChild(content);
        this._data = {// Define data
            name:'Username'.email:'[email protected]'.message:'two-way'
        }
        this.compileNode(this);// Parse elements
        this.observe();// Listen for data
        this.bindEvent();// Handle events
    }
    bindEvent(){
        this.event = new popEvent({
            obj:this.popup:true
        });
    }
    observe(){
        let _this = this;
        this._data = new Proxy(this._data,{
            set(obj, prop, value){
                let event = new CustomEvent(prop,{
                    detail: value
                });
                _this.dispatchEvent(event);
                return Reflect.set(...arguments);
            }
        });
    }
    compileNode(el){
        let child = el.childNodes;
        [...child].forEach((node) = >{
            if(node.nodeType === 3) {let text = node.textContent;
                let reg = /\{\{\s*([^\s\{\}]+)\s*\}\}/g;
                if(reg.test(text)){
                    let $1 = RegExp. $1;this._data[$1] && (node.textContent = text.replace(reg,this._data[$1]));

                    this.addEventListener($1.(e) = >{
                        node.textContent = text.replace(reg,e.detail)
                    })
                };
            }else if(node.nodeType === 1) {let attrs = node.attributes;
                if(attrs.hasOwnProperty('v-model')) {let keyname = attrs['v-model'].nodeValue;
                    node.value = this._data[keyname];
                    node.addEventListener('input'.e= >{
                        this._data[keyname] = node.value;
                    });
                }

                if(node.childNodes.length > 0) {this.compileNode(node); }}})}open(){
        console.log('Triggered the open method'); }}Copy the code

BindEvent = bindEvent = bindEvent = bindEvent = bindEvent = bindEvent = bindEvent = bindEvent = bindEvent

class popEvent{
    constructor(option){
        /* * receives four arguments: * 1, the object's this * 2, the element to listen on * 3, the event to listen on, the default to listen on the click event * 4, whether to bubble * */
        this.eventObj = option.obj;
        this.target = option.target || this.eventObj;
        this.eventType = option.eventType || 'click';
        this.popup = option.popup || false;
        this.bindEvent();
    }
    bindEvent(){
        let _this = this;
        _this.target.addEventListener(_this.eventType,function(ev){
            let target = ev.target;
            let dataset,parent,num,b;
            popup(target);
            function popup(obj){
                if(obj === document) {return false; } dataset = obj.dataset; num =Object.keys(dataset).length;
                parent = obj.parentNode;
                if(num<1){
                    _this.popup && popup(parent);
                    num = 0;
                }else{
                    for(b in dataset){
                        if(_this.eventObj.__proto__[b]){
                            _this.eventObj.__proto__[b].call(_this.eventObj,{obj:obj,ev:ev,target:dataset[b],data:_this.eventObj}); } } _this.popup && popup(parent); }})}}Copy the code

The other one is the open method, so what does this method do?

{{name}}

That’s right, implement the event directive

When clicked with custom properties:data-openYou can trigger the open method in the component and get any parameters you need from the open method. :When the user name is clicked, the open method is triggered.

Complete code is attached, pay attention to the last details of the code oh ~

<! DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <style> </style> </head> <body> <template id="userCardTemplate"> <style> .image { width: 100px; } .container { background: #eee; border-radius: 10px; width: 500px; padding: 20px; } </style> <img src="img/bg_03.png" class="image"> <div class="container"> <p class="name" data-open="true">{{name}}</p>  <p class="email">{{email}}</p> <input type="text" v-model="message"> <span>{{message}}</span> <button class="button">Follow</button> </div> </template> <user-card data-click="123"></user-card> <script type="module"> class PopEvent {constructor(option){/* * Receive four arguments: * */ eventObj = option.obj; * */ eventObj = option.obj; this.target = option.target || this.eventObj; this.eventType = option.eventType || 'click'; this.popup = option.popup || false; this.bindEvent(); } bindEvent(){ let _this = this; _this.target.addEventListener(_this.eventType,function(ev){ let target = ev.target; let dataset,parent,num,b; popup(target); function popup(obj){ if(obj === document){ return false; } dataset = obj.dataset; num = Object.keys(dataset).length; parent = obj.parentNode; if(num<1){ _this.popup && popup(parent); num = 0; }else{ for(b in dataset){ if(_this.eventObj.__proto__[b]){ _this.eventObj.__proto__[b].call(_this.eventObj,{obj:obj,ev:ev,target:dataset[b],data:_this.eventObj}); } } _this.popup && popup(parent); } } }) } } class UserCard extends HTMLElement { constructor() { super(); var templateElem = document.getElementById('userCardTemplate'); var content = templateElem.content.cloneNode(true); this.appendChild(content); This. _data = {name:' username ', email:'[email protected]', message:' bib '} this.pilenode (this); this._data = {name:' username ', email:'[email protected]', message:' biB '} this.pilenode (this); this.observe(this._data); this.bindEvent(); this.addevent = this.__proto__; } bindEvent(){ this.event = new popEvent({ obj:this, popup:true }); } observe(){ let _this = this; this._data = new Proxy(this._data,{ set(obj, prop, value){ let event = new CustomEvent(prop,{ detail: value }); _this.dispatchEvent(event); return Reflect.set(... arguments); }}); } compileNode(el){ let child = el.childNodes; [...child].forEach((node)=>{ if(node.nodeType === 3){ let text = node.textContent; let reg = /\{\{\s*([^\s\{\}]+)\s*\}\}/g; if(reg.test(text)){ let $1 = RegExp.$1; this._data[$1] && (node.textContent = text.replace(reg,this._data[$1])); this.addEventListener($1,(e)=>{ node.textContent = text.replace(reg,e.detail) }) }; }else if(node.nodeType === 1){ let attrs = node.attributes; if(attrs.hasOwnProperty('v-model')){ let keyname = attrs['v-model'].nodeValue; node.value = this._data[keyname]; node.addEventListener('input',e=>{ this._data[keyname] = node.value; }); } if(node.childNodes.length > 0){ this.compileNode(node); }}})} open(){console.log(' triggered the open method '); } } window.customElements.define('user-card',UserCard); let card = document.querySelector('user-card'); Card.addevent ['click'] = function(){console.log(' trigger click event! '); } </script> </body> </html>Copy the code

The last

Relax