Web Components Learning (3) - In-depth understanding of custom elements

attributeand property

Before introducing the life cycle of the Web Components update phase, we need to talk about the difference betweenattribute and .property

The translation results of both are **"attribute"**, and the two usually have a mapping relationship in the application, causing many people to think that they are the same thing, but in fact the two are very different.

For ease of distinction, hereinafter will attributebe referred to as "properties" and will propertybe referred to as "attributes".

attribute It is the attribute written on the HTML tag, such as id and class:

<h2 id="a" class="b"></h2>

propertyIt is the field on the JavaScript object, such as the following properties on the DOM object obtained through the JS object:

const dom = document.querySelector('h2')
console.log(dom.id)
console.log(dom)

The reason why it is easy to treat the two as one thing is because the result console.log(dom) of the dom is obtained by JS, it should be a JS object, but the browser will display it as an HTML element:
insert image description here
if you modify its id attribute:

const dom = document.querySelector('h2')
console.log(dom.id)
console.log(dom)
dom.id = 'A'

The id of the HTML element displayed on the console will also be modified synchronously, but in fact the id attribute modified by JS is not the id attribute of the HTML element, but their names are all called id, and they have a two-way binding function similar to Vue. So we have created an illusion of "the same thing as the elementdom.id ".h2 id

In fact, it is not the case. First, let the browser display the dom in the form of JS:

console.dir(dom)
// 或者
console.log('%O', dom) // 注意是 大写的英文O,不是数字0

insert image description here

This is what the dom really looks like, because there are too many attributes displayed in the form of JS, so the browser helps us display them as HTML tags, so that we can see the key points.

So just remember, attribute the attributes written on the HTML tags property are the fields on the JavaScript objects.

The attributes in them are not all the same on both sides like id, for example attribute the is class in the middle .propertyclassName

And not all of their attributes idhave two-way binding characteristics like . Only the built-in attributes in DOM have two-way binding characteristics. If you add a custom attribute to DOM, it will not be mapped to the attribute:

dom.vue = 'VUE'
console.log(dom)
// <h2 id="a" class="b"></h2>

It is also possible to add a customattribute:

<h2 id="a" class="b" react="React"></h2>

property will also not map on:

console.log(dom.react) // undefined

The attribute obtained directly through the JS object method is a property. If you want to obtain the attribute feature, you can use the special API - getAttribute:

console.log(dom.getAttribute('react')) // React

Similarly, if you want to set the characteristics of HTML tags, you can pass setAttribute:

dom.setAttribute('vue', 'Vue')
console.log(dom.vue) // undefined

However, since vue itself is not a built-in property of DOM, it will not be mapped to property. If necessary, it can be set manuallydom.vue = 'Vue'。

We usually modify attributes in HTML tags or update data by modifying properties in JS objects. And in order to make components more convenient to use, we need to imitate the two-way binding function of DOM's attribute and property.

And the lifecycle function of Web Components attributeChangedCallbackis the callback function after the feature attribute changes .

The difference between Web Components and MVVM framework update phase

Vue is able to update components automatically, while Web Components are updated manually.

The life cycle corresponding to the update phase of Vue is beforeUpdateand updated, and there is no corresponding life cycle function in Web Components.

In fact, it is not without, but there is no life cycle function directly in the update process, but there is a life cycle function that can do this indirectly, that is, attributeChangedCallbacktranslated as "the callback function when the property changes".

So why isn't it equivalent to Vue updated's ?

First think about why components are updated in Vue and React, isn't it because the data has changed? The main slogan of these MVVM frameworks is data-driven views .

Without considering the boundary condition of forceUpdatesuch forced update, it can be considered that: usually, the update phase can be almost equal to the data change phase.

Similarly for Web Components, if you want to change the data of a DOM, you usually change its attributes and properties, attributeChangedCallbackwhich is the callback function after attribute the change .

It is different from Vue updated. Although there are similarities, for example, they are all lifecycle functions that run after data changes, but the difference is that Vue will automatically update the view after the data is updated, while Web Components will not .

When we write code in Vue, we don’t need to update the relational view at all, we only need to change the data, when to update the view and how to update it, Vue has already encapsulated it.

But after all, Web Components is only responsible for encapsulating components that can run natively in browsers, which is a relatively low-level technology. If we want to do it like Vue, we need to encapsulate it ourselves.

The content of the package is that attributeChangedCallback, unlike Vue, when a feature changes, you can choose not to update the view, or you can choose to update the view attributeChangedCallbackin .

1. Features

attributeChangedCallbackIt will be triggered after the feature changes, but to monitor the change of the feature, you also need to pass the observedAttributes()defined get function, and the function body returns what you want to monitor attribute. This is for performance considerations and reduces unnecessary monitoring.

Example:

<life-cycle color="pink">Hello World</life-cycle>

customElements.define('life-cycle', class extends HTMLElement {
    
    
  // 相当于 Vue 的 data
  static get observedAttributes () {
    
    
    return ['color']
  }
  // 或者
  // static observedAttributes = ['color']

  attributeChangedCallback(name, oldValue, newValue) {
    
    
    // 相当于 Vue 的 watch
    console.log('attributeChanged')
    if (oldValue === newValue) return
    console.log(name, newValue)
    if (name === 'color') {
    
    
      this.style.color = newValue
    }
  }
})

const dom = document.querySelector('life-cycle')
setTimeout(() => {
    
    
  dom.setAttribute('color', 'blue')
}, 1000)

Although Vue does not have a attributeChangedCallbackvery similar life cycle function, it has a function very similar to it, that is watch, for example:

data() {
    
    
  return {
    
    
    color: 'pink'
  }
},
watch: {
    
    
  color(newVaue, oldValue) {
    
    
    this.$refs.dom.style.color = newValue
  }
}

One detail is attributeChangedCallbackthat oldValulethe parameter in Web Components is placed in newValuefront of the parameter, while the Vue watch is the opposite. This is because the bottom layer newValue === oldValueof scene, the component will not make any changes, and the watch callback will not be triggered. So usually in Vue, you don't need to care about whether the values ​​ofnewValue and are equal, so you rarely write the formal parameters of .oldValue oldValue

But Web Components is different, newValue === oldValue even in the case of , it will trigger attributeChangedCallback:

setInterval(() => {
    
    
  dom.setAttribute('color', 'blue')
}, 1000)

2. Attributes

The same is for attribute update, Web Components only has attributeChangedCallback and no propertyChangedCallback, because property is an attribute on a JS object, you can directly use getter/setter to monitor changes in object attributes.

customElements.define(
  'life-cycle',
  class extends HTMLElement {
    
    
    get color() {
    
    
      return this.getAttribute('color')
    }
    set color(value) {
    
    
      this.setAttribute('color', value)
    }

    // 相当于 Vue 的 data
    static get observedAttributes() {
    
    
      return ['color']
    }
    attributeChangedCallback(name, oldValue, newValue) {
    
    
      // 相当于 Vue 的 watch
      console.log('attributeChanged')
      if (oldValue === newValue) return
      console.log(name, newValue)
      if (name === 'color') {
    
    
        this.style.color = newValue
      }
    }
  }
)
const dom = document.querySelector('life-cycle')
setTimeout(() => {
    
    
  dom.setAttribute('color', 'blue')
  console.log(dom.color)
}, 1000)
console.log(dom.color)

This realizes the monitoring and setting of the property attribute, and attributerealizes property the two-way binding of and .

3. Inheritance

Why do custom elements inherit from HTMLElement?
Before I said that the class that defines the behavior of a custom element must inherit from HTMLElement. Only by inheriting it can I use the attributes on the element, such as onclick, styleetc., but in fact, it is also possible to inherit its subclasses.

The DOM obtained with JS is all Elements, and they are all instances Element of :

<div id="div"></div>
<script>
  const div = document.getElementById('div')
  console.log(div instanceof Element) // true
</script>

So why is it HTMLElement not Element, and the prompt given by the editor is also HTMLElement:

insert image description here

The reason is that Element is the parent class of all elements, which provides basic properties and methods of elements, such as id, onclick, onmouseover, onkeydown, and onkeyupso on.

However, elements are divided into two types, SVG elements and HTML elements, which provide different attributes and methods, so they are divided into two more specific classes to extend Element: SVGElement and HTMLElement.

Reference: Element | MDN

console.log(Object.getPrototypeOf(HTMLElement) === Element) // true
console.log(Object.getPrototypeOf(SVGElement) === Element) // true

The attributes and methods of different HTML elements are also different. For example, the placeholder of input does not exist on div element.

So HTMLElementand SVGElementcan be further subdivided, for example, HTMLElement there are HTMLDivElement, HTMLInputElement, and HTMLAnchorElement so on.

You can check which class an element belongs to by getting the constructor of the DOM object:

console.log(document.createElement('button').constructor)
// ƒ HTMLButtonElement() { [native code] }

Note document.createElement that is a method for creating HTML elements and cannot be used to create SVG elements:

console.log(document.createElement('svg').constructor)
// ƒ HTMLUnknownElement() { [native code] }

Methods for creating SVG elements:

// NS:namespace 命名空间
// 第一个参数是命名空间,SVG 元素的命名空间是 http://www.w3.org/2000/svg
console.log(document.createElementNS('http://www.w3.org/2000/svg', 'svg').constructor)
// ƒ SVGSVGElement() { [native code] }

console.log(document.createElementNS('http://www.w3.org/2000/svg', 'circle').constructor)
// ƒ SVGCircleElement() { [native code] }

Inheriting built-in elements
Because HTMLElement has most of the properties and methods of the HTML elements we use every day, it is convenient for us to use this.xxx to call the properties and methods of HTML elements, so custom elements should inherit HTMLElement.

But after all, some elements are special. They have many more attributes and methods than ordinary HTML elements, such as input placeholders. To make custom elements inherited from HTMLElement also have placeholders, you need to write a lot of code:

<my-input placeholder="请输入内容"></my-input>
<script>
  customElements.define(
    'my-input',
    class extends HTMLElement {
      
      
      static observedAttributes = ['placeholder']

      get placeholder () {
      
      
        return this.querySelector('input').getAttribute('placeholder')
      }

      set placeholder (value) {
      
      
        return this.querySelector('input').setAttribute('placeholder', value)
      }

      constructor() {
      
      
        super()
        this.innerHTML = '<input />'
      }

      attributeChangedCallback(name, oldValue, newValue) {
      
      
        if (oldValue === newValue) return

        this.placeholder = newValue
      }

    }
  )
</script>

It would be too much trouble to write similar code for these non-built-in properties. Is it possible to make custom elements directly inherit from built-in elements: HTMLInputElement?

customElements.define(
  'my-input',
  class extends HTMLInputElement {
    
    
    ...
  }
)

As a result, the console reports an error:

Uncaught TypeError: Illegal constructor: autonomous custom elements must extend HTMLElement
# 自定义元素必须继承自 HTMLElement

This also uses the third parameter customElements.define() of , which is an object with only one field extends, and its value is the tag name of the element to be inherited:

customElements.define(
  'my-input',
  class extends HTMLInputElement {
    
    
    // 删除其它多余的代码
    constructor() {
    
    
      super()
      this.disabled = true
    }
  },
  {
    
     extends: 'input' }
)

However, if you use the inheritance parameter, you cannot directly use the tag name of the custom element when using the custom element. You can only use the tag name of the inherited element, and then use the is attribute to point to the tag name of the custom element:

<!-- <my-input placeholder="请输入内容"></my-input> -->
<input is="my-input" placeholder="输入内容" />

Why can't it be used directly?

In fact, this is because some tags in HTML are fixed collocations, such as ul > li, table > tr > td, dl > dt + dd, select > option , it is not impossible to write other elements in these tags, but writing Anything else will lose its original effect.

For example, the following example has no drop-down options:

<select>
  <p value="a">A</p>
  <p value="b">B</p>
</select>

If a <my-option> element :

customElements.define(
  'my-option',
  class extends HTMLOptionElement {
    
    
    constructor() {
    
    
      super()
      this.innerText = 'my-option'
    }
  },
  {
    
     extends: 'option' }
)

Since the HTML specification requires <select> only to be recognized <option>, the following code will not work as expected:

<select>
  <my-option value="a">A</my-option>
  <my-option value="b">B</my-option>
</select>

In order to ensure that the element<select> contained in it can be expanded and encapsulated into a component, so the wording of is appears.<option><option>

<select>
  <option is="my-option" value="a">A</option>
  <option is="my-option" value="b">B</option>
</select>

But the Safari browser does not implement the is function.
insert image description here
The Safari browser partially supports Custom Elements: "Supports custom elements, but does not support custom built-in elements".

However, there is already a special polyfill to solve this problem. Zhang Xinxu also extended the browser distinction function for it. For details, please refer to: Safari does not support the compatibility processing of build-in custom elements « Zhang Xinxu

JS to create custom elements
If you need to manually create elements with JS, the createElement() method supports the second parameter, which is also an object, and only contains one field is, which is used to specify the tag name of the custom element.

const myOptionDom = document.createElement('option', {
    
     is: 'my-option' })
document.querySelector('select').append(myOptionDom)

Although the is attribute is not visible on the Elements panel, it is already a custom element:
insert image description here
there is another way to create an element directly by instantiating an instance of the class that defines the behavior of the element:

class MyOption extends HTMLOptionElement {
    
    
  constructor() {
    
    
    super()
    this.innerText = 'my-option'
  }
}
customElements.define('my-option', MyOption, {
    
     extends: 'option' })

const myOptionDom = new MyOption()
document.querySelector('select').append(myOptionDom)

insert image description here

Guess you like

Origin blog.csdn.net/woyebuzhidao321/article/details/129444968