对于客户项目,我需要使用 Vue.js 版本 2。
在组件中,我对一些使用
<template>
和 v-for
的字段定义 JSON 进行了 2 次迭代。
无论我首先放置哪一个迭代,都将被忽略 - 内容永远不会显示。如果我改变他们的顺序,它将是相应的另一个。
大量编辑:现在包括一个可重现的示例,希望
src/__tests__/ConcreteFormComponentFillingSlots.spec.js
// @vitest-environment jsdom
import { describe, expect, it } from 'vitest'
import { mount } from '@vue/test-utils'
import ConcreteFormComponentFillingSlots from '@/__tests__/ConcreteFormComponentFillingSlots.vue'
describe('My bug with multiple template v-for iterations', () => {
it('renders all form fields', () => {
const wrapper = mount(ConcreteFormComponentFillingSlots)
const renderedHtml = wrapper.html()
console.log(`\n\n### + ${renderedHtml}\n\n####`)
expect(renderedHtml).includes('Search | Key: |searchResultSlot1|')
expect(renderedHtml).includes('Search | Key: |searchResultSlot2|')
expect(renderedHtml).includes('Upload | Key: |uploadResultSlot1|')
expect(renderedHtml).includes('Upload | Key: |uploadResultSlot2|')
})
})
src/__tests__/GenericFormComponentWithSlots.vue
<script>
export default {
name: 'GenericFormComponentWithSlots',
props: {
id: String,
formConfig: Object
},
data () {
return {
pageSelectionIndex: 0,
sectionSelectionIndex: 0
}
},
methods: {
getFieldLabel (field) {
return field.label
}
},
computed: {
getSelectedSection () {
return this.formConfig.sections[this.sectionSelectionIndex]
}
}
}
</script>
<template>
<div v-bind:id="`c_smart-form-body_some_id`"
class="c_smart-form-body">
<transition-group name="show" tag="div">
<template v-for="(field) in getSelectedSection.fields">
<div v-bind:key="`${field.id}_${pageSelectionIndex}_outerForm`"
class="c_smart-form-field">
<div v-if="field.type === 'Slot'">
<p v-html="'Slot goes here: field(' + field.id + ')'"/>
<slot v-bind:name="`field(${field.id})`"
v-bind:field="field">
</slot>
</div>
<div v-else>
<p v-html="'Unknown field type: ' + field.type + ' for field ' + field.id"/>
</div>
</div>
</template>
</transition-group>
</div>
</template>
<style lang="less">
</style>
src/__tests__/ConcreteFormComponentFillingSlots.vue
<script>
import GenericFormComponentWithSlots from '@/__tests__/GenericFormComponentWithSlots.vue'
import theUserForm from '@/__tests__/the-user-form.json'
import _ from 'lodash'
export default {
name: 'ConcreteFormComponentFillingSlots',
mixins: [
],
components: {
GenericFormComponentWithSlots
},
props: {
},
data () {
return {
customForm: _.cloneDeep(theUserForm),
uploadFieldConfigs: {
uploadResultSlot1: {
isUploading: false,
slotName: 'field(uploadResultSlot1)'
},
uploadResultSlot2: {
isUploading: false,
slotName: 'field(uploadResultSlot2)'
}
},
searchFieldConfigs: {
searchResultSlot1: {
slotName: 'field(searchResultSlot1)'
},
searchResultSlot2: {
slotName: 'field(searchResultSlot2)'
}
}
}
}
}
</script>
<template>
<div class="custom-task-content-container">
<GenericFormComponentWithSlots v-if="customForm" id="customForm" v-bind:form-config=this.customForm
>
<!-- Iterates over uploadFieldConfigs; fieldConfig will be the child object, key will be the key under which it is stored as part of uploadFieldConfigs -->
<!-- '#' is a shorthand for v-slot: (referencing the slot name dynamically is easier with the shorthand) https://vuejs.org/guide/components/slots.html#dynamic-slot-names -->
<!-- '=data' passes in the field data, and we pass name and field in EssentialSmartForm, cf. https://vuejs.org/api/built-in-directives.html#v-slot -->
<template v-for="(fieldConfig, key) in uploadFieldConfigs" #[fieldConfig.slotName]="data">
<p :key="key + '_upload_debug1'" v-html="'Upload | Key: |' + key + '| Data: |' + JSON.stringify(data) + '|'"></p>
</template>
<template v-for="(fieldConfig, key) in searchFieldConfigs" #[fieldConfig.slotName]="slotProps">
<p :key="key + '_upload_debug2'" v-html="'Search | Key: |' + key + '| Data: |' + JSON.stringify(slotProps) + '|'"></p>
</template>
</GenericFormComponentWithSlots>
</div>
</template>
<style lang="less" scoped>
</style>
src/__tests__/the-user-form.json
{
"id": "myForm",
"version": 1,
"headline": "My form",
"sections": [
{
"id": "BaseInfo",
"title": "Some Hints",
"allowMultiple": false,
"fields": [
{
"id": "searchResultSlot1",
"type": "Slot",
"label": "Search result slot 1",
"keyLabel": "labelName",
"isReadOnly": false
},
{
"id": "searchResultSlot2",
"type": "Slot",
"label": "Search result slot 2",
"keyLabel": "labelName",
"isReadOnly": false
},
{
"id": "uploadResultSlot1",
"type": "Slot",
"label": "Upload Result Slot 1",
"upload": true,
"required": true
},
{
"id": "uploadResultSlot2",
"type": "Slot",
"label": "Upload Result Slot 2",
"upload": true,
"required": true
}
],
"defaultValues": {
},
"pages": [{}]
}
]
}
vite.config.js
import vue from '@vitejs/plugin-vue2'
import { fileURLToPath, URL } from "node:url";
export default {
plugins: [ vue() ],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
}
package.json
{
"name": "My-App",
"version": "1.0.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"test:unit": "vitest --environment jsdom --root src/",
"coverage": "vitest --coverage --environment jsdom --root src/"
},
"dependencies": {
"isomorphic-fetch": "^3.0.0",
"register-service-worker": "^1.7.2",
"vue": "^2.7.16",
"vue-dompurify-html": "4.1",
"vue-router": "^3.5.3"
},
"devDependencies": {
"@vitejs/plugin-vue2": "^2.3.1",
"@vitest/coverage-c8": "^0.29.2",
"@vue/cli-plugin-eslint": "^5.0.8",
"@vue/cli-plugin-pwa": "^5.0.4",
"@vue/cli-plugin-router": "^5.0.4",
"@vue/cli-service": "^5.0.8",
"@vue/eslint-config-standard": "^6.1.0",
"@vue/test-utils": "^1.3.6",
"axios": "^1.6.5",
"eslint": "^7.32.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^5.2.0",
"eslint-plugin-vue": "^7.20.0",
"jsdom": "^20.0.3",
"keycloak-js": "^23.0.7",
"less": "^4.1.2",
"less-loader": "^10.2.0",
"lodash": "^4.17.21",
"msw": "^1.1.0",
"portal-vue": "^2.1.7",
"terser": "^5.30.4",
"vitest": "^0.29.8",
"vue-axios": "^3.4.1",
"vue-i18n": "^8.27.1",
"vue-sweetalert2": "^5.0.5",
"vue-template-compiler": "^2.7.16",
"vue-virtual-scroller": "^1.0.10",
"workbox-webpack-plugin": "6.5.3"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"@vue/standard"
],
"parserOptions": {
"ecmaVersion": 2021
},
"rules": {
"indent": [
"error",
4
]
}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}
编辑:如果我在填充槽的同一组件中生成槽,问题仍然存在。如果我只有两次迭代并且不使用任何插槽,它就会消失。
这似乎是一个很奇怪的错误,这似乎是由两个
fieldConfig
的迭代器 v-for
的同名引起的。在我看来,无论出于何种原因,变量都是共享的。最简单的解决方法是为每个 v-for
循环使用不同的迭代器变量名称,例如:
<template v-for="(fieldConfig, key) in uploadFieldConfigs" #[fieldConfig.slotName]="data">
<p :key="key + '_upload_debug1'" v-html="'Upload | Key: |' + key + '| Data: |' + JSON.stringify(data) + '|'"></p>
</template>
<template v-for="(fieldConfig2, key) in searchFieldConfigs" #[fieldConfig2.slotName]="slotProps">
<p :key="key + '_upload_debug2'" v-html="'Search | Key: |' + key + '| Data: |' + JSON.stringify(slotProps) + '|'"></p>
</template>
注意我如何使用
fieldConfig2
作为 searchFieldConfigs
上的迭代器。