One, foreword

The Element – UI open source has become the most popular UI framework in the middle – and back-end systems.

If Vue, React and Angular are the front three, then Element-UI accounts for half of the middle and back end, with 43K github stars. To date, it has 84 components (Version 2.13.0).

The first two lines are empty, starting at line 2.

Second, the cause of

Requirement: Due to the business needs of the company, there is often a need to select (check) more than one table in the page, and then assemble the checked ID into a string and submit it to the background.

The solution is to add a column of Type Selection to the table component. See the document on the Elementary-UI website.

<el-table
    ref="multipleTable"
    :data="tableData"
    tooltip-effect="dark"
    style="width: 100%"
    @selection-change="handleSelectionChange">
    <el-table-column
      type="selection"
      width="55">
    </el-table-column>
</el-table>
Copy the code

With the selection-change event, you get an array of rows selected by the user.

The effect is as follows:

Everything looks perfect, but in practice there are a lot of problems.

Why do you say so? Because in the actual business of the company, the table is a pagination table, and every time the page is switched, the data is retrieved and the table is re-rendered, then the first problem arises: the row selected by the user in the table with page number 1 disappears after the page number is switched.

A paged table should look like this:

I went to the Element-UI website to look at the documentation and found a method in the table component called toggleRowSelection that toggles the selected status of a particular row in the table.

With this method, after we get the table data, we immediately set the previously selected data using this method, so that we can render the previously selected row state when the user switches.

The pit is coming again!!

Because the select-change event gets an array named Selection that contains information about the row selected by the user. We store this array in a variable so that the user can still see the previously selected row after switching the page number, and then set the row selection information with the toggleRowSelection method.

selection.forEach(row= > {
    this.$refs.multipleTable.toggleRowSelection(row);
});
Copy the code

This is fine at first glance, but in the table, it does not check the effect!! ?

Call this method in nextTick:

this.$nextTick((a)= > {
    selection.forEach(row= > {
        this.$refs.multipleTable.toggleRowSelection(row);
    });
});
Copy the code

Yes, no error, open the page to see, yes?? How is still not selected!!

Heart a w grass nima passing by…

After determining if the ref name is consistent, if the data in selection exists, and if the call method fires, I still don’t get the results I want.

Play a string…

After a few moments of calm, I decided to open the element-UI source code to see how the table component is checked.

Third, source code analysis

Only table component part of the source code.

3.1 structure

First look at the structure of the Table component

This is the structure, the outermost index.js is used to export the table module, the code inside is also very simple, must be able to understand.

// index.js
import ElTable from './src/table';

/* istanbul ignore next */
ElTable.install = function(Vue) {
  Vue.component(ElTable.name, ElTable);
};

export default ElTable;
Copy the code

SRC contains a store folder and some table components: Body, column, footer, header, layout, etc., utility class file util. Js, configuration file config.js, And a dropdown, a layout-observer, and a filter-panel.

The code in the Store folder implements a private Vuex that is only used for data exchange between components in the Table component.

3.2 to find it

According to my requirements, I only need to look at part of the source code for Selection. So in terms of layout, I can start with the column and start with the hand, which is the table-column.js file.

Table column.js does contain some information about columns, but there is no code for selection.

So INSTEAD of looking for layout, I went straight to method: toggleRowSelection. SRC /store/watcher.js: toggleRowSelection: toggleRowSelection: toggleRowSelection

/ / watcher. Js 158 lines
toggleRowSelection(row, selected, emitChange = true) {
  const changed = toggleRowStatus(this.states.selection, row, selected);
  if (changed) {
    const newSelection = (this.states.selection || []).slice();
    // Call the API to change the selected value without triggering the SELECT event
    if (emitChange) {
      this.table.$emit('select', newSelection, row);
    }
    this.table.$emit('selection-change', newSelection); }}Copy the code

This method is exposed for us to call, the first line is the main information, call toggleRowStatus and get the changed value, then emit this value. It’s going to look like this, so you’re going to start with toggleRowStatus.

Notice that this.states. Selection in the first line will be the key for the rest.

By searching for the keyword directly, you can find that the method is referenced by the external export.

import { getKeysMap, getRowIdentity, getColumnById, getColumnByKey, orderBy, toggleRowStatus } from '.. /util';
Copy the code

Open the util.js file and find the following code:

export function toggleRowStatus(statusArr, row, newVal) {
  let changed = false;
  const index = statusArr.indexOf(row);
  constincluded = index ! = =- 1;

  const addRow = (a)= > {
    statusArr.push(row);
    changed = true;
  };
  const removeRow = (a)= > {
    statusArr.splice(index, 1);
    changed = true;
  };

  if (typeof newVal === 'boolean') {
    if(newVal && ! included) { addRow(); }else if (!newVal && included) {
      removeRow();
    }
  } else {
    if (included) {
      removeRow();
    } else{ addRow(); }}return changed;
}
Copy the code

It’s not too hard to read, either. There are two methods in there, an addRow and a removeRow, both literal. Main implementation function: check if it is a new value (newVal), if not (! Included () add () add () Array.prototype.indexof = array.prototype.indexof = array.prototype.indexof

Array.prototype.indexof () : Method returns the first index in the Array where a given element can be found, or -1 if none exists.

  • Pit 1: If this element is an Object, you should know that the Object is a reference type, that is, using indexOf to determine whether the address referenced by the Object is the same, but not whether the value inside is the same.

But, I thought about it, and it doesn’t seem to matter. Concrete thinking: When we initialize table 10, the array selection that holds the selected row in the table component is initially empty. Then we call toggleRowSelection to actively set the selected row. These rows are placed into selection in the table component (via the addRow method in toggleRowStatus).

Already put in, why not render the corresponding state!! ?

Now that you know that the table component holds the selected row through Selection, search for Selection.

We got 78 results, in seven files. We get too many results, and we don’t want those results.

Then go further with full match search:

We got 38 results, in five files.

Narrowed it down a bit, but it’s still a lot. There’s nothing we can do. One file at a time.

The first file is the config.js file in the order that Vscode found it for me.

This keyword appears four times in the config.js file. You can see that the previous two matches result in a style, which is not what we want.

The last two are worth watching.

/ / config. Js line 29
// These options should not be overridden
export const cellForced = {
  selection: {
    renderHeader: function(h, { store }) {
      return<el-checkbox disabled={ store.states.data && store.states.data.length === 0 } indeterminate={ store.states.selection.length > 0 && ! this.isAllSelected } nativeOn-click={ this.toggleAllSelection } value={ this.isAllSelected } />; }, renderCell: function(h, { row, column, store, $index }) { return <el-checkbox nativeOn-click={ (event) => event.stopPropagation() } value={ store.isSelected(row) } disabled={ column.selectable ? ! column.selectable.call(null, row, $index) : false } on-input={ () => { store.commit('rowSelectedChanged', row); }} / >; }, sortable: false, resizable: false } // ... Omit}Copy the code

Posted only useful, from the big ear view, exported a module called cellForced, although I don’t know what that means. (Not pass level 4, smashing brilliance). But inside the two functions I can understand, see the render keywords, this is not the meaning of rendering, look inside again, mom ah, happiness!! The el-checkbox component is included in this column. (It’s not possible to put anything else in the table!) .

It’s actually only where the fourth keyword appears, in line 34, that’s the selection thing we want.

indeterminate={ store.states.selection.length > 0 && ! this.isAllSelected }

Store.states. Selection: I am the array containing the selected row.

In fact, if you look at the search results in the third file, watcher.js, it’s obvious:

And in the fifth file: table.vue, mapStates is used to map selection, and its shadow can also be found:

Then these two files are left alone as we find the location of the layout and go back to config.js:

/ / config. Js line 29
// These options should not be overridden
export const cellForced = {
  selection: {
    renderHeader: function(h, { store }) {
      return<el-checkbox disabled={ store.states.data && store.states.data.length === 0 } indeterminate={ store.states.selection.length > 0 && ! this.isAllSelected } nativeOn-click={ this.toggleAllSelection } value={ this.isAllSelected } />; }, renderCell: function(h, { row, column, store, $index }) { return <el-checkbox nativeOn-click={ (event) => event.stopPropagation() } value={ store.isSelected(row) } disabled={ column.selectable ? ! column.selectable.call(null, row, $index) : false } on-input={ () => { store.commit('rowSelectedChanged', row); }} / >; }, sortable: false, resizable: false } // ... Omit}Copy the code

There are two rendering functions in total, one rendering header and one rendering grid. From the attribute value of the el-checkbox component, we can determine that in line 41:

value={ store.isSelected(row) }
Copy the code

This line is what makes the render selected or not. The logic is simple. It calls a method called isSelected and tells us it’s a method in store.

Ok, go to the Store folder and search for isSelected. In watcher.js, we found it:

/ / watcher. Js 120 lines
/ / select
isSelected(row) {
  const { selection = [] } = this.states;
  return selection.indexOf(row) > - 1;
},
Copy the code

The logic is even simpler: take the selection array that holds the selected row, and return whether the position of row in selection is greater than -1. Render is selected if it returns true and not if it returns true.

  • Pit 2: Once again indexOf is used to determine if an object is in an array.

It’s deadly here. Why do you say that?

Because Selection does hold rows that come in through the toggleRowSelection setting. But in isSelected the parameter row is passed from data in props in the table component.

Data is retrieved from the interface again, so rows in Data and selection are not a row.

Row is an object, it is a reference type, as long as the reference address is different, then you are not you.

Although both rows are the same in terms of data structure and content, assume the following:

const row1 = { name: '1'.id: 0.code: 110110.area: 'Beijing'.street: '二环' }
const row2 = { name: '1'.id: 0.code: 110110.area: 'Beijing'.street: '二环' }
Copy the code

Row1 and Row2 are not equal.

But for our actual business, row1 and Row2 have the same structure and the same ID, so the two things are the same thing.

A more practical example: your second uncle comes out of his tile-roofed house in the village and you recognize him; You see your second uncle coming out of a unit in a residential area on the east Second Ring Road in Beijing. Your second uncle is not your second uncle anymore. This is crazy!!

So, this means that the source code rendering in the table component is too simple, there is no deeper judgment, just compare whether the reference address is the same.

Finding the root of the problem is also quite easy to solve.

Iv. Solutions

  • 1. Wait for the elemental-UI update to be resolved and issue raised.
  • ToggleRowSelection (tableData[index]); toggleRowSelection(tableData[index]); Make sure your uncle is your uncle.
  • 3. Encapsulate the multi-selection table component and implement it yourself instead of selecting it (see Shaonialife in the comments section for a great way).
  • 4. Rewrite array.prototype. indexOf method to make internal judgment logic perform depth comparison on objects.
  • 5. Set the row-key in the el-table to ID and reserve-selection to true for the column type=”selection”, so that when you switch page numbers, the selected values of the previous page numbers are retained. Thank you. Note, however, that if you want to check by default, you still need to determine the position of the default checked rows in the data source tableData, and then set them to toggleRowSelection.

I have implemented the above methods, 1,2,3 and 4, depending on the specific business needs.

Depth comparison, I’ve only implemented one layer. My idea is to first compare the number of key values, and then determine whether the key of your uncle to your uncle exists, and whether the values in it are equal. Because my business data only has one layer of attribute values.

4.1 The first method renders a new el-checkbox(shaonialifeChildren’s shoes proposed) :

// Render a new one by yourself<el-checkbox />
<el-table-column
  :align="tableColumn.align"
  type="index"
  label="Serial number"
  width="70">
  <template slot-scope="scope">
    <el-checkbox :value="!!!!! checkedRowIds[scope.row.id]" @change="(val) => { toggleRowSelection(val, scope.row) }"/>
  </template>
</el-table-column>
Copy the code

CheckedRowIds is an object that contains a collection of ids with keys, like this:

const checkedRowIds = {
    0: true.1: true.2: false
}
Copy the code

When row.id = 1, checkedRowIds[row.id] is equivalent to something like checkedRowIds.1, which is true.

Then let’s look at the toggleRowSelection callback for the change event

toggleRowSelection(val, row) {
  const { checkedRowIds } = this.$props
  const { id } = row
  
  this.$set(checkedRowIds, id, val)
  
  const includes = checkedRowIds.hasOwnProperty(id)
  
  const remove = (a)= > delete checkedRowIds[id]
  if(! val && includes) remove()const keys = Object.keys(checkedRowIds)
  const arrToString = arr= > arr.join(', ')
  
  const ids = arrToString(keys)
  / / ids: 1, 2, 3
},
Copy the code

Get the checkedRowIds and row.id, and then use this.$set to set the item whose key is id and value is val into the checkedRowIds collection.

Then check if val is false and the key exists in the checkedRowIds, and delete the property if both conditions are met. Keys () is used to fetch all the keys in the checkedRowIds, and join() to form ids.

Analysis: this method is very good!! CheckedRowIds [scope.row.id] is very short and very beautiful. And then it’s very convenient to go to the page and check the default check box, as long as it’s in the checkedRowIds, you don’t have to call toggleRowSelection.

4.2 The second combination of row-key and Reserve-selection (han __ Children’s shoes)

// Use row-key and reserve-selection<el-table
  v-loading="loading"
  :data="tableData"
  row-key="id"
  @selection-change="rowChange"
>

  <el-table-column
    :reserve-selection="true"
    type="selection"
    width="55"/>
    
</el-table>
Copy the code

When row-key is set to id, each row has a unique key value, and reserve-selection is used to preserve the selected value regardless of how the page number is switched.

Analysis: This method is very simple, and mainly official provided properties, not a lot of code changes, which is good. But when you enter the page, to display the default row, you need to compare it to the data in tableData and set it with the toggleRowSelection method.

Five, write in the back

I succeeded because I stood on the shoulders of giants. Newton — –

Thank you very much for your brainstorming. I carefully read every message in the comments section and I will practice every solution. Thank you all. I hope I can go further on this road.