1. A switch button

Let’s start with a simple switch button. In the following code, we implement a switch button. Clicking the switch toggles on/off.

<! DOCTYPEhtml>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
<div class='wrapper'>
  <button class='switch-btn'>
    <span class='switch-text'>open</span>
  </button>
</div>
<script>
  const button = document.querySelector('.switch-btn')
  const text = button.querySelector('.switch-text')
  let on = false
  button.addEventListener('click'.() = >{ on = ! onif (on) {
      text.innerHTML = 'on'
    } else {
      text.innerHTML = 'off'}},false)
</script>
</body>
</html>
Copy the code

At this point, another page needs the switch button, or you want to share the button with other front-end students. The question is, should I copy the entire Button code and all the JavaScript code? Obviously there is no reusability in this approach.


2. Code reuse

Let’s modify the above code to make our on/off buttons reusable. Let’s start by writing a class with the render method that calls render() to return a string representing an HTML structure.

<! DOCTYPEhtml>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
<div class='wrapper'>
</div>
<script>
  class SwitchButton {
    render () {
      return `  `}}const wrapper = document.querySelector('.wrapper')
  const switchButton1 = new SwitchButton()
  wrapper.innerHTML = switchButton1.render()
  const switchButton2 = new SwitchButton()
  wrapper.innerHTML += switchButton2.render()
</script>
</body>
</html>
Copy the code

We use innerHTML very aggressively, inserting the two buttons into the wrapper, and clicking the buttons doesn’t respond because we haven’t added the event-handling JS code to the component yet. Although this implementation does not meet the requirements so far, we have managed to reuse the structure, which we will optimize later.

2.1 Simple componentization

We now need to add click events to the SwitchButton, but this thing is in the string, how can you add events to the string? The DOM event API can only be used by the DOM structure. That is, we need the DOM structure, or more specifically, the HTML string representation of this on/off button. Suppose we now have a function called strToDOM, and you pass in an HTML string, but it returns the corresponding DOM element to you. Wouldn’t that solve the problem?

const strToDOM = (domString) = > {
  const div = document.createElement('div')
  div.innerHTML = domString
  return div
}
Copy the code

Now use this function to transform our SwitchButton!

class SwitchButton {
  render () {
    this.el = strToDOM(`  `)
    this.el.addEventListener('click'.() = > console.log('click'), false)
    return this.el
  }
}
Copy the code

Instead of an HTML string, render() now returns a DOM generated from the HTML string. We add an event to the DOM element before returning it. Because render now returns the DOM element, we can’t use innerHTML to insert a wrapper violently. Instead, plug it in using the DOM API as shown in the following code.

const wrapper = document.querySelector('.wrapper')
const switchButton1 = new SwitchButton()
wrapper.appendChild(switchButton1.render())
const switchButton2 = new SwitchButton()
wrapper.appendChild(switchButton2.render())
Copy the code

At this point, clicking the button triggers the () => console.log(‘click’) callback, which looks at the console and outputs the ‘click’ string. This is not what we want, we want the on/off callback on the toggle button. Let’s optimize the SwitchButton class definition to do this.

class SwitchButton {
  constructor () {
    this.state = { on: false }
  }
  changeText () {
    const text = this.el.querySelector('.switch-text')
    this.state.on = !this.state.on
    text.innerHTML = this.state.on ? 'on' : 'off'
  }
  render () {
    this.el = strToDOM(`  `)
    this.el.addEventListener('click'.this.changeText.bind(this), false)
    return this.el
  }
}
Copy the code

The above code is not complicated, we do two things: 1. Add a variable on to control the state of the switch; 2. Define a method called changeText as a callback to the button click event, so that the button can switch state according to the click event.

Now this component is pretty reusable, just instantiate it and insert it into the DOM, but is it really perfect?


3. Optimize DOM operations

Taking a look at our code in the previous section, take a closer look at the changeText method, which contains DOM operations such as this.el.querySelector() and text.innerhtml =. That’s because there’s only one on state. Since data state changes can cause you to update the content of the page, imagine that if your component relies on a lot of state, then your component is basically DOM manipulation.

It is very common for a component’s display morphology to be determined by multiple states. Intermixing DOM manipulation with code is actually a bad practice, and manually managing the relationship between data and DOM can make code less maintainable and error-prone. So there’s room for optimization in our example: How do you minimize this manual DOM manipulation?

3.1 State changes ➞ Build a new DOM element to update the page

Switch gears: Once the state changes, call the Render method again, build a new DOM element, put the state change into the Render method, and render a different view depending on the state. You can use the latest this.state in the Render method to construct strings with different HTML structures, and construct different DOM elements from that string. The page is updated! Instead of triggering component updates through the DOM API. Sounds a bit abstract, but take a look at the code and you’ll see!

class SwitchButton {
  constructor () {
    this.state = { on: false }
  }
  setState (state) {
    this.state = state
    this.el = this.render()
  }
  changeText () {
    this.setState({
      on:!this.state.on
    })
  }
  render () {
    this.el = strToDOM(`
      <button class='switch-btn'>
         <span class='switch-text'>The ${this.state.on ? 'on' : 'off'}</span>
      </button>
    `)
    this.el.addEventListener('click'.this.changeText.bind(this), false)
    return this.el
  }
}
Copy the code

Just a few minor changes:

  • renderThe HTML string inside the function is based onthis.stateDifferent but different (ES6 template strings are used here, which is handy for this kind of thing).
  • A newsetStateFunction, which takes an object as an argument; It will set the instancestateAnd then call it againrenderMethods.
  • When the user clicks the button,changeTextWill build a newstateObject, this new onestateAnd the incomingsetStateIn a function. The result is that every time a user clicks,changeTextChange the component state and callsetStatesetStateWill be calledrenderrenderThe method will be based onstateDifferent DOM elements are rebuilt from different DOM elements.

Refresh the page, click the button, huh? What’s the holdup?

3.2 Re-insert a new DOM element

Take a closer look at the code. Although clicking on the button triggers the changeText method, the setState method triggers the Render method, which assigns a new DOM node to this.el, we don’t add the DOM node to our DOM tree. The wrapper. AppendChild (SwitchButton1.render ()) is left intact and the page is still the same DOM node. So, outside of the component, you need to know that the component has changed and that the new DOM element has been updated into the page.

Modify the component setState method:

setState (state) {
  const oldEl = this.el
  this.state = state
  this.el = this.render()
  if (this.onStateChange) this.onStateChange(oldEl, this.el)
}
Copy the code

The purpose of onStateChange is to specify the logic of onStateChange as to how to handle the original and new elements when the component changes:

const wrapper = document.querySelector('.wrapper')
const switchButton1 = new SwitchButton()
wrapper.appendChild(switchButton1.render())
switchButton1.onStateChange = (oldEl, newEl) = > {
  wrapper.insertBefore(newEl, oldEl) // Insert a new element
  wrapper.removeChild(oldEl) // Delete the old element
}
Copy the code

The onStateChange method is called every time setState is instantiated, so you can customize the behavior of onStateChange. The behavior here is that whenever a new DOM element is constructed in setState, onStateChange tells the outside world to insert a new DOM element, then removes the old one, and the page is updated, so you don’t need to manually update the page anymore.

This version has nice on/off buttons that don’t require manual manipulation of the DOM. But the downside is that if I were to create a new component, say a comment component, then I would have to rewrite all of these setState methods, and all of these things could be pulled out and turned into a generic pattern.


4. Abstract out the common component classes

4.1 Abstract Component classes

To make the code more flexible and write more components, we abstracted this pattern into a Component class:

class Component {
  setState (state) {
    const oldEl = this.el
    this.state = state
    this._renderDOM()
    if (this.onStateChange) this.onStateChange(oldEl, this.el)
  }
  _renderDOM () {
    this.el = strToDOM(this.render())
    if (this.onClick) {
      this.el.addEventListener('click'.this.onClick.bind(this), false)}return this.el
  }
}
Copy the code

This is a Component parent class from which all components can be built. It defines two methods, one of which is setState, which we are familiar with; One is the private method _renderDOM. The _renderDOM method calls this.render to build the DOM element and listens for the onClick event. Therefore, component subclasses simply implement a Render method that returns an HTML string.

Add an additional mount method, which inserts the component’s DOM element into the page and sets onStateChange to tell the application how to update the page when setState is set:

const mount = (component, wrapper) = > {
  wrapper.appendChild(component._renderDOM())
  component.onStateChange = (oldEl, newEl) = > {
    wrapper.insertBefore(newEl, oldEl)
    wrapper.removeChild(oldEl)
  }
}
Copy the code

In this case, our simplified switch assembly would become:

class SwitchButton extends Component {
  constructor () {
    super(a)this.state = { on: false }
  }

  onClick () {
    this.setState({
      on:!this.state.on
    })
  }
  render () {
    return `
      <button class='switch-btn'>
         <span class='switch-text'>The ${this.state.on ? 'on' : 'off'}</span>
      </button>
    `}}Copy the code

Mount the component to the page using the new mount method:

const wrapper = document.querySelector('.wrapper')
mount(new SwitchButton(), wrapper)
Copy the code

That’s not good enough. In real development, you may need to pass in some custom configuration data to the component. For example, if I want to set the background color of a button, if I pass it a parameter that tells it how to set its own color. So we can pass both the component class and its subclasses a parameter, props, as the component’s configuration parameter. The constructor for adding Component is:

constructor (props = {}) {
  this.props = props
}
Copy the code

When the SwitchButton inherits Component, it passes props to the parent class via super(props), so that the configuration parameters can be retrieved from this.props. Also modified the original SwitchButton render method so that it can generate a different style attribute based on the parameter this.props. BgColor passed in. This gives you the freedom to configure the colors of the components.

class SwitchButton extends Component {
  constructor (props) {
    super(props)
    this.state = { on: false }
  }

  onClick () {
    this.setState({
      on:!this.state.on
    })
  }
  render () {
    return `
        <button class='switch-btn' style="background-color: The ${this.props.bgColor}">
           <span class='switch-text'>The ${this.state.on ? 'on' : 'off'}</span>
        </button>
      `}}Copy the code

When instantiating and mounting the component, pass props to the constructor:

mount(new SwitchButton({ bgColor: 'red' }), wrapper)
Copy the code

4.2 Defining a new component

If we need to create a new Component, which is text that changes color, starting with red and turning blue when clicked, we simply define a RedBlueText class that inherits Component:

class RedBlueText extends Component {
  constructor (props) {
    super(props)
    this.state = {
      color: 'red'
    }
  }
  onClick () {
    this.setState({
      color: 'blue'
    })
  }
  render () {
    return `
      <div style='color: The ${this.state.color}; '>The ${this.state.color}</div>
    `}}Copy the code

To mount with a mount:

mount(new RedBlueText(), wrapper)
Copy the code

4.3 Complete Code

Now that you have the flexibility to componentize your page, the complete code is here:

<! DOCTYPEhtml>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
<div class='wrapper'>
</div>
<script>
  const strToDOM = (domString) = > {
    const div = document.createElement('div')
    div.innerHTML = domString
    return div
  }
  const mount = (component, wrapper) = > {
    wrapper.appendChild(component._renderDOM())
    component.onStateChange = (oldEl, newEl) = > {
      wrapper.insertBefore(newEl, oldEl)
      wrapper.removeChild(oldEl)
    }
  }
class Component {
  constructor (props = {}) {
    this.props = props
  }
  setState (state) {
    const oldEl = this.el
    this.state = state
    this._renderDOM()
    if (this.onStateChange) this.onStateChange(oldEl, this.el)
  }
  _renderDOM () {
    this.el = strToDOM(this.render())
    if (this.onClick) {
      this.el.addEventListener('click'.this.onClick.bind(this), false)}return this.el
  }
}
  class SwitchButton extends Component {
    constructor (props) {
      super(props)
      this.state = { on: false }
    }

    onClick () {
      this.setState({
        on:!this.state.on
      })
    }
    render () {
      return `
        <button class='switch-btn' style="background-color: The ${this.props.bgColor}">
           <span class='switch-text'>The ${this.state.on ? 'on' : 'off'}</span>
        </button>
      `}}class RedBlueText extends Component {
    constructor (props) {
      super(props)
      this.state = {
        color: 'red'
      }
    }
    onClick () {
      this.setState({
        color: 'blue'
      })
    }
    render () {
      return `
        <div style='color: The ${this.state.color}; '>The ${this.state.color}</div>
      `}}const wrapper = document.querySelector('.wrapper')
  mount(new SwitchButton(), wrapper)
  mount(new SwitchButton({ bgColor: 'red' }), wrapper)
  mount(new RedBlueText(), wrapper)

</script>
</body>
</html>
Copy the code

5, summary

Componentization can help us solve the reuse problem of the front-end structure. The whole page can be composed of such different components, nested.

A component has its own display morphology (HTML structure and content above) behavior, which can be determined by both the data state (STATE) and the configuration parameters (props). Changes in data state and configuration parameters affect the appearance of the component.

The display of the component needs to be updated as the data changes. So if the componentized pattern provides an efficient way to automatically help us update pages, it can greatly reduce the complexity of our code and lead to better maintainability.

Reference: http://huziketang.com/books/react/lesson1