The demo corresponding to this article has been modified, because I added permission management to it, corresponding to the article “To do permission management? You may look at this first”, combined with better oh, can better reflect the convenience of component encapsulation


To the company for a year now, the project is made of a years through six or seven, however most of them are some of the function of the platform, and management platform is shows various forms, so in the process of development, in order to improve the development efficiency, encapsulate some generic functional components is very necessary, Here I will put me in the development process of encapsulation of the form components to share, of course there are certainly a lot of deficiencies, because so far I still have some ideas did not achieve, but also hope to communicate with each other, when thrown a brick to attract jade, hit who don’t blame me ah 0.0

The development environment

I use vue family bucket +ElementUI, after all, the management platform, not so high requirements, version of the word random, after all, just illustrate a design method, general

Demand analysis

The table page of the management platform generally contains the following functions: 1. Operation buttons (add or delete in batches). 2. Table data filtering items (commonly used have SELECT filter, time filter, search filter, etc.); 3. Form body; 4. Paging.

1. Operation button design

There are two ways to add operation buttons in my mind. The first way is to directly use the slot function provided by VUE to directly define buttons, styles and operation events of buttons externally and insert them into components internally through slots. The second type: the external component defines a button object, which is transmitted to the table component through parent-child communication. The internal component parses and renders the button object in the following format:

[{name: 'addBtn', text: 'addBtn', // button icon: 'el-icon-plus', // button icon: 'primary', // button style: 'primary', // button style () class: 'addBtn', // custom button class func: 'toAdd' // button click event}, {name: 'multiDelBtn', text: 'batch delete', icon: 'el-icon-delete', style: 'danger', class: 'multiDel', func: 'toMultiDel' } ]Copy the code

2. Table data filtering items

The design of the filter item did not expect any good, just set several commonly used ones on the above, and then judge whether to display through the parameters, other if there is a custom requirement can be defined externally, and then insert through the slot

3. Form body

The table mainly includes two parts: the table header and the table body, which are analyzed separately

My design of the table header is to define configuration items outside the component, pass them inside the component, and parse them inside the component. The format is as follows:

[{prop: 'name'.// Table data corresponding to the field
        label: 'User name'.// The table header displays the information
        sortable: false./ / whether the columns support the sorting, true | | false 'custom', can not pass (front end to true be ranked, but the front sorting doesn't make sense, half it sorts or 'custom' service side sort)
        minWidth: '100'.// The minimum width of this column (use minWidth because there is a limit that will not distort the table when it is too wide, and can extend the table proportionally when it is too wide, perfect!)
    }, {
        prop: 'address'.label: 'address'.minWidth: '170'
    }, {
        prop: 'age'.label: 'age'.sortable: 'custom', minWidth:'80'}]Copy the code

The table body is fine, just look at element-UI, okay

4. Paging

There is no such thing as pagination, which is built into the component and does not show up on less than one page

Component development

Components are encapsulated according to requirements

1. Operation button design

<div class="header-operate">
    <! -- Operation button slot -->
    <slot name="operateBtn"></slot>
    <el-button
      v-for="(btnItem, btnIndex) in propConfig.btnList"
      :key="btnIndex"
      :class="item.class"
      :type="item.style"
      :icon="item.icon"
      @click="toEmitFunc(item.func)"
    >{{item.text}}</el-button>
</div>
Copy the code

2. Design of screening items

<div class="header-filter">
    <! -- Filter item slot -->
    <slot name="filter"></slot>
    <el-select
      class="filter-iterm"
      v-if="propConfig.showFilter"
      v-model="filter"
      size="small"
      :placeholder="propConfig.filterPlaceholder"
      :clearable="true"
      filterable
      @change="handleSelect"
    >
      <el-option
        v-for="(item, index) in propConfig.filterOptions"
        :key="index"
        :label="item.label"
        :value="item.value"
      ></el-option>
    </el-select>
    <el-date-picker
      v-if="propConfig.showDatePick"
      v-model="dateRange"
      class="filter-iterm"
      align="right"
      size="small"
      format="timestamp"
      value-format="timestamp"
      :type="propConfig.datePickType"
      :clearable="true"
      :start-placeholder="propConfig.startPlaceholder"
      :end-placeholder="propConfig.endPlaceholder"
      @change="handleTimerange"
    ></el-date-picker>
    <el-input
      class="table-search filter-iterm"
      v-if="propConfig.showSearch"
      size="small"
      :placeholder="propConfig.searchPlaceholder"
      v-model="search"
      @keyup.13.native="toSearch"
    >
      <i slot="suffix" class="el-input__icon el-icon-search" @click="toSearch"></i>
    </el-input>
</div>
Copy the code

3. Main design of table

<el-table
    :data="tableData.tbody"
    style="width: 100%"
    @selection-change="handleSelection"
    @sort-change="handleSort"
    >
    <el-table-column v-if="tableData.isMulti" type="selection" width="50"></el-table-column>
    <template v-for="(item, index) in tableData.thead">
      <el-table-column
        :key="index"
        :prop="item.prop"
        :label="item.label"
        :width="item.width"
        :min-width="item.minWidth"
        :sortable="item.sortable"
      >
        <template slot-scope="scope">
          <span class="default-cell" :title="scope.row[item.prop]">{{scope.row[item.prop]}}</span>
        </template>
      </el-table-column>
    </template>
</el-table>
Copy the code

4. Paging design

<el-pagination
    class="table-pagination"
    v-if="tableData.pageInfo.total > tableData.pageInfo.size"
    layout="total, prev, pager, next, jumper"
    :current-page.sync="tableData.pageInfo.page"
    :page-size="tableData.pageInfo.size"
    :total="tableData.pageInfo.total"
    @current-change="pageChange"
></el-pagination>
Copy the code

5. Accept and participate in methods

props: {
    tableConfig: {
      type: Object.default: (a)= > {
        return{}}},tableData: {
      type: Object.default: (a)= > {
        return {
          thead: [].tbody: [].isMulti: false.// Whether to display multiple options
          pageInfo: { page: 1.size: 10.total: 0 } // 10 data per page by default}}}},methods: {
    toEmitFunc (funName, params) {
      this.$emit(funName, params)
    },
    toSearch () {
      this.toEmitFunc('setFilter', { search: this.search, page: 1 })
    },
    pageChange (val) {
      this.toEmitFunc('setFilter', { page: val })
    },
    handleSelection (val) {
      let cluster = {
        id: [].status: [].grantee: [].rows: []
      }
      val.forEach(function (element) {
        cluster.id.push(element.id)
        cluster.status.push(element.status)
        cluster.rows.push(element)
        if (element.grantee) cluster.grantee.push(element.grantee)
      })
      this.toEmitFunc('selectionChange', cluster)
    },
    handleSort (value) {
      this.toEmitFunc('setFilter', {
        prop: value.prop,
        order: value.order
      })
    },
    handleTimerange () {
        if (this.dateRange) {
            this.eventBus('setFilter', {
              startTime: this.dateRange[0].endTime: this.dateRange[1]})}else {
            this.eventBus('setFilter', {
              startTime: ' '.endTime: ' '
            })
        }
    },
    handleSelect () {
      this.toEmitFunc('setFilter', {
        filter: this.filter
      })
    }
  }
Copy the code

If you don’t use element’s table, do you? B: well… That makes sense, and I understand you guys 0.0, and I understand you guys the better I will add some of my preferences

Write the demo first, in the process of writing step by step to improve

<! -- External reference file -->
<template>
  <div class="home">
    <TableComponent :loading="loading" :tableConfig="tableConfig" :tableData="tableData"/>
  </div>
</template>

<script>
import TableComponent from '.. /components/TableComponent'
export default {
  name: 'home'.components: { TableComponent },
  data () {
    return {
      tableConfig: {
        btnList: [{name: 'addBtn'.text: 'new'.icon: 'el-icon-plus'.style: 'primary'.class: 'addBtn'.func: 'toAdd'
          }, {
            name: 'multiDelBtn'.text: 'Batch delete'.icon: 'el-icon-delete'.style: 'danger'.class: 'multiDel'.func: 'toMultiDel'}].showFilter: true.filterOptions: [].showDatePick: true
      },
      tableData: {
        thead: [].tbody: [].isMulti: false.pageInfo: { page: 1.size: 10.total: 0}},loading: false
    }
  },
  created () {
    this.toSetTdata()
  },
  methods: {
    toSetTdata () {
      let that = this
      that.loading = true
      that.tableData.thead = [
        {
          prop: 'name'.label: 'User name'.minWidth: '100'
        }, {
          prop: 'age'.label: 'age'.sortable: 'custom'.minWidth: '92'
        }, {
          prop: 'address'.label: 'Home Address'.minWidth: '130'
        }, {
          prop: 'status'.label: 'Account Status'.minWidth: '100'
        }, {
          prop: 'email'.label: 'Email address'.minWidth: '134'
        }, {
          prop: 'createdTime'.label: 'Add time'.minWidth: '128'
        }
      ]
      setTimeout((a)= > {
        that.tableData.tbody = [
          { "id": "810000199002137628"."name": "Deng Lei"."age": 23."address": Menghai County."status": "offline"."email": "[email protected]"."createdTime": 1560218008 },
          { "id": "650000197210064188"."name": "Cai Gang"."age": 26."address": "Square Platform Area"."status": "online"."email": "[email protected]"."createdTime": 1500078008 },
          { "id": "450000199109254165"."name": "Girls"."age": 22."address": "Other districts"."status": "online"."email": "[email protected]"."createdTime": 1260078008 },
          { "id": "440000198912192761"."name": "Cao Ming"."age": 25."address": "Other districts"."status": "online"."email": "[email protected]"."createdTime": 1260078008 },
          { "id": "310000198807038763"."name": "Hou Jing"."age": 21."address": Laicheng District."status": "offline"."email": "[email protected]"."createdTime": 1560078008 },
          { "id": "310000198406163029"."name": "Tan Tao"."age": 29."address": "Zhabei District"."status": "offline"."email": "[email protected]"."createdTime": 1500078008 },
          { "id": "220000199605161598"."name": "Luo Xiulan"."age": 27."address": "Other districts"."status": "online"."email": "[email protected]"."createdTime": 1560078008 },
          { "id": "37000019810301051X"."name": "Li Min"."age": 27."address": "Other districts"."status": "online"."email": "[email protected]"."createdTime": 1560078008 },
          { "id": "440000201411267619"."name": "Huang Qiang"."age": 24."address": Hepu County."status": "offline"."email": "[email protected]"."createdTime": 1560218008 },
          { "id": "440000200504038626"."name": "Ye Yan"."age": 25."address": Dadukou District."status": "offline"."email": "[email protected]"."createdTime": 1560078008 }
        ]
        that.tableData.isMulti = true
        that.tableData.pageInfo = { page: 1.size: 10.total: 135 }
        that.loading = false
      }, 500) // Simulate request latency}}}</script>
Copy the code

After writing the demo, I found that many functions are not very good, the function is very single, I found the problem:

  • For the time column, the back end doesn’t necessarily pass in data that can be displayed directly, but what if it’s a timestamp? This is where you need the front end to do some formatting
  • In the status column, simple text is not obvious, and a status label or other style is often needed to display it
  • Many times I’ve encountered interaction design that requires clicking on a name in a list (not necessarily a name, just a click that takes you to the details page) to go to the details page of that user
  • What if there are action items in the list? How can action items be configured to be passed inside components?

There are problems. 0.0 one by one

First, the time column needs to be formatted in the same way as the column in the table, so we can put the configuration information in the table header and parse it in the table component, defining the formatFn:

{
  prop: 'createdTime'.label: 'Add time'.minWidth: '128'.formatFn: 'timeFormat'
}
Copy the code

Add formatFn judgment to the component

<template slot-scope="scope">
  <span
    class="default-cell"
    v-if=! "" item.formatFn || item.formatFn === ''"
    :title="scope.row[item.prop]"
  >{{scope.row[item.prop]}}</span>
  <span
    v-else
    :class="item.formatFn"
    :title="formatFunc(item.formatFn, scope.row[item.prop], scope.row)"
  >{{formatFunc(item.formatFn, scope.row[item.prop], scope.row)}}</span>
</template>
Copy the code

Add utils classes, write formatting data methods, and register globals

// Format method file (format.js)
export default {
  install (Vue, options) {
    Vue.prototype.formatFunc = (fnName = 'default', data = ' ', row = {}) = > {
      const fnMap = {
        default: data= > data,
        /** * Timestamp conversion time * the data that takes two arguments needs to be formatted * To prevent certain formatting rules that require additional data information from the current row of the table to be extended to pass the argument row (not passed) */
        timeFormat: (data, row) = > {
          return unixToTime(data) // unixToTime is a timestamp method I wrote. If your project has references to other similar methods, return the formatted data here}}return fnMap[fnName](data, row)
    }
  }
}
Copy the code

So if there are other formatting rules can also be through custom formatting method, and then to do is call the method defined in a header, and the format method not only can be used in the table, other any place you want to format can be defined in this document, and use directly, and don’t have to introduce a method to use, Will it be convenient (the better we understand you guys ~ 3 ~)

This should make it clear that the core of the table component is the use of this formatting method

Moving on to the second question, the state column requires not only the change of data, but also the transformation of corresponding state data into corresponding labels, which requires the expansion of the table component and the addition of new judgment logic

{
    prop: 'status'.label: 'Account Status'.minWidth: '100'.formatFn: 'formatAccountStatus'.formatType: 'dom'
}
Copy the code
<template slot-scope="scope">
  <! -- No need to handle -->
  <span
    class="default-cell"
    v-if=! "" item.formatFn || item.formatFn === ''"
    :title="scope.row[item.prop]"
  >{{scope.row[item.prop]}}</span>
  <! -- Needs to be processed as a tag -->
  <span
    v-else-if="item.formatType === 'dom'"
    :class="item.formatFn"
    v-html="formatFunc(item.formatFn, scope.row[item.prop], scope.row)"
  ></span>
  <! Simple data processing -->
  <span
    v-else
    :class="item.formatFn"
    :title="formatFunc(item.formatFn, scope.row[item.prop], scope.row)"
  >{{formatFunc(item.formatFn, scope.row[item.prop], scope.row)}}</span>
</template>
Copy the code

I was thinking of format as an element-UI tag, as in

Define the state relation table and add the format method

const accountStatusMaps = {
  status: {
    online: 'online'.offline: 'offline'
  },
  type: {
    online: 'success'.offline: 'warning'}}// User account status transfer label
formatAccountStatus: (data, row) = > {
  return `<span class="status-tag ${accountStatusMaps.type[data]}">${accountStatusMaps.status[data]}</span>`
}
Copy the code

Such general style format can be satisfied, but some of the more complex needs his handwriting is not easy, but I didn’t know that a good solution, plus it’s a difficult demand less, so I have been playing (maybe that’s why I grow technology co., LTD., 0.0). If you have any good suggestions, welcome to raise ah

Continue to the third, in the background management platform is easy to meet this demand, click on the name into the details, I still want to use the format, the format of an a label, then the href is want to jump to the address, but the function is achieved, but using a tag has a big problem is the page out of the feeling is too strong, can only give up this approach, Then it’s not a good way to make an issue of thought in the header, and a new definition formatType = > ‘link, optional parameters linkUrl definition jump links, modify the table component template

<span
    v-if="item.formatType === 'link'"
    :class="item.formatClass || 'to-detail-link'"
    :title="scope.row[item.prop]"
    @click="toLink(item.linkUrl, scope.row)"
>{{scope.row[item.prop]}}</span>
Copy the code
toLink (url, row) {
  if (url) {
    this.$router.push(`${url}/${row.id}`)}else {
    this.$router.push(this.$route.path + '/detail/' + row.id)
  }
}
Copy the code
.to-detail-link { color: #1c92ff; cursor: pointer; &:hover { color: #66b1ff; }}Copy the code

To satisfy the demand, but just a compromise, how to return to the label on the format string binding method above, if you have any idea, be obliged, solved the problem makes this component functions to improve a big step, because the details function is general, can be in the component compatibility, but if it is other click method? You can’t always be compatible bit by bit, so you lose the point of encapsulating components, because compatibility is inexhaustible.

Moving on, operation items in the background management platform is indispensable, the main problem is how to pass, throw click method, my side is so implemented

To modify the template

<span class="table-operation" v-if="item.prop === 'operation' && scope.row.hasOwnProperty('operation')">
    <span
      class="text-btn"
      v-for="(item, index) in scope.row.operation"
      :class="item.type"
      :key="index"
      @click="toEmitFunc(item.event, scope.row)"
    >{{item.text}}</span>
</span>
Copy the code

Set the operation item data

computed: {
    operateConfig () {
      return {
        optType: {
          toEdit: {
            event: 'toEdit'.// The method invoked by the action button
            text: 'edit'.// Handle the copy displayed by the button
            type: 'primary' // The style displayed by the operation button
          },
          toDel: {
            event: 'toDel'.text: 'delete'.type: 'danger'}},optFunc: function (row) {
        // Online Users cannot be deleted
          if (row.status === 'offline') {
            return ['toEdit'.'toDel']}else {
            return ['toEdit']}}}}}Copy the code

Pull some generic properties and methods out of tableMixins to reduce the amount of writing per call

// tableMixins.js
// Table method mixins
export default {
  data() {
    return {
      // Table data, refer to interface data
      tableData: {
        thead: [].tbody: [].isMulti: false.pageInfo: { page: 1.size: 10.total: 0}},// Whether the table is in loading state
      loading: true.// Select multiple, selected data
      selection: [],
      Query criteria, including sort, search, and filter
      searchCondition: {}
    }
  },
  mounted: function () {},methods: {
    // Multiple select events that return the selected rows and the current state of each row
    selectionChange(value) {
      thisAfterListSet (res) {.selection = value}, thead, tBody, pageInfo, etc.let formData = this.setOperation(res)
      if (formData.thead) {
        this.tableData.thead = JSON.parse(JSON.stringify(formData.thead))
      }
      this.tableData.tbody = formData.tbody
      if (formData.pageInfo) {
        this.tableData.pageInfo = JSON.parse(JSON.stringify(formData.pageInfo))
      }
      formData.isMulti && (this.tableData.isMulti = formData.isMulti)
      let query = JSON.parse(JSON.stringify(this.$route.query))
      this.$router.replace({
        query: Object.assign(query, { page: this.tableData.pageInfo.page })
      })
      this.loading = false
    },
    // Iterate over the raw data, insert the action item method sequence defined in the front end, set the action item
    setOperation(res) {
      let that = this
      let tdata = JSON.parse(JSON.stringify(res))
      if (that.operateConfig && that.operateConfig.optFunc) {
        for (let i in tdata.tbody) {
          let temp = that.operateConfig.optFunc(tdata.tbody[i])
          let operation = []
          for (let j in temp) {
            operation.push(that.operateConfig.optType[temp[j]])
          }
          that.$set(tdata.tbody[i], 'operation', operation)
        }
      }
      return tdata
    }
  }
}
Copy the code

After such a set of combination, the common table functions of the management platform have been basically realized. Please paste the results map (the style is not written, this can be adjusted according to your own project style).

The source address

thinking

Component is finished, the article also finished water, reflections on some shortage, one is how to render the UI component tag, the other one is how to format the label on the binding method, the true component development process there are a lot of compromise, the compromise is level or their own field of vision, failed to hit form of cast components come out is to put our heads together, If this article is helpful to you and can help you improve, I will be very happy. If you help me solve the difficulties I meet and help me improve, I will be even more happy. That is what I am for

Of course, IN the process of component development, I still made some very humanized designs. One is to provide a method for the simultaneous effect of multiple flush filter conditions, that is, to maintain a searchCondition, and add it to the searchCondition every time the filter works. I then pass the searchCondition to the data query method. This method can be tried in the demo, the console will have output, another little feature that I have to make fun of is an anti-human design of a lot of open source components, rich text editor many of you have used, there will be a menu configuration item, If you wear nothing for this configuration item it will display all menu items by default, but if you do not want one of the menu items, you set this item to false in the menu configuration……. Then all the menus are completely gone, I have seen some source code is directly using the menu passed in to override the default menu configuration item, so I just want to cancel one of the items, I have to configure all menu items and all set to true, this… So I added a method to pass parameters inside the component, using a deconstructed assignment to maintain a computed object, as follows:

computed: {
    propConfig () {
      // defaultConfig is the default configuration item, tableConfig is the configuration item passed in by the parent component
      return{... this.defaultConfig, ... this.tableConfig } } }Copy the code

In this way, the default configuration item is changed only when one of the parent components is changed

That’s it. I hope it’ll be helpful