I’ve seen a few Web Component articles and examples, but they’re pretty simple to get started with. Curious about how Web Component implements a more complex Component. Get your hands on it today and try to write a component that will be used in your business.

Some time ago, I wrote an article called “How to Write a Conditional Composition Component”. Today, I will try to implement this Component using Web Component. The component effect is shown as follows:

The conditional groups in the diagram are parentheses, so let’s say we want to write oneKey1 > 0 && (Key2 < 20 || Key3 > 10)Such an expression, then, can be expressed as follows:



Note: the above picture comes from “Hand to hand teach you to write a conditional combination of components”, using the UI library, this article uses native JS so there will be differences in the style of the controls.

The main feature of A Web Component is the ability to create custom tags, so the first solution I came up with was to split the Component up<relation-tree>,<relation-group>,<relation-item>So three tags. Tags are then used to build the content of the component. Structural decomposition is shown as follows:



In order toKey1 > 0 && (Key2 < 20 || Key3 > 10)For example, the combination of the three tags is as follows:

<relation-tree>
  <relation-item></relation-item>
  <relation-group>
    <relation-item></relation-item>
    <relation-item></relation-item>
  </relation-group>
</relation-tree>
Copy the code

To consider reusability, we pass Term as a custom part in the form of

tag children, so the code of the above expression is as follows:

<relation-tree>
  <relation-item>
    <div class="term" slot="element-term">
      <span class="element">
        <select name="key" value="Key1" placeholder="Please select conditions">
          <option value="Key1">Key1</option>
          <option value="Key2">Key2</option>
          <option value="Key3">Key3</option>
        </select>
      </span>
      <span class="comparison">
        <select name="op" value=">" placeholder="Please select a relation">
          <option value="= =">Is equal to the</option>
          <option value=! "" =">Is not equal to</option>
          <option value=">">Is greater than</option>
          <option value="<">Less than</option>
        </select>
      </span>
      <span class="value">
        <input name="value" value="0" />
      </span>
    </div>
  </relation-item>
  <relation-group>
    <relation-item>
      <div class="term" slot="element-term">
        <span class="element">
          <select name="key" value="Key2" placeholder="Please select conditions">
            <option value="Key1">Key1</option>
            <option value="Key2">Key2</option>
            <option value="Key3">Key3</option>
          </select>
        </span>
        <span class="comparison">
          <select name="op" value="<" placeholder="Please select a relation">
            <option value="= =">Is equal to the</option>
            <option value=! "" =">Is not equal to</option>
            <option value=">">Is greater than</option>
            <option value="<">Less than</option>
          </select>
        </span>
        <span class="value">
          <input name="value" value="20" />
        </span>
      </div>
    </relation-item>
    <relation-item>
      <div class="term" slot="element-term">
        <span class="element">
          <select name="key" value="Key3" placeholder="Please select conditions">
            <option value="Key1">Key1</option>
            <option value="Key2">Key2</option>
            <option value="Key3">Key3</option>
          </select>
        </span>
        <span class="comparison">
          <select name="op" value=">" placeholder="Please select a relation">
            <option value="= =">Is equal to the</option>
            <option value=! "" =">Is not equal to</option>
            <option value=">">Is greater than</option>
            <option value="<">Less than</option>
          </select>
        </span>
        <span class="value">
          <input name="value" value="10" />
        </span>
      </div>
    </relation-item>
  </relation-group>
</relation-tree>
Copy the code

This scheme is somewhat similar to the structure of the table tag. The advantage of this scheme is that it is easy to implement and close to native HTML in use. The disadvantage is that the use is still more troublesome, unable to achieve data-driven.

Code implementation

Next, the three tags

,

and

are respectively defined.


Definition tags

relation-tree

Of the three tags, the

tag is the simplest, serving only as a container. However, considering the convenience of use, the author adds the

tag in

by default. There is no need for the user to manually type, so that

can be placed directly under

. The specific code is as follows:




// Inherit HTMLElement to implement RelationTree component function
class RelationTree extends HTMLElement {
  constructor() {
    super(a);// Place all of innerHTML under 
      
    const content = this.innerHTML;
    const group = document.createElement('relation-group');
    group.innerHTML = content
		
    // The mode must be open. If closed is used, the linkage between the three tags will be blocked
    this.attachShadow({ mode: 'open'}).appendChild(group); }};// Register RelationTree with relation-tree tag
window.customElements.define('relation-tree', RelationTree);
Copy the code


is the simplest Web Component with the following three elements:

  1. Create a class or function to specify the function of the Web component, the RelationTree object
  2. Attach the shadow DOM to the custom element using the attachShadow method, in open mode
  3. Use the customElements. Define method to register the new custom element to the custom tag



relation-group


contains some fixed HTML structure, so we need a template to implement it. The template content is placed in HTML as follows:

<template id="wc-template-relation-group">
  <div class="relational">
    <select class="sign">
      <option value="and">and</option>
      <option value="and">or</option>
    </select>
  </div>
  <div class="conditions">
    <! -- Children placeholder in the relation-group tag -->
    <slot name="relation-content"></slot>
    <div class="operators">
      <button class="add-term">Add conditions</button>
      <button class="add-group">And conditions set</button>
    </div>
  </div>
</template>
Copy the code

Function definition and tag registration:

class RelationGroup extends HTMLElement {
  constructor() {
    super(a);const temp = document.getElementById('wc-template-relation-group').content;

    // Add the template cloneNode to shadowRoot
    const shadowRoot = this.attachShadow({ mode: 'open' }).appendChild(temp.cloneNode(true));
    
    // Find the label placeholder defined in the template, then use children
    const slot = this.shadowRoot.querySelector('slot[name=relation-content]');
    for (let i = this.children.length - 1; i > -1; i--) {
      if (i === 0) {
        slot.replaceWith(this.children[i]);
      } else {
        slot.after(this.children[i]); }}}};window.customElements.define('relation-group', RelationGroup);
Copy the code


increases the use of templates compared with

. The slot is replaced with JS, and the posture is slightly crooked. The use of the standard will be described in

.


relation-item

Template definition:

<template id="wc-template-relation-item">
  <! -- Define the placeholder -->
  <slot name="element-term">Set relationship condition items</slot>
  <button class="delete">delete</button>
</template>
Copy the code

Function definition and tag registration:

class RelationItem extends HTMLElement {
  constructor() {
    super(a);const template = document.getElementById('wc-template-relation-item');
    const templateContent = template.content;

    const shadowRoot = this.attachShadow({ mode: 'open' });
    shadowRoot.appendChild(templateContent.cloneNode(true)); }};window.customElements.define('relation-item', RelationItem);
Copy the code

Slot placeholders are defined in the template and are not dealt with in subsequent function definitions. The standard way to use it is to replace the definition in an HTML tag, as follows:

<relation-item>
  <! -- Identifies the placeholder to be replaced by the slot property -->
  <div class="term" slot="element-term">
    <span class="element">
      <select name="key" value="Key2" placeholder="Please select conditions">
        <option value="Key1">Key1</option>
        <option value="Key2">Key2</option>
        <option value="Key3">Key3</option>
      </select>
    </span>
    <span class="comparison">
      <select name="op" value="<" placeholder="Please select a relation">
        <option value="= =">Is equal to the</option>
        <option value=! "" =">Is not equal to</option>
        <option value=">">Is greater than</option>
        <option value="<">Less than</option>
      </select>
    </span>
    <span class="value">
      <input name="value" value="20" />
    </span>
  </div>
</relation-item>
Copy the code

At this point, all three tags have been defined, but our component implementation is not complete, and it still lacks style and event definitions.

Add the style

The style of a Web Component can be defined inside the Component to avoid conflicts with the style definition in the business code.

The style of the RelationTree tag needs to be defined in RelationTree. Since there is no template for RelationTree, add it to the function definition. Add style definition after the RelationTree code is as follows:

class RelationTree extends HTMLElement {
  constructor() {
    super(a);const content = this.innerHTML;
    const group = document.createElement('relation-group');
    group.innerHTML = content

    const shadowRoot = this.attachShadow({ mode: 'open' }).appendChild(group);
    
    // Define the relation-group tag style
    const style = document.createElement('style');
    style.innerHTML = ` relation-group { display: flex; } `;
    this.shadowRoot.appendChild(style); }};Copy the code

The rest of the styles can be placed in the relation-group template. The template with the style code added is as follows:

<template id="wc-template-relation-group">
  <style>
    relation-group {
      display: flex;
      margin-bottom: 12px;
    }
    .relational {
      padding: 20px 8px 0 16px;
      border-right: 1px solid #d9d9d9;
    }
    .conditions {
      flex: auto;
    }
    .conditions > * {
      position: relative;
    }
    .conditions > *::before {
      content: "";
      display: inline-block;
      position: absolute;
      top: 0;
      left: 0px;
      width: 16px;
      height: 14px;
      border-bottom: 1px solid #d9d9d9;
      background-color: #fff;
    }
    .conditions > *:first-child::before {
      left: -1px;
      width: 17px;
    }
    .conditions > relation-group::before {
      top: 20px;
    }
    .conditions > div:last-child::before {
      top: inherit;
      bottom: 0;
      left: -1px;
      width: 17px;
      border-bottom: 0;
      border-top: 1px solid #d9d9d9;
    }
    relation-item {
      display: block;
      height: 30px;
      padding-left: 16px;
      margin-bottom: 12px;
    }
    .operators {
      padding-left: 16px;
    }
    .operators .btn {
      padding: 4px 12px;
      font-size: 12px;
    }
    .operators span {
      display: inline-block;
      margin-right: 8px;
    }
    .term {
      display: inline-block;
    }
  </style>
  <div class="relational">
    <select class="sign">
      <option value="and">and</option>
      <option value="and">or</option>
    </select>
  </div>
  <div class="conditions">
    <slot name="relation-content"></slot>
    <div class="operators">
      <button class="add-term">Add conditions</button>
      <button class="add-group">And conditions set</button>
    </div>
  </div>
</template>
Copy the code

Note: The implementation logic of CSS is described in the “CSS Style” section of the article “How to Write a Conditional Composition Component”, which is not described here.

Add event

Finally to “add condition”, “add condition group”, “delete” three buttons with events, taking into account the HTML tag may change dynamically, we in RelationTree in the form of event proxy to achieve:

class RelationTree extends HTMLElement {
  constructor() {
    super(a);const content = this.innerHTML;
    const group = document.createElement('relation-group');
    group.innerHTML = content

    const shadowRoot = this.attachShadow({ mode: 'open' }).appendChild(group);
    
    // Define the relation-group tag style
    const style = document.createElement('style');
    this.shadowRoot.appendChild(style);
    this.shadowRoot.querySelector('style').innerHTML = ` relation-group { display: flex; } `;
    
    
    const onitemadd = this.getAttribute('onitemadd'); / / condition
    const ongroupadd = this.getAttribute('ongroupadd'); // add condition group
    const onitemdelete = this.getAttribute('onitemdelete'); / / delete
    this.shadowRoot.addEventListener('click'.(e) = > {
      // Return the event path; When attachShadow's mode is closed, the node of shadow species is not returned
      const path = e.composedPath();
      const target = path && path[0];
      if(target? .classList? .contains('add-term')) {
        if (onitemadd && window[onitemadd]) {
          window[onitemadd](e, target); }}else if(target? .classList? .contains('add-group')) {
        if (ongroupadd && window[ongroupadd]) {
          window[ongroupadd](e, target); }}else if(target? .classList? .contains('delete')) {
        if (onitemdelete && window[onitemdelete]) {
          window[onitemdelete](e, target); }}}); }};Copy the code

At this point,<relation-tree>,<relation-group>,<relation-item>The basic functions of the three labels have been basically realized, and the effect as shown in the following figure can be seen by using the initial method:



The above scheme is only for demonstration, many details are not perfect, interested partners can continue to write.



conclusion

The above three tag implementations can be used with other frameworks (such as React) to encapsulate logic such as traversals and events. I wanted to try data-driven rendering directly with Web Component, but it didn’t work because I couldn’t bridge the gap between HTML and JS. I want the content of the Term (condition, operator, value) part to be passed in by the component, and if Template is used, the condition is relatively fixed. However, the actual scene is often rendered according to the interface data, and the value often needs to do different rendering and restriction according to the conditions. So the Template approach doesn’t work, the function approach does, but on HTML attributes functions can only use global functions, which is not elegant. So the author finally gave up this attempt, if readers have better suggestions hope to be free to comment.

Reference: developer.mozilla.org/zh-CN/docs/… Github.com/mdn/web-com…