Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”

This article has participated in the “Digitalstar Project” and won a creative gift package to challenge the creative incentive money.

preface

When we do the background management system, in fact, we contact the most component is the table component, so the table component is good or not, directly related to the efficiency of our background management system. So today we’re going to talk about how we can encapsulate the table component of the Element-UI and integrate more functionality into it, so that we can write less code and implement more and more stable functionality. Without further ado, let’s get started.

function

Let’s start by sorting out what features we need to implement.

  1. Combination of pagingPaginationComponent to implement table pagination
  2. Table add slot, you can customize the style, data into the table inside
  3. Implement multiple table headers
  4. Implement cross-page check data
  5. Use of query Function[Input box, multi-check box, single check box, date box]And other components to achieve query interaction
  6. Ordinal cumulative sort
  7. Better operation of ascending and descending sequence function
  8. To achieve query conditions display, may be table query can be based on a lot of conditions query, with the query conditions display, you can be clear, see the list is based on which some conditions query results.
  9. Dynamic table header implementation, it is possible that the table has a lot of table header, the user at a glance is difficult to find want to see some table header. So you need a header filtering function.

Basic implementation

To implement the feture mentioned above, we must first add the inherent functionality of the Element-UI. So let’s start by putting these basic functions on our own components.

<template>
    <el-table v-bind="$attrs" v-on="$listeners">
        <template v-for="(item) in columns">
          <el-table-column
            :key="item.prop"
            v-bind="item"
            show-overflow-tooltip> 
          </el-table-column>
        </template>
    </el-table>
</template>

<script>
 export default {
  name: 'miniTable'.props: {
    columns: {
      type: Array.default: () = >[],,}}}</script>

<mini-table border :columns="columns"></mini-table>
columns: [
    { label: 'name'.prop: 'name'.align: 'center' },
    { label: 'age'.prop: 'age'.align: 'center' },
    { label: 'hobby'.prop: 'hobby'.align: 'center' },
    { label: 'degree'.prop: 'education'.align: 'center' },
    { label: 'native'.prop: 'nativePlace'.align: 'center' },
    { label: 'note'.prop: 'remark'.align: 'left'.width: 200}].Copy the code

This completes our basic functionality in Element-UI. The above mainly depends onvueTo provide the$attrand$listenersTo achieve the function.

Listeners contain ‘V −on’ event listeners in the parent scope (without the ‘. Native ‘modifier). Listeners can include v-on event listeners in their parent scope (without the. Native modifier) via ‘V −on=”. The listeners can include ‘V −on’ event listeners in the parent scope (without the ‘. Native ‘modifier) via’ V-on =”. It can be passed into internal components via ‘V −on=” Listeners “– useful for creating higher-level components. $attrs contains attribute bindings (except class and style) that are not recognized (and retrieved) as prop in the parent scope. When a component does not declare any prop, all parent-scoped bindings (except class and style) are included, and internal components can be passed in via V-bind =”$attrs” — useful when creating higher-level components.

These two attributes are described in the official VUE documentation. These two properties provide a higher level of encapsulation for existing components. To allow users to pass in properties or methods to the element-UI component while using our component. Of course, if there’s something we need to do in the element-UI event, we can tweak the code. We’ll talk about that later. Then there is the need to pass attributes transparently to elemental-UI components. To avoid unnecessary conflicts, we will prefix the names of the attributes we use in our components with __ to indicate that they are private to our own components.

Integrated paging

On the first code

<el-pagination
  class="pagination"
  background
  v-if="hasPagination"
  @size-change="sizeChange"
  @current-change="currentChange"
  :total="pagination.totalRow"
  :current-page="pagination.pageIndex"
  :page-size="pagination.pageSize"
  :page-sizes="pageSizes"
  :layout="layout">
</el-pagination>

// methods
/** * Switch the number of pages *@param { Number } PageSize pages * /
sizeChange (pageSize) {
  this.pagination.pageIndex = 1
  this.pagination.pageSize = pageSize
  this.queryData()
},
/** * Switch page number *@param { Number } PageIndex page * /
currentChange (pageIndex) {
  this.pagination.pageIndex = pageIndex
  this.queryData(true)},Copy the code

There’s nothing special to say about pagination; the paging logic is basically a function that’s coupled to a table. Therefore, the pageSizes and layout receive external input while the pageSizes and layout are defined directly in the component’s data. If not, give a default value. There are also cases where page numbers are not required. So you can also pass in hasPagination externally, false if you don’t need a page number. The default true

Table custom content

It doesn’t have to be plain text in the table. You might need to render a button, or an image might be a percentage progress bar. Then what exactly is unknown in our component. So we need to provide a slot in the component for the user to customize the content. Keep components flexible. So what do you need to do? Let’s look at the code implementation:

/ / add the slot
<el-table-column
  :key="item.prop"
  v-bind="item"
  show-overflow-tooltip> 
    <template v-if="item.__slotName" v-slot="scope">
      <slot :name="item.__slotName" :data="scope"></slot>
    </template>
</el-table-column>

// data
columns: [{ label: 'avatar'.prop: 'avatar'.align: 'center'.__slotName: 'avatar' }]

// Use the miniTable component
<mini-table size="small" border :columns="columns">
  <template slot="avatar" slot-scope="scope">
    <img slot="avatar" width="40" :src="scope.data.row.avatar" />
  </template>
</mini-table>
Copy the code

If __slotName is passed, then slot is used, and scope is passed to the slot. The name of the slot is passed as __slotName. In this way, when we use the component, we can define a slot=”avatar” slot and get the row data given by the component.

Then, we can write a row with the head data to the table.

More header

In real projects, we also encounter the problem of multiple headers. It could be secondary headers, it could be tertiary headers, it could be more. In order to satisfy all cases, we implement multiple headers using component recursion.

<template>
<el-table-column
v-bind="item"
:key="item.prop"
show-overflow-tooltip> 
  <template v-for="obj in item.__children">
    <my-table-column v-if="obj.__children" :item="obj" v-bind="obj" :key="obj.prop"></my-table-column>
    <el-table-column
      v-else
      :key="obj.prop"
      v-bind="obj"
      show-overflow-tooltip> 
      <template v-if="obj.__slotName" v-slot="scope">
        <slot :name="obj.__slotName" :data="scope"></slot>
      </template>
    </el-table-column>
  </template>
</el-table-column>
</template>

<script>
import MyTableColumn from './tableColumn'

export default {
 name: 'MyTableColumn'.components: {
   MyTableColumn
 },
 props: {
   item: {
     type: Object.default: () = >{}}}}</script>
Copy the code

A tableColumn file is created above, which is a recursive component if __children is present in the header, and a secondary header if not. When we use it, add a data test like this

{ label: 'Address information'.prop: 'address'.align: 'center'.__children: [{label: 'province'.prop: 'province'.align: 'center'
    },
    {
      label: 'city'.prop: 'city'.align: 'center'.__children: [{label: 'area'.prop: 'area'.align: 'center'}, {label: 'county'.prop: 'county'.align: 'center',}]}]}Copy the code

And finally we’re going to do it according to ourjsonData description, get what we want.

Select data across pages

Cross-page check, in fact, we are more common a requirement. The user wants to check the data at the bottom of the first page and at the head of the second page. In general, if you check the data on the first page, then click the second page, because the data is refreshed. Check it and it’s gone. It’s also easy to do cross-page checkboxes, and Element-UI actually provides that. Two main attributes are used.

Reserve-selection: applies only to columns with type=selection. The type is Boolean. If true, the previously selected data will be retained after the data is updated (row-key required).

Row-key: the key of the row data, used to optimize the rendering of the Table; In the case of reserve-selection, this property is mandatory. User.info [0]. Id is not supported. In this case, use Function

// Expose an isCheckMemory property to the outside of the component, default false, need to cross pages, set to true
props: {
     /** * Whether to select */ across pages
    isCheckMemory: {
      type: Boolean.default: false
    },
    /** * table row data unique key */
    idKey: {
      type: String.default: 'id'}}// Set row-key for table and reserve-selection for tableColumn whose type="selection"
<template>
    <el-table 
      ref="__table"
      :data="tableData"
      v-bind="$attrs"
      :row-key="idKey"
      v-on="listeners">
      <el-table-column
        v-if="isCheck"
        align="center"
        width="70"
        type="selection"
        :reserve-selection="isCheckMemory"
      >
      </el-table-column>.</el-table>
</template>
Copy the code

The above cross-page check to do a good job. And then we go back to the same thing. You go to select-change and you get all of the checked data. Note that the listeners need to do something about the data on the process internally:

  computed:  {
    listeners: function () {
      var vm = this
      return Object.assign(
        {}, 
        this.$listeners, {
          'selection-change': function (val) {
            // dosomething
            vm.selectData = val
            vm.$emit('selection-change', val)
          }
        }
      )
    }
  },
Copy the code

Column sorting

Column sorting, in fact, is also a more commonly used function, because the table is generally paging data, the front end can not get all the data, so sorting most cases are background sorting, the front end is responsible for some fields to the background, tell the background according to what field sorting, descending or ascending order.

//props passes a sortArr externally to tell the component which fields need to be sorted
/** ** sort */
sortArr: {
  type: Array.default: () = > { return[]}}// add sortable to el-table-column
<el-table-column
  v-else
  :key="item.prop"
  v-bind="item"
  :sortable="sortFun(item.prop) ? 'custom' : false "
  show-overflow-tooltip> 
    <template v-if="item.__slotName" v-slot="scope">
      <slot :name="item.__slotName" :data="scope"></slot>
    </template>
</el-table-column>

// methods
sortFun (prop) {
  if (this.sortArr && this.sortArr.length > 0) {
    return this.sortArr.indexOf(prop) > -1
  } else {
    return false}}// Monitor event sort-change
listeners: function () {
  var vm = this
  return Object.assign(
    {}, 
    this.$listeners, {
      'sort-change': function (column) {
        vm.order.sortName = column.prop
        switch (column.order) {
          case 'ascending':
            vm.order.sortOrder = 'asc'
            break
          case 'descending':
            vm.order.sortOrder = 'desc'
            break
          default:
            vm.order.sortOrder = ' '
        }
        vm.$emit('sortChange', vm.order)
      }
    }
  )
}

/ / usage
// Add sortArr to set which items need sorting
<mini-table size="small" border :sortArr="['age', 'area', 'county']" :columns="columns"></mini-table>
Copy the code

Then we can see what fields we need to sort by. SortName,sortOrder directly passed to the background to re-query ok.

The serial number sum

In the list, we need to add up the serial number. For example, the serial number 1-20 is displayed on the first page. If we do not process it and switch to the second page, the serial number displayed is still 1-20. If we want it to display the serial number 21-40, we need to do a little processing.

// template adds :index="typeIndex"
<el-table-column
    v-if="isIndex"
    show-overflow-tooltip
    align="center"
    :index="typeIndex"
    type="index"
    :fixed="fixed">
      <template slot="header">
        <span>The serial number</span>
      </template>
</el-table-column>
      
// methods
typeIndex (index) {
  const tabIndex = index + (this.pagination.pageIndex - 1) * this.pagination.pageSize + 1
  return tabIndex
}
Copy the code

So here we’re going to do a little bit of page number processing to get what we want.

Query data

Because there may be no query conditions in the table, only one table, so we put the function of querying data in the table component to do. After will also do an independent query component, query component to the user input query conditions collected, and then passed to the table component inside. For query, AXIos was first introduced. Since there was no online API to support us, I used MockJS. So let’s take a look at Axios

import axios from 'axios'

const service = axios.create({
  baseURL: '/'.timeout: 10000
})

export const get = function (url, params) {
  return service.get(url, { params })
}

export const post = function (url, data) {
  return service.post(url, { data })
}
Copy the code

Here according to the situation of their own company background modification, add interceptor. I will simply export the get and post methods. If you mockjs, you can go to the Internet to look for information, and I will directly post the code here and briefly explain

import Mock from 'mockjs'

const data = Mock.mock({
  "list|60-400": [{"id": '@increment(1)'.// Generate cumulative ids
      "name": "@cname()".// Generate the name
      "age|1-50": 1.// Generate numbers from 1 to 50
      "avatar": "@image('40x40', '#50B347', '#FFF', 'Mock.js')".// Create a 40*40 avatar
      "hobby": "@ctitle(6)".// Generate 6 characters of invalid text
      "education": "@ctitle(6)".// Generate 6 characters of invalid text
      "nativePlace": "@ctitle(6)".// Generate 6 characters of invalid text
      "province": "@ctitle(6)".// Generate 6 characters of invalid text
      "area": "@ctitle(6)".// Generate 6 characters of invalid text
      "county": "@ctitle(6)".// Generate 6 characters of invalid text
      "remark": "@csentence(20)".// Generate 20 characters of invalid text}]})// Intercepts axios' getList interface and returns the data defined above
Mock.mock(/\/getList/.'get'.(options) = > {
   // Get the passed parameter pageIndex
   const pagenum = getQuery(options.url,'pageOffset')
   // Get the passed parameter pagesize
   const pagesize = getQuery(options.url,'pageSize')
   // The starting position of the intercepting data
   const start = (pagenum-1)*pagesize
   // The end position of the intercepted data
   const end = pagenum*pagesize
   // Count the total pages
   const totalPage = Math.ceil(data.list.length/pagesize)
   // Start position of data: (pageIndex -1)*pagesize End position of data: pageIndex *pagesize
   constlist = pagenum>totalPage? []:data.list.slice(start,end)return {
    status: 200.success: true.message: 'Succeeded in getting news list'.list: list,
    total: data.list.length
  }
})

// Get the query interface? The argument after the ampersand
const getQuery = (url,name) = >{
  const index = url.indexOf('? ')
  if(index ! = = -1) {
    const queryStrArr = url.substr(index+1).split('&')
    for(var i=0; i<queryStrArr.length; i++) {const itemArr = queryStrArr[i].split('=')
      if(itemArr[0] === name) {
        return itemArr[1]}}}return null
}

export default Mock
Copy the code

The interception and definition of mockJS data is explained briefly in the comments on the code above. So the query method is as follows

    async queryData (isReset) {
      if (this.query && this.query.url) {
        this.loading = true
        this.pagination.pageIndex = isReset ? this.pagination.pageIndex : 1
        let param = Object.assign(
          {},
          this.query.queryParam, {
            [this.pageParam.pageOffset]: this.pagination.pageIndex,
            [this.pageParam.pageSize]: this.pagination.pageSize
          })
        if (this.order.sortName && this.order.sortOrder) {
          param.sortName = this.order.sortName
          param.sortOrder = this.order.sortOrder
        }
        let result = null
        try {
          switch(this.query.method) {
            case 'get':
              result = await get(this.query.url, param)
              break
            case 'post':
              result = await post(this.query.url, param)
              break
            default:
              result = await get(this.query.url, param)
              break}}catch(e) {
          console.warn(e)
        } finally {
          this.loading = false
          const { data } = result
          if (data && data.success) {
            this.pagination.totalRow = data.total
            this.tableData = data.list
            this.$emit('fetchData', data)
          } else {
            this.$message({
              type: 'warning'.message: data ? data.message : 'Query failed! '
            })
            this.tableData = []
            this.pagination.pageIndex = 1
            this.pagination.totalRow = 0}}}}Copy the code

There are a couple of things that say, request interfaces, just for good experience. Add loading and set loading to false after the request ends. There’s also pageIndex and pageSize, and everyone has a different background, so everyone’s page number might be called differently. We can pass in the name of the field for our background page number in props. And then this side will change to the field that you want.

/** * The field */ is passed in the background for paging
pageParam: {
  type: Object.default: () = > {
    return {
      pageOffset: 'pageOffset'.pageSize: 'pageSize'}}},Copy the code

Method supports post and GET requests. Just pass it in when you use the component

    <mini-table :isAutoQuery="true" :query="query" size="small" border :sortArr="['age', 'area', 'county']" :columns="columns">... </mini-table>// data
query: {
    url: '/getList'.method: 'get'.queryParam: {}},Copy the code

Ok, so let’s look at the result:

Write in the last

Ok, so a common table requires some functionality, and we’ve basically integrated it into our own component. If there are some minor problems, we only need to make some changes inside the component. After a period of use in the project, the component will become more and more stable, and at the same time, it will be flexible. After the project, the more we can reflect the value of doing such a component. We’ll add form search and dynamic table headers to this later. If you think the article has some of the most use, you might as well like a wave of attention. In addition, after completing all the features I mentioned. I’ll submit the code to Gitee for anyone who wants to clone it and look at it. See you next time!