Changes made to a computed array won't show after a prop binding - vue.js

I'm quite new to vue and right now I'm trying to figure out how to make changes to a computed array and make an element react to this change. When I click the div element (code section 4), I want the div's background color to change. Below is my failed code.
Code section 1: This is my computed array.
computed: {
arrayMake() {
let used = [];
for (let i = 0; i < 5; i++) {
used.push({index: i, check: true});
}
return used;
Code section 2: This is where I send it as a prop to another component.
<test-block v-for="(obj, index) in arrayMake" v-bind:obj="obj" v-on:act="act(obj)"></card-block>
Code section 3: This is a method in the same component as code section 1 and 2.
methods: {
act(obj){
obj.check = true;
}
Code section 4: Another component that uses the three sections above.
props: ["obj"],
template: /*html*/`
<div v-on:click="$emit('act')">
<div v-bind:style="{ backgroundColor: obj.check? 'red': 'blue' }">
</div>
</div>

Easiest way to achieve this, store the object into another data prop in the child component.
child component
data() => {
newObjectContainer: null
},
onMounted(){
this.newObjectContainer = this.obj
},
methods: {
act(){
// you don't need to take any param. because you are not using it.
newObjectContainer.check = !newObjectContainer.check
}
}
watch: {
obj(val){
// updated if there is any changes
newObjectContainer = val
}
}
And if you really want to update the parent component's computed data. then don't use the computed, use the reactive data prop.
child component:
this time you don't need watcher in the child. you directly emit the object from the method
methods: {
act(){
newObjectContainer.check = !newObjectContainer.check
this.emits("update:modelValue", nextObjectContainer)
}
}
parent component:
data() => {
yourDynamicData: [],
},
onMounted(){
this.yourDynamicData = setAnArray()
},
methods(){
setAnArray(){
let used = [];
for (let i = 0; i < 5; i++) {
used.push({index: i, check: true});
}
return used;
}
}
okay above you created a reactive data property. Now you need the update it if there is a change in the child component,
in the parent first you need object prop so, you can update that.
<test-block v-for="(obj, index) in arrayMake" v-model="updatedObject" :obj="obj"></card-block>
data() => {
yourDynamicData: [],
updatedObject: {}
},
watch:{
updatedObject(val){
const idx = val.index
yourDynamicData[idx] = val
}
}

Related

Vue.js 3 Pinia store is only partially reactive. Why?

I'm using Pinia as Store for my Vue 3 application. The problem is that the store reacts on some changes, but ignores others.
The store looks like that:
state: () => {
return {
roles: [],
currentRole: 'Administrator',
elements: []
}
},
getters: {
getElementsForCurrentRole: (state) => {
let role = state.roles.find((role) => role.label == state.currentRole);
if (role) {
return role.permissions.elements;
}
}
},
In the template file, I communicate with the store like this:
<template>
<div>
<draggable
v-model="getElementsForCurrentRole"
group="elements"
#end="onDragEnd"
item-key="name">
<template #item="{element}">
<n-card :title="formatElementName(element.name)" size="small" header-style="{titleFontSizeSmall: 8px}" hoverable>
<n-switch v-model:value="element.active" size="small" />
</n-card>
</template>
</draggable>
</div>
</template>
<script setup>
import { NCard, NSwitch } from 'naive-ui';
import draggable from 'vuedraggable'
import { usePermissionsStore } from '#/stores/permissions';
import { storeToRefs } from 'pinia';
const props = defineProps({
selectedRole: {
type: String
}
})
const permissionsStore = usePermissionsStore();
const { getElementsForCurrentRole, roles } = storeToRefs(permissionsStore);
const onDragEnd = () => {
permissionsStore.save();
}
const formatElementName = (element) => {
let title = element.charAt(0).toUpperCase() + element.slice(1);
title = title.replace('-', ' ');
title = title.split(' ');
if (title[1]) {
title = title[0] + ' ' + title[1].charAt(0).toUpperCase() + title[1].slice(1);
}
if (typeof title == 'object') {
return title[0];
}
return title;
}
</script>
My problem is the v-model="getElementsForCurrentRole". When making changes, for example changing the value for the switch, the store is reactive and the changes are made successfully. But: If I try to change the Array order by dragging, the store does not update the order. I'm confused, because the store reacts on other value changes, but not on the order change.
What can be the issue here? Do I something wrong?
-Edit- I see the following warning on drag: Write operation failed: computed value is readonly
Workaround
As workaround I work with the drag event and write the new index directly to the store variable. But...its just a workaround. I would really appreciate a cleaner solution.
Here is the workaround code:
onDrag = (event) => {
if (event && event.type == 'end') {
// Is Drag Event. Save the new order manually directly in the store
let current = permissionsStore.roles.find((role) => role.value == permissionsStore.currentRole);
var element = current.permissions.elements[event.oldIndex];
current.permissions.elements.splice(event.oldIndex, 1);
current.permissions.elements.splice(event.newIndex, 0, element);
}
}
You should put reactive value on v-model.
getElementsForCurrentRole is from getters, so it is treated as computed value.
Similar to toRefs() but specifically designed for Pinia stores so
methods and non reactive properties are completely ignored.
https://pinia.vuejs.org/api/modules/pinia.html#storetorefs
I think this should work for you.
// template
v-model="elementsForCurrentRole"
// script
const { getElementsForCurrentRole, roles } = storeToRefs(permissionsStore);
const elementsForCurrentRole = ref(getElementsForCurrentRole.value);

Using $refs in a computed property

How do I access $refs inside computed? It's always undefined the first time the computed property is run.
Going to answer my own question here, I couldn't find a satisfactory answer anywhere else. Sometimes you just need access to a dom element to make some calculations. Hopefully this is helpful to others.
I had to trick Vue to update the computed property once the component was mounted.
Vue.component('my-component', {
data(){
return {
isMounted: false
}
},
computed:{
property(){
if(!this.isMounted)
return;
// this.$refs is available
}
},
mounted(){
this.isMounted = true;
}
})
I think it is important to quote the Vue js guide:
$refs are only populated after the component has been rendered, and they are not reactive. It is only meant as an escape hatch for direct child manipulation - you should avoid accessing $refs from within templates or computed properties.
It is therefore not something you're supposed to do, although you can always hack your way around it.
If you need the $refs after an v-if you could use the updated() hook.
<div v-if="myProp"></div>
updated() {
if (!this.myProp) return;
/// this.$refs is available
},
I just came with this same problem and realized that this is the type of situation that computed properties will not work.
According to the current documentation (https://v2.vuejs.org/v2/guide/computed.html):
"[...]Instead of a computed property, we can define the same function as a method. For the end result, the two approaches are indeed exactly the same. However, the difference is that computed properties are cached based on their reactive dependencies. A computed property will only re-evaluate when some of its reactive dependencies have changed"
So, what (probably) happen in these situations is that finishing the mounted lifecycle of the component and setting the refs doesn't count as a reactive change on the dependencies of the computed property.
For example, in my case I have a button that need to be disabled when there is no selected row in my ref table.
So, this code will not work:
<button :disabled="!anySelected">Test</button>
computed: {
anySelected () {
if (!this.$refs.table) return false
return this.$refs.table.selected.length > 0
}
}
What you can do is replace the computed property to a method, and that should work properly:
<button :disabled="!anySelected()">Test</button>
methods: {
anySelected () {
if (!this.$refs.table) return false
return this.$refs.table.selected.length > 0
}
}
For others users like me that need just pass some data to prop, I used data instead of computed
Vue.component('my-component', {
data(){
return {
myProp: null
}
},
mounted(){
this.myProp= 'hello'
//$refs is available
// this.myProp is reactive, bind will work to property
}
})
Use property binding if you want. :disabled prop is reactive in this case
<button :disabled="$refs.email ? $refs.email.$v.$invalid : true">Login</button>
But to check two fields i found no other way as dummy method:
<button :disabled="$refs.password ? checkIsValid($refs.email.$v.$invalid, $refs.password.$v.$invalid) : true">
{{data.submitButton.value}}
</button>
methods: {
checkIsValid(email, password) {
return email || password;
}
}
I was in a similar situation and I fixed it with:
data: () => {
return {
foo: null,
}, // data
And then you watch the variable:
watch: {
foo: function() {
if(this.$refs)
this.myVideo = this.$refs.webcam.$el;
return null;
},
} // watch
Notice the if that evaluates the existence of this.$refs and when it changes you get your data.
What I did is to store the references into a data property. Then, I populate this data attribute in mounted event.
data() {
return {
childComps: [] // reference to child comps
}
},
methods: {
// method to populate the data array
getChildComponent() {
var listComps = [];
if (this.$refs && this.$refs.childComps) {
this.$refs.childComps.forEach(comp => {
listComps.push(comp);
});
}
return this.childComps = listComps;
}
},
mounted() {
// Populates only when it is mounted
this.getChildComponent();
},
computed: {
propBasedOnComps() {
var total = 0;
// reference not to $refs but to data childComps array
this.childComps.forEach(comp => {
total += comp.compPropOrMethod;
});
return total;
}
}
Another approach is to avoid $refs completely and just subscribe to events from the child component.
It requires an explicit setter in the child component, but it is reactive and not dependent on mount timing.
Parent component:
<script>
{
data() {
return {
childFoo: null,
}
}
}
</script>
<template>
<div>
<Child #foo="childFoo = $event" />
<!-- reacts to the child foo property -->
{{ childFoo }}
</div>
</template>
Child component:
{
data() {
const data = {
foo: null,
}
this.$emit('foo', data)
return data
},
emits: ['foo'],
methods: {
setFoo(foo) {
this.foo = foo
this.$emit('foo', foo)
}
}
}
<!-- template that calls setFoo e.g. on click -->

vue.js components - pass default values from parent (across multiple instances)

I am new to Vue.js and am trying to create components that will simplify form creation, based on a library I have been using for a while now (PHP).
I have created a component that renders a label + textbox, styled via Bootstrap.
In order to avoid having to pass all the parameters every time, I want to be able to define defaults from within the parent, so that they will stay in effect until changed.
The component looks like this (MyTextBox.vue)
<template>
<div v-bind:class="myDivWidth">
<label v-bind:class="`control-label ${myLabelWidth}`">{{label}}</label>
<div v-bind:class="myControlWidth">
<input class="form-control col-md-12" v-bind:value="value">
</div>
</div>
</template>
<script>
export default {
data: function() {
return {
// trying to use this as 'class' variable but most likely wrong
myDefaultLabelWidth: 4
}
},
props: {
label: String,
labelWidth: String,
controlWidth: String,
divWidth: String,
value: {required: false},
defaultLabelWidth: {type: String}
},
computed: {
myLabelWidth: function () {
let lw;
//debugger;
do {
if (typeof this.defaultLabelWidth !== 'undefined') {
lw = this.defaultLabelWidth;
// ****** Note the call to the parent function
this.$parent.setDefault('defaultLabelWidth', lw);
break;
}
if (typeof this.labelWidth !== 'undefined') {
lw = this.labelWidth;
break;
}
if (typeof this.lw !== 'undefined') {
lw = this.lw;
break;
}
// ****** Note the call to the parent function
lw = this.$parent.getDefault('defaultLabelWidth');
} while (false);
return `col-md-${lw}`;
},
// snip....
}
}
</script>
and it is used like this (I am only showing attributes relating to label, for brevity)
(StoryEditor.vue)
<my-textbox label="LableText1" default-label-width=4></my-textbox>
<my-textbox label="LableText2"></my-textbox>
<my-textbox label="LableText3" label-width=5></my-textbox>
<my-textbox label="LableText4"></my-textbox>
<my-textbox label="LableText5" default-label-width=6></my-textbox>
<my-textbox label="LableText6"></my-textbox>
<my-textbox label="LableText7"></my-textbox>
What this is meant to do, is set the label with to 4, for the first 2 instances
then force a width of 5 for the next instance
then go back to 4
then set a new default of 6 for the remaining 3 components.
This is useful in cases where a lot of components (of the same type) are used, most of which are of the same width.
This mechanism will also used for all other applicable attributes.
Please note that what is important here is that the default is set in the parent and can change between instances of the component.
(I am aware that I can have a default value in the template itself but, as I understand it, that would apply to all instances of that component)
Any help would be greatly appreciated!
[Edit]
I have found one solution:
I added these methods to the parent (StoryEditor.vue).
They are called by the component code, shown above with '******' in the comments
<script>
export default {
created: function () {
// make sure the variable exists
if (typeof window.defaultOptions === 'undefined') {
window.defaultOptions = {
defaultLabelWidth: 3,
defaultControlWidth: 7
};
}
},
data() {
return {
story: {
}
}
},
methods: {
getDefaultOptions: () => {
console.log('getDefaultOptions', window.defaultOptions);
},
setDefaultOptions: (opts) => {
window.defaultOptions = opts;
},
getDefault: (option) => {
console.log(' getDefault', window.defaultOptions);
return window.defaultOptions[option];
},
setDefault: (option, v) => {
window.defaultOptions[option] = v;
console.log('setDefault', window.defaultOptions);
}
}
}
</script>
This uses this.$parent. to call methods in the parent.
The parent then uses a window variable to store/retrieve the relevant parameters.
A window variable is used because I want to have a single variable that will be used by all instances of the component.

vuejs2 passing data between parent-child is wiping childs value

In VueJS 2 I am trying to create a component that gets and passes data back to the parent which then passes it to another component to display.
The component that gets the data has a user input field it uses to search. When I have it pass data back to the parent using $emit the value in the input keeps being wiped.
I am receiving the below mutation error but I haven't directly tried to change the userSearch field in the component so I am not sure why.
"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: "userSearch" (found in PersonField)"
Relevant html
<person-field v-on:event_child="eventChild"></person-field>
<person-search :prop="personListArray" ></person-search>
Parent app
var app = new Vue({
el: '#app',
data: {
personListArray : [],
tempArray: []
},
methods: {
eventChild: function (arr) {
this.personListArray = arr
}
}
})
Component 1, displays a user input. Uses the input to search and bring back data. Starts search when the length of the input is more then 2. As soon as you hit the 3rd character something is causing the input to clear which I don't want.
Vue.component('person-field', {
props: ['userSearch'],
template: '<input class="form-control" v-model="userSearch" >',
watch: {
userSearch: function () {
var arr = []
if (typeof this.userSearch !== 'undefined') { //added this because once i passed 3 characters in the field the userSearch variable becomes undefined
if (this.userSearch.length > 2) {
$.each(this.getUsers(this.userSearch), function (index, value) {
var obj = {
Title: value.Title,
ID: value.ID
}
arr.push(obj)
});
this.$emit('event_child', arr) //emits the array back to parent "eventChild" method
} else {
console.log('no length')
}
} else {
console.log('cant find field')
}
},
},
methods: {
getUsers: function (filter) {
//gets and returns an array using the filter as a search
return arr
},
}
});
Component 2 - based on the personListArray which is passed as a prop, displays the results as a list (this works)
Vue.component('person-search', {
props: ['prop'],
template: '<ul id="personList">' +
'<personli :ID="person.ID" v-for="person in persons">' +
'<a class="" href="#" v-on:click="fieldManagerTest(person.Title, person.ID)">{{person.Title}}</a>' +
'</personli></ul>',
computed: {
persons: function () {
return this.prop
}
},
methods: {
fieldManagerTest: function (title, ID) { //Remove item from users cart triggered via click of remove item button
//var user = ID + ';#' + title
//this.internalValue = true
//this.$emit('fieldManagerTest');
//this.$parent.$options.methods.selectManager(user)
},
},
});
Component 3, part of component 2
Vue.component('personli', {
props: ['ID'],
template: '<transition name="fade"><li class="moving-item" id="ID"><slot></slot></li></transition>'
})
;
The reason you get the warning,
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:
"userSearch" (found in PersonField)
Is because of this line
<input class="form-control" v-model="userSearch" >
v-model will attempt to change the value of the expression you've told it to, which in this case is userSearch, which is a property.
Instead, you might copy userSearch into a local variable.
Vue.component('person-field', {
props: ['userSearch'],
data(){
return {
searchValue: this.userSearch
}
},
template: '<input class="form-control" v-model="searchValue" >',
...
})
And modify your watch to use searchValue.
Here is an example.

VueJS and lodash, filtered array displays empty unless main array is utilized in template

My mixin:
export default {
data() {
return {
charges: [],
catCharges: [],
offenses: ['Class I Offenses', 'Class II Offenses', 'Class III Offenses', 'Class IV Offense']
}
},
methods: {
getCharges() {
axios.get('admin/charges').then((response) => {
this.charges = response.data;
for(let offense = 1; offense <= this.offenses.length; offense++) {
this.catCharges[offense - 1] = this.chargesAtOffense(offense);
}
});
},
chargesAtOffense(offense) {
return _.filter(this.charges, { offense_level: offense });
}
},
created() {
this.getCharges();
}
};
Fetching data works, the array 'charges' gets populated with the following:
After populating the array, I start looping over the offenses array and filter all 'charges' from the main array into the 'catCharges' array, so all offenses are split into 4 separated arrays in that array.
Chrome's developer tools shows the array just fine and the charges are properly filtered.
This is my component:
<template>
<div>
<h1>Total charges: {{charges.length}}</h1>
<h1>Total offense categories: {{catCharges.length}}</h1>
<div v-for="(charges, offenseIdx) in catCharges">
{{charges}}
</div>
</div>
</template>
<script>
import chargesMixin from '../mixins/chargesMixin';
export default {
mixins: [chargesMixin],
data() {
return {
}
},
methods: {
},
computed: {
},
mounted() {
console.log('Disciplinary Segregation mounted.')
}
}
</script>
It uses the mixin provided above, and IT works and shows the catCharges array properly, HOWEVER when I remove the following line from the template:
<h1>Total charges: {{charges.length}}</h1>
The catCharges array is displayed as EMPTY, why do I need to use the charges array too along with the filtered array? This is driving me crazy.
I also tried the following method in the mixin which also causes the same issue:
chargesAtOffense(offense) {
var newCharges = [];
for(var i = 0; i < this.charges.length; i++) {
if(this.charges[i].offense_level != offense) continue;
const cloned = _.clone(this.charges[i]);
newCharges.push(cloned);
}
return newCharges;
}
I think your use case is linked to the reactivity system of VueJS.
https://v2.vuejs.org/v2/guide/reactivity.html
If you delete the line
<h1>Total charges: {{charges.length}}</h1>
you tell to VueJS to refresh your template only on catCharges get / set.
catCharges is an array, and so it's not as 'reactive' as a simple variable.
If you read precisely https://v2.vuejs.org/v2/guide/list.html#Caveats, prefer use a push on your catCharges to explain correctly to Vue that your array has changed.
I'll try this code :
getCharges() {
axios.get('admin/charges').then((response) => {
this.charges = response.data;
for(let offense = 1; offense <= this.offenses.length; offense++) {
this.catCharges.push(this.chargesAtOffense(offense));
}
});
},
Hope this will solve your problem.