Anton Tikhonov
Anton Tikhonov

Reputation: 1

Reactivity for property is not working in Vue3 when input element is in slot

I'm trying to implement a wrapper class for validating forms. The idea is that this Form class receives a data object with handler, which performs necessary checks and populates error list. This class also has a method to return a list of errors for the specific field.

Additionally, I`ve created a container component with a slot to display label for the field. This component defines a slot which contains the form element (eg. input) with value bound using v-model directive.

My expectation is that when user updates the input field, validation error placeholder is updated on field change if there are errors. But when the input is in slot, this does not happen. If I place input outside the slot or display the bound value in another place (eg. div element), the error placeholder is updated.

I've created a minimal example in the Vue playground that demonstrates the issue.

Sample code:

// Container.vue
<template>
    <div>
        Component with slot
        <div><slot></slot></div>
    </div>
</template>


// App
<script setup lang="ts">
import { ref } from 'vue'
import Container from './Container.vue';

type ErrorRecord = {key: string, error: string}

class DataForm {
    errors: ErrorRecord[] = []
    data = {
        _desc: '',
        get description() {return this._desc},
        set description(val) { this._desc = val; if (this.handler) { this.handler(val) } },
        handler: undefined as unknown as (val: any) => void
    }
    constructor() {
        this.data.handler = this.handler.bind(this)
    }
    handler(value: any) {
        if (String(value).length < 5) {
            this.errors = [{key: 'description', error: `Too Short`}]
        }
        else {
            this.errors = []
        }
    }
    getErrors(path: string) {
        return this.errors.find(err => err.key == path)?.error
    }    
}
const data1 = ref(new DataForm())
const data2 = ref(new DataForm())
const data3 = ref(new DataForm())


</script>

<template>
    <h1>Error not updated when input is in slot: </h1>
    <Container>
        <input type="text" v-model="data1.data.description">
    </Container>
    <div> {{ data1.getErrors('description') }} </div>

    <h1>Error updated when value is used somewhere else: </h1>
    <Container>
        <input type="text" v-model="data2.data.description">
    </Container>
    <div>{{ data2.data.description }}</div>
    <div> {{ data2.getErrors('description') }} </div>

    <h1>Error updated when input is not in slot: </h1>
    <input type="text" v-model="data3.data.description">
    <div> {{ data3.getErrors('description') }} </div>
</template>

Upvotes: 0

Views: 428

Answers (2)

Anton Tikhonov
Anton Tikhonov

Reputation: 1

Posting this as answer, as personally for me the reason was not obvious. Although it is pretty straightforward from the JS/Vue perspective. My main experience is in C#. Thus, I followed the OOP pattern. But in Vue reactive objects are proxies, so in my code I was referencing the non-reactive this. Rewriting my code that caused the issue to follow the suggested pattern resolved the problem. So for those, who like me come from other programming languages, it is a good example not to blindly follow patterns that we were used to in other languages.

Another example on SO: link.

Many thanks to @estus-flask. Link to corrected working example.

Working code:

// App.vue
<script setup lang="ts">
import { ref, reactive } from 'vue'
import Container from './Container.vue';

type ErrorRecord = {key: string, error: string}

const createForm = () => {
    var o = reactive({
        errors: [],
        data:{
            _desc: '',
            get description() {return o._desc},
            set description(val) { o._desc = val; o.data.handler(val) },
            handler(value: any) {
            if (String(value).length < 5) {
                o.errors = [{key: 'description', error: `Too Short`}]
            }
            else {
                o.errors = []
            }
        },
        },
        
        getErrors(path: string) {
            return o.errors.find(err => err.key == path)?.error
        }    
    })

    return o
}
const data1 = createForm()
const data2 = createForm()
const data3 = createForm()


</script>

<template>
    <h1>Error not updated when input is in slot: </h1>
    <Container>
        <input type="text" v-model="data1.data.description">
    </Container>
    <div> {{ data1.getErrors('description') }} </div>

    <h1>Error updated when value is used somewhere else: </h1>
    <Container>
        <input type="text" v-model="data2.data.description">
    </Container>
    <div>{{ data2.data.description }}</div>
    <div> {{ data2.getErrors('description') }} </div>

    <h1>Error updated when input is not in slot: </h1>
    <input type="text" v-model="data3.data.description">
    <div> {{ data3.getErrors('description') }} </div>
</template>

Upvotes: 0

moon
moon

Reputation: 1132

I changed your minimal example,as you do a lot of things in class DataForm befor using a ref or reactive, vue will not help you update the dom, you need create a ref first,then update it . besides,I left a question for you that why data using reactive instead of ref

Upvotes: 0

Related Questions