preface

Recently, I am working on a project that needs to display and edit a lot of compound tables. Would you like to share how to build shit mountain

Vue3 + Vite + TS is used in this project

demand

As shown in the picture, there are many areas, each area can be edited and saved separately, without interference.

When you click Edit, the area that can be edited becomes an input box.

The figure above is just a simple demonstration, but there are actually a lot of cells, merging cells, and even multiple tables in one area.

design

  1. Block: Area component that encapsulates the request and save of data, the title and edit buttons.
  2. Table: Table component that encapsulates the Table layout.
  3. Row: A Row component that represents each Row of a table.
  4. Col: Column components, components that represent each column of the table, namely cells.
  5. Edit: Edit component that becomes an input box when edited and displays text when not edited.

You can form merged cells by nesting rows and col over and over again.

Open is dubious

The data requested from within the Block component is exposed through the scope slot for use by the Edit binding.

Unfortunately, since the Edit state is wrapped in a Block, isEdit is also exposed to control whether Edit displays the input box.

We can see that this seems silly, with thousands of boxes, each Edit component has to be explicitly bound to isEdit.

When I was thinking hard, I suddenly realized that element’s Form component can set size attribute to control the size of its Form component. Then I read the source code and found that it is implemented with Provide/Inject, so I do the same.

// Block.vue
provide('isEdit', isEdit)

// Edit.vue
<template>
  <el-input v-if="isEdit" :modelValue="modelValue" v-bind="$attrs" />
  <span v-else >{{ modelValue }}</span>
</template>

<script setup>
const isEdit = Inject('isEdit')
</script>
Copy the code

So far, so good.

The iteration

Now that the business requires that the edited areas be highlighted red and permanently recorded, things get interesting

At that time, a group of us had a long and heated discussion about the idea of implementation.

{
    id1: { a: { original: '123', fresh: '456' }, b: { original: '789', fresh: '456' } },
    id2: { c: { original: '123', fresh: '456' } },
}
Copy the code

As shown in the figure above, the interface returns that fields A and B of id1 have been modified, and fields C and B of ID2 have been modified. As long as they appear here, it means that they have been modified.

The reason why there are multiple ids is that some regions may be objects, which naturally have one ID, and some regions may be arrays, which have multiple ids, as shown in the following figure:

If it’s a nested array, just smack it flat.

As for {original: ‘123’, fresh: ‘456’}, it is reserved for later in case there is a need to see the old values modified previously.

The Block component then makes a built-in request to get the corresponding edit record for that area with different parameters.

Now that we have the data, how do we match it?

This is the way I envisioned it in the beginning, passing records, ids, and attribute names, and judging in Edit or directly. However, it would seem silly to write amount to a variable already bound in the V-Model, and then write amount again to determine if it has been edited

This eliminates the duplication problem, but it’s still a lot of code to change, because there are many, many, many cells, and it’s exhausting to change every cell like this.

After all, laziness is the primary productivity, so I decided to keep the original business code as much as possible. How does the Edit component get records, ids, and property names?

Id is obtained by setting the key property when v-for traverses the Row component, or manually if not traversed.

The attribute name is obtained by intercepting the value of the V-Model binding.

Take a look at the implementation.

Access to records

// Block.vue
provide('records', records)

// Edit.vue
const records = Inject('records')
Copy the code

As with isEdit, it is passed by Provide/Inject

It’s all right here. It’s all dirty down here

Get attribute name

// Edit.vue <template> <el-input v-if="isEdit" :modelValue="modelValue" v-bind="$attrs" /> <span v-else >{{ modelValue }} < / span > < / template > / / using the < Edit v - model = "item. Amount" / >Copy the code

To review how the Edit component is written and used, instead of manually binding input events and then emitting them, I used the V-model syntax to automatically generate update events, put them in attrs, and then internal V −bind=”attrs, and then internal V-bind =”attrs, Then the internal v – bind = “attrs” can bind an event, don’t know the students can take a look at this v3.cn.vuejs.org/guide/migra…

// vue2
console.log(this.$attrs)

// vue3
setup(props, { attrs }) {
    console.log(attrs)
}
Copy the code

We can print it out and look at it

const fn = String(attrs['onUpdate:modelValue'])
const temp = fn.slice(0, fn.lastIndexOf('=')).trim()
const key = temp.slice(temp.lastIndexOf('.') + 1)
Copy the code

  • modelValueIs vue3v-modelThe variable name of the default binding
  • Don’t be fooled by the console,attrs['onUpdate:modelValue']The function that you get, I’m going to use hereString()Convert it to a string
  • F sub n will print something like this$event => item.amount = $event, we can get the amount by string clipping

Of course there are big holes. We’ll talk about that later

Get the id

We said that since v-for sets the key value, and the key value is set with the ID value, we can use this to pass the ID

The props key is used to obtain the props key.

Then I printed attrs again, and nothing!

It’s weird, where did this key go, so I’ll print this

It is eventually found in the vnode inside the $or _

Vue is used as a virtual DOM

// Row.vue
setup(){
    const { vnode } = getCurrentInstance()
    provide(businessId, vnode.key)
}

// Edit.vue
const businessId = Inject('businessId**')**
Copy the code

Vue3 we can get the key in this way and communicate with Provide/Inject as before, but there is a pit in this way, as discussed below

The Edit component can then determine if it has been edited. The complete code looks like this:

const records = inject<Ref>('records', ref({})) const businessId = inject<string>('businessId', '') const hasEdit = computed(() => { if (! businessId) return false const fn = String(attrs['onUpdate:modelValue']) const temp = fn.slice(0, fn.lastIndexOf('=')) const key = temp.slice(temp.lastIndexOf('.') + 1) return !! records.value[businessId]? .[key] })Copy the code

? Is the es6 optional chain operator, do not understand children’s shoes can be checked

We got the ID, but we can do better

First, it is packaged into hook

Then use it in the Row and Table components, because some of them are not arrays, just one object, and there are multiple rows, it is much easier to set the ID directly on the Table once.

pit

Don’t get id

If the parent component provides a variable called A, and then the child component also provides a variable called A, then I would like to ask whose value is the variable A received by the grandson component?

Then I tried it out and got the child component, which is intuitive, like position: Absolute up to the nearest position: relative, and the attributes of the access object up to the prototype chain.

Insert key into Table; insert key into Row; insert key into Table; insert key into Row;

Add a check, Provide only if the key value is set

Some boxes are not reflected properly

<Edit v-model="a ? item.b : item.c" />
Copy the code

If this is the case, the method above is invalid, so we can change it to this:

<Edit v-if="a" v-model="item.b" />
<Edit v-else v-model="item.c" />
Copy the code

There are better solutions, but I’m lazy

All the boxes in the test environment are not displayed properly

After investigation, it was found that there was a problem when obtaining the attribute name:

// dev
e => item.a = e

// test
e=>item.a=e
Copy the code

Never use whitespace to intercept strings, because only development environments have whitespace, and packaged code has no whitespace because it is compressed

For compatibility with different environments, trim() can be used to remove whitespace

conclusion

In fact, in this article I mainly share the use of key to pass the attribute and the intercection of the v-model binding of the attribute name of the two methods, there are a lot of points in the business itself, I did not say, for example, if the binding of the attribute is A.B.C.D, then how to do? When editing, too many boxes can become input boxes at once, so use requestAnimationFrame to change the edit status in batches. For example, if there are multiple tables in a certain area, is it more convenient to package a component that is specifically used to provide keys? There are many problems such as not setting keys for each table.

Finally, I wish you all the year of the Tiger, wish you only shit mountain first tuo xiang, don’t then pull others