英文:
Vue3 - Bind multiple named v-model dynamically
问题
I am currently building a tool with Vue3 that allows users to create custom plugins. In their respective plugins, users can define a set of settings and specify which component should render the setting. Some of these rendering components might have multiple v-model bindings, and their names can vary depending on the component. I am currently able to dynamically build a plugin setting with a main JavaScript object, but I have no idea how to bind a named v-model dynamically.
I tried to pass the additional models to the v-bind attribute, and I am able to get the value. However, since no v-model is assigned to these properties, I am not able to receive incoming changes from the child component.
Is there a way to add dynamic named v-model in MainRenderer.vue to have the relevant named model assigned to the right components?
When I hard-coded the v-model binding as shown below, I achieved the desired behavior for my main component. However, since the components are various and custom, this solution won't work in production.
<component
v-for="field in formFields"
:is="field.component"
v-model="field.models.modelValue"
v-model:foo="field.models.foo"
v-model:foo="field.models.bar"
v-bind="field.props"
></component>
I created a simplified version of this issue with the following four files to describe the problem. Customization is managed via the fields
object in App.vue.
App.vue
<template>
<MainRenderer :form-fields="fields" :action="action">
Submit
</MainRenderer>
<div>{{foo}} - {{inputFoo}}</div>
<div>{{bar}} - {{inputBar}}</div>
</template>
<script lang="ts" setup>
import { ref, reactive, shallowRef } from 'vue'
import MainRenderer, { FormField } from './MainRenderer.vue'
import Foo from './Foo.vue'
import Bar from './Bar.vue'
const inputFoo = ref('fooValue')
const inputBar = ref('barValue')
const foo = ref(false)
const bar = ref(false)
const fields: FormField[] = reactive([
{
name: "Foo - String Input Field",
model: shallowRef(inputFoo),
models: { modelValue: inputFoo, foo: foo },
props: { class: 'dummy-class', placeholder: "Input A" },
component: Foo
},
{
name: "Bar - Multi Item Input Field",
model: shallowRef(inputBar),
models: { modelValue: inputBar, bar: bar },
props: { class: 'dummy-class', 'v-model:foo': shallowRef(bar) },
component: Bar
},
])
const action = async function () {
console.log(fields)
}
</script>
MainRenderer.vue
<template>
<form @submit.prevent="action">
<div v-for="field in formFields">
<component :is="field.component" v-bind="field.props" v-model="field.model"></component>
</div>
<button type="submit">
<slot></slot>
</button>
</form>
</template>
<script lang="ts">
import { Ref, defineComponent, PropType, Component } from 'vue';
import Foo from './Foo.vue';
import Bar from './Foo.vue';
export interface FormField {
name: String
props: Object
component: Component
model: Ref
models: Object
}
export default defineComponent({
components: { Foo, Bar },
props: {
formFields: {
type: Array as PropType<Array<FormField>>,
},
action: {
type: Function as PropType<() => Promise<any>>,
}
}
})
</script>
Foo.vue
<template>
<input :name="name" v-model="value">
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
props: {
name: String,
modelValue: String,
foo: Boolean
},
watch: {
modelValue: function () {
const newFooValue = (typeof this.modelValue === 'number'); // Example
this.$emit('update:foo', newFooValue);
}
},
computed: {
value: {
get() {
return this.modelValue;
},
set(v: string) {
return this.$emit('update:modelValue', v);
}
}
}
})
</script>
Bar.vue
<template>
<input :name="name" v-model="value">
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
props: {
name: String,
modelValue: String,
bar: Boolean
},
watch: {
modelValue: function () {
const newBarValue = this.modelValue.charAt(0) === 'B';
this.$emit('update:bar', newBarValue);
}
},
computed: {
value: {
get() {
return this.modelValue;
},
set(v: string) {
return this.$emit('update:modelValue', v);
}
}
}
})
</script>
PS: I understand that the use of multiple v-models in the example above may seem unnecessary, but I tried to avoid duplicating a lot of out-of-scope code.
英文:
I am currently building a tool with Vue3 that allow user for custom plugin building. In their respective plugin, user can define a set of settings and define which component should render the setting. Some of these rendering components might have multiple v-model bindings and their name can be different depending on the component. I am currently able to build a plugin setting dynamically with a main JS object but I have no idea how to bind a named v-model
dynamically.
I tried to pass the additional models to the v-bind
attribute and I am able to get the value but since no v-model
is assigned to these properties I am not able to get incoming changes from the child component.
Is there a way to add dynamic named v-model in MainRenderer.vue to have the relevant named model assigned to the right components.
When I hard coded the v-model binding as below, I have the desired behavior for my main component. But since the component are various and custom, this solution won't work in production.
<component
v-for="field in formFields"
:is="field.component"
v-model="field.models.modelValue"
v-model:foo="field.models.foo"
v-model:foo="field.models.bar"
v-bind="field.props"
></component>
I created a lighter version of this issue with the 4 following files to describe the issue. The customization is managed via the fields
object in App.vue
App.vue
<template>
<MainRenderer :form-fields="fields" :action="action">
Submit
</MainRenderer>
<div>{{foo}} - {{inputFoo}}</div>
<div>{{bar}} - {{inputBar}}</div>
</template>
<script lang="ts" setup>
import { ref, reactive, shallowRef } from 'vue'
import MainRenderer, { FormField } from './MainRenderer.vue'
import Foo from './Foo.vue'
import Bar from './Bar.vue'
const inputFoo = ref('fooValue')
const inputBar = ref('barValue')
const foo = ref(false)
const bar = ref(false)
const fields: FormField[] = reactive([
{
name: "Foo - String Input Field",
model: shallowRef(inputFoo),
models: { modelValue: inputFoo, foo: foo },
props: { class: 'dummy-class', placeholder: "Input A" },
component: Foo
},
{
name: "Bar - Multi Item Input Field",
model: shallowRef(inputBar),
models: { modelValue: inputBar, bar: bar },
props: { class: 'dummy-class', 'v-model:foo':shallowRef(bar) },
component: Bar
},
])
const action = async function () {
console.log(fields)
}
</script>
MainRenderer.vue
<template>
<form @submit.prevent="action">
<div v-for="field in formFields">
<component :is="field.component" v-bind="field.props" v-model="field.model"></component>
</div>
<button type="submit">
<slot></slot>
</button>
</form>
</template>
<script lang="ts">
import { Ref, defineComponent, PropType, Component } from 'vue';
import Foo from './Foo.vue'
import Bar from './Foo.vue'
export interface FormField {
name: String
props: Object
component: Component
model: Ref
models: Object
}
export default defineComponent({
components: { Foo, Bar },
props: {
formFields: {
type: Array as PropType<Array<FormField>>
},
action: {
type: Function as PropType<() => Promise<any>>
}
}
})
</script>
Foo.vue
<template>
<input :name="name" v-model="value">
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
props: {
name: String,
modelValue: String,
foo: Boolean
},
watch: {
modelValue: function () {
const newFooValue = (typeof this.modelValue === 'number') // Example
this.$emit('update:foo', newFooValue)
}
},
computed: {
value: {
get() {
return this.modelValue
},
set(v: string) {
return this.$emit('update:modelValue', v)
}
}
}
})
</script>
Bar.vue
<template>
<input :name="name" v-model="value">
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
props: {
name: String,
modelValue: String,
bar: Boolean
},
watch: {
modelValue: function () {
const newBarValue = this.modelValue.charAt(0) === 'B'
this.$emit('update:bar', newBarValue)
}
},
computed: {
value: {
get() {
return this.modelValue
},
set(v: string) {
return this.$emit('update:modelValue', v)
}
}
}
})
</script>
PS: I know from the example above, the use of multiple v-model seems unnecessary but I tried to avoid copy/paste a lot of 'out-of-scope' code.
答案1
得分: 1
你可以像这样构建自己的v-model
:
<!-- 父组件 -->
<template>
<component :is="field.component" v-bind="field.props" @updateValue="onUpdateValue"></component>
</template>
<script lang="ts" setup>
function onUpdateValue(field: string, value: any){
field.models[field] = value
}
</script>
<!-- 子组件 -->
<template>
<input :value="somePropsField" @input="$emit('updateValue', 'somePropsField', $event)"></input>
</template>
在子组件中,你会触发一个名为updateValue
的事件,传递字段名称和要更新的值。在父组件中,你会监听该事件并相应地更新数据。
英文:
You can build your own v-model
like this:
<!-- parent component -->
<template>
<component :is="field.component" v-bind="field.props" @updateValue="onUpdateValue"></component>
</template>
<script lang="ts" setup>
function onUpdateValue(field: string, value: any){
field.models[field] = value
}
</script>
<!-- child component -->
<template>
<input :value="somePropsField" @input="$emit('updateValue', 'somePropsField', $event)"></input>
</template>
In the child component, you emit an event called updateValue
with a field name to update, and the value of that field. And in the parent component, you listen to that event and update the data accordingly
答案2
得分: 0
对于遇到相同需求的人,您可以为 :<REF_NAME>
和 onUpdate:<REF_NAME>
添加绑定。父组件将控制子组件模型,这可能不是最干净的实现方式,但可以实现目的。
如果需要,我链接了一个更新的演示。
<script lang="ts" setup>
import { ref, reactive, shallowRef } from 'vue'
import MainRenderer, { FormField } from './MainRenderer.vue'
import Foo from './Foo.vue'
import Bar from './Bar.vue'
function defineNamedModel(name: string, model: Ref){
// 一个通用的函数,以避免重复的代码
let binding = `:${name}`
let updateCallback = `onUpdate:${name}`
return {
[binding]: model,
[updateCallback]:((v: any) => model.value = v)
}
}
const inputFoo = ref('fooValue')
const inputBar = ref('barValue')
const foo = ref(false)
const bar = ref(false)
const fields: FormField[] = reactive([
{
name: "Foo - String Input Field",
model: shallowRef(inputFoo),
props: { class: 'dummy-class', placeholder: "Input A" },
component: Foo
},
{
name: "Bar - Multi Item Input Field",
model: shallowRef(inputBar),
props: { class: 'dummy-class', ...defineNamedModel('bar', bar)},
component: Bar
},
])
const action = async function () {
console.log(fields)
}
</script>
英文:
To whoever encounters the same need, you can add bindings for :<REF_NAME>
and onUpdate:<REF_NAME>
. The parent component will be in control of the child component model, it might not be the cleanest way to achieve it but it achieve the purpose.
I link an updated DEMO if needed
<script lang="ts" setup>
import { ref, reactive, shallowRef } from 'vue'
import MainRenderer, { FormField } from './MainRenderer.vue'
import Foo from './Foo.vue'
import Bar from './Bar.vue'
function defineNamedModel(name: string, model: Ref){
// A generic function to avoid repeating code
let binding = `:${name}`
let updateCallback = `onUpdate:${name}`
return {
[binding]: model,
[updateCallback]:((v: any) => model.value = v)
}
}
const inputFoo = ref('fooValue')
const inputBar = ref('barValue')
const foo = ref(false)
const bar = ref(false)
const fields: FormField[] = reactive([
{
name: "Foo - String Input Field",
model: shallowRef(inputFoo),
props: { class: 'dummy-class', placeholder: "Input A" },
component: Foo
},
{
name: "Bar - Multi Item Input Field",
model: shallowRef(inputBar),
props: { class: 'dummy-class', ...defineNamedModel('bar', bar)},
component: Bar
},
])
const action = async function () {
console.log(fields)
}
</script>
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论