Element-ui table

Table displays multiple pieces of data with similar structures

Table Attributes

parameter instructions type An optional value The default value
data Displayed data Array
fit Whether the width of the column spreads itself boolean true
show-header Whether to display the table header boolean true
row-key Row data Key, used to optimize Table rendering Function(row) / string
empty-text The text content to be displayed when the data is empty, or slot=”empty” String Temporarily no data

Table-column Attributes

parameter instructions type An optional value The default value
label Display title string
prop The name of the field corresponding to the column content string
width Corresponding to the column width string

Basic table presentation usage

<template>
    <el-table
      :data="tableData"
      style="width: 100%">
      <el-table-column prop="date" label="Date" width="180"></el-table-column>
      <el-table-column prop="name" label="Name" width="180"></el-table-column>
      <el-table-column prop="address" label="Address"></el-table-column>
    </el-table>
  </template>
Copy the code

The Table structure

Implementation approach

  • Table. Vue controls table rendering, table-column controls column rendering, table-header header rendering, table-body rendering, Table-layout defines attributes and methods for sharing the overall table. Table-observer A table observer (a callback after changes to the overall table, notifies the table header and body to make corresponding updates, and ensures that the table-header and table-body change synchronizes).
  • Create the table in table.vueState manager storeTo share the table-column, table-header, and table-body components.
  • The table-column component inserts the attributes and methods associated with the column intostore.states.columns, table-header and table-body render cells according to store;
  • By setting the tableWidth property of colEnsure that the table head and body cells are the same width, there is no need to repeatedly set the width of each cell;

The directory structure

  • table.vue
  • table-column.js
  • table-header.js
  • table-body.js
  • table-layout.js
  • table-observer.js

table.vue

Table rendering implementation

Tamplate rendering
<template> <! How does the table component render the tabel-column child? Columns y-table: data submitted to a public datapurp Y-table: Data submitted to a public datapurp Table layout ---------------------- How to ensure th and TD are the same width? Select * from table_column where colGroup = colgroup; select * from table_column where colGroup = colgroup;<div class="y-table"
        :class="[{ 'y-table__border': border }]">
        <! -- Native -- child -- column rendering -->
        <div class="hidden-columns" ref="hiddenColumns">
            <slot></slot>
        </div>
        <! -- -- -- > meter
        <div v-if="showHeader"
            class="y-table__header-wrapper"
            ref="headerWrapper">
            <table-header
                ref="tableHeader"
                :store="store"
                :style="{ width: bodyWidth }"></table-header>
        </div>
        <! - table body -- -- >
        <div
            class="y-table__body-wrapper"
            ref="bodyWrapper">
            <table-body
                :store="store"
                :style="{ width: bodyWidth }"></table-body>
        </div>
        <! -- Table data is empty -->
        <div
            v-if=! "" data || data.length === 0"
            class="y-table__empty-block"
            ref="emptyBolck">
            <span class="y-table__empty-text">
                <! -- Custom table null data display -->
                <slot name="empty">{{emptyText}}</slot>
            </span>
        </div>
        <! - footer - >
    </div>
</template>
Copy the code
script
<script>
import TableLayout from './table-layout';
import TableHeader from './table-header';
import TableBody from './table-body';
import {
    createStore, 
    mapStates
} from './store/helper';
import {
    addResizeListener,
    removeEventListener
} from '.. /.. /.. /src/utils/resize-event';

let tableIdSeed = 1;
export default {

    name: 'YTable'.components: {
        TableHeader,
        TableBody,
    },

    props: {
        // Data must be Array, and the default value is empty Array []
        data: {
            // Specify the array type of data. If the data passed is not an array type, vue will report an error
            type: Array.// Object or array defaults must be obtained from a factory function
            default: function () {
                return[]; }},// border must be a Boolean type
        border: Boolean.// Whether to display the header, Boolean type, default true
        showHeader: {
            type: Boolean.default: true
        },

        // The row key is used to optimize table rendering
        rowKey: [String.Function].// Whether the width of the column is automatically spread
        fit: {
            type: Boolean.default: true
        },

        /** ** The text to be displayed when the table data is empty. */ can also be set with slot="empty"
        emptyText: {
            type: String.default: 'No data at present',}},computed: {
        / * * * reference: an operator * https://vuex.vuejs.org/zh/guide/state.html#mapstate-%E8%BE%85%E5%8A%A9%E5%87%BD%E6%95%B0 * Bablrc file :{"plugins": ["transform-object-rest-spread"]} */. mapStates({columns: 'columns'.tableData: 'data',}}),data() {
        this.store = createStore(this, {
            rowKey: this.rowKey,
        });
        // TableLayout Sets the related properties of the table
        const layout = new TableLayout({
            store: this.store,
            table: this.showHeader: this.showHeader,
        });
        return {
            layout,
            resizeState: {
                width: null.height: null,}}},methods: {
        doLayout() {
            this.layout.updateColumnsWidth(); }},watch: {
        / / to monitor data
        data: {
            // Listen immediately
            immediate: true.handler(value) {
                // Commit data to the public datapool
                this.store.commit('setData', value); }}},/** * child components: table-header and table-body * $children: [] empty array */
    created() {
        // debugger;
        this.tableId = 'y-table_' + tableIdSeed++;
    },

    beforeMount() {
        // debugger;
    },

    /** * $children: [table-column, table-header, table-body] */
    mounted() {
        // debugger;
        
        / / update the column
        this.store.updateColumns();

        // Set the width of col
        this.doLayout();

        this.resizeState = {
            width: this.$el.offsetWidth,
            height: this.$el.offsetHeight,
        }
        
        this.$ready = true;
    },
}
</script>
Copy the code

table-column.js

/** * table-column:table sub-component, column ** Note: this file is not a Vue file, but a JS file * JSX language render DOM (need to install the corresponding plug-in to support JSX) * https://cn.vuejs.org/v2/guide/render-function.html * the js is table - column components, so when using the component, will be passed two attributes prop and label * How do these two attributes work in order to add the attributes of the column to the columns? * /
import {
    mergeOptions, 
    parseWidth,
    parseMinWidth,
    compose,
} from './utils';

import {
    defaultRenderCell,
} from './config';

let columnIdSeed = 1;

export default {
    name: 'YTableColumn'./ / prop by value
    props: {
        // Table data item field name
        prop: String.property: String./ / title
        label: String./ / column width
        width: {},
        minWidth: {},},data() {
        return {
            isSubColumn: false.columns: [].}},// Compute attributes for caching
    computed: {
        // owner Chinese meaning the parent of the table component
        owner() {
            let parent = this.$parent;
            while(parent && ! parent.tableId) { parent = parent.$parent; }return parent;
        },
        /** * Is the parent component of this table-column table or table-column * if it is table-column, it is used for cell combination */
        columnOrTableParent() {
            let parent = this.$parent;
            while(parent && ! parent.tableId && ! parent.columnId) { parent = parent.$parent; }return parent;
        },

        / / width
        realWidth() {
            return parseWidth(this.width);
        },
        
        // Minimum width
        realMinWidth() {
            return parseMinWidth(this.minWidth); }},methods: {
        // reduce pairwise comparison operation, concatenation between arrays
        getPropsData(. props) {
            return props.reduce((prev,cur) = > {
                if(Array.isArray(cur)) {
                    cur.forEach((key) = > {
                        prev[key] = this[key]; // Assign the value corresponding to the key in the data})}return prev;
            }, {});
        },

        // Get the position of child in children
        getColumnElIndex(children, child) {
            // console.log('getColumnElIndex', children, child);
            return [].indexOf.call(children, child);
        },

        // Set the column width
        setColumnWidth(column) {
            if(this.realWidth) {
                column.width = this.realWidth;
            }

            if(this.realMinWidth) {
                column.minWidth = this.realMinWidth;
            }

            if(! column.minWidth) { column.minWidth =80;
            }

            column.realWidth = column.width === undefined ? column.minWidth : column.width;
            
            return column;
        },

        /** * Set the render function for the column in the table-column * the render of the column is called with the table-header *@param {Object} column 
         * @returns column* /
        setColumnRenders(column) {
            // console.log('setColumnRenders', column);
            let originRenderCell = column.renderCell;
            // console.log('setColumnRenders', originRenderCell, defaultRenderCell)

            originRenderCell = originRenderCell || defaultRenderCell;

            // Wrap renderCell
            column.renderCell = (h, data) = > {
                let children = null;
                // console.log('renderCell', h, data, this.$scopeSlots);

                // console.log('renderCell', this.$scopedSlots, this)

                // If there is a default slot for the column, the default slot is rendered so that the custom rendering for each column is collated to each column of the table-body
                // The rendering of the cells in the table body needs to be separated separately and the rendering function for each column needs to be called so that the customization of each column in the table column is synchronized to the table body
                // Otherwise, only the corresponding data values of the column are displayed
                if(this.$scopedSlots.default) {
                    // vm.$scopedSlots Sample scope slots and pass in data
                    children = this.$scopedSlots.default(data);
                }else {
                    children = originRenderCell(h, data);
                }

                const props = {
                    class: 'cell'.style: {}};return (
                    <div {. props} >
                        {children}
                    </div>)}returncolumn; }},/ / initialization
    beforeCreate() {
        // debugger;
        this.column = {};
        this.columnId = ' ';
    },

    / / initialization
    created() {
        // debugger;
        
        // Define the columnId of the table-column component
        const parent = this.columnOrTableParent;
        // Is a child column, if the two are not equal, it is a child column (the direct parent component is also table-column)
        this.isSubColumn = this.owner ! == parent;this.columnId = (parent.tableId || parent.columnId) + '_column_' + columnIdSeed++;

        // Define the default attribute, temporarily only id
        const defaults = {
            id: this.columnId,
            // The name of the field corresponding to the contents of the column. You can also use the property attribute
            property: this.prop || this.property,
        }

        // Base attributes
        // const basicProps = ['label', 'prop',];
        const basicProps = ['label',];
        
        Collect the column attribute
        let column = this.getPropsData(basicProps);
        column = mergeOptions(defaults, column);

        "// Note that compose is executed from right to left, and now chains equals a function
        // const chains = compose(this.setColumnRenders, this.setColumnWidth, this.setColumnForcedProps);
        const chains = compose(this.setColumnRenders, this.setColumnWidth);
        column = chains(column);

        this.columnConfig = column;
    },
    
    mounted() {
        // debugger;
        const owner = this.owner;
        const parent = this.columnOrTableParent;

        // vm.$children direct child of the current instance (virtual node)
        // vm.$el.children direct children of the current DOM node (real DOM node)
        // If the direct parent component is table-column
        const children = this.isSubColumn ? parent.$el.children : parent.$refs.hiddenColumns.children;
        
        const columnIndex = this.getColumnElIndex(children, this.$el);
        // It is critical to store the attributes of this column into the table's public pool
        // Store of the parent component table
        owner.store.commit('insertColumn'.this.columnConfig, columnIndex, this.isSubColumn ? parent.columnConfig : null);
    },

    / / render rendering
    render(h) {
        console.log('table-column', h,  this.$slots);
        return h('div'.this.$slots.default); }}Copy the code

[note] :

  • Key code: Store the column’s attributes into the common pool of the table
owner.store.commit('insertColumn'.this.columnConfig, columnIndex, this.isSubColumn ? parent.columnConfig : null);
Copy the code
  • Render function
render(h) {
    return h('div'.this.$slots.default);
}
Copy the code

table-header.js

JSX rendering

render(h) {
    let columnRows = convertToRows(this.columns);
    return (
        <table
             class="y-table__header"
             cellspacing="0"
             cellpadding="0"
             border="0">
             <colgroup>
                {
                  this.columns.map(column => <col name={column.id} key={column.id} />)}</colgroup>
             <thead>
                 <tr>
                    {
                        this._l(columnRows, (column, cellIndex) => (
                           <th
                              key={cellIndex}
                              colspan={column.colspan}
                              rowspan={column.rowspan}>
                              <div
                                 class="cell">
                                 {column.label}
                              </div>
                            </th>))}</tr>
            </thead>
       </table>)}Copy the code

[note] :

  • This. _l is a function that renders a list, renderList

table-body.js

JSX rendering

render(h) {
    const data = this.data || [];
    const columns = this.columns || [];
    return (
        <table
            class="y-table__body"
            cellspacing="0"
            cellpadding="0"
            border="0">
            <colgroup>
                {
                    columns.map(column => <col name={column.id} key={column.id} />)}</colgroup>
            <tbody>
                {
                    data.reduce((acc, row) => {
                       return acc.concat(this.wrappedRowRender(row, acc.length))
                    }, [])
                }
            </tbody>
       </table>)}Copy the code

[note]

  • JSX requires plug-in support
  • The official document: cn.vuejs.org/v2/guide/re…
  • npm install @vue/babel-preset-jsx @vue/babel-helper-vue-jsx-merge-props
  • {“plugins”: [“transform-object-rest-spread”,”transform-vue-jsx”]}

table-layout.js

import Vue from 'vue';

/** * TableLayout defines attributes and methods related to the table as a whole * used in table.vue to control the table as a whole */

class TableLayout {
    / / the constructor
    constructor(options) {
        console.log('tableLayout', options);
        this.table = null;
        this.store = null;
        this.showHeader = true;

        this.fit = true; / / what meaning?

        this.bodyWidth = null;

        this.observers = [];

        / / assignment
        for(let name in options) {
            if(options.hasOwnProperty(name)) {
                this[name] = options[name]; }}if(!this.table) {
            throw new Error('table is required for Table Layout');
        }

        if(!this.store) {
            throw new Error('store is required for Table Layout'); }}/** * public table handling methods * such as: setHeight setHeight, etc. */

    /** * Vue. Prototype.$isServer * Whether a Vue instance is running on the server, the value of the attribute true indicates that the instance is running on the server, each Vue instance can be determined by this attribute. This property is typically used for server rendering to distinguish whether code is running on the server. * * Dynamically calculates the width (columns with no width assigned) * If there is only one such column, allocate the remaining width to it * If there are more than one such column, * * Update the realWidth of columns * After updating the columns, both the header and the footer DOM need to be changed accordingly */
    updateColumnsWidth() {

        // console.log('updateColumnsWidth', this.table.columns, Vue.prototype.$isServer);
        // console.log('updateColumnsWidth', this.table.$el);

        // Returns if running on a server
        if(Vue.prototype.$isServer) return;

        const fit = this.fit;
        
        const bodyWidth = this.table.$el.clientWidth;
        let bodyMinWidth = 0;

        const flattenColumns = this.table.columns;
        // filter does not change the original array and returns the pointer to the address of the corresponding array item
        // So if you change the returned array value, the original array value will change accordingly
        let flexColumns = flattenColumns.filter((column) = > typeofcolumn.width ! = ='number');

        // If width exists and realWidth also exists, set realWdith to null
        flattenColumns.forEach((column) = > {
            if(typeof column.width === 'number' && column.realWidth) column.realWidth = null;
        })

        if(flexColumns.length > 0 && fit) {
            flattenColumns.forEach((column) = > {
                bodyMinWidth += column.width || column.minWidth || 80;
            })

            const scrollYWidth = 0;

            // If there is no scroll bar
            // Check by bodyMinWidth
            // console.log('999', bodyMinWidth <= bodyWidth - scrollYWidth)
            if(bodyMinWidth <= bodyWidth - scrollYWidth) {

                // Total width of columns with no allocated width
                const totalFlexWidth = bodyWidth - scrollYWidth - bodyMinWidth;

                if(flexColumns.length === 1) {
                    flexColumns[0].realWidth = (flexColumns[0].minWidth || 80) + totalFlexWidth;
                }else {
                    // Calculate the minimum width of all columns with no width
                    const allColumnsWidth = flexColumns.reduce((prev, column) = > prev + (column.minWidth || 80), 0);
                    // console.log('allColumnsWidth', flexColumns, allColumnsWidth)
                    
                    // Calculate the multiple
                    const flexWidthPerPixel = totalFlexWidth / allColumnsWidth;
                    // console.log('3333', flexWidthPerPixel, allColumnsWidth, totalFlexWidth);

                    // Width of non-first column
                    let noneFirstWidth = 0;

                    flexColumns.forEach((column, index) = > {
                        if(index === 0) return;
                        // console.log('7', column, index)

                        // Take an integer not greater than this value, which will allocate the extra width to the first column
                        const flexWidth = Math.floor((column.minWidth || 80) * flexWidthPerPixel);
                        // console.log('7', flexWidth, flexWidthPerPixel)
                    
                        noneFirstWidth += flexWidth;
                        // console.log('7', noneFirstWidth)
                        
                        column.realWidth = (column.minWidth || 80) + flexWidth;
                    })

                    // Calculate the realWidth of the first one
                    flexColumns[0].realWidth = (flexColumns[0].minWidth || 80) + totalFlexWidth - noneFirstWidth; }}this.bodyWidth = Math.max(bodyMinWidth, bodyWidth);
            this.table.resizeState.width = this.bodyWidth;

            // console.log('999', this.table.columns)
            // console.log('999', bodyWidth, scrollYWidth, bodyMinWidth, bodyWidth - scrollYWidth - bodyMinWidth)
        } else {
            // There are horizontal scroll bars}}}export default TableLayout; 
Copy the code

table-observer.js

/** * mixin layout viewer * LayoutObserver layout viewer * for table layout components: table-header and table-body */
export default {

    computed: {
        // Table layout properties
        tableLayout() {
            let layout = this.layout;
            // console.log('tableLayout', layout);

            if(! layout &&this.table) {
                layout = this.table.layout;
            }

            // If there is still none, an error is thrown
            if(! layout) {throw new Error('Can not find table layout.');
            }

            returnlayout; }},methods: {
        /** * Set the width of col in colGroup * bind the table cell width by controlling the col in colgroup * Without having to set the style repeatedly for each cell or row * col property: * -width: specifies the width of the cell * th/ TD property height: * -colspan: specifies the number of columns that a cell can span * -rowSPAN: specifies the number of rows that a cell can span * * vue: Gets the actual DOM of the current component this.$el * js: one of the ways to get dom nodes * js: Gets all elements that match the specified CSS selector QuerySelectorAll (querySelector gets the first matched element) * js: getAttribute * js: sets the DOM attribute setAttribute *@param {Object} layout 
         * @returns undefined* /
        onColumnsChange(layout) {
            // console.log('layout', layout);

            // Get the real DOM, filtered by querySelectorAll
            // The querySelectorAll method returns all elements in the document that match the specified CSS selector, returning the NodeList object
            const cols = this.$el.querySelectorAll('colgroup > col');
            // console.log('77', cols, this.columns);
            
            const flattenColumns = this.columns;
            const columnsMap = {};

            flattenColumns.forEach((column) = > {
                columnsMap[column.id] = column;
            })
            // console.log('flatcolumnsMaptenColumns', columnsMap)
            
            if(! cols.length)return;

            for(let i = 0, j = cols.length; i < j; i++) {
                const col = cols[i];
                const name = col.getAttribute('name');
                const column = columnsMap[name];
                // console.log(i, col, name, columnsMap, column);
                
                if(column) {
                    col.setAttribute('width', column.realWidth || column.width); }}}},mounted() {
        this.onColumnsChange(this.tableLayout);
    },
    
    updated() {
        // console.log(' module update ', this.$el);
        this.onColumnsChange(this.tableLayout); }},Copy the code

conclusion

Implementation approach

  • Table. Vue controls table rendering, table-column controls column rendering, table-header header rendering, table-body rendering, Table-layout defines attributes and methods for sharing the overall table. Table-observer A table observer (a callback after changes to the overall table, notifies the table header and body to make corresponding updates, and ensures that the table-header and table-body change synchronizes).
  • Create the table in table.vueState manager storeTo share the table-column, table-header, and table-body components.
  • The table-column component inserts the attributes and methods associated with the column intostore.states.columns, table-header and table-body render cells according to store;
  • By setting the tableWidth property of colEnsure that the table head and body cells are the same width, there is no need to repeatedly set the width of each cell;

The directory structure

  • table.vue
  • table-column.js
  • table-header.js
  • table-body.js
  • table-layout.js
  • table-observer.js