TypeScript VueJS: Using watcher with computed property

Viewed 1584

I'm building a custom component that will allow me to use arbitrary <div>s as radio buttons. Currently my code looks like so:

<template>
    <div
        class="radio-div"
        :class="selected ? 'selected' : ''"
        @click="handleClick"
    >
        <slot></slot>
    </div>
</template>

<style lang="scss" scoped>
.radio-div {
    display: inline-block;
    border-radius: 5px;
    border: 1px solid #000;
    padding: 5px;
    margin: 5px;
}

.radio-div.selected {
    box-shadow: 0 0 10px rgba(255, 255, 0, 0.5);
    border: 2px solid #000;
}
</style>

<script lang="ts">
import { Vue, Component, Prop, Watch } from "vue-property-decorator";

@Component
export default class RadioDiv extends Vue {
    @Prop()
    val!: string;

    @Prop({
        required: true
    })
    value!: string;

    selected = false;

    mounted() {
        this.selected = this.value === this.val;
    }

    @Watch("value")
    onChange() {
        this.selected = this.value === this.val;
    }

    handleClick(e: MouseEvent) {
        this.$emit("input", this.val);
    }
}
</script>

To utilize this I can put it in a template like so:

<template>
    <div class="q-pa-md">
        <q-card>
            <q-card-section class="bg-secondary">
                <div class="text-h6">Political Affiliation</div>
            </q-card-section>
            <q-separator />
            <q-card-section>
                <radio-div v-model="politicalParty" val="Republican">
                    <div class="text-h6">Republican</div>
                    <p>Wrong answer</p>
                </radio-div>
                <radio-div v-model="politicalParty" val="Democrat">
                    <div class="text-h6">Democrat</div>
                    <p>Wrong answer</p>
                </radio-div>
                <radio-div v-model="politicalParty" val="Independent">
                    <div class="text-h6">Independent</div>
                    <p>These people clearly know what's up</p>
                </radio-div>
            </q-card-section>
        </q-card>
    </div>
</template>

<script lang="ts">
import { Vue, Component, Watch } from "vue-property-decorator";
import RadioDiv from "../components/RadioDiv.vue";

@Component({
    components: { RadioDiv }
})
export default class Profile extends Vue {
    politicalParty = "Independent";
}
</script>

This works as expected. I can click on the <div>s and it switches which one is selected and updates the variable appropriately.

But now I want to tie this into a global state manager. So instead of a local politicalParty variable, I have a computed property like so:

<script lang="ts">
import { Vue, Component, Watch } from "vue-property-decorator";
import RadioDiv from "../components/RadioDiv.vue";
import globalState from "../globalState";

@Component({
    components: { RadioDiv }
})
export default class Profile extends Vue {
    get politicalParty() {
        return globalState.politicalParty;
    }

    set politicalParty(val) {
        globalState.politicalParty = val;
    }
}
</script>

Putting a console.log statement in the setter I can see that it is getting called, and the variable is being updated. But putting a console.log statement in my value watcher (in the RadioDiv component) shows it's no longer being called now that I'm using computed properties.

What's the secret to get my RadioDiv reactive again, now that I'm using global state?

Update

The issue doesn't seem to be specific to my custom components, or to watchers. I decided to ignore this and move on while waiting for an answer from StackOverflow and ran into the issue again with Quasar's components:

<template>
...
            <q-card-section>
                <div class="row">
                    <div class="col">
                        <q-slider v-model="age" :min="0" :max="100" />
                    </div>
                    <div class="col">
                        {{ ageText }}
                    </div>
                </div>
            </q-card-section>
...
</template>

<script lang="ts">
...
    get age() {
        return globalState.age;
    }

    set age(val) {
        globalState.age = val;
        this.ageText = "You are " + val + " years old";
    }
...
</script>

This led me to try using no custom components whatsoever:

<template>
...
    <input type="text" v-model="test" />
    <p>Text: {{ test }}</p>
...
</template>

<script lang="ts">
let testVal = "";
...
    get test() { return testVal; }
    set test(val) { testVal = val; }
...
</script>

Once again: No reactivity. When I use a computed property with v-model nothing seems to change after the call to set

1 Answers

If globalState were just an Object, then it would not be reactive, so computed is only going to read its value once. Same for testVal, which is just a String (also not reactive itself).

To make the test computed prop reactive to testVal, create testVal with Vue.observable():

const testVal = Vue.observable({ x: '' })

@Component
export default class Profile extends Vue {
  get test() { return testVal.x }
  set test(val) { testVal.x = val }
}

Similarly for globalState, exporting a Vue.observable() would allow your computed props to be reactive:

// globalState.js
export default Vue.observable({
  politicalParty: ''
})
Related