One of the components used in many of the company’s recent designs is roughly a reference to the cascading panel in the toutiao – advertising system. Here is a brief note on the design and development of components.

Demand analysis

According to the effect drawing, the data of provinces and cities should be displayed in columns in the left area. Click the parent node to display the sub-level data, and click each time to expand the column where the next level of the node is located. If the parent node is selected, all the child nodes are selected. If the child node is selected, the parent node becomes selected. After each check, the check result is displayed on the right panel. One detail here is that the selection shown in the right panel does not simply show each selected node. It is based on the principle that only the parent node is displayed when all the child nodes under a node are selected. I call it simply the compression principle for cascading data. After the above analysis, it can be found that this component is essentially a flat checkbox-tree component. In the past, for data selection with cascading relationships like provinces and cities, the traditional interaction design often adopted checkbox-tree.

Prior to developing this component, I had experienced refactoring the Checkbox-tree from an old project, referring to the Element-UI tree component, and learned a lot about building components that rely on tree structures. The component borrows from the Element-UI and iView tree components, and I feel strongly about these open source projects. With the above analysis, let’s start coding. Because it is using VUE as the technology stack. The first and most critical step is to define the props and data for the component.

Props and data

Props: {data: {// Show the datatype: Array,
      required: true}, props: {// Alias of the key and label fields in the data, because the fields that mark label and key in the external data (such as the tree structure returned by the back end) are often not fixedtype: Object,
      default() {
        return {
          key: "id",
          label: "label"}; }}, Settings: {/* configuration, allow to customize each level, eg:[{level: 1,// level of the column, because there is a virtual root node inside the component, so the level starts from 1:"First class classification"// Column header hasAllCheck:trueCheckbox showCheckBox:true}, {level: 2, title:"Secondary classification",
          hasAllCheck: true,
          showCheckBox: true}] * /type: Array,
      required: true}, checkedLevel: {// Level of data presentationtype: Number,
      required: true}, zipLevel: {// Data compression leveltype: Number,
      required: true}, isSingle: {// Whether is radio mode, if sotrue, is degraded to a cascading selectortype: Boolean,
      default: false}}data() {
    return{rootNode: null,// The tree data structure used internally by the component. A flattening data along the bank makes it easy to search for any Node curShowList: [],// control the current panel expansion and flattening state checkedData: []// checkdata}; }Copy the code

Node data type

An object of type Node is used inside the component to wrap the data passed in by the user and to store the current state of the Node. The Node type packaging process will not be described here, if you need to read the source code or search for related data structure introduction. Just compare the data passed in by the user with the nodes inside the component. User incoming:

[
      {
        id: 1,
        label: "Level 1",
        children: [
          {
            id: 4,
            label: "The secondary 1-1",
            children: [
              {
                id: 9,
                label: "Triple the 1-1-1"
              },
              {
                id: 10,
                label: "Triple the 1-1-2"
              }
            ]
          }
        ]
      },
      {
        id: 2,
        label: "Level 2",
        children: [
          {
            id: 5,
            label: "Secondary 2-1"
          },
          {
            id: 6,
            label: "Secondary 2-2"
          }
        ]
      },
      {
        id: 3,
        label: "Level 3",
        children: [
          {
            id: 7,
            label: "Secondary 3-1"
          },
          {
            id: 8,
            label: "Secondary 3-2",
            children: [
              {
                id: 11,
                label: "Triple the 3-2-1"
              },
              {
                id: 12,
                label: "Triple the 3-2-2"
              },
              {
                id: 13,
                label: "Triple the 3-2-3"}]}]Copy the code

A packed rootNode inside the component


Once you have the root node, you can build the template of the cascading panel by looking up childNodes and recursively.

Node linkage

 handleCheck(isCheck, id, immediate = true*/) {const checkedLevel = this.checkedLevel; // check the current level and a flattener const selectNode = this. flatten. find(item => item.id === id);if(! selectNode) {return; } // recursive // from parent to childfunction setCheck(node) {
     node.checked = isCheck;
     if(! node.childNodes.length && node.level < checkedLevel) { node.noChildChecked = isCheck; }if(! Array.isArray(node.childNodes) || ! node.childNodes.length) {return;
     }
     node.childNodes.forEach(node => {
       setCheck(node); }); } // From child to parentfunction setParentCheck(parent) {
     if(! parent || ! parent.parent) {return;
     }
     parent.checked = parent.childNodes.every(child => {
       return child.checked === true;
     });
     setParentCheck(parent.parent);
   }

   setCheck(selectNode);
   setParentCheck(selectNode.parent);
   if(immediate) { this.getCheckedData(); }}Copy the code

Node linkage first finds the current node through flattening data. The object is then checked from parent to child and from child to parent.

There is a parameter indicating whether to call the data compression method immediately because it involves selecting all or setting the check status of nodes in batches.

Columns of a

Column expansion is triggered by a node’s SELECT, including check and click events.

HandleSelect (id) {// select const selectNode = this.id. Find (item => item.id === id); selectNode.parent.childNodes.forEach(node => (node.selected =false));
   selectNode.selected = true; // The next level is displayed, the deeper levels are not renderedif(selectNode.level < this.maxLevel) { this.curShowList[selectNode.level] = !! selectNode.childNodes.length;for (let i = selectNode.level + 1; i < this.maxLevel; i++) {
       this.curShowList[i] = false; }} // In single mode, the logic becomes similar to cascading selector. If you select the non-deepest node, you can directly clear all checked results under the current node, which is regarded as re-selectionif(this.isSingle && selectNode.level ! == this.maxLevel) { this.flattenData.forEach(p => (p.checked =false)); this.getCheckedData(); }}Copy the code

Column expansion by controlling the curShowList array, the true or false value of each item in the array corresponds to the expansion or collapse of each column. An additional isSingle props is provided to demote the component to a cascade selector, allowing it to perform only one option.

Data compression after node selection

 getCheckedData() {
      const result = [];
      const toZipData = this.flattenData.filter(p => p.level === this.zipLevel);
      function step(nodes) {
        if(! nodes || ! nodes.length) {return; } const curSelectData = nodes.filter(p => p.checked || p.noChildChecked); const noSelectData = nodes.filter( p => ! (p.checked || p.noChildChecked) ); result.push(... curSelectData); noSelectData.forEach(p => step(p.childNodes)); } step(toZipData); this.checkedData = result; }Copy the code

Firstly, the data of the target compression level is filtered out through the flat array, and the selected data is directly pushed into the result. Only the unselected data is regarded as the target data of the next recursive process, and the recursive exit is that the node does not exist or there is no child node.

methods

  set/* public API */ keys.forEach(key => {this.setCheckedNode(key,false);
      });
      this.getCheckedData();
    },
    getCheckedNodes(isZip = true// get the selected data /* public API */if (isZip) {
        return this.checkedData.map(item => {
          return {
            id: item.id,
            text: item.text,
            data: item.data,
            level: item.level
          };
        });
      } else {
        returnthis.flattenData.filter(p => p.checked || p.noChildChecked); }}Copy the code

The direction

  1. Components must support lazy loading to meet the requirement of large data volume.
  2. Components do not provide slots, such as customizations for data display in the right panel, headers for columns on the left, and so on.

conclusion

Components are still relatively basic and need to be improved. If there are mistakes, welcome to correct! Hopefully, it’s a little informative. Below is the github address of the project github.com/juenanfeng/…