Andrii Diachenko
Andrii Diachenko

Reputation: 243

Vue child prop is not updated on bound value change in parent

I'm using vue-property-decorator lib. I've got 2 components: parent and child, child is a component that has a prop 'modalHeader':

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

@Component({
    name: "modal"
})
export default class Modal extends Vue {
    @Prop({default: "Header"}) public modalHeader!: string;
}
</script>

Child component's template:

<template>
    <div
        class="modal fade"
        id="detailsModal"
        tabindex="-1"
        role="dialog"
        aria-labelledby="detailsModalLabel"
        aria-hidden="true"
    >
        <div class="modal-dialog modal-lg" role="document">
            <div class="modal-content">
                <div class="modal-header">
                    <h5 class="modal-title" id="detailsModalLabel">
                        {{ modalHeader }}
                    </h5>
                </div>
                ...
                </div>
            </div>
        </div>
    </div>
</template>

I have a property 'buttonPressed' in my parent component and I want to bind its properties to child prop, thus every time depending on the button pressed I'll get a different header for modal:

<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import Modal from '../modal/Modal.vue';

interface ControlButtonSetting {
    id: string,
    name: string,
    enabled: boolean,
    isDanger: boolean,
    okText?: string,
    componentType?: IModalComponent,
}

@Component({
    name: "admin-dashboards-page",
    components: {
        "modal": Modal
    }
})
export default class AdminDashboardsPage extends Vue {

    private buttonsArray: ControlButtonSetting[] = [
        { id: "removeButton", name: "Remove", enabled: false, isDanger: true, okText: "Remove" }
    ];

    public buttonPressed!: ControlButtonSetting;

    private showModal = false;

    created(): void {
        this.buttonPressed = this.buttonsArray[0];
    }

    public controlButtonClicked(button: ControlButtonSetting): void {
        this.buttonPressed = button;
        this.showModal = true;
    }
}
</script>

Parent component's template:

<template>
    <div>
        ...

        <!-- Modal -->
        <modal
            :modal-header.sync="buttonPressed.name"
            <template v-slot:modalBody>
                <component
                    :is="displayComponent()"
                    :dashboard-id="selectedListItemId">
                </component>
            </template>
        </modal>
    </div>
</template>

However, the 'modalHeader' value is set only once when the page loaded, but when 'selectedButton' object is changed, 'modalHeader' value is not being updated on the child component.

What helped me is to define parent's object as a Prop():

@Prop() public buttonPressed!: ControlButtonSetting;

This way child component's prop is updated whenever parent's prop is updated. But I start to see this message in a console:

[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "buttonPressed"

I'm not really sure what is wrong here. Could you help me with it?

P.S. One guess I have is that I'm not mutating bound property in parent but replacing it with a new object, that's why parent-child binding may not work. Gonna check it out.

Upvotes: 3

Views: 547

Answers (1)

Kapcash
Kapcash

Reputation: 6909

In short: your buttonPressed data isn't reactive because it is not initialised. It has to be properly initialised for Vue to mark it as reactive.

public buttonPressed!: ControlButtonSetting; // not initialised

You have two options to initialise it properly:

#1 You give it a proper default value, even empty, directly after declaring it.

public buttonPressed: ControlButtonSetting = {
  id: "",
  name: "",
  enabled: false,
  isDanger: false,
  okText: ""
}

#2 If you want its default value to rely on other data / props, use the data factory. (Don't use created hook for this)

public buttonPressed!: ControlButtonSetting; // use !: to tell typescript it's declared but in another place

// Even using `vue-property-decorator`, this will work and merge both data() and class properties
data () {
  return {
     buttonPressed: this.buttonsArray[0],
  }
}

Don't mark buttonPressed as a prop, because it isn't. And a Vue prop can't be updated from childs, it would break the reactivity of the parent.


Note: Vue props should always have a type provided. Even using vue-property-decorator, you have to specify it.

@Prop({ type: String, default: 'Header' }) <- Vue prop type, only for runtime
readonly modalHeader!: string // <- typescript type, only for build time

(I advise you to use readonly keyword to tell Typescript this property cannot be updated, because props are immutable in Vue)

Upvotes: 2

Related Questions