Для клиентского проекта мне нужно использовать Vue.js версии 2.
В компоненте у меня есть 2 итерации над некоторыми JSON определения поля, которые используют <template> с v-for.
Какую бы итерацию я ни поставил первой, она будет проигнорирована — контент никогда не появится. Если я изменю их порядок, это будет соответствующий другой.
СИЛЬНО ОТРЕДАКТИРОВАНО: теперь, надеюсь, включает воспроизводимый пример.
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.info(`\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"
]
}
Обновлено: Проблема остается, если я создаю слоты в том же компоненте, где я их заполняю. Он исчезает, если у меня есть только две итерации и я не использую никаких слотов.
Это объект JSON, ключи не конфликтуют.
Да, вероятно, так и есть.
На сокращение тысяч строк кода у меня ушло почти два часа, но у меня есть небольшой тестовый пример, показывающий проблему; теперь он находится здесь, в вопросе, а не в исходных фрагментах кода. @kissu, если чего-то не хватает, обращайся





Кажется, это такая странная ошибка, и похоже, она вызвана одним и тем же именем итератора 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. Учитывая это, я предлагаю также изменить имя key, указав что-то уникальное для каждого цикла.
Спасибо, это работает! Я мог бы поклясться, что когда-нибудь пробовал это... тем не менее, это стоит обещанной награды, большое спасибо!!
uploadFieldConfigs— это объект или массив? Потому что использование индекса из массива — плохая идея для:key. В остальном я не думаю, что у Vue есть такие ограничения, но код в его текущем состоянии немного сложно читать. Поэтому, пожалуйста, предоставьте нам минимально воспроизводимый пример, это может очень помочь.