Vue3 – 动态绑定多个命名的 v-model

huangapple go评论69阅读模式
英文:

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.

DEMO ON PLAY VUEJS

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.

&lt;component 
    v-for=&quot;field in formFields&quot; 
    :is=&quot;field.component&quot; 
    v-model=&quot;field.models.modelValue&quot;
    v-model:foo=&quot;field.models.foo&quot;
    v-model:foo=&quot;field.models.bar&quot;
    v-bind=&quot;field.props&quot;
&gt;&lt;/component&gt;

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

DEMO ON PLAY VUEJS

App.vue

&lt;template&gt;
    &lt;MainRenderer :form-fields=&quot;fields&quot; :action=&quot;action&quot;&gt;
      Submit
    &lt;/MainRenderer&gt;
    &lt;div&gt;{{foo}} - {{inputFoo}}&lt;/div&gt;
    &lt;div&gt;{{bar}} - {{inputBar}}&lt;/div&gt;
&lt;/template&gt;

&lt;script lang=&quot;ts&quot; setup&gt;
import { ref, reactive, shallowRef } from &#39;vue&#39;
import MainRenderer, { FormField } from &#39;./MainRenderer.vue&#39;
import Foo from &#39;./Foo.vue&#39;
import Bar from &#39;./Bar.vue&#39;

const inputFoo = ref(&#39;fooValue&#39;)
const inputBar = ref(&#39;barValue&#39;)
const foo = ref(false)
const bar = ref(false)

const fields: FormField[] = reactive([
    {
        name: &quot;Foo - String Input Field&quot;,
        model: shallowRef(inputFoo),
        models: { modelValue: inputFoo, foo: foo },
        props: { class: &#39;dummy-class&#39;, placeholder: &quot;Input A&quot; },
        component: Foo
    },
    {
        name: &quot;Bar - Multi Item Input Field&quot;,
        model: shallowRef(inputBar),
        models: { modelValue: inputBar, bar: bar },
        props: { class: &#39;dummy-class&#39;, &#39;v-model:foo&#39;:shallowRef(bar) },
        component: Bar
    },
])


const action = async function () {
    console.log(fields)
}
&lt;/script&gt;

MainRenderer.vue

&lt;template&gt;
    &lt;form @submit.prevent=&quot;action&quot;&gt;
        &lt;div v-for=&quot;field in formFields&quot;&gt;
            &lt;component :is=&quot;field.component&quot; v-bind=&quot;field.props&quot; v-model=&quot;field.model&quot;&gt;&lt;/component&gt;
        &lt;/div&gt;
        &lt;button type=&quot;submit&quot;&gt;
            &lt;slot&gt;&lt;/slot&gt;
        &lt;/button&gt;
    &lt;/form&gt;
&lt;/template&gt;

&lt;script lang=&quot;ts&quot;&gt;
import { Ref, defineComponent, PropType, Component } from &#39;vue&#39;;
import Foo from &#39;./Foo.vue&#39;
import Bar from &#39;./Foo.vue&#39;

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&lt;Array&lt;FormField&gt;&gt;
        },
        action: {
            type: Function as PropType&lt;() =&gt; Promise&lt;any&gt;&gt;
        }
    }
})
&lt;/script&gt;

Foo.vue

&lt;template&gt;
    &lt;input :name=&quot;name&quot; v-model=&quot;value&quot;&gt;
&lt;/template&gt;

&lt;script lang=&quot;ts&quot;&gt;
import { defineComponent } from &#39;vue&#39;

export default defineComponent({
    props: {
        name: String,
        modelValue: String,
        foo: Boolean
    },
    watch: {
        modelValue: function () {
            const newFooValue = (typeof this.modelValue === &#39;number&#39;) // Example
            this.$emit(&#39;update:foo&#39;, newFooValue)
        }
    },
    computed: {
        value: {
            get() {
                return this.modelValue
            },
            set(v: string) {
                return this.$emit(&#39;update:modelValue&#39;, v)
            }
        }
    }
})
&lt;/script&gt;

Bar.vue

&lt;template&gt;
    &lt;input :name=&quot;name&quot; v-model=&quot;value&quot;&gt;
&lt;/template&gt;

&lt;script lang=&quot;ts&quot;&gt;
import { defineComponent } from &#39;vue&#39;

export default defineComponent({
    props: {
        name: String,
        modelValue: String,
        bar: Boolean
    },
    watch: {
        modelValue: function () {
            const newBarValue = this.modelValue.charAt(0) === &#39;B&#39;
            this.$emit(&#39;update:bar&#39;, newBarValue)
        }
    },
    computed: {
        value: {
            get() {
                return this.modelValue
            },
            set(v: string) {
                return this.$emit(&#39;update:modelValue&#39;, v)
            }
        }
    }
})
&lt;/script&gt;

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:

&lt;!-- parent component --&gt;
&lt;template&gt;
&lt;component :is=&quot;field.component&quot; v-bind=&quot;field.props&quot; @updateValue=&quot;onUpdateValue&quot;&gt;&lt;/component&gt;
&lt;/template&gt;

&lt;script lang=&quot;ts&quot; setup&gt;
function onUpdateValue(field: string, value: any){
  field.models[field] = value
}
&lt;/script&gt;
&lt;!-- child component --&gt;
&lt;template&gt;
&lt;input :value=&quot;somePropsField&quot; @input=&quot;$emit(&#39;updateValue&#39;, &#39;somePropsField&#39;, $event)&quot;&gt;&lt;/input&gt;
&lt;/template&gt;

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

对于遇到相同需求的人,您可以为 :&lt;REF_NAME&gt;onUpdate:&lt;REF_NAME&gt; 添加绑定。父组件将控制子组件模型,这可能不是最干净的实现方式,但可以实现目的。

如果需要,我链接了一个更新的演示

&lt;script lang=&quot;ts&quot; setup&gt;
import { ref, reactive, shallowRef } from &#39;vue&#39;
import MainRenderer, { FormField } from &#39;./MainRenderer.vue&#39;
import Foo from &#39;./Foo.vue&#39;
import Bar from &#39;./Bar.vue&#39;

function defineNamedModel(name: string, model: Ref){
    // 一个通用的函数,以避免重复的代码
    let binding = `:${name}`
    let updateCallback = `onUpdate:${name}` 
    return {
        [binding]: model,
        [updateCallback]:((v: any) =&gt; model.value = v)
    }
}

const inputFoo = ref(&#39;fooValue&#39;)
const inputBar = ref(&#39;barValue&#39;)
const foo = ref(false)
const bar = ref(false)

const fields: FormField[] = reactive([
    {
        name: &quot;Foo - String Input Field&quot;,
        model: shallowRef(inputFoo),
        props: { class: &#39;dummy-class&#39;, placeholder: &quot;Input A&quot; },
        component: Foo
    },
    {
        name: &quot;Bar - Multi Item Input Field&quot;,
        model: shallowRef(inputBar),
        props: { class: &#39;dummy-class&#39;, ...defineNamedModel(&#39;bar&#39;, bar)},
        component: Bar
    },
])

const action = async function () {
    console.log(fields)
}
&lt;/script&gt;
英文:

To whoever encounters the same need, you can add bindings for :&lt;REF_NAME&gt; and onUpdate:&lt;REF_NAME&gt;. 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

&lt;script lang=&quot;ts&quot; setup&gt;
import { ref, reactive, shallowRef } from &#39;vue&#39;
import MainRenderer, { FormField } from &#39;./MainRenderer.vue&#39;
import Foo from &#39;./Foo.vue&#39;
import Bar from &#39;./Bar.vue&#39;


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) =&gt; model.value = v)
    }
}

const inputFoo = ref(&#39;fooValue&#39;)
const inputBar = ref(&#39;barValue&#39;)
const foo = ref(false)
const bar = ref(false)

const fields: FormField[] = reactive([
    {
        name: &quot;Foo - String Input Field&quot;,
        model: shallowRef(inputFoo),
        props: { class: &#39;dummy-class&#39;, placeholder: &quot;Input A&quot; },
        component: Foo
    },
    {
        name: &quot;Bar - Multi Item Input Field&quot;,
        model: shallowRef(inputBar),
        props: { class: &#39;dummy-class&#39;, ...defineNamedModel(&#39;bar&#39;, bar)},
        component: Bar
    },
])


const action = async function () {
    console.log(fields)
}
&lt;/script&gt;

huangapple
  • 本文由 发表于 2023年6月26日 09:43:39
  • 转载请务必保留本文链接:https://go.coder-hub.com/76553082.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定