Recently, I just had time to see the table of the encapsulation between hungry me, feeling that the previous encapsulation method is not ideal, so I had the idea of reconstruction, so I started my reconstruction journey.
Project environment
"Vue" : "^ 3.2.16", "sass" : "^ 1.43.4", "typescript" : "^ 4.4.3" "vite."Copy the code
Refactoring point
- JSX + JSON configuration generates tables
- Supports slot custom table header, list items
- The Api exposed by the package is consistent with the official documentation
- Support paging
Initialize the
Create a DynamicTable component and configure the configuration items that need to be passed in from outside
<script lang="tsx">
import {defineComponent} from "vue";
import {isEmpty} from "element-plus/es/utils/util";
export default defineComponent({
name: "DynamicTable".props: {
// An instance of the parent component
parentDom: Object./ / table
columns: {
type: Array.default: () = >([])},// Table configuration
options: {
type: Object.default: () = >([])},// Operation button group
operations: {
type: Object.default: () = >({})},// Table data
tableData: {
type: Array.default: () = >([])},// Paging configuration
pagination: {type:Object.default:() = >({})
}
},
</script>
Copy the code
What is the use of the parent component instance parentDom? The main purpose of this is to allow the current child component to call the parent component’s methods directly, avoiding the emit and/or passing the function body directly into the configuration, and since I’m using the composite APi, the current this is not available in setup. If you don’t understand, the following will explain
To start, install @vitejs/plugin-vue-jsx and configure it in vite.config.js:
import {defineConfig} from 'vite'
import vueJsx from '@vitejs/plugin-vue-jsx'
export default defineConfig({
plugins: [
vueJsx({
// options are passed on to @vue/babel-plugin-jsx}),],... })Copy the code
JSX + JSON configuration generates tables
We can configure and generate tables according to columns and options, and pass in tableData to display data.
setup({columns, tableData}, ctx: any) {
// Delete the fields to be customized
const deleteField = (column: any) = > {
delete column['align']
return column
}
return () = > (
<div>
<el-table
data={tableData}
{. options}
>{ columns.map((n: any) => { const {align}: Any = n / / column can be based on a custom location const _align = the align | | options. The align | | 'center' / / delete a custom field deleteField (n) return<el-table-column
align={_align}
{. n} / >})}</el-table>
</div>)}Copy the code
Then test it out:
<DynamicTable
:columns="columns"
:tableData="tableData"
:options="options"
/>
const columns:Array= [{label:'name'.prop:'name'}, {label:'gender'.prop:'sex'},
{label:'age'.prop:'num',}]const options = {
border:true,}const tableData:Array<any> = [
{name:'1'.sex:'male'.num:12}, {name:'2'.sex:'male'.num:12},
{name:'3'.sex:'male'.num:12},]Copy the code
Unsurprisingly, he should look like this by now
border
Is the official API, mainly to add lines, so at this point, we have completed the most basic functions, but in the actual project development, there are often several requirements:
- Requirement 1: The header of the table has a custom component
- Requirement 2: Customize a component for a column of the table
- Requirement 3: Requires that the action button change based on the current row
- Requirement 4: Table action items must be customizable
- Requirement 5: The data in the table needs to produce corresponding results according to the status returned by the background
So! We continue to refine our components to make sure that they fit most of the requirements, most of the time.
Add component support for additional requirements
So in terms of the above requirements, individual’s biggest trouble points, in fact is how data should be interactive, for example, I have a form the custom column, if be in, which defines an input box, then we can consider this possibility, after I modify the values in the input box, make changes in the table data synchronization, It doesn’t require us to do it again. Of course, the above is only one possibility, and components such as dropdown boxes, time pickers, etc. that can change data at any time can also support synchronous modification. So at this point, we need to use the reference to pass this thing, you should be familiar with reference to pass, in fact, reference type will happen a phenomenon, so we pass in tableData is actually stored in the object, you can use this phenomenon to operate on the data. Take care of the above requirements once and for all
Custom header + column content + action item
ElementUi already provides a render header API to support custom table headers, but I don’t like using JSX or h functions to do this, because adding functions to a configuration item doesn’t allow me to retrieve the current instance. Moreover, some API support for VUE is not elegant enough, such as V-Model, in the H function, you need to customize once, about this part of the content of the official website said, if interested, you can find it by yourself. So we’re all using vUE, and there’s such a nice thing as slots, so I just changed all my customizations to support slots.
Custom header + column content
Let’s take a look at how we want to use the custom header, namely in code, how to write it in a way that saves the most effort and doesn’t require extra time to learn a new API. That’s definitely to follow the official documentation, and the way slots are commonly used is as follows:
<template v-slot:Slot name >
<template/>
Copy the code
So, we want it to work the same way when we define a header or specify the contents of a column
<DynamicTable
:columns="columns"
:tableData="tableData"
:options="options"
>
// Define the table header for column name
<template v-slot:name_header>
<h2>I'm the header of name</h2>
</template>
// Define the name of the column
<template v-slot:name_content="slotProps">
<el-input v-model="slotProps.row.name" placeholder="Please input" />
</template>
</DynamicTable>
Copy the code
So to support this, we can use the slots variable. In setup, it’s stored in the CTX context, and modify the code as follows:
.export default defineComponent({
name: "DynamicTable".props: {
parentDom: Object./ / table
columns: {
type: Array.default: () = >([])},// Table configuration
options: {
type: Object.default: () = >([])},// Operation button group
operations: {
type: Object.default: () = >({})},// Table data
tableData: {
type: Array.default: () = >([])},// Paging configuration
pagination: {type:Object.default:() = >({})}},setup({columns, tableData, options, operations, parentDom,pagination}, ctx: any) {
// Delete the fields to be customized
const deleteField = (column: any) = > {
delete column['align']
return column
}
return () = > (
<div>
<el-table
data={tableData} {. options}
>{ columns.map((n: any) => { const {align}: Any = n / / column can be based on a custom location const _align = the align | | options. The align | | 'center' / / delete a custom field deleteField (n) / / define slot const slots = { default: ctx.slots[`${n.prop}_content`] ? ctx.slots[`${n.prop}_content`] : null, header: ctx.slots[`${n.prop}_header`] ? ctx.slots[`${n.prop}_header`] : null } return<el-table-column
align={_align}
{. n}
v-slots={n.type= = ='selection' ? null : slots} / >})}</el-table>
</div>)}})Copy the code
Then test if you can customize the table header, the contents of the list, and can automatically modify the contents of the table items, unsurprisingly, now the interface should be
As you can see from the right, the first item of the name column was modified as we changed it, and the header and contents of the table were successfully customized with slots. So at this point, we’ve completed about 60 percent of the basic functionality of a table.
As you can see from the picture, there is a multi-select function on the left side of the table. In the official documentation, when we multi-select, we can provide a function to receive the data of the currently selected row, so we must support this function in our component, so at this time, all of the aboveparentDom
There are several ways in which a child can trigger a parent function and pass its arguments:
- use
Emit
To trigger the parent component’s function - Provide a function question in the configuration item to call
These two methods are the most common, but they pose different problems when used in this type of component.
- Emit way:
Using this way, you need to be added to the components of the binding function, resulting in less than optimal, read and not need binding function on components, also need to be in the heart of the configuration items need to use the corresponding function of incoming components for the call, then many steps, people don’t like that, so pass away.
- Provide a function body in the configuration item
So this is the way
.const options = {
name: {onClick:() = >{call or pass the parent component's method here}}}Copy the code
If you want to use the parent component’s data, you need to fetch the parent component’s $parent from the DynamicTable and pass it back to the onClick function. Needless to say, if we use our component in a Dialog, its $parent gets a completely different instance from what we want, so use it at this point
const options = {
name: {onClick:(vm) = >{
// Call or pass in the parent component's methods here
vm.$parent.$parent
}
}
}
Copy the code
This was painful, ugly, and against the idea of being easy to use, so IN the end, I went with the third approach, which had some problems, such as passing a large amount of data to another component, but was not worth talking about in our actual use.
- Pass the instance of the parent component directly to the child component
Finally, this scheme is adopted, which can not only solve the problem of redundant call, but also very convenient to call the method in the parent component, without worrying about the problem of this in the function. In this case, when we need to call a function, we only need to pass in the name of the function that triggers the event in the configuration item, so we still follow the idea of the API as consistent as possible with the official documentation, in order to complete the multi-choice function to improve our code
.setup({columns, tableData, options, operations, parentDom,pagination}, ctx: any) {
// Delete the fields to be customized
const deleteField = (column: any) = > {
delete column['align']
return column
}
// The function that triggers the parent component
// handleName: the function name
// params: the parameter passed in
const handleFn = (handleName:string,params:any = null) = > {
return parentDom && parentDom[handleName] && parentDom[handleName](params)
}
return () = > (
<div>
<el-table
data={tableData} {. options}
onSelectionChange={(selection: any) = >handleFn(options['selection-change'],selection)} > { columns.map((n: any) => { const {align}: Any = n / / column can be based on a custom location const _align = the align | | options. The align | | 'center' / / delete a custom field deleteField (n) / / define slot const slots = { default: ctx.slots[`${n.prop}_content`] ? ctx.slots[`${n.prop}_content`] : null, header: ctx.slots[`${n.prop}_header`] ? ctx.slots[`${n.prop}_header`] : null } return<el-table-column
align={_align}
{. n}
v-slots={n.type= = ='selection' ? null : slots} / >
})
}
{renderOperations()}
</el-table>
{ !isEmpty(pagination) && renderPagination()}
</div>)}Copy the code
If the selection option is selected, you don’t need to do anything for the el-table-column, but you need to support the API of the original EL-table-column. You can use this by defining a Type: Selection object in columns.
const columns = [
{type:'selection'}]Copy the code
That because of the relationship of time, the above is here first, the rest of the operation items, paging in fact according to the above ideas is not difficult to do, and here, after the following in detail it! Take leave