Nitish Kumar
Nitish Kumar

Reputation: 6276

Deep reactivity of props in vuejs components

I'm trying to build a simple page builder which has a row elements, its child column elements and the last the component which I need to call. For this I designed and architecture where I'm having the dataset defined to the root component and pushing the data to its child elements via props. So let say I have a root component:

<template>
    <div>
        <button @click.prevent="addRowField"></button>
        <row-element v-if="elements.length" v-for="(row, index) in elements" :key="'row_index_'+index" :attrs="row.attrs" :child_components="row.child_components" :row_index="index"></row-element>
    </div>
</template>

<script>
    import RowElement from "../../Components/Builder/RowElement";

    export default {
        name: "edit",
        data(){
            return{
                elements: [],
            }
        },
        created() {
            this.listenToEvents();
        },
        components: {
            RowElement,
        },
        methods:{
            addRowField() {
                const row_element = {
                    component: 'row',
                    attrs: {},
                    child_components: [

                    ]
                }
                this.elements.push(row_element)
            }
        },
    }
</script>

Here you can see I've a button where I'm trying to push the element and its elements are being passed to its child elements via props, so this RowElement component is having following code:

<template>
    <div>
        <column-element v-if="child_components.length" v-for="(column,index) in child_components" :key="'column_index_'+index"  :attrs="column.attrs" :child_components="column.child_components" :row_index="row_index" :column_index="index"></column-element>
    </div>
    <button @click="addColumn"></button>
</template>

<script>
    import ColumnElement from "./ColumnElement";
    export default {
        name: "RowElement",
        components: {ColumnElement},
        props: {
            attrs: Object,
            child_components: Array,
            row_index: Number
        },
        methods:{
            addColumn(type, index) {
                this.selectColumn= false
                let column_element = {
                    component: 'column',
                    child_components: []
                };
                let component = {}

                //Some logic here then we are emitting event so that it goes to parent element and there it can push the columns

                eventBus.$emit('add-columns', {column: column_element, index: index});
            }
        }
    }
</script>

So now I have to listen for event on root page so I'm having:

eventBus.$on('add-columns', (data) => {
    if(typeof this.elements[data.index] !== 'undefined')
        this.elements[data.index].child_components.push(data.column)
});

Now again I need these data accessible to again ColumnComponent so in columnComponent file I have:

<template>
    //some extra div to have extended features
    <builder-element
        v-if="!loading"
        v-for="(item, index) in child_components"
        :key="'element_index_'+index" :column_index="column_index"
        :element_index="index" class="border bg-white"
        :element="item" :row_index="row_index"
    >
    </builder-element>
</template>

<script>
    export default {
        name: "ColumnElement",
        props: {
            attrs: Object,
            child_components: Array,
            row_index: Number,
            column_index: Number
        },
    }
</script>

And my final BuilderElement

<template>
    <div v-if="typeof element.component !== 'undefined'" class="h-10 w-10 mt-1 mb-2 mr-3 cursor-pointer  font-bold text-white rounded-lg">
        <div>{{element.component}}</div>
        <img class="h-10 w-10 mr-3" :src="getDetails(item.component, 'icon')">
    </div>
    <div v-if="typeof element.component !== 'undefined'" class="flex-col text-left">
        <h5 class="text-blue-500 font-bold">{{getDetails(item.component, 'title')}}</h5>
        <p class="text-xs text-gray-600 mt-1">{{getDetails(item.component, 'desc')}}</p>
    </div>
</template>

<script>
    export default {
        name: "BuilderElement",
        data(){
            return{
                components:[
                    {id: 1, title:'Row', icon:'/project-assets/images/row.png', desc:'Place content elements inside the row', component_name: 'row'},
                    //list of rest all the components available
                ]
            }
        },
        props: {
            element: Object,
            row_index: Number,
            column_index: Number,
            element_index: Number,
        },
        methods:{
            addElement(item,index){
                //Some logic to find out details
                let component_element = {
                    component: item.component_name,
                    attrs: {},
                    child_components: [
                    ]
                }

                eventBus.$emit('add-component', {component: component_element, row_index: this.row_index, column_index: this.column_index, element_index: this.element_index});

            },
            getDetails(component, data) {
                let index = _.findIndex(this.components, (a) => {
                    return a.component_name === component;
                })
                console.log('Component'+ component);
                console.log('Index '+index);
                if(index > -1) {
                    let component_details = this.components[index];
                    return component_details[data];
                }
                else
                    return null;
            },
        },
    }
</script>

As you can see I'm again emitting the event named add-component which is again listened in the root component so for this is made following listener:

eventBus.$on('add-component', (data) => {
    this.elements[data.row_index].child_components[data.column_index].child_components[data.element_index] = data.component
});

which shows the data set in my vue-devtools but it is not appearing in the builder element:

Images FYR:

This is my root component:

Root element

This is my RowComponent:

Row component

This is my ColumnComponent:

Column component

This is my builder element:

Last builder element

I don't know why this data not getting passed to its child component, I mean last component is not reactive to props, any better idea is really appreciated.

Thanks

Upvotes: 0

Views: 3503

Answers (1)

painotpi
painotpi

Reputation: 6996

The issue is with the way you're setting your data in the addComponent method.

Vue cannot pick up changes when you change an array by directly modifying it's index, something like,

arr[0] = 10

As defined in their change detection guide for array mutations,

Vue wraps an observed array’s mutation methods so they will also trigger view updates. The wrapped methods are:

  1. push()
  2. pop()
  3. shift()
  4. unshift()
  5. splice()
  6. sort()
  7. reverse()

So you can change.

this.elements[data.row_index].child_components[data.column_index].child_components[data.element_index] = data.component

To,

this.elements[data.row_index].child_components[data.column_index].child_components.splice(data.element_index, 1, data.component);

Upvotes: 3

Related Questions