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.customElements
Method can be used to create custom elements insidedefine
Method 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, inul
The body is bound to click events, using the event bubble principle, click any oneli
Both trigger their parentul
Click the event to passul
Events can also be found in reverse order to be precisely clickedli
Element, and put the correspondingli
Print out the content, how about, very simple ~
You may have noticed that in the code above, there are twoli
Has 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 as
data-open
This 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 intoconstructor
Run 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-open
You 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