Why isn't the prop reactive in the dynamic component? - javascript

I have a dynamic component being injected in the slot of another one and I pass the props object to it. But when I update the data (an array) which has been assosiated with the prop (dataT: this.tableData), that prop isn't being updated inside the component.
It seems like I have a deal with two different objects but the array was passed by the reference, wasn't it?
This is the main component
<template>
<Button #click="addWindows"></Button>
<Window v-for="window in windows" :key="window.id">
<component :is="window.name" v-bind="window.props" #onDeleteRow="handleDeleteRow"></component>
</Window>
</template>
<script>
export default{
data(){
return{
windows:[],
tableData:[
{
id: '0',
name: 'dog'
},
{
id: '1',
name: 'cow'
},
{
id: '2',
name: 'cat'
}
]
}
},
methods:{
addWindows(){
this.windows = [
{
id: 0,
name: 'Component1',
props: {
dataT: this.tableData
}
},
{
id: 1,
name: 'Component2',
props: {}
}];
},
handleDeleteRow(id){
this.tableData = this.tableData.filter(r => r.id != id);
}
}
}
</script>
I expect updating dataT prop in Component1 when I modify this.tableData in the main component.

Original answer based on an earlier version of the question
If you make windows a computed property it can depend on tableData:
export default {
data() {
return {
tableData: [
{
id: '0',
name: 'dog'
},
{
id: '1',
name: 'cow'
},
{
id: '2',
name: 'cat'
}
]
}
},
computed: {
windows () {
return [
{
id: 0,
name: 'Component1',
props: {
dataT: this.tableData
}
}, {
id: 1,
name: 'Component2',
props: {}
}
]
}
}
}
If you can't make all of it a computed property, e.g. because you need to be able to modify it, then keep it as data and just use the computed property to create the array needed in your template. In that case the computed property would just be merging together different parts of the data into the correct form.
In your original code, the line dataT: this.tableData won't work because this.tableData doesn't exist yet, it'll just be undefined. There's no lazy evaluation here, it needs to resolve to the correct object at the point it hits that line.
Even if it was able to get access to the correct object it wouldn't help because in handleDeleteRow you're reassigning tableData to point to a different object. Passing 'by reference' has nothing to do with the name you use to identify the object, it refers to a reference in memory.
Incidentally, v-on also supports an object syntax, just like v-bind, so you could make onDeleteRow optional in a similar fashion.
Update based on the edited question
When you write this in addWindows:
props: {
dataT: this.tableData
}
This will assign the current value of this.tableData to dataT. That current value will be an array and as arrays are reference types any modifications made to that array will apply no matter what identifier is used to reference it.
However, this line...
this.tableData = this.tableData.filter(r => r.id != id);
... does not modify that array. Instead it assigns a totally new array to this.tableData. This will have no effect on the array referenced by dataT, which is unchanged.
There are several ways you could approach solving this, including using a computed property. However, a property getter might provide a convenient sleight-of-hand:
addWindows () {
const vm = this;
this.windows = [
{
id: 0,
name: 'Component1',
props: {
get dataT () {
return vm.tableData
}
}
},
{
id: 1,
name: 'Component2',
props: {}
}
];
}
This will always evaluate to the current value of tableData. Vue's reactivity should be fine with this extra indirection, it just sees it as equivalent to accessing tableData directly.

TL;DR
The issue is with your binding. Use the following:
<component
:is="window.name"
:dataT="window.props.dataT"
#onDeleteRow="handleDeleteRow">
</component>
Explanation
the v-bind attribute specifies what prop is bound to what value (or reference). In your case, you didn't specify what values you're binding to what props, thus the component props weren't bound as expected.

Related

Add data dynamically to an object recursively

Given the following code:
const data = {
name: 'product',
children: [
{ name: 'title' },
{ name: 'code' },
{ name: 'images', children: [{ name: 'image1' }, { name: 'image2' }] },
],
}
let result = {}
function resolveNodes(nodes, parentNode){
if(nodes.children){
parentNode = nodes.name;
result[parentNode] = {};
nodes.children.forEach(node => resolveNodes(node, parentNode));
}else{
result[parentNode][nodes.name] = nodes.name;
}
}
resolveNodes(data);
console.log(result);
Current output
{
product: {
title:"title",
code:"code"
},
images: {
image1:"image1",
image2:"image2"
}
}
Desired output:
{
product: {
title:"title",
code:"code",
images: {
image1:"image1",
image2:"image2"
}
}
}
I'm not sure how to recursively add a nested object within the parent (product) object. I've tried many things like passing the parent and the child to the function and from there try to work out the structure of the new object however it get very messy and I know is not the ideal way to do it.
This function will need to handle at least one extra layer of children but I've just added 2 to simplify the question.
What is the most ideal way to implement this?
You could take a recursive approach and have a look to children and get a combined object, otherwise just the name as value.
Instead of giving a simplified solution, let's have a closer look to the code.
resolveNodes = ({ name, children }) => ({ [name]: children
? Object.assign(...children.map(resolveNodes))
: name
})
The first part,
resolveNodes = ({ name, children }) =>
^^^^^^^^^^^^^^^^^^
the arguments of the function, contains a destructuring assignment, where an object is expected and from this object, the named properties are taken as own variables.
The following returned expression
resolveNodes = ({ name, children }) => ({
})
contains an object with a
vvvvvvv
resolveNodes = ({ name, children }) => ({ [name]:
})
computed property name, which takes the value of the variable as property name.
The following conditional (ternary) operator ?:
children
? Object.assign(...children.map(resolveNodes))
: name
checks children.
If this value is truthy, like an array, it evaluates the part after ? and if the value is falsy, the opposite of truthy, then it takes the expression after :.
In short, if no children, then take name as property value.
The part for truthy, if children exists,
Object.assign(...children.map(resolveNodes))
returns a single object with all children properties.
Object.assign(...children.map(resolveNodes))
^^^^^^^^^^^^^ create a single object
^^^ spread syntax
^^^^^^^^^^^ return all element by using
^^^^^^^^^^^^ the actual function again
This part is easier to understand if order of execution is followed.
children.map(resolveNodes)
the inner part with a mapping of children by calling resolveNodes returns an array of objects with a single property. It looks like this
[
{ title: 'title' },
{ code: 'code' },
{ images: // this looks different, because nested
{ // children are called first
image1: 'image1',
image2: 'image2'
}
}
]
and contains objects with a single property.
To get an single object with all properties of the array, Object.assign takes objects and combines all objects to one and replaces same properties in the target object with the latest following same named property. This is not the problem here, because all properties are different. Actually the problem is to take the array as parameters for calling the function.
This problem has two solutions, one is the old call of a function with Function#apply, like
Object.assign.apply(null, children.map(resolveNodes))
The other one and used
Object.assign(...children.map(resolveNodes))
is spread syntax ..., which takes an iterable and adds the items as parameters into the function call.
const
resolveNodes = ({ name, children }) => ({ [name]: children
? Object.assign(...children.map(resolveNodes))
: name
}),
data = { name: 'product', children: [{ name: 'title' }, { name: 'code' }, { name: 'images', children: [{ name: 'image1' }, { name: 'image2' }] }] },
result = resolveNodes(data);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }

How to put return value into another return value in JS vue

I want the "topic1" to be the value of my breed name and key, but when I try to put this.topic1 to replace the manual typing, it shows nothing.
Or there are any other method to have my button name same as my retrieve API param, and sent it name when I click it?
new Vue({
el: '#app2',
components: { Async },
data() {
return {
topic1: null,
topic2: null,
currentBreed: 0,
breeds: [
{ name: this.topic1 , key: this.topic1 },
{ name: "German Shepherd", key: "germanshepherd" },
{ name: "Husky", key: "husky" },
{ name: "Pug", key: "pug" },
{ name: "(Error)", key: "error" },
]
}
},
async created() {
try {
this.promise = axios.get(
"https://k67r3w45c4.execute-api.ap-southeast-1.amazonaws.com/TwitterTrends"
);
const res = await this.promise;
this.topic1 = res.data[0].Trends;
this.topic2 = res.data[1].Trends;
} catch (e) {
console.error(e);
}
},
async mounted () {
let test = this.topic1;
},
computed: {
breedKey() {
return this.breeds[this.currentBreed].key;
}
}
})
There are several problems here.
The data function is called just once, when the corresponding Vue instance is created. Within that function you can get a reference to its Vue instance via this. At that point some properties, such as those corresponding to props, will already exist. However, others won't.
The object returned from data is used to create new properties on the instance. In this case you're creating 4 properties: topic1, topic2, currentBreed and breeds. Vue creates those properties based on that returned object, so they won't exist until after the data function is run.
So when you write { name: this.topic1 , key: this.topic1 }, within that data function you're attempting to access a property called topic1 that doesn't exist yet. As such it will have a value of undefined. So you're creating an entry equivalent to { name: undefined , key: undefined },.
Further, there is no link back to topic1. That object won't be updated when the value of topic1 changes.
It's also worth noting a few points about timing.
The data function will be called before the created hook, so the axios call isn't made until after the data properties are populated.
An axios call is asynchronous.
Using await may make the code a little easier to read but the 'waiting' is mostly just an illusion. The remaining code inside the function won't run until the awaited promise is resolved but that won't cause anything outside of the function to wait. await is equivalent to using then.
The component will render just after the created hook is called. This is synchronous, it won't wait for the axios request. The mounted hook will then be called, all before the axios call has completed.
All of this means you may need to adjust your template to handle the case where the axios call hasn't completed yet as it will initially render prior to the values of topic1 and topic2 being available.
Specifically addressing the breeds property you have a few options. One is to inject the values in once the value has loaded:
breeds: [
{ name: "" , key: "" }, // Initially empty values
{ name: "German Shepherd", key: "germanshepherd" },
// ...
const res = await this.promise;
this.topic1 = res.data[0].Trends;
this.topic2 = res.data[1].Trends;
this.breeds[0].name = this.breeds[0].key = this.topic1;
Another is to use a computed property for breeds (you'd remove it from the data for this):
computed: {
breeds () {
return [
{ name: this.topic1 , key: this.topic1 },
{ name: "German Shepherd", key: "germanshepherd" },
{ name: "Husky", key: "husky" },
{ name: "Pug", key: "pug" },
{ name: "(Error)", key: "error" },
]
}
}
As we're using a computed property it will be updated when topic1 changes as it's a reactive dependency.
Using a computed property is probably the most natural solution in this case but there are other tricks you can use to get this to work.
For example, you could use property getters for the two properties in that first breed object (that's JavaScript property getters, nothing to do with Vue):
data () {
const vm = this;
return {
topic1: null,
topic2: null,
currentBreed: 0,
breeds: [
{
get name () {
return vm.topic1;
},
get key () {
return vm.topic1;
}
},
{ name: "German Shepherd", key: "germanshepherd" },
{ name: "Husky", key: "husky" },
{ name: "Pug", key: "pug" },
{ name: "(Error)", key: "error" },
]
}
},
I'm not advocating this approach for your use case but it is an interesting way to do it that can sometimes be useful. The key thing to note is how the dependency on topic1 is evaluated only when the properties name and key are accessed, not when the data function is executed. This allows topic1 to be registered as a dependency of whatever is accessing name and key, e.g. during rendering.

Delete a json inside another in React

Imagine that I have this kind of JSON object on my react state:
this.state={
parent:{
childs:[
child1:{
},
child2:{
},
child3:null,
(...)
]
}
}
to delete the child1 I did the following method:
deleteChild1 = (index,test) => {
const childs= [...this.state.parent.childs];
childs[index] = {
...childs[index],
child1: null
}
this.setState(prevState => ({
...prevState,
parent: {
...prevState.parent,
childs: [
...childs,
]
}
}))
}
This works with no problem, but imagine that I have 100 childs, I have to do 100 methods like this but instead putting the child1 to null I have to put the child100, child99, you get the idea.
My question is that is another way to put the variable null.
Thanks!
Currently your data structure isn't valid so its hard to write up a solution. If I try to create that exact state object it raises an exception Uncaught SyntaxError: Unexpected token : This is because you have an array written like an object. So first thing to do is adjust your state model to be valid syntax
this.state = {
parent: {
childs: {
child1: {
},
child2: {
},
child3: null,
...
}
}
}
Now, what you are describing / what you want to do is a dynamic key reference.
You can do this like so
const childTarget = 'child1'
childs = {
...childs,
[childTarget]: null
}
so to abstract that concept a little more. make it a function parameter
deleteChild = (childTarget) => {
then when you want to remove any particular child you can let them pass their value to this function
<Child onRemove={this.deleteChild} name={name} />
// name here would be like 'child1'
// assuming you are looping over state and rendering a child component for each item
and when the child calls this function
this.props.onRemove(this.props.name)
The answer is very simple, this would be my approach.
Create a function which updates your state with the expect result (removing that property).
If you wish to assign null that you can replace .filter() with a .map() solution. Typically if you are removing a piece of data it does not make sense to null it, but to remove it.
FYI your property childs is an array you have an object within, so you need a list of objects to fix this.
E.g.
[
{
name: "child1"
},
{
name: "child2"
},
{
name: "child3"
}
]
removeChild = (child) => {
const newChildList = this.state.parent.childs.filter(({name}) => name !== child);
this.setState(previousState => ({
...previousState,
parent: {
...previousState.parent,
childs: newChildList
}
}));
}
The key part here is that the data is being updated and overriding the original array. Because you have nested data structure we don’t want to delete any pre-existing data (hence the spreading).
Call the function however you want and if your childs array has an object with the property called name that matches the child function argument, it will be not be present on the next re-render.
Hope this helps.

Prevent prop from overwriting the data

I'm new to vue.js and struggling with the following scenario.
I send an array filled with objects via props to my router-view.
Inside one of my router-view components I use this array in multiple functions, reference it with 'this.data' and safe it inside the functions in a new variable so I don't overwrite the actual prop data.
However the functions overwrite the original prop data and manipulate the data of the prop.
Here is an abstract example of my question:
App.vue
<template>
<div>
<router-view :data='data'></router-view>
</div>
</template>
<script>
export default {
data: function() {
return {
data: [],
};
},
created: function() {
this.getData();
},
methods: {
getData: function() {
this.data = // array of objects
},
}
route component:
<script>
export default {
props: {
data: Array,
},
data: function() {
return {
newData1 = [],
newData2 = [],
}
}
created: function() {
this.useData1();
this.useData2();
},
methods: {
useData1: function() {
let localData = this.data;
// do something with 'localData'
this.newData1 = localData;
}
useData2: function() {
let localData = this.data;
// do something with 'localData'
this.newData2 = localData;
}
}
}
</script>
The 'localData' in useData2 is manipulated from changes in useData1, whereby I don't overwrite the data prop.
Why do I overwrite the prop and how can i prevent it?
The problem you're experiencing a side effect of copying this.data by reference, rather than value.
The solution is to use a technique commonly referred to as cloning. Arrays can typically be cloned using spread syntax or Array.from().
See below for a practical example.
// Methods.
methods: {
// Use Data 1.
useData1: function() {
this.newData1 = [...this.data]
},
// Use Data 2.
useData2: function() {
this.newData2 = Array.from(this.data)
}
}
#Arman Charan is right on his answer. Object and arrays are not primitive types but reference.
There is an awesome video explanation here => JavaScript - Reference vs Primitive Values/ Types
So for reference types you first have to clone it on another variable and later modify this variable without the changes affecting the original data.
However for nested arrays and objects in high level the spead and Array.from will not work.
If you are using Lodash you can use _.cloneDeep() to clone an array or an object safely.
I like functional programming and I use Lodash which I strongly recommend.
So you can do:
let original_reference_type = [{ id:1 }, { id: 2 }]
let clone_original = _.cloneDeep(original_reference_type)
clone_original[0].id = "updated"
console.log(original_reference_type) //[{ id:1 }, { id: 2 }] => will not change
console.log(clone_original) // [{ id: "updated" }, { id: 2 }]
Suggestion: For simple arrays and objects use:
Objects:
let clone_original_data = {...original_data} or
let clone_original_data = Object.assign({}, original_data)
Arrays:
let clone_original_data = [...original_data] or
let clonse_original_data = original_data.slice()
For complex and high nested arrays or Objects go with Lodash's _.cloneDeep()
I think this is most readable, "declarative" way:
First, install lodash npm i lodash. Then import desired function, not the whole library, and initialize your data with array from props.
<script>
import cloneDeep from 'lodash/cloneDeep'
export default {
props: {
data: Array
},
data () {
return {
// initialize once / non reactive
newData1: cloneDeep(this.data),
newData2: cloneDeep(this.data)
}
}
}
</script>

Revert changes to array in Vue

I'm trying to have a component which can change some elements in it. In reality, a change will be like swapping the object in a given position. I did some POC and tried to do the reverting method to be able to leave it how it was before.
export default {
name: 'Landing',
data () {
return {
items: [
{
id: 1,
category: 'Something something'
},
{
id: 2,
category: 'Something something'
}
]
};
},
created() {
this.originals = this.items.slice(0);
},
methods: {
change() {
this.items[0].category = 'Changed';
},
revert() {
// ??
}
}
};
I've tried a couple of things especially after reading this: https://vuejs.org/2016/02/06/common-gotchas/#Why-isn%E2%80%99t-the-DOM-updating
while (this.snacks.length) {
this.items.pop();
}
this.originals.slice(0).forEach(o => this.items.push(o));
But it doesn't work. If I delete the pushing part, I get an empty list correctly but if I try somehow to push it back, it won't work.
What am I missing here?
If you give a working fiddle I can show you what happened.
Basically, because you are modifying the same array object. this.originals refers to the same array as this.items
Slice returns a shallow copy of the object. You should either take a deep copy or your revert should be the one initializing the object.
this.items.pop();
will remove the items from this.originals as well.

Categories

Resources