Deep reactivity of props in vuejs components - javascript

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:
This is my RowComponent:
This is my ColumnComponent:
This is my 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

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:
push()
pop()
shift()
unshift()
splice()
sort()
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);

Related

How to pass dynamically props from one child component to another on the parent page

I have two child components I have to pass dynamically props from first child to parent and from parent to second child.
Parent
<script>
data: () => ({
model: {}
}),
methods: {
changeData(payload) {
this.model.personalData = {...payload}
}
}
</script>
<template>
<first-child #changeData="(payload) => changeData(payload)"/>
<second-child :enter-object="model" />
</template>
Child one
<script>
data: () => ({
model: {}
}),
methods: {
changeData() {
this.$emit("changeData", this.model);
}
}
</script>
<template>
<v-text-field v-model="model.name" #input="changeData()">
<v-text-field v-model="model.email" #input="changeData()">
</template>
Child two
<script>
props: {
enterObject: {
type: Object,
required: false,
default: () => ({})
}
},
data: () => ({
model: {}
}),
watch: {
enterObject: {
immediate: true,
handler() {
Object.assign(this.model.personalData, this.enterObject.personalData);
}
}
</script>
<template>
<div>
<div v-if="model.personalData.name || model.personalData.email">
<span class="mr-3">{{ model.personalData.name }}</span>
<span>{{ model.personalData.email }}</span>
</div>
<div v-else>
No data
</div>
</div>
</template>
I get data in parent component with no problem, but this data doesn't pass to second child, why I have always "No data" ?
I tested your code and found a few things:
You need to create "personalData" inside the model in "childTwo".
<template>
<div>
// I changed the validation for personalData
<div v-if="model.personalData">
<span class="mr-3">{{ model.personalData.name }}</span>
<span>{{ model.personalData.email }}</span>
</div>
<div v-else>No data</div>
</div>
</template>
export default {
props: {
enterObject: {
type: Object,
required: false,
default: () => ({})
}
},
data: () => ({
model: {
personalData: {}
}
}),
watch: {
enterObject: {
deep: true,
handler() {
// Add a validation in the handler, you can use Object assign inside the validation.
if(this.enterObject) {
Object.assign(this.model.personalData, this.enterObject.personalData)
}
}
}
}
It's worked for me.I hope it helps you.
You have to assign the value of the object using this.$set for more about object reactivity click here
your Parent component should be like this:-
here is the working example
<template>
<div>
<first-child #change-data="(payload) => changeData(payload)" />
<second-child :enter-object="model" />
</div>
</template>
<script>
import FirstChild from "./FirstChild";
import SecondChild from "./SecondChild";
export default {
data: () => ({
model: {},
compKey: 0,
}),
components: {
FirstChild,
SecondChild,
},
methods: {
changeData(payload) {
this.$set(this.model, "test", payload);
//this.model.test = payload;
},
},
};
</script>

how to bind value of v-for to v-if

I'm working with BootstrapVue. To my problem: I have a v-for in my template in which I have two buttons.
Looping over my v-for my v-if doesn't generate unique IDs and than after clicking one button each button will be triggered (from Open me! to Close me! and other way around).
How can I manage to get each button only triggers itself and doesn't affect the other?
I think I have to use my n of my v-for but I actually don't know how to bind this to a v-if..
Thanks in advance!
<template>
<div>
<div v-for="n in inputs" :key="n.id">
<b-button v-if="hide" #click="open()">Open me!</b-button>
<b-button v-if="!hide" #click="close()">Close me! </b-button>
</div>
<div>
<b-button #click="addInput">Add Input</b-button>
</div>
</div>
</template>
<script>
export default {
data() {
return {
id: null,
inputs: [{
id: 0
}],
hide: true,
};
},
methods: {
open() {
this.hide = false
},
close() {
this.hide = true
},
addInput() {
this.inputs.push({
id: this.id += 1;
})
}
}
};
</script>
Everything seems to look fine. In order to handle each button triggers,
you can maintain an object like so:
<script>
export default {
data() {
return {
inputs: [{id: 0, visible: false}],
};
},
methods: {
open(index) {
this.inputs[index].visible = false
},
close(index) {
this.inputs[index].visible = true
},
addInput() {
this.inputs.push({id: this.inputs.length, visible: false});
}
}
};
</script>
and your template should be like
<template>
<div>
<div v-for="(val, index) in inputs" :key="val.id">
<b-button v-if="val.visible" #click="open(index)">Open me!</b-button>
<b-button v-if="!val.visible" #click="close(index)">Close me! </b-button>
</div>
</div>
</template>
Edit:
You don't need to insert an id every time you create a row, instead can use the key as id. Note that the inputs is an object and not array so that even if you want to delete a row, you can just pass the index and get it removed.
I would create an array of objects. Use a boolean as property to show or hide the clicked item.
var app = new Vue({
el: '#app',
data: {
buttons: []
},
created () {
this.createButtons()
this.addPropertyToButtons()
},
methods: {
createButtons() {
// Let's just create buttons with an id
for (var i = 0; i < 10; i++) {
this.buttons.push({id: i})
}
},
addPropertyToButtons() {
// This method add a new property to buttons AFTER its generated
this.buttons.forEach(button => button.show = true)
},
toggleButton(button) {
if (button.show) {
button.show = false
} else {
button.show = true
}
// We are changing the object after it's been loaded, so we need to update ourselves
app.$forceUpdate();
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<template>
<div>
<div v-for="button in buttons" :key="button.id">
<button v-if="button.show" #click="toggleButton(button)">Open me!</button>
<button v-if="!button.show" #click="toggleButton(button)">Close me! </button>
</div>
</div>
</template>
</div>

Vue.js - Computed property not updating - child component

I've created a simple component named DefaultButton.
It bases on properties, that are being set up whenever this component is being created.
The point is that after mounting it, It does not react on changes connected with "defaultbutton", that is an object located in properties
<template>
<button :class="buttonClass" v-if="isActive" #click="$emit('buttonAction', defaultbutton.id)" >
{{ this.defaultbutton.text }}
</button>
<button :class="buttonClass" v-else disabled="disabled">
{{ this.defaultbutton.text }}
</button>
</template>
<script>
export default {
name: "defaultbutton",
props: {
defaultbutton: Object
},
computed: {
buttonClass() {
return `b41ngt ${this.defaultbutton.state}`;
},
isActive() {
return (this.defaultbutton.state === "BUTTON_ACTIVE" || this.defaultbutton.state === "BUTTON_ACTIVE_NOT_CHOSEN");
}
}
};
</script>
Having following component as a parent one:
<template>
<div v-if="state_items.length == 2" class="ui placeholder segment">
{{ this.state_items[0].state }}
{{ this.state_items[1].state }}
{{ this.current_active_state }}
<div class="ui two column very relaxed stackable grid">
<div class="column">
<default-button :defaultbutton="state_items[0]" #buttonAction="changecurrentstate(0)"/>
</div>
<div class="middle aligned column">
<default-button :defaultbutton="state_items[1]" #buttonAction="changecurrentstate(1)"/>
</div>
</div>
<div class="ui vertical divider">
Or
</div>
</div>
</template>
<script type="text/javascript">
import DefaultButton from '../Button/DefaultButton'
export default {
name: 'changestatebox',
data() {
return {
current_active_state: 1
}
},
props: {
state_items: []
},
components: {
DefaultButton
},
methods: {
changecurrentstate: function(index) {
if(this.current_active_state != index) {
this.state_items[this.current_active_state].state = 'BUTTON_ACTIVE_NOT_CHOSEN';
this.state_items[index].state = 'BUTTON_ACTIVE';
this.current_active_state = index;
}
},
},
mounted: function () {
this.state_items[0].state = 'BUTTON_ACTIVE';
this.state_items[1].state = 'BUTTON_ACTIVE_NOT_CHOSEN';
}
}
</script>
It clearly shows, using:
{{ this.state_items[0].state }}
{{ this.state_items[1].state }}
{{ this.current_active_state }}
that the state of these items are being changed, but I am unable to see any results on the generated "DefaultButtons". Classes of objects included in these components are not being changed.
#edit
I've completely changed the way of delivering the data.
Due to this change, I've abandoned the usage of an array; instead I've used two completely not related object.
The result is the same - class of the child component's object is not being
DefaulButton.vue:
<template>
<button :class="buttonClass" v-if="isActive" #click="$emit('buttonAction', defaultbutton.id)" >
{{ this.defaultbutton.text }}
</button>
<button :class="buttonClass" v-else disabled="disabled">
{{ this.defaultbutton.text }}
</button>
</template>
<style lang="scss">
import './DefaultButton.css';
</style>
<script>
export default {
name: "defaultbutton",
props: {
defaultbutton: {
type: Object,
default: () => ({
id: '',
text: '',
state: '',
})
}
},
computed: {
buttonClass() {
return `b41ngt ${this.defaultbutton.state}`;
},
isActive() {
return (this.defaultbutton.state === "BUTTON_ACTIVE" ||
this.defaultbutton.state === "BUTTON_ACTIVE_NOT_CHOSEN");
}
}
};
</script>
ChangeStateBox.vue:
<template>
<div class="ui placeholder segment">
{{ this.state_first.state }}
{{ this.state_second.state }}
{{ this.current_active_state }}
<div class="ui two column very relaxed stackable grid">
<div class="column">
<default-button :defaultbutton="state_first" #buttonAction="changecurrentstate(0)"/>
</div>
<div class="middle aligned column">
<default-button :defaultbutton="state_second" #buttonAction="changecurrentstate(1)"/>
</div>
</div>
<div class="ui vertical divider">
Or
</div>
</div>
</template>
<script type="text/javascript">
import DefaultButton from '../Button/DefaultButton'
export default {
name: 'changestatebox',
data() {
return {
current_active_state: 1
}
},
props: {
state_first: {
type: Object,
default: () => ({
id: '',
text: ''
})
},
state_second: {
type: Object,
default: () => ({
id: '',
text: ''
})
},
},
components: {
DefaultButton
},
methods: {
changecurrentstate: function(index) {
if(this.current_active_state != index) {
if(this.current_active_state == 1){
this.$set(this.state_first, 'state', "BUTTON_ACTIVE_NOT_CHOSEN");
this.$set(this.state_second, 'state', "BUTTON_ACTIVE");
} else {
this.$set(this.state_first, 'state', "BUTTON_ACTIVE");
this.$set(this.state_second, 'state', "BUTTON_ACTIVE_NOT_CHOSEN");
}
this.current_active_state = index;
}
},
},
created: function () {
this.state_first.state = 'BUTTON_ACTIVE';
this.state_second.state = 'BUTTON_ACTIVE_NOT_CHOSEN';
}
}
</script>
You're declaring props wrong. It is either an array of prop names or it is an object with one entry for each prop declaring its type, or it is an object with one entry for each prop declaring multiple properties.
You have
props: {
state_items: []
},
but to supply a default it should be
props: {
state_items: {
type: Array,
default: []
}
},
But your problem is most likely that you're mutating state_items in such a way that Vue can't react to the change
Your main problem is the way you are changing the button state, according with Array change detection vue can't detect mutations by indexing.
Due to limitations in JavaScript, Vue cannot detect the following
changes to an array:
When you directly set an item with the index, e.g.
vm.items[indexOfItem] = newValue When you modify the length of the
array, e.g. vm.items.length = newLength
In case someone will be having the same issue:
#Roy J as well as #DobleL were right.
The reason behind this issue was related with the wrong initialization of state objects.
According to the documentation:
Vue cannot detect property addition or deletion.
Since Vue performs the getter/setter conversion process during instance
initialization, a property must be present in the
data object in order for Vue to convert it and make it reactive.
Before reading this sentence, I used to start with following objects as an initial data:
var local_state_first = {
id: '1',
text: 'Realized',
};
var local_state_second = {
id: '2',
text: 'Active'
};
and the correct version of it looks like this:
var local_state_first = {
id: '1',
text: 'Realized',
state: 'BUTTON_ACTIVE'
};
var local_state_second = {
id: '2',
text: 'Active',
state: 'BUTTON_ACTIVE'
};
whereas declaring the main component as:
<change-state-box :state_first="local_state_first" :state_second="local_state_second" #buttonAction="onbuttonAction"/>
Rest of the code remains the same ( take a look at #edit mark in my main post )

why can't child components get the data from parent component?

I disunderstood the documentation of passing parent component to child component.The parent component as the follow code:
<template>
<div className="fixed-table">
<basic-container>
<el-table
:data="dataSource"
border
style="width: 100%">
<el-table-column label="操作" align="center">
<template slot-scope="scope">
<el-button type="primary" size="mini" round #click="handleEdit(scope.$index, scope.row)">编辑
</el-button>
<edit-dialog ref="editDialog" :row="scope.row" :index="scope.$index" :allBaseRules="allBaseRules" :allActions="allActions"></edit-dialog>
</template>
</el-table-column>
</el-table>
</basic-container>
</div>
</template>
<script>
import BasicContainer from '#vue-materials/basic-container';
import EditDialog from "./components/EditDialog";
import HttpUtils from '../../../../components/HttpUtils.js'
export default {
components: {
BasicContainer,
EditDialog
},
name: 'FixedTable',
data() {
return {
dataSource: [],
allActions: [],
};
},
methods: {
handleEdit(index, row) {
this.$refs.editDialog.$emit("open",row);
}
},
mounted() {
let self = this;
HttpUtils.get('/api/rule/action/queryAll')
.then(function (response) {
self.allActions = response.data.data;
})
.catch(function (error) {
});
}
}
</script>
I want to parse "allActions" to child components.The child components is a dialog,I open it by control the 'dialogFormVisible'. The child components like this:
<template>
<el-dialog title="编辑业务规则" :visible.sync="dialogFormVisible" :append-to-body="true">
<el-form :model="formRow" :label-position="labelPosition" label-width="20%">
<el-form-item label="前置行为" prop="preHandlers">
<el-select v-model="preHandler" clearable placeholder="请选择">
<el-option v-for="item in allActions" :key="item.actionID" :label="item.actionName"
:value="item.actionID"></el-option>
</el-select>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button #click="dialogFormVisible = false">取 消</el-button>
<el-button type="primary" #click="handleSubmit('ruleForm')">确 定</el-button>
</div>
</el-dialog>
</template>
<script>
export default {
data() {
return {
dialogFormVisible: false
preHandler: ""
};
},
computed: {
formRow() {
return Object.assign({}, this.row);
}
},
props: {
row: {
type: Object,
default: {}
},
index: {
type: Number,
default: 0
}
allActions: {
type: Array,
default: function () {
return []
}
}
},
created: function () {
this.$on('open', function (row) {
this.dialogFormVisible = true
});
}
};
</script>
However,I can't get 'allActions' from parent component. How can i get it?
When defining the attributes on an HTML element keep in mind that HTML is case insensitive. So a prop defined as allBaseRules really is seen as allbaserules. So you need to use "Kabab-case" when defining the attribute name as below.
<edit-dialog
ref="editDialog"
:row="scope.row"
:index="scope.$index"
:all-base-rules="allBaseRules"
:all-actions="allActions"
>
Vue automatically recognizes the kabab-case props and converts them to camelCase for you. So you are fine to still receive the props as you are currently.
https://v2.vuejs.org/v2/guide/components-props.html#Prop-Casing-camelCase-vs-kebab-case
See the the example here https://jsfiddle.net/skribe/1dbfj8h6/

Input field unable to change model when component and model are dynamically created

I have a data structure with nested objects that I want to bind to sub-components, and I'd like these components to edit the data structure directly so that I can save it all from one place. The structure is something like
job = {
id: 1,
uuid: 'a-unique-value',
content_blocks: [
{
id: 5,
uuid: 'some-unique-value',
block_type: 'text',
body: { en: { content: 'Hello' }, fr: { content: 'Bonjour' } }
},
{
id: 9,
uuid: 'some-other-unique-value',
block_type: 'text',
body: { en: { content: 'How are you?' }, fr: { content: 'Comment ça va?' } }
},
]
}
So, I instantiate my sub-components like this
<div v-for="block in job.content_blocks" :key="block.uuid">
<component :data="block" :is="contentTypeToComponentName(block.block_type)" />
</div>
(contentTypeToComponentName goes from text to TextContentBlock, which is the name of the component)
The TextContentBlock goes like this
export default {
props: {
data: {
type: Object,
required: true
}
},
created: function() {
if (!this.data.body) {
this.data.body = {
it: { content: "" },
en: { content: "" }
}
}
}
}
The created() function takes care of adding missing, block-specific data that are unknown to the component adding new content_blocks, for when I want to dynamically add blocks via a special button, which goes like this
addBlock: function(block_type) {
this.job.content_blocks = [...this.job.content_blocks, {
block_type: block_type,
uuid: magic_uuidv4_generator(),
order: this.job.content_blocks.length === 0 ? 1 : _.last(this.job.content_blocks).order + 1
}]
}
The template for TextContentBlock is
<b-tab v-for="l in ['fr', 'en']" :key="`${data.uuid}-${l}`">
<template slot="title">
{{ l.toUpperCase() }} <span class="missing" v-show="!data.body[l] || data.body[l] == ''">(missing)</span>
</template>
<b-form-textarea v-model="data.body[l].content" rows="6" />
<div class="small mt-3">
<code>{{ { block_type: data.block_type, uuid: data.uuid, order: data.order } }}</code>
</div>
</b-tab>
Now, when I load data from the API, I can correctly edit and save the content of these blocks -- which is weird considering that props are supposed to be immutable.
However, when I add new blocks, the textarea above wouldn't let me edit anything. I type stuff into it, and it just deletes it (or, I think, it replaces it with the "previous", or "initial" value). This does not happen when pulling content from the API (say, on page load).
Anyway, this led me to the discovery of immutability, I then created a local copy of the data prop like this
data: function() {
return {
block_data: this.data
}
}
and adjusted every data to be block_data but I get the same behaviour as before.
What exactly am I missing?
As the OP's comments, the root cause should be how to sync textarea value between child and parent component.
The issue the OP met should be caused by parent component always pass same value to the textarea inside the child component, that causes even type in something in the textarea, it still bind the same value which passed from parent component)
As Vue Guide said:
v-model is essentially syntax sugar for updating data on user input
events, plus special care for some edge cases.
The syntax sugar will be like:
the directive=v-model will bind value, then listen input event to make change like v-bind:value="val" v-on:input="val = $event.target.value"
So adjust your codes to like below demo:
for input, textarea HTMLElement, uses v-bind instead of v-model
then uses $emit to popup input event to parent component
In parent component, uses v-model to sync the latest value.
Vue.config.productionTip = false
Vue.component('child', {
template: `<div class="child">
<label>{{value.name}}</label><button #click="changeLabel()">Label +1</button>
<textarea :value="value.body" #input="changeInput($event)"></textarea>
</div>`,
props: ['value'],
methods: {
changeInput: function (ev) {
let newData = Object.assign({}, this.value)
newData.body = ev.target.value
this.$emit('input', newData) //emit whole prop=value object, you can only emit value.body or else based on your design.
// you can comment out `newData.body = ev.target.value`, then you will see the result will be same as the issue you met.
},
changeLabel: function () {
let newData = Object.assign({}, this.value)
newData.name += ' 1'
this.$emit('input', newData)
}
}
});
var vm = new Vue({
el: '#app',
data: () => ({
options: [
{id: 0, name: 'Apple', body: 'Puss in Boots'},
{id: 1, name: 'Banana', body: ''}
]
}),
})
.child {
border: 1px solid green;
}
<script src="https://unpkg.com/vue#2.5.16/dist/vue.js"></script>
<div id="app">
<span> Current: {{options}}</span>
<hr>
<div v-for="(item, index) in options" :key="index">
<child v-model="options[index]"></child>
</div>
</div>

Categories