3
votes

The Context

I’m building a big form with lots of inputs in Vue.js 2.6.10 using class-based components in Typescript 3.5.3. (In the code snippets below I’ve converted it to JS since I’m positive that the TS is not the problem.) In order to not overwhelm the user, the form is broken down in multiple components; only one at a time is shown to the user.

Parent component

<template>
    <form>
        <Basic :Container.sync="Container"/>
        <!-- More custom components -->
        <output>{{Container.name}}</output>
    </form>
</template>

The Container object is a wrapper for all the data to be collected. You see I’m passing the whole object instead of only the properties that are asked for to the child components. This may seem opinionated, but I have my reasons.

The Form component boils down to this:

// Imports left out for brevity
@Component({ components: {  Basic, }, })
export default class Form extends Vue {
    private Container
    async created() {
        this.Container = {
            name: null
        }
        setInterval(()=>{console.log(this.Container.name)}, 5000)
        // Logs whatever will be entered in the input in the Basic component
    }
}

Child component

The referenced component Basic is a plain input element:

<template>
    <input v-model="Container.name" @input="emitUpdate">
</template>

with an equally trivial component definition:

// Imports left out for brevity
@Component({})
export default class Basic extends Vue {
    @Prop() Container
    emitUpdate() {
        console.log(this.Container.name) // Logs whatever was entered in the input
        this.$emit('update:container', this.Container)
    }
}

The Problem

What I’m expecting to see is the container to update, and the <output> from the first HTML snippet to display the current name. However, it doesn’t, even though the variable is changed, as is proven by the two console logs which both register the input value correctly.

It seems like Vue’s reactivity cannot register the change to the Container object and thus won’t re-render any of the HTML. How can I fix this?

Attempted solutions

I’ve tried listening explicitly for the update events using <Basic … :Container.sync="Container" @Container="Container = $event" />, but to no avail.

Thinking that Vue might just not react to changes in object properties (after all, watching objects requires the deep option to be set, too), I’ve also tried to pass the Container’s properties individually, leading to the following changes:

In the form’s html the object is used as the argument to v-bind (I’ve tried with and without @update:name):

<Basic … v-bind.sync="Container" @update:name="name=$event"/>

And in the child component I’ve changed to a getter and a setter in order to get around the “Thou shall not change Props” warning:

<input v-model="Name" @input="emitUpdate">`
@Prop() name
get Name() {return this.name }
set Name(newName) {this.$emit('update:name', newName)}
2
I can write solution usig pure 'JS' but not TS, should I?Andrew Vasilchuk
@EvilArthas Yes, please do. The problem is most likely not rooted in the typescript.bleistift2

2 Answers

1
votes

Since Vue does not allow us to mutate props we need to create a local copy of our Container, so let's use a computed property for it. When we try to access this property we get our Component, but when we want to set it we just trigger event to our parent component with new value of Component that we will get through props.

Reference

<Basic v-model="container" />

// Basic.vue
<input v-model="localContainer.name">

props: {
  value: {
    type: Object,
    required: true
  }
},
computed: {
  localContainer: {
    get() {
      return this.value
    },
    set(container) {
      this.$emit('input', container)
    }
  }
}
1
votes

Seems to me the problem here is that you initialized the Container object inside the created hook, which makes it non-reactive. If you want to make a piece of state reactive you need to initialize it as initial data (or use the Vue.observable API). I don't know much about TS but looking at the docs I think what you need to do is something along the lines of ..

@Component({ components: {  Basic, }, })
export default class Form extends Vue {
    Container: object = {
        name: null
    }
}