Child prop not updating, even though the parent does - javascript

I have a pretty weird situation where a change in a child component's prop is not triggering a re-render.
Here is my setup. In the parent I have:
<child :problemProp="proplemPropValue""></child>
In the child I have defined the prop:
{
name: "Child",
props: {
problemProp: {
type: [File], //yes it is a file
required: true
}
}
and then I try to render it (still in the child component)
<template>
<div id="dropzone-preview" class="file-row">
{{problemProp}} <--This should just show the prop as a JSON string-->
</div>
</template>
This is rendered correctly initially. But problemProp has a property upload.progress, and when I change it in the parent (I can confirm that it does change on the parent) it does NOT change in the child.
If I now add a second prop, dummyProp to the child:
{
name: "Child",
props: {
problemProp: {
type: [File], //yes it is a file
required: true
},
dummyProp: {
type: Number
}
}
Now, when dummyProp changes, propblemProp also changes. What is going on here? Why does the change in dummyProp force a re-render but a change in propblemProp does not?

The root of the problem appears to be that when you add a File object to the array, Vue fails to convert it into an observed value. This is because Vue ignores browser API objects:
The object must be plain: native objects such as browser API objects
and prototype properties are ignored.
That being the case, any changes to that object will not be reflected in the Vue automatically (as you would typically expect) because Vue doesn't know they changed. That is also why it appears to work when you update dummyProp, because changes to that property are observed and trigger a re-render.
So, what to do. I've been able to get your bin to work by making a small change to the addedFile method.
addedfile(file){
console.log(file);
self.problemPropValues.push(Object.assign({}, file));
},
First, you don't need (or want) to use $data, just reference the property directly. Second, by making a copy using Object.assign, Vue properly observes the object and updates are now reflected in the child. It may be the case that you will need to use a deep copy method instead of Object.assign at some point, depending on your use case, but for now this appears to be working.
Here is the code I ended up with getting the bin to work (converted to codepen because I find working with codepen easier).

Related

Draggable vue components

While replicating:
https://sortablejs.github.io/Vue.Draggable/#/nested-example
(code)
I ran into an issue related to the model; how can I add draggable to vue components instead of a raw json (as used in the example).
What I want to do is, add drag and drop to:
https://codesandbox.io/s/gg1en
(each item can be moved ("dragged") from one group to another, each group can be dragged from a lower position to an upper one (and vice-versa).
I tried:
https://codesandbox.io/s/quirky-sutherland-5s2zz?file=/src/components/InventorySectionC.vue
and got:
Unexpected mutation of "itemSectionData" prop. (vue/no-mutating-props)
but in this case the data comes from (through?) a prop.
The hierarchy of components is:
ChromePage --> InventorySectionC --> InventorySectionGroupC --> InventorySectionGroupItemC
I need components instead of plain JSON as each component has additional features (e.g. CRUD).
How should I go about combining components with draggable?
If you have a component prop that's being mutated, there are multiple options:
Convert your prop to a custom v-model. It will allow two-ways bindings without problems. (Use the prop name value and sent $emit('input', this.value) to update it.
Use prop.sync modifier, which is kinda the same as using v-model but the prop has a specific name. Use :data.sync="myItems" on the parent, and run $emit('update:data', this.data) inside the component to update it.
Copy the prop to a data variable inside the component. So that the prop is only used as a default value for this data, but then it's only the data that's being mutated. But this won't allow the parent to see any modifications on that prop since it's not being updated directly.
<template>
<draggable v-model="_data"></draggable>
</template>
<script>
props: {
data: { type: Object, required: true }
}
data() {
return {
_data: {}
}
}
mounted() {
// Copy
Object.assign(this._data, this.data)
}
</script>

vue - $emit vs. reference for updating parent data

We need to use $emit to update the parent data in a vue component. This is what has been said everywhere, even vue documentation.
v-model and .sync both use $emit to update, so we count them $emit here
what I'm involved with is updating the parent data using reference type passing
If we send an object or array as prop to the child component and change it in the child component, changes will be made to the parent data directly.
There are components that we always use in a specific component and we are not going to use them anywhere else. In fact, these components are mostly used to make the app codes more readable and to lighten the components of the app.
passing reference type values as prop to children for directly change them from children is much easier than passing values then handle emitted event. especially when there are more nested components
code readability is even easier when we use reference type to update parent.
For example, suppose we have grand-parent, parent and child components. in parent component we have a field that change first property of grand-parent data and in child component we have another field that change second property of grand-parent data
If we want to implement this using $emit we have something like this : (we are not using .sync or v-model)
// grand-parent
<template>
<div>
<parent :fields="fields" #updateFields="fields = $event" >
</div>
</template>
<script>
import parent from "./parent"
export default {
components : {parent},
data(){
return {
fields : {
first : 'first-value',
second : 'second-value',
}
}
}
}
</script>
// parent
<template>
<div>
<input :value="fields.first" #input="updateFirstField" />
<child :fields="fields" #updateSecondField="updateSecondField" >
</div>
</template>
<script>
import child from "./child"
export default {
components : {child},
props : {
fields : Object,
},
methods : {
updateFirstField(event){
this.$emit('updateFields' , {...this.fields , first : event.target.value})
},
updateSecondField(value){
this.$emit('updateFields' , {...this.fields , second : value})
}
}
}
</script>
// child
<template>
<div>
<input :value="fields.first" #input="updateSecondField" />
</div>
</template>
<script>
export default {
props : {
fields : Object,
},
methods : {
updateFirstField(event){
this.$emit('updateSecondField' , event.target.value)
},
}
}
</script>
Yes, we can use .sync to make it easier or pass just field that we need to child. but this is basic example and if we have more fields and also we use all fields in all component this is the way we do this.
same thing using reference type will be like this :
// grand-parent
<template>
<div>
<parent :fields="fields" >
</div>
</template>
<script>
import parent from "./parent"
export default {
components : {parent},
data(){
return {
fields : {
first : 'first-value',
second : 'second-value',
}
}
}
}
</script>
// parent
<template>
<div>
<input v-model="fields.first" />
<child :fields="fields" >
</div>
</template>
<script>
import child from "./child"
export default {
components : {child},
props : {
fields : Object,
}
}
</script>
// child
<template>
<div>
<input v-model="fields.second" />
</div>
</template>
<script>
export default {
props : {
fields : Object,
}
}
</script>
as you see using reference type is much easier. even if there was more fields.
now my question :
should we use reference type for updating parent data or this is bad approach ?
even if we use a component always in the same parent again we should not use this method ?
what is the reason that we should not use reference type to update parent?
if we should not use reference type why vue pass same object to children and not clone them before passing ? (maybe for better performance ?)
The "always use $emit" rule isn't set in stone. There are pros and cons of either approach; you should do whatever makes your code easy to maintain and reason about.
For the situation you described, I think you have justified mutating the data directly.
When you have a single object with lots of properties and each property can be modified by a child component, then having the child component mutate each property itself is fine.
What would the alternative be? Emitting an event for each property update? Or emitting a single input event containing a copy of the object with a single property changed? That approach would result in lots of memory allocations (think of typing in a text field emitting a cloned object for each keypress). Having said that, though, some libraries are designed for this exact purpose and work pretty well (like Immutable.js).
For simple components that manage only small data like a textbox with a single string value, you should definitely use $emit. For more complex components with lots of data then sometimes it makes sense for the child component to share or own the data it is given. It becomes a part of the child component's contract that it will mutate the data in certain circumstances and in some particular way.
what is the reason that we should not use reference type to update parent?
The parent "owns" the data and it knows that nobody but itself will mutate it. No surprises.
The parent gets to decide whether or not to accept the mutation, and can even modify it on-the-fly.
You don't need a watcher to know when the data is changed.
The parent knows how the data is changed and what caused the change. Imagine there are multiple ways that the data can be mutated. The parent can easily know which mutation originated from a child component. If external code (i.e. inside a child component) can mutate the data at any time and for any reason, then it becomes much more difficult for the parent to know what caused the data to change (who changed it and why?).
if we should not use reference type why vue pass same object to children and not clone them before passing ? (maybe for better performance ?)
Well yes, for performance, but also many other reasons such as:
Cloning is non-trivial (Shallow? Deep? Should the prototype be copied too? Does it even make sense to clone the object? Is it a singleton?).
Cloning is expensive memory- and CPU-wise.
If it were cloned then doing what you describe here would be impossible. It would be silly to impose such a restrictive rule.
#Vue Detailed usage of $refs, $emit, $on:
$refs - parent component calls the methods of the child component. You can pass data.
$emit - child components call methods of the parent component and pass data.
$on - sibling components pass data to each other.

Vue Router and VueJS reactivity

I've got this code for my vue-router:
{
path: '/templates/:id/items/:itemId', component: Item,
name: 'item'
},
On the item object, I've got a computed property templateId:
templateId() {
return parseInt(this.$route.params.id, 10);
},
The issue I have, is, each time I add an anchor to the url (clicking a link, INSIDE the item component), even if the component doesn't change, this property is computed again.
It means that all computed properties depending of templateId will be computed again.
But the templateId value doesn't change at all.
Here is a really simple jsfiddle to explain the issue:
https://jsfiddle.net/1Lgfn9qh/1/
If I remove the watch property (never called), nothing is logged in the console anymore.
Can you explain me what's happening here?
Why the computed properties are recomputed, even if no values has been updated?
How can I avoid that?
What's causing this behaviour is the fact, that the route object is immutable in Vue. Any successful navigation will result in a completely new route object therefore triggering a re-computation of all computed and watched properties. See https://router.vuejs.org/api/#the-route-object for more details.
To solve this you can watch the route object and filter the relevant vs irrelevant changes for you there
watch: {
'$route' (to, from) {
if(to.path != from.path) { // <- you may need to change this according to your needs!
this.relevantRoute = to
}
}
}
And then reference the manually set variable in your computed and/or watched properties
templateId() {
return parseInt(this.relevantRoute.params.id, 10);
},

VueJS XHR inside reusable component

Asking for best practice or suggestion how to do it better:
I have 1 global reusable component <MainMenu> inside that component I'm doing XHR request to get menu items.
So if I place <MainMenu> in header and footer XHR will be sent 2 times.
I can also go with props to get menu items in main parent component and pass menu items to <MainMenu> like:
<MainMenu :items="items">
Bet that means I cant quickly reuse it in another project, I will need pass props to it.
And another way is to use state, thats basically same as props.
What will be best option for such use case?
If you don't want to instantiate a new component, but have your main menu in many places you can use ref="menu" which will allow you to access it's innerHTML or outerHTML. I've created an example here to which you can refer.
<div id="app">
<main-menu ref="menu" />
<div v-html="menuHTML"></div>
</div>
refs aren't reactive so if you used v-html="$refs.menu.$el.outerHTML" it wouldn't work since refs are still undefined when the component is created. In order to display it properly you would have to create a property that keeps main menu's HTML and set it in mounted hook:
data() {
return {
menuHTML: ''
}
},
mounted() {
this.menuHTML = this.$refs.menu.$el.outerHTML;
}
This lets you display the menu multiple times without creating new components but it still doesn't change the fact that it's not reactive.
In the example, menu elements are kept in items array. If the objects in items array were to be changed, those changes would be reflected in the main component, but it's clones would remain unchanged. In the example I add class "red" to items after two seconds pass.
To make it work so that changes are reflected in cloned elements you need to add a watcher that observes the changes in items array and updates menuHTML when any change is noticed:
mounted() {
this.menuHTML = this.$refs.menu.$el.outerHTML;
this.$watch(
() => {
return this.$refs.menu.items
},
(val) => {
this.menuHTML = this.$refs.menu.$el.outerHTML;
}, {
deep: true
}
)
}
You can also watch for changes in any data property with:
this.$refs.menu._data
With this you don't need to pass props to your main menu component nor implement any changes to it, but this solution still requires some additional logic to be implemented in it's parent component.

Angular with Redux triggering change detection

I'm new to Redux so I'm sure this is something I'm missing, but I have a component that is subscribing to a Redux store, which is an observable of an array of objects. The problem is that even though a single property is changing on one of the objects, I'm returning a copy of the object, so Angular thinks everything changed and triggers a redraw. This would be fine except that my component has CSS transitions that don't get fired correctly because they should only react to the property change and now be removed and added again. Here's a simple example (pseudo code)
const state = {
items: [
{
open: false,
},
{
open: true,
}
]
}
Then a component is something like:
<div *ngFor='for let item of items'>
<div #openClose='item.open'>Some stuff</div>
</div>
So when my store sets the open property from true to false, instead of closing my component through a triggered animation, it simply redraws each div.
This must be a common problem, so is there an accepted way to set this up so the component is only created once?

Categories

Resources