Final effect GIF

I wanted to record a video, but I don’t think it’s supported

A complete example will be given at the end of the article

You can also view it here: jsrun.net/AAIKp

Related data structures

Input Part of the input box

Clicking on color, Size, weight generates the SKu_ARR data structure

sku_arr: [
    { attr:"Color".valueList: ["Black"."White"] {},attr:"Size".valueList: ["Big"."In"]}]Copy the code

Table Indicates the data in the table header

table_column: ["Color"."Image"."Size"."Sale price"."Market price"."Inventory"]
Copy the code

Table Indicates the data in the table

Here the name of the property is directly used as the key of the object

table_data: [
	{ "Color": "Black"."Size": "Big"."Sale price": ""."Market price": ""."Inventory": ""."Image": "" },
	{ "Color": "Black"."Size": "In"."Sale price": ""."Market price": ""."Inventory": ""."Image": "" },
	{ "Color": "White"."Size": "Big"."Sale price": ""."Market price": ""."Inventory": ""."Image": "" },
	{ "Color": "White"."Size": "In"."Sale price": ""."Market price": ""."Inventory": ""."Image": ""},]Copy the code

Things that need to be done

Generated when click Add Specificationsku_arrWhere the input field is

This one’s a little easier. You just push into the array

  this.form.sku_arr.push({ attr: attr_name, valueList: ['black'.'white']})Copy the code

If v-for=”(item, index) in arr” input loop, v-model should be assigned as arR [index], not item

At the same time generateThe table header

In addition to attributes, the table header also contains images, prices, and inventory.

And the image is always in the second column, so the rules for merging rows are the same as for the first column (more on that later)

Use the array map function to get the name of the property, and then perform array merge

// Generate the header method
async generateTableColumn() {
    this.table_column = this.form.sku_arr.map(x= > x.attr).concat( ['Selling price'.'Market price'.'inventory'])
    /* There is a bug when you do not write '$nextTick' in the file, but vue is too late to update the DOM
    await this.$nextTick()
    if (this.form.sku_arr.length ! =0) this.table_column.splice(1.0.'images')},Copy the code

$nextTick() = await this.$nextTick() After I delete the first attribute (delete the first column), I re-execute generateTableColumn() to generate the header data. I find that the image column is not inserted into the second column as I expected, so I manipulate the image column after vUE asynchronous update

At the same time generateThe data in the table

This involves calculating the Cartesian product, (I feel like I’m combining all the permutations).

For example, the data above

sku_arr: [
    { attr:"Color".valueList: ["Black"."White"] {},attr:"Size".valueList: ["Big"."In"]}]Copy the code
  • You need black-big, black-middle, white-big, white-middle, four rows of data in the table

  • If you add a weight of 1kg and 2kg, you should get black-big-1kg, black-big-2kg, black-middle-1kg, black-middle-5kg, white-big-1kg, white-big-5kg, white-middle-1kg, white-middle-2kg, i.e., eight rows of data in the table

Calculation of Cartesian product (Core)

/* Reimplements the cartesian product as an array 'empty ',' length 1', 'length greater than 1'. [{attr: "color", valueList: [" black ", "white"]}, {attr: "size", valueList:} [" big ", "in the"]] returns an array of formats: [{" color ", "black", "size" : "big"}, {" color ":" black ", "size" : ""}, {" color" : "white", "size" : "big"}, {" color ":" white ", "size" : ""}] * /
generateBaseData(arr) {
    if (arr.length === 0) return []
    if (arr.length === 1) {
        let [item_spec] = arr
        return item_spec.valueList.map(x= > {
            return {
                [item_spec.attr]: x
            }
        })
    }
    if (arr.length >= 1) {
        return arr.reduce((accumulator, spec_item) = > {
            let acc_value_list = Array.isArray(accumulator.valueList) ? accumulator.valueList : accumulator
            let item_value_list = spec_item.valueList
            let result = []
            for (let i in acc_value_list) {
                for (let j in item_value_list) {
                    let temp_data = {}
                    // If it is an object
                    if (acc_value_list[i].constructor === Object) { temp_data = { ... acc_value_list[i], [spec_item.attr]: item_value_list[j] }// Otherwise if it is a string
                    } else {
                        temp_data[accumulator.attr] = acc_value_list[i]
                        temp_data[spec_item.attr] = item_value_list[j]
                    }
                    result.push(temp_data)
                }
            }
            return result
        })
    }
}
Copy the code

Final tabular data

The map loop returns the new array using the array returned by the cartesian product above

this.table_data = generateBaseData(arr).map(item= > ({ ...item, 'Selling price': ' '.'Market price': ' '.'inventory': ' '.'images': ' ' }))
Copy the code

Now that we’re done generating new data, rendering the page is easy. Loop through the el-table-column and pay attention to the key (and of course determine the column attributes for different page displays).

<el-table :span-method="spanMethod" border>
    <template v-for="item_column in table_column">
        <el-table-column :key="item_column" :prop="item_column" :label="item_column" />
    </template>
</el-table>
Copy the code

The operation of deleting an attribute

Deleting attributes is relatively simple: after deleting the sku_arR data, regenerate the header and table data

The operation of deleting an attribute value

Delete the sku_arR data and then regenerate the table header and table data. Of course you can

But but but….. If the user has already entered some product information, this would cause the product information to be lost, so I didn’t do this

// Delete attribute value four parameters: 'primary array index ',' secondary array index ', 'attribute name ',' attribute value '
deleteAttrVal(idx, kdx, attr_name, attr_val) {
    this.form.sku_arr[idx].valueList.splice(kdx, 1)

    // Delete the table row
    let data_length = this.form.table_data.length
    for (let i = 0; i < data_length; i++) {
        if (this.form.table_data[i][attr_name] == attr_val) {
            this.form.table_data.splice(i, 1)
            i = i - 1
            data_length = data_length - 1}}}Copy the code

The last two arguments to the splice method are the current property name and the current property value, and the table data is deleted directly using the array splice method

The operation to edit the value of a property

Similarly, editing property values should not cause user data to be lost

newAttrValueBlur(curr_attr, newVal) {
    if (newVal === old_attr_value) return

    // The index of rows that need to be changed is calculated from the cartesian product generated by the specification.
    let cartesian_arr = this.generateBaseData(this.form.sku_arr)
    let change_idx_arr = [] // The index of the row to be changed
    for (let i in cartesian_arr) {
        if (cartesian_arr[i][curr_attr] === newVal) change_idx_arr.push(Number(i))
    }
    console.log('change_idx_arr', change_idx_arr)

    // The length of the new table should be compared to the length of the existing table to distinguish between additions and modifications
    let length_arr = this.form.sku_arr.map(x= > x.valueList.length)
    let new_table_length = length_arr.reduce((acc, curr_item) = > acc * curr_item) // Multiply the length of the new table
    let old_table_length = this.form.table_data.length // Old table data length

    // If it is modified
    if (new_table_length === old_table_length) {
        this.form.table_data.forEach((item, index) = > {
            if (change_idx_arr.includes(index)) this.form.table_data[index][curr_attr] = newVal
        })
        return
    }
    // If it is a new one
    if (new_table_length > old_table_length) {
        // Get the current value of the current attribute and other specifications of sku_arr to construct new table data
        let other_sku_arr = this.form.sku_arr.map(item= > {
            if(item.attr ! == curr_attr)return item
            else return { attr: item.attr, valueList: [newVal] }
        })
        // Get the new table data
        let ready_map = this.generateBaseData(other_sku_arr)
        let new_table_data = this.mergeTableData(ready_map)
        change_idx_arr.forEach((item_idx, index) = > {
            this.form.table_data.splice(item_idx, 0, new_table_data[index])
        })
    }
}
Copy the code

When the input field for a property value loses focus, the save operation is performed

Differentiate betweenModify the oldCreate a new

  • Lose focus and get a new onesku_arr, recalculate the Cartesian product
  • I’m going to iterate over the new Cartesian product, based on what happens when we lose focusattributeandAttribute valuesFind what needs to be changedIndex of table data, i.e.,change_idx_arr(New or modified indexes are stored here.)
  • withNew array lengthandExisting table data array size comparisonIf the new value is greater than the old value, the new value is added

Modify the logic

It’s simple, just modify the array element

// If it is modified
if (new_table_length === old_table_length) {
    this.form.table_data.forEach((item, index) = > {
        if (change_idx_arr.includes(index)) this.form.table_data[index][curr_attr] = newVal
    })
    return
}
Copy the code

The new logic

When adding, it generates a new Cartesian product with the value of the newly added attribute and all values of the other attributes, iterating through change_idx_arr and inserting the data into the specified location using splice

// If it is a new one
if (new_table_length > old_table_length) {
    // Get the current value of the current attribute and other specifications of sku_arr to construct new table data
    let other_sku_arr = this.form.sku_arr.map(item= > {
        if(item.attr ! == curr_attr)return item
        else return { attr: item.attr, valueList: [newVal] }
    })
    // Get the new table data
    let ready_map = this.generateBaseData(other_sku_arr)
    let new_table_data = this.mergeTableData(ready_map)
    change_idx_arr.forEach((item_idx, index) = > {
        this.form.table_data.splice(item_idx, 0, new_table_data[index])
    })
}
Copy the code

Infinite merge row

At this point, all the table data related operations are complete, and it is time to merge the rows of the EL-Table

Thanks to my colleague Wang Sihan’s help, we finally realized the infinite merger

<el-table :data="table_data" :span-method="spanMethod" border>

/ / merger
spanMethod({ row, column, rowIndex, columnIndex }) {
    if (columnIndex == 0) {
        let key_0 = column.label
        let first_idx = this.form.table_data.findIndex(x= > x[key_0] == row[key_0])
        const calcSameLength = () = > this.form.table_data.filter(x= > x[key_0] == row[key_0]).length
        first_column_rule = rowIndex == first_idx ? [calcSameLength(), 1] : [0.0]
        return first_column_rule

        // Use the same merge rules for the second column as for the first column
    } else if (columnIndex == 1) {
        return first_column_rule
        / / the other columns
    } else {
        // For each item in the table,
        const callBacks = (table_item, start_idx = 0) = > {
            if (columnIndex < start_idx) return true
            let curr_key = this.table_column[start_idx]
            return table_item[curr_key] === row[curr_key] && callBacks(table_item, ++start_idx)
        }
        let first_idx = this.form.table_data.findIndex(x= > callBacks(x))
        const calcSameLength = () = > this.form.table_data.filter(x= > callBacks(x)).length
        return rowIndex == first_idx ? [calcSameLength(), 1] : [0.0]}}Copy the code

There are two caveats here

  • Due to business requirements, the merging rules of the images in the second column should be the same as those in the first column. It happens that this method moves horizontally one cell at a time in the table, so save the rules in the first column when processing the first column, and use the rules in the first column when moving to the second column.

    It can’t be stored in data

  • The next step is infinite merging

A big problem is that the later merge depends on the previous merge up to the first column.

ColumnIndex < start_IDx, columnIndex < start_IDx,

If the columnIndex is fixed in a certain cell, start_IDx is added until the value is greater than the columnIndex, indicating that the calculation of the cell is complete and the merging rule is obtained

It is highly recommended to learn about HTML native tables, try rowspan and Colspan,

You can’t ignore these basics just because you’re using components

From here, all functions are introduced

I went through revision after revision, and finally I was a little more satisfied,

Compared with the previous version, solved the user in some extreme operations, resulting in page bugs (mainly when modifying attribute values)

  • In the modifiedattributeorDelete the propertiesI did not save the data input by the user, because I think the original data will become garbage after modifying the attributes.

Deficiency in

  • Some calculations may not be very good and may affect performance
  • When you have too many attributes, the speed of table generation slows down
    • From clicking on properties to the complete table data is printed in the console in seconds
    • Is it the vue or component rendering problem

The HTML sample runs directly

<! DOCTYPEhtml>
<html lang="en">
	<head>
		<meta charset="UTF-8">
		<title>sku</title>
		<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
		<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
		<script src="https://unpkg.com/element-ui/lib/index.js"></script>
		
		<style type="text/css">
			
			 /* To trigger the browser to scroll, this is to set the sales price of the table in the final render of the Label style */
			label[for*='table_data'] {
				visibility: hidden;
				margin-top: -40px;
			}
		</style>
	</head>
	<body>
		<div id="app">
			<el-form ref="form" :rules="rules" :model="form">
				<el-form-item label="Add specifications" prop="sku_arr">
					<div style="display: flex;">
						<div>
							<el-button v-for="(item, idx) in default_attr" :key="idx" :disabled="attrBtnDisabled" @click="clickDefaultAttr(item)">{{item}}</el-button>
						</div>

						<el-popover placement="top" width="240" v-model="add_popover_bool" @after-enter="$refs.addValueInput.focus()">
							<div style="display: flex; grid-gap: 10px;">
								<el-input ref="addValueInput" v-model.trim="add_value" @keyup.enter.native="confirm()" />
								<el-button type="primary" @click="confirm()">determine</el-button>
							</div>

							<el-button slot="reference" size="small" type="primary" :disabled="attrBtnDisabled" style="margin-left: 40px;">The custom</el-button>
						</el-popover>
						
						
						<el-button type="primary" @click="onSubmit()" style="margin-left: 100px;">submit</el-button>
					</div>
				</el-form-item>

				<! -- Specification list and form -->
				<section style="margin: 0 0 20px 50px;">
					<! -- Show selected -->
					<div v-for="(item, index) in form.sku_arr" :key="index" style="margin-top: 10px;">

						<! - - - >
						<div>
							<el-input v-model.trim="item.attr" placeholder="Properties" style="width: 120px;" @focus="attrFocus(item.attr)" @blur="attrBlur(item.attr)"></el-input>
							<el-button type="danger" size="mini" icon="el-icon-delete" circle @click="deleteAttr(index)"></el-button>
						</div>

						<! -- Attribute value -->
						<div style="display: flex; margin-top: 10px;">
							<div v-for="(ktem, kndex) in item.valueList" :key="kndex" style="margin-right: 20px;">
								<el-input size="small" ref="attrValueInput" v-model.trim="item.valueList[kndex]" placeholder="Value" style="width: 100px;" @focus="attrValueFocus(item.valueList[kndex])" @blur="newAttrValueBlur(item.attr, item.valueList[kndex])"></el-input>

								<el-button v-if="item.valueList.length > 1" type="danger" size="mini" icon="el-icon-delete" circle @click="deleteSmall(index, kndex, item.attr, item.valueList[kndex])" />
							</div>

							<el-button type="primary" size="mini" :disabled="item.valueList.includes('')" icon="el-icon-plus" @click="addAttributeValue(index)">Add value</el-button>
						</div>
					</div>

					<el-table :data="form.table_data" :span-method="spanMethod" style="margin-top: 20px;" border>
						<template v-for="item_column in table_column">

							<el-table-column v-if="Item_column == 'image '" :key="item_column" min-width="150" width="170" align="center" :resizable="false" label="Image">
								<template slot-scope="{ row, $index }">
									<! -- <el-form-item :prop="`table_data.${ $index }. ":rules="rules.sku_img" label-width="0">Image components<! -- </el-form-item> -->
								</template>
							</el-table-column>

							<! -- Sales price using form validation and custom table header -->
							<el-table-column v-else-if="Item_column == 'sale price '" :key="item_column" align="center" :resizable="false">
								<! -- Custom table header -->
								<template slot="header">
									<div><span style="color: #ff5353;">*</span>The sales price</div>
								</template>

								<template slot-scope="{ row, $index }">
									<el-form-item :prop="' table_data.${$index. :rules="rules.sku_sale_price" label-width="0" label="">
										<el-input v-model="row[item_column]" :placeholder="item_column" />
									</el-form-item>
								</template>
							</el-table-column>

							<! -- Market price -->
							<el-table-column v-else-if="Item_column == 'market price '" :key="item_column" align="center" :resizable="false" :label="item_column">
								<template slot-scope="{ row }">
									<div style="height: 62px;">
										<el-input v-model="row[item_column]" :placeholder="item_column" />
									</div>
								</template>
							</el-table-column>

							<! - inventory -- -- >
							<el-table-column v-else-if="Item_column == 'inventory '" :key="item_column" align="center" :resizable="false" :label="item_column">
								<template slot-scope="{ row, $index }">
									<div style="height: 62px;">
										<el-input v-model="row[item_column]" :placeholder="item_column" />
									</div>
								</template>
							</el-table-column>


							<! -- Other attributes column -->
							<el-table-column v-else align="center" :resizable="false" :key="item_column" :prop="item_column" :label="item_column" />
						</template>
					</el-table>
				</section>
			</el-form>
		</div>
		<script>
		window.onload = () = > {

			let attr_name_value = new Map([ // Map data structure, according to the property name to get the corresponding property value return array
				[The 'color'['black'.'white'.'red']],
				['size'['big'.'in'.'small']],
				['weight'['500g'.'1kg'.'5kg']]])let base_column = ['Selling price'.'Market price'.'inventory'] // Basic columns

			let first_column_rule = [] // The first column uses the same merge rule as the second column (cannot exist in data)
			let old_attr = ' ' // The value in the input box is retrieved each time the property gets focus, stored here
			let old_attr_value = ' ' // Get the value in the input box each time the attribute value gets focus, save it here

			new Vue({
				el: '#app'.computed: {
					// Added attributes (array of strings)
					selectedAttr() {
						return this.form.sku_arr.map(x= > x.attr)
					},
					// Whether you can add a maximum of two attributes
					attrBtnDisabled() {
						return false
						return this.form.sku_arr.length >= 2}},data: {
					default_attr: [The 'color'.'size'.'weight'].// Default specifications
					table_column: base_column, / / table columns
					add_popover_bool: false.// Add a small popover for attributes
					add_value: ' '.// Add attributes
					// The data above is related to the input of skU

					/ / form
					form: {
						sku_arr: [].table_data: [].// Data in the table
					},

					// Validate the rule
					rules: {
						// SKU-related validation
						sku_arr: {
							validator: (rule, value, callback) = > {
								if (value.length === 0) return callback(new Error('Please add specifications'))
								else return callback()
							},
							trigger: 'blur'
						},
						sku_img: [{required: true.message: 'Picture cannot be empty'.trigger: 'blur' },
							{ type: 'string'.message: 'Please wait until the picture is uploaded'.trigger: 'blur'},].sku_sale_price: { required: true.message: 'Price cannot be empty'.trigger: 'blur'}}},methods: {
					// Click the default specs button
					clickDefaultAttr(attr_name) {
						if (this.selectedAttr.includes(attr_name)) return
						this.form.sku_arr.push({ attr: attr_name, valueList: [...attr_name_value.get(attr_name)] }) // Resolve problems caused by reference types

						this.generateTableColumn()
						this.traverseSku() // Process the SKU and generate the table

						console.log(this.form.sku_arr)
					},
					// Click Ok in Custom to add new specifications
					confirm() {
						if (!this.add_value) return
						this.form.sku_arr.push({ attr: this.add_value, valueList: [' ']})this.generateTableColumn()
						this.traverseSku()

						this.add_popover_bool = false
						this.add_value = ' '
					},
					// The property gets the old value when it gets focus
					attrFocus(oldVal) {
						old_attr = oldVal
					},
					// Attribute loses focus
					attrBlur(newVal) {
						console.log('attrBlur')
						// If 'new value equals old value' or 'empty' do nothing
						if (newVal === old_attr || newVal === ' ') return

						// Generate table header data and table data
						this.generateTableColumn()
						this.traverseSku()
					},
					// Delete attributes
					deleteAttr(idx) {
						this.form.sku_arr.splice(idx, 1)
						// Generate table header data and table data
						this.generateTableColumn()
						this.traverseSku()
					},


					// Add attribute values
					async addAttributeValue(idx) {
						this.form.sku_arr[idx].valueList.push(' ')
						// Give focus to the new input box
						await this.$nextTick()
						this.$refs.attrValueInput[this.$refs.attrValueInput.length - 1].focus()
					},
					If the input box loses focus, if the value does not change, do nothing
					attrValueFocus(oldVal) {
						old_attr_value = oldVal
					},
					// When attribute values lose focus, manipulate table data (new version can implement unlimited specifications)
					newAttrValueBlur(curr_attr, newVal) {
						if (newVal === old_attr_value) return

						// The index of rows that need to be changed is calculated from the cartesian product generated by the specification.
						let cartesian_arr = this.generateBaseData(this.form.sku_arr)
						console.log(cartesian_arr)
						let change_idx_arr = [] // The index of the row to be changed
						for (let i in cartesian_arr) {
							if (cartesian_arr[i][curr_attr] === newVal) change_idx_arr.push(Number(i))
						}
						console.log('change_idx_arr', change_idx_arr)

						// The length of the new table should be compared to the length of the existing table to distinguish between additions and modifications
						let length_arr = this.form.sku_arr.map(x= > x.valueList.length)
						let new_table_length = length_arr.reduce((acc, curr_item) = > acc * curr_item) // Multiply the length of the new table
						let old_table_length = this.form.table_data.length // Old table data length

						// If it is modified
						if (new_table_length === old_table_length) {
							this.form.table_data.forEach((item, index) = > {
								if (change_idx_arr.includes(index)) this.form.table_data[index][curr_attr] = newVal
							})
							return
						}
						// If it is a new one
						if (new_table_length > old_table_length) {
							// Get the current value of the current attribute and other specifications of sku_arr to construct new table data
							let other_sku_arr = this.form.sku_arr.map(item= > {
								if(item.attr ! == curr_attr)return item
								else return { attr: item.attr, valueList: [newVal] }
							})
							// Get the new table data
							let ready_map = this.generateBaseData(other_sku_arr)
							let new_table_data = this.mergeTableData(ready_map)
							change_idx_arr.forEach((item_idx, index) = > {
								this.form.table_data.splice(item_idx, 0, new_table_data[index])
							})
						}
					},
					// Delete attribute value four parameters: 'primary array index ',' secondary index ', 'attribute name ',' attribute value 'the last two parameters are used to delete rows
					deleteSmall(idx, kdx, attr_name, attr_val) {
						this.form.sku_arr[idx].valueList.splice(kdx, 1)

						// Delete the table row
						let data_length = this.form.table_data.length
						for (let i = 0; i < data_length; i++) {
							if (this.form.table_data[i][attr_name] == attr_val) {
								this.form.table_data.splice(i, 1)
								i = i - 1
								data_length = data_length - 1}}},// Generate the table column according to 'this.form.sku_arr', 'table_column' is used for v-for for el-table-column
					async generateTableColumn() {
						this.table_column = this.form.sku_arr.map(x= > x.attr).concat(base_column)
						/* There is a bug when you do not write '$nextTick', and then check the color, then check the size, then uncheck the color, and observe the el-table */
						await this.$nextTick()
						if (this.form.sku_arr.length ! =0) this.table_column.splice(1.0.'images')},/ / merger
					spanMethod({ row, column, rowIndex, columnIndex }) {
						if (columnIndex == 0) {
							let key_0 = column.label
							let first_idx = this.form.table_data.findIndex(x= > x[key_0] == row[key_0])
							const calcSameLength = () = > this.form.table_data.filter(x= > x[key_0] == row[key_0]).length
							first_column_rule = rowIndex == first_idx ? [calcSameLength(), 1] : [0.0]
							return first_column_rule

							// Use the same merge rules for the second column as for the first column
						} else if (columnIndex == 1) {
							return first_column_rule

							/ / the other columns
						} else {
							// For each item in the table,
							const callBacks = (table_item, start_idx = 0) = > {
								if (columnIndex < start_idx) return true
								let curr_key = this.table_column[start_idx]
								return table_item[curr_key] === row[curr_key] && callBacks(table_item, ++start_idx)
							}
							let first_idx = this.form.table_data.findIndex(x= > callBacks(x))
							const calcSameLength = () = > this.form.table_data.filter(x= > callBacks(x)).length
							return rowIndex == first_idx ? [calcSameLength(), 1] : [0.0]}},// Merge skUs with 'picture ',' sales price ', 'inventory ',' market price ', return the entire table data array
					mergeTableData(arr) {
						return arr.map(item= > ({ ...item, 'Selling price': ' '.'Market price': ' '.'inventory': ' '.'images': ' '}})),// Iterate through 'sku_arr' to generate table data
					traverseSku() {
						let ready_map = this.generateBaseData(this.form.sku_arr)
						this.form.table_data = this.mergeTableData(ready_map)
					},
					// reimplement the Cartesian product argument: this.form.sku_arr passes array 'empty ',' length 1', 'length greater than 1'
					generateBaseData(arr) {
						if (arr.length === 0) return []
						if (arr.length === 1) {
							let [item_spec] = arr
							return item_spec.valueList.map(x= > {
								return {
									[item_spec.attr]: x
								}
							})
						}
						if (arr.length >= 1) {
							return arr.reduce((accumulator, spec_item) = > {
								let acc_value_list = Array.isArray(accumulator.valueList) ? accumulator.valueList : accumulator
								let item_value_list = spec_item.valueList
								let result = []
								for (let i in acc_value_list) {
									for (let j in item_value_list) {
										let temp_data = {}
										// If it is an object
										if (acc_value_list[i].constructor === Object) { temp_data = { ... acc_value_list[i], [spec_item.attr]: item_value_list[j] }// Otherwise if it is a string
										} else {
											temp_data[accumulator.attr] = acc_value_list[i]
											temp_data[spec_item.attr] = item_value_list[j]
										}
										result.push(temp_data)
									}
								}
								return result
							})
						}
					},

					onSubmit() {
						this.$refs.form.validate(async (valid, object) => {
							if(! valid) {// Get the distance of the element from the top of the body
								let getTop = dom= > {
									let top = dom.offsetTop
									// The value in the while parenthesis is dom.offsetParent
									while (dom = dom.offsetParent) top = top + dom.offsetTop
									return top
								}
								let [first_prop] = Object.keys(object)
								let top = getTop(document.querySelector(`label[for='${first_prop}'] `))
								window.scrollTo({ top: top - 70.behavior: 'smooth' })
								
								return
							}

							this.btn_loading = true})}}})}</script>
	</body>
</html>

Copy the code