1. Introduction

In the front-end development work, especially in the initial stage of the project, third-party component library is often selected for rapid construction. Taking Vue ecology as an example, the public is generally fond of Element-UI and iView, among which Element-UI is mostly used for connecting with the foreground project, and iView is mostly used for connecting with the background management project. However, no matter which component library you use, as long as it’s easy to use, extensible, and sensual, it’s usually a good experience. However, it can also cause problems when we become overly dependent on third-party component libraries. For example, project A relies on Element-UI, project B relies on iView, and to avoid component style contamination, usually A project references only one component library. So when I “wrapped” A new component in project A by using some of the basic elements of Element-UI (such as SELECT), I couldn’t use it directly in project B and had to rely on iView to rewrite it. Over time, if the two projects share more and more components, that can be a lot of work. So, this time we need to do their own food and clothing.

There are many articles on how to build a component library project on various forums, but there are few discussions and summaries of specific component development ideas, so this article will focus on the latter. As for the specific component library project construction method, I prefer vuePress based implementation. In this also recommend dig friend @_ an ge article [Vue advanced] bronze player, how to develop a set of UI library? . This article takes you to see a few classic examples, I believe that novice friends will have some harvest. Thank you for your support and thumbs up.

Let’s take a look at one of the component libraries I’ve been working on so far. There’s still a lot of room for improvement, but GIVEN time I could do better:

Block-level selector:

Tree components:

After thinking about it, HERE are some of the benefits of writing your own component library:

  • More in-depth component lifecycle hooks, a deep grasp of the state of components under different hooks;
  • Use more methods that are less common when writing business code, such as<slot>Slot, bidirectional binding, component recursion, etc.
  • The expansibility and reuse of components should be considered more fully to improve the capability of business-driven development;
  • Take more account of the uniqueness of components and make sure they are not overwritten by styles from other third-party component libraries.

I’ll take a closer look at the implementation process and ideas, using selectors and tree components as examples

2. Selector seletor

< SELECT > selector is one of the most common components, which can be divided into general selector, multiple selector, remote search selector and so on. When we use selectors, we might wonder if many third-party component libraries base their selectors on traditional

  1. The realization of option adaptation to meet the compatibility of different data;
  2. Realize the real-time association between value and label;
  3. Realize bidirectional binding of value to facilitate real-time update and acquisition of value.

2.1 Structural Optimization

In response to the above problems, we can see that there are major drawbacks to the layout of components in this way:

<template>
   <kd-select 
     :options='optionList'// Pass in the option set @getresult ='getResult' 
   />
</template>
export default {
   data() {
      return {
        result: ' '
        getResult: [
          {
            value: 'Juventus',
            label:'Juventus',
          },
          {
            value: 'RealMadrid',
            label:'Real Madrid',
          },
          {
            value: 'Barcelona',
            label:'Barcelona', } ] } }, methods: { getResult(val) { this.result = val; }}}Copy the code

This method can be implemented, but it is too limited. First,

<template>
    <kd-select v-model="teamName">
        <kd-option 
          v-for="(item,i) in teams" 
          :key="i" 
          :value='item.value' 
          :label='item.label'
        />
    </kd-select>
</template>
export default {
   data() {
      return {
        teamName: ' ',
        teams: [
            {
              value: 'Juventus',
              label:'Juventus',
            },
            {
              value: 'RealMadrid',
              label:'Real Madrid',
            },
            {
              value: 'Barcelona',
              label:'Barcelona',}]}}}Copy the code

This visually solves problem 1 and also divides the SELECT selector into two sub-components.

// select.vue
<template>
  <div class="kd-input-select">
     <div class="kd-input" v-outsideClick='showDropdown=false'>
        <input 
          class="kd-input-inner" 
          type="text" 
          readonly
          :value="result"
          placeholder="Please select" 
          @click="showDropdown = ! showDropdown"
        />
     </div>
     <div v-show="showDropdown">
        <ul ref="dropdownList" class="kd-select-dropdown_list">
          <slot></slot>
        </ul>       
     </div>
  </div>
</template>
<script>
export default {
  model: {
    prop: 'bindVal',
    event: 'bindEvent'
  },
  props: {
    bindVal: [Number,String],
  },
  data() {
    return {
       result: ' ',
       showDropdown: false, } }, methods: { getChoice(val) { this.result = val.label; // result only serves as the selected label, not as the binding value this.$emit('bindEvent', val.value); }}}; </script>Copy the code

Component resolution:

  • Usually the label and value of the selector are different, so the result is just the label to display, and really the value is done by bindVal, and passedv-modelImplement bidirectional binding;

The following code is a simple implementation of the corresponding option:

// option.vue
<template>
  <div>
    <li 
      class="kd-select-dropdown__item"
      @click="choice({ value:value, label:label })"
    >
      {{label}}
    </li>
  </div>
</template>
<script>
export default {
   props: ['value'.'label'],
   watch: {
     '$parent.bindVal': {
         handler(newVal , val) {
            if(newVal && newVal === this.value) {
               this.$parent.result = this.label;
            }
         },
         immediate: true
     }
   },
   methods: {
      choice(val) {
        this.$parent.getChoice(val)
      }
   }
}
</script>
Copy the code

By listening to the bindVal of the parent component SELECT, assign the label value to the result of the parent component, so as to solve the problem of the result display. The above is a basic selector implementation, we can also do more refined, more extensible. For example, set icon, set read-only and disable properties, and implement remote search functions. I won’t go into details here for lack of space.

3. Tree components

Tree components are very powerful, mainly used for the hierarchical and complex data intuitive two-dimensional display. At the beginning of the year, when I saw the business requirements, I wanted to use Element’s existing components directly, but then realized that our visual design and data structure were so complex that I had to do it myself. For tree components, at least the following issues should be addressed:

  1. Data expansibility, that is, data hierarchy can be divided infinitely, components can also be infinite load;
  2. Fully expose component hook states, such as the loading, ready-to-use, and destruction phases of the data lifecycle;
  3. Triggering events at any level should be fully and efficiently exposed to the outermost part of the component;
  4. Resolve component initialization state issues;
  5. Ensure that each level of identity is unique, so that special operations such as styling, initialization events, etc. can be performed on different levels.

Let’s start with an example data structure:

[{"code": "1212"."id": "1"."name": "Cloud Product System"."type": "product-line"."childs": [{"name": "Public Cloud Product"."id": 1,
            "code": "gyyxl"."type": "product"."childs": [{"name": "Financial accounting"."id": 1,
                    "code": "cwkj"."type": "domain"."childs": [{"code": "GL"."id": 4979,
                            "name": "General ledger"."type": "module"."childs": null
                        },
                    ]
                },
                {
                    "name": "Financial Overview"."id": 2."code": "cwzl"."type": "domain"."childs": null
                }
            ]
        }
    ]
  }
]
Copy the code

It can be seen that each layer has fields such as name, ID, code, type, and childs, and the childs field defines the sub-layer. As the saying goes, “Man is born good”. With the product manager constantly reassuring me and patting my chest that the data level must be 4 and will never change, as an honest and honest person, I started to really write the following components:

// tree.vue
<template>
      <section class="kd-product-line" v-for="(productLine , pl) in data">
        <p class="kd-product-bar"> {{productLine.name}} </p> <ul> <! -- layer 1 --> <section class="kd-product" v-for="(product , p) in productLine.childs">
              <p class="kd-product-bar">
               {{product.name}}
              </p>
              <ul class="kd-rank-domain"> <! -- layer 2 --> <section class="kd-domain" v-for="(domain , d) in product.childs">
                        <p class="kd-doamin-bar">
                           {{domain.name}}
                        </p>
                        <ul class="kd-rank-module"> <! --> <section v-for="(moduleObj, d) in domain.childs">
                               <p class="kd-module-bar">
                                 {{moduleObj.name}}
                                </p>
                                <ul class="kd-rank-bizentity"> <! -- layer 4 --> <p class="kd-bizentity-bar" v-for="(bizentity, b) in moduleObj.childs">
                                       {{bizentity.name}}
                                    </p>
                                </ul>
                            </section>
                        </ul>
                </section>
              </ul>
           </section>
        </ul>
      </section> 
</template>
Copy the code

Is it too much to watch? Yes, write such a similar data results hierarchy of components, not only is not flat, is not conducive to the later expansion and maintenance, we all know that the product manager’s commitment to listen to good, seriously you will lose. For example, one day the data suddenly changes from 4 to 5 layers, and you have to write a deeper layer. What if it becomes 100 layers? That can not be directly abandoned.

3.1 Component recursion

This is where we need to use component recursion, which abstracts and flattens the structure definition by analyzing the hierarchy of components to find common parts that can be extracted as sub-components (
components in the following code). The code is as follows:

// tree.vue
<div class="kd-tree card root">
  <tree-item 
    v-for="item in data"
    :key="item.id"
    :item='item'
  >
  </tree-item>
</div>
import TreeItem from './treeItem'
export default {
  name: 'kd-tree',
  props: {
    data: [Array],
  },
  components: {
     TreeItem
  },
}
</script>
Copy the code

Tree.vue acts primarily as a container to host the child
and pass data, and to get the cycles of the component. Let’s look at the implementation of
:

// TreeItem.vue
<template>
    <section class="tree-item">
       <div
         class="kd-bar">
          <span>{{item.name}}</span>
       </div>
       <ul v-if="item.childs && item.childs.length > 0"> <! --> <tree-item v-for="secondItem in item.childs"
            :key="secondItem.id"
            :item='secondItem'
          ></tree-item>
       </ul>
    </section>
</template>
<script>
export default {
   name: 'tree-item',
   props: {
      item: [Object],
   },
}
</script>
Copy the code

3.2 Hierarchy uniqueness

Through the form of component recursion, the code can be flat and concise. So how do you achieve the hierarchy style differences? This is where you need to define the uniqueness of the styles at each level. Therefore, the most obvious way is to define the difference of the class of each level. Here, I define the top class of each level as “kd-level number”, and then define “kd-level number bar” and” kd-level number name” respectively. The implementation method is as follows:

// tree.vue
<template>
  <div class='kd-tree'>
      <tree-item 
        v-for="item in data"
        :rankNum='initRank'
        :key="item.id"
        :item='item'
        :initFold='initFold ? initFold : true'
      >
      </tree-item>
  </div>
</template>
export default {
  props: {
     data: [Array], initFold: [Boolean]
  },
  data() {
      return}} // treeitem. vue <template> <section class="tree-item" :class="[outClassName]">
       <div
         class="kd-bar"
         :class="[barClass]"  
       >
          <span :class='[nameClass]'>{{item.name}}</span>
       </div>
       <ul v-if='item.childs && item.childs.length > 0'>
          <tree-item 
            v-for='secondItem in item.childs'
            :key='secondItem.id'
            :item='secondItem'
            :rankNum='(rankNum + 1)'
            :initFold='initFold'
          ></tree-item>
       </ul>
    </section>
</template>
<script>
export default {
   name: 'tree-item',
   props: {
      item: [Object],
      parentNode: [Object],
      rankNum: [Number],
      initFold: [Boolean]
   },
   data() {
       return {
           outClassName: `kd-${this.rankNum}`,
           barClass: `kd-${this.rankNum}-bar`,
           nameClass: `kd-${this.rankNum}-name`,
           foldChildNodes: true, // Whether to collapse the child node}},created() { this.foldChildNodes = this.initFold; }},Copy the code

This implements hierarchical style differences and exposes the way the external initialization state is handled. Look at the parsed elements:

3.3 Event Transmission

Then comes the exposure of the
event. The recursion of the
causes the abstraction of the component, so the traditional $emit method of continuously passing events to the parent is inefficient and cumbersome. The answer is to expose events directly to the tree via cross-component communication in eventBus:

// bus.js
import Vue from 'vue'
export default new Vue({ })

// treeItem.vue
<template>
    <section>
       <div>
          <span 
            @click="controlChildNodes"
          >{{item.name}}</span>
       </div>
       <ul 
         v-if="item.childs && item.childs.length > 0 && ! foldChildNodes">
          <tree-item 
            v-for='secondItem in item.childs'
            :key='secondItem.id'
            :item='secondItem'
            :rankNum='(rankNum + 1)'
          ></tree-item>
       </ul>
    </section>
</template>
<script>
import eventBus from '.. /.. /.. /libs/utils/bus.js'; 
export default {
   name: 'tree-item',
   props: {
      item: [Object],
      rankNum: [Number]
   },
   methods: {
      controlChildNodes() { this.foldChildNodes = ! this.foldChildNodes; eventBus.$emit("node-click", this.item);
      }
   },
}

// tree.vue
<div class="kd-tree card root">
  <tree-item 
    v-for="item in data"
    :rankNum='initRank'
    :key="item.id"
    :item='item'
  >
  </tree-item>
</div>
<script>
import eventBus from '.. /.. /.. /libs/utils/bus.js'; 
import TreeItem from './treeItem'
export default {
  props: {
    data: [Array],
    initFold: [Boolean], 
  },
  data() {
    return { initRank: 1 }
  },
  mounted() {
     eventBus.$on("node-click",(itemData) => {
        this.$emit("node-trigger", itemData);
     })
  },
}
</script>
Copy the code

This way, when the component is called externally, the node-trigger node can be retrieved. After that, target the specific business field.

Finally, the dish is cooked. Here’s how to eat it:

<template>
   <div>
      <kd-tree 
        :data='testData'
        @node-trigger="handleNodeClick">
      </kd-tree>
      <section v-show="curNode"Type: {{curnode. type}} </section> </div> </template> <script> import Tree from'@components/tree'
export default {
   name: 'kd-tree-demos',
   components: {
      'kd-tree': Tree
   },
   data() {
      return {
          testData: require('.. /public/treeData.json'),
          curNode:' '
      }
   },
   methods: {
      handleNodeClick(nodeVal) {
         if(nodeVal) {
            this.curNode = nodeVal;
         }
      }
   }
}
</script>
Copy the code

The feeling that uses so is still very brief and bright.

4. To summarize

From writing a set of component library, we can learn and comprehend many methods seldom used in writing business code at ordinary times, and strengthen our knowledge system and development ability. For the component library, we should also have a continuous development and expansion of the long-term battle preparation, only continuous optimization, will continue to adapt to a variety of complex novel business. Finally, Tencent @ when the god of a word:

The better you write, the more hair will fall out, the more convenient it will be for others, the less hair will fall out.