How to set reactive object in Vuejs 3 - javascript

I have a reactive object in the global space:
let cart = Vue.reactive({
order: {
itemsCount: 0
},
items: []
});
It works great. When we change this cart in any way (for example cart.order.itemsCount += 1;) other UI parts get updated automatically.
But if we set it to a new object (let's say we have received a new order object from server) then it breaks and won't work anymore:
cart = serverCart
We even tried cart = Vue.reactive(serverCart) and it still does not cause other UI parts to re-render.
It's possible that we set every property manaully:
cart.order.itemsCount = serverCart.order.ItemsCount;
cart.items[0].productTitle = serverCart.order[0].productTitle;
...
But this is idiotic of course.
How can we re-set a reactive object entirely in Vue.js 3?
Update:
You can see this codesandbox in action.

For setting new values reactive objects in vue3, you need to use Object.assign function and you should not reassign it
Here is an example:
<template>
{{ reactiveTest }}
<button #click="change">Change</button>
</template>
<script>
import { reactive } from "vue";
export default {
setup() {
let reactiveTest = reactive({
a: 1,
b: 2,
});
function change() {
const serverReactive = {
a: 3,
b: 4,
};
// reactiveTest = serverReactive; Do not do this
Object.assign(reactiveTest, serverReactive)
}
return {
reactiveTest,
change,
};
},
};
</script>

You are trying to copy by reference, try to copy by value with spread operator ... :
const { reactive } = Vue
const app = Vue.createApp({
el: "#demo",
setup() {
let cart = reactive({
order: {
itemsCount: 0
},
items: []
});
const someCart = {order: {itemsCount: 3}, items: [{id: 1, name: "aaa"}, {id: 2, name: "bbb"}, {id: 3, name: "444"}]}
cart = reactive({...someCart})
return {cart }
}
})
app.mount('#demo')
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
<div id="demo">
<p> {{ cart }}</p>
<label>change items count</label>
<input type="number" v-model="cart.order.itemsCount" />
</div>

Related

Working with state on recursive components

I'm writing a component that renders itself inside recursively and is data-driven
Attaching my sandbox snippet, as it will be easier to see there.
This is my data:
var builderStructureData = [
{
id: 123,
value: 3,
children: []
},
{
id: 345,
value: 5,
children: [
{
id: 4123,
value: 34,
children: [
{
id: 342342,
value: 33,
children: []
}
]
},
{
id: 340235,
value: 3431,
children: [
{
id: 342231342,
value: 3415,
children: []
}
]
}
]
}
];
and it renders like this:
This is my App.js:
import { useState } from "react";
import "./App.css";
import Group from "./components/group";
import builderStructureData from "./components/builderStructureData";
function App() {
const [builderStructure, setBuilderStructure] = useState(
builderStructureData
);
return (
<div className="App">
{builderStructure.map((x) => {
return <Group key={x.id} children={x.children} value={x.value} />;
})}
</div>
);
}
export default App;
And this is my recursive component:
import React from "react";
export default function Group(props) {
let childrenArray = [];
if (props.children) {
props.children.map((x) => childrenArray.push(x));
}
return (
<div className="group" draggable>
<p>this is value: </p>
<input value={props.value} readOnly={true}></input>
<button>Add Group</button>
{childrenArray.map((x) => {
return <Group key={x.id} children={x.children} value={x.value} />;
})}
</div>
);
}
I can render the components based on the data, and it seems to be handling recursion fine. I need to store the state on the App.js page and be able to change it from within child components. For example, if I update the "value" field of the component with ID = 342342, I want it to update that corresponding object in the state no matter how deeply nested it is, but not sure how to do that as it is not as simple as just passing a prop.
Am I taking the right approach with my code snippet? How can I do the state update?
I would advise the state normalization approach - here is an example for redux state - https://redux.js.org/usage/structuring-reducers/normalizing-state-shape - but you can use this approach with your state. So - your state will look like this:
state = {
items: {
[123]: {
id: 123,
value: 3,
childrenIds: []
},
[345]: {
id: 345,
value: 5,
childrenIds: [4123, 340235]
},
[4123]: {
id: 4123,
value: 34,
parentId: 345,
childrenIds: [342342]
},
[342342]: {
id: 342342,
value: 33,
parentId: 4123,
childrenIds: []
},
[340235]: {
id: 340235,
value: 3431,
parentId: 345,
childrenIds: [342231342]
},
[342231342]: {
id: 342231342,
value: 3415,
parentId: 340235
childrenIds: []
}
}
}
Here the field "childrenIds" is an optional denormalization for ease of use, if you want - you can do without this field. With this approach, there will be no problem updating the state.
You are thinking this in a wrong way, it should be very easy to do what you want.
The most imported thing is to make a little small changes in Group
Please have a look
import React from "react";
export default function Group(props) {
const [item, setItem] = React.useState(props.item);
let childrenArray = [];
if (item.children) {
item.children.map((x) => childrenArray.push(x));
}
const updateValue = ()=> {
// this will update the value of the current object
// no matter how deep its recrusive is and the update will also happen in APP.js
// now you should also use datacontext in app.js togather with state if you want to
// trigger somethings in app.js
item.value =props.item.value= 15254525;
setState({...item}) // update the state now
}
return (
<div className="group" draggable>
<p>this is value: </p>
<input value={item.value} readOnly={true}></input>
<button>Add Group</button>
{childrenArray.map((x) => {
return <Group item={x} />;
})}
</div>
);
}
The code above should make you understand how easy it is to think about this as an object instead of keys.
Hop this should make it easy for you to understand

Why is v-model inside a component inside a v-for (using a computed list) changing weirdly when the list changes?

I'm using a computed list to display several forms for changing comments in a database. (backend Symfony / api requests via axios, but unrelated)
The form for the comments itself is in a Vue component.
The computed list is based on a list that gets loaded (and set as data property) when the page is mounted which is then filtered by an input search box in the computed property.
Now when i type different things in the input box and the comment component gets updated the v-model and labels are messing up.
I've tested in several browsers and the behaviour is the same in the major browsers.
I've also searched the docs and haven't found a solution.
Example to reproduce behaviour:
<!DOCTYPE html>
<html>
<div id="app"></app>
</html>
const ChangeCommentForm = {
name: 'ChangeCommentForm',
props: ['comment', 'id'],
data() {
return {
c: this.comment,
disabled: false
};
},
template: `
<form>
<div>{{ comment }}</div>
<input :disabled="disabled" type="text" v-model="c">
<button type="submit" #click.prevent="changeComment">
Change my comment
</button>
</form>
`,
methods: {
changeComment() {
this.disabled = true;
// do the actual api request (should be unrelated)
// await api.changeCommentOfFruit(this.id, this.c),
// replacing this with a timeout for this example
window.setTimeout(() => this.disabled = false, 1000);
}
}
};
const App = {
components: {ChangeCommentForm},
data() {
return {
fruits: [
{id: 1, text: "apple"},
{id: 2, text: "banana"},
{id: 3, text: "peach"},
{id: 4, text: "blueberry"},
{id: 5, text: "blackberry"},
{id: 6, text: "mango"},
{id: 7, text: "watermelon"},
],
search: ''
}
},
computed: {
fruitsFiltered() {
if (!this.search || this.search === "")
return this.fruits;
const r = [];
for (const v of this.fruits)
if (v.text.includes(this.search))
r.push(v);
return r;
}
},
template: `
<div>
<form><input type="search" v-model="search"></form>
<div v-for="s in fruitsFiltered">
<ChangeCommentForm :id="s.id" :comment="s.text"/>
</div>
</div>
`
};
const vue = new Vue({
el: '#app',
components: {App},
template: '<app/>'
});
Just type some letters in the search box
Example on codepen: https://codepen.io/anon/pen/KLLYmq
Now as shown in the example the div in CommentChangeForm gets updated correctly, but the v-model is broken.
I am wondering if i miss something or this is a bug in Vue?
In order to preserve state of DOM elements between renderings, it's important that v-for elements also have a key attribute. This key should remain consistent between renderings.
Here it looks like the following might do the trick:
<div v-for="s in fruitsFiltered" :key="s.id">
<ChangeCommentForm :id="s.id" :comment="s.text"/>
</div>
See:
https://v2.vuejs.org/v2/guide/list.html#Maintaining-State
https://v2.vuejs.org/v2/api/#key

How to pass data to nested child components vue js?

I get how to pass data from parent to child with props in a situation like:
<template>
<div>
<div v-for="stuff in content" v-bind:key="stuff.id">
<ul>
<li>
{{ stuff.items }}
</li>
</ul>
</div>
</div>
</template>
<script>
export default {
name: stuff,
props: ['content'],
data () {
return {
}
}
}
</script>
And then bind the data to the component in the parent component like,
<template>
<div>
<stuff v-bind:content="stuffToPass"></stuff>
</div>
</template>
<script>
import stuff from './stuff.vue';
export default {
data () {
return {
stuffToPass: [
{id: 1, items: 'foo'},
{id: 2, items: 'bar'},
{id: 3, items: 'baz'}
]
}
},
components: {
stuff
}
}
</script>
But say I have the root component, and I want to pass data to the stuff component, like in the above, but when I have a number of other components like parent > x > y > stuff, and it's still the stuff component that will ultimately be receiving that data, I don't know how to do that.
I heard of provide/inject, but I'm not sure that's the appropriate use, or at least I couldn't get it working.
Then I tried passing props, but then I found myself trying to bind a prop to a component to pass as a prop to a child component and that doesn't sound right, so then I just re-wrote my components in the 'stuff' component, but I feel that's probably re-writing way to much code to be close to reasonable.
there are a few possibilities to pass data parent > x > y > stuff
props - applicable but you would have to pipe the data through all components...
store (vuex) - applicable but could become complicated to handle
event bus - the most flexible and direct way
below, a simple example on how to implement the event bus:
// src/services/eventBus.js
import Vue from 'vue';
export default new Vue();
the code from where you want to emit the event:
// src/components/parent.vue
<script>
import EventBus from '#/services/eventBus';
export default {
...
methods: {
eventHandler(val) {
EventBus.$emit('EVENT_NAME', val);
},
},
...
};
</script>
the code for where you want to listen for the event:
// src/components/stuff.vue
<script>
import EventBus from '#/services/eventBus';
export default {
...
mounted() {
EventBus.$on('EVENT_NAME', val => {
// do whatever you like with "val"
});
},
...
};
</script>
Use watchers or computed properties https://v2.vuejs.org/v2/guide/computed.html
const Stuff = Vue.component('stuff', {
props: ['content'],
template: `<div>
<div v-for="stuff in content" v-bind:key="stuff.id">
<ul>
<li>
{{ stuff.items }}
</li>
</ul>
</div>
</div>`
});
const Adapter = Vue.component('adapter', {
components: { Stuff },
props: ['data'],
template: `<div>
<Stuff :content="newData"/>
</div>`,
data() {
return {
newData: []
};
},
created() {
this.changeData();
},
watch: {
data: {
deep: true,
handler: function() {
this.changeData();
}
}
},
methods: {
changeData() {
this.newData = JSON.parse(JSON.stringify(this.data));
}
}
});
const app = new Vue({
el: '#app',
components: { Adapter },
data() {
return {
stuffToPass: [
{ id: 1, items: 'foo' },
{ id: 2, items: 'bar' },
{ id: 3, items: 'baz' }
]
};
},
methods: {
addItem() {
this.stuffToPass.push({ id: this.stuffToPass.length + 1, items: 'new' });
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.1/vue.js"></script>
<div id="app">
<button #click="addItem">Add</button>
<Adapter :data="stuffToPass"/>
</div>

vue.js data not updated after clear

I have a vue.js item in my page that tracks changes made to a form. It looks like this:
var changes_applied = [];
var changes_applied_block = new Vue({
name: "ChangesApplied",
el: '#changes-applied',
data: {
items: changes_applied
},
methods: {
remove: function(index) {
changes_applied.splice(index, 1);
}
}
});
When a change is detected the change is pushed onto the changes_applied array, and it shows up in the "Changes Applied" div as expected. The deletes also work, which just calls the remove method on the vue object.
I also have a "clear" button that's not connected to the vue instance, and when it's clicked it sets the data source back to an empty array using changes_applied = [];
The problem is that after this is cleared using the button, the changes / additions to the changes array no longer show up in the vue element-- it's like the vue element is no longer attached to the changes_applied array.
Am I missing a binding or something here that needs to happen, or is there a "vue way" to clear the vue data without touching the actual source array?
You shouldn't be changing the changes_applied array; Vue isn't really reacting to changes on that array. It only sort of works when this.items is pointed to the same array reference. When you change that reference by reassigning changes_applied it breaks because you are then manipulating changes_applied but it is not longer the same array as this.items.
You should instead be manipulating this.items directly:
methods: {
remove: function(index) {
this.items.splice(index, 1);
}
To clear it you can set:
this.items = []
and it will work as expected.
Your items array is initialized with changes_applied but does not mantaing bindings, it's just the default value for items when the instance is created. So if you change the changes_applied this will not affect the items array on vue instance.
example
new Vue({
el: '#app',
data: function () {
return {
items: myArr,
newItem: ''
}
},
methods: {
addItem () {
this.items.push(this.newItem)
this.newItem = ''
},
remove (index) {
this.items.splice(index, 1)
},
clear () {
this.items = []
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.13/vue.js"></script>
<div id="app">
<input type="text" v-model="newItem" />
<button #click="addItem">Add</button>
<button #click="clear">Clear</button>
<p v-for="(item, index) in items" #click="remove(index)">{{item}}</p>
</div>
<!-- from outside vue instance-->
<button onClick="clearFromOutside()">clear from outside</button>
<script>
var myArr = ['hola', 'mundo'];
function clearFromOutside() {
console.log(myArr)
myArr = [];
console.log(myArr)
}
</script>
Mark_M already provided a good explanation, I'll add a demo, since I think its easier to understand how it works.
You can copy the value of the array to data, but then all operations must be done to the data directly:
const changes_applied = [
{id: 1},
{id: 2},
{id: 3}
];
const vm = new Vue({
el: '#app',
data: {items: changes_applied},
methods: {
add() {
const id = this.items.length + 1
this.items.push({id})
},
remove() {
this.items.pop()
},
clear() {
this.items = []
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.13/vue.js"></script>
<div id="app">
<div>
<button type="button" #click="add">Add</button>
<button type="button" #click="remove">Remove</button>
<button type="button" #click="clear">Clear</button>
</div>
<ul name="list">
<li v-for="item in items" :key="item.id">
Item {{ item.id }}
</li>
</ul>
</div>

How can I change defaultValue of input when I have got props from the server in admin-on-rest?

I'm working with admin-on-rest and I'm creating <Edit> component. In <Edit> I have <RadioButtonInputGroup> with defined defaultValue:
const defaultValues = {
user_to_app_to_role_by_user_id: 5
};
...
const roleChoices = [
{ id: 5, name: 'User' },
{ id: 4, name: 'Admin' }
];
...
<Edit title="Edit user">
<SimpleForm defaultValue={defaultValues}>
<RadioButtonGroupInput
label="Role"
source="user_to_app_to_role_by_user_id"
choices={roleChoices}
/>
...
But now I want to et up default value like value that I want to get from this.props, like this:
const defaultValues = {
user_to_app_to_role_by_user_id: this.getRoleByProps()
};
...
getRoleByProps() {
let role;
if(this.props.user && this.props.user.roles) {
return this.props.user.roles[0].role_id
}
else {
return 5;
}
}
In process of debugging I see, that when properties come in, my defaultValues object update. But what's problem is that component <RadioButtonGroupInput> doesn't update. It's value as before.
I already tried to inpur default velues in state and write <SimpleForm defaultValue={this.state.defaultValues} but it doesn't help.
Maybe you can give some advice for me what I can do in thi situation.

Categories

Resources