Data property no longer reactive after doing simple re-assignment? - vue.js

I have a data property called current_room where initially it has an empty object {}.
I have a component that will receive current_room as a "prop".
In the parent component, in the mounted() hook, re-assignment takes place: this.current_room = new_room
In the child component, the current_room prop appears to be... an empty object. In the parent component, it's not an empty object, it has the data I expect to see.
What would be the proper way to make this work? It seems as though simple re-assignment doesn't work in this case, that once I define a property on the data object... and that property is an object... I have to add/remove properties to the object, rather than just wholesale re-assigning a new object to that data property.

I guess it's just a simple mistake somewhere in your code. Because following to your question - it should work, furthermore I created a simple example where I defined components and functionality as you've described - and it works. I will provide async example to make you sure for 100 percents.
Here is the working example:
App.vue
<template>
<div id="app">
<CurrentRoom :room="current_room" />
</div>
</template>
<script>
import CurrentRoom from './components/CurrentRoom.vue'
export default {
name: "App",
components: {
CurrentRoom
},
data () {
return {
current_room: {}
}
},
mounted () {
setTimeout(() => {
this.current_room = {
door: true,
windowsCount: 2,
wallColor: 'white',
members: [
{
name: 'Heisenberg',
age: 46
},
{
name: 'Pinkman',
age: 26
}
]
}
}, 2000)
}
};
</script>
CurrentRoom.vue
<template>
<div>
Current room is: <br>
<pre>{{ room }}</pre>
</div>
</template>
<script>
export default {
name: 'CurrentRoom',
props: {
room: {
type: Object,
default: () => {}
}
}
}
</script>
Codesandbox demo:
https://codesandbox.io/s/epic-banach-clyz4
And for the end, following to your question:
... What would be the proper way to make this work? ...
The answer is - 'Please, compare your code with provided example'

Related

Vue child component not rerending after parent component's data value updates

I am trying to have a child component update its props that were passed from the parents at the start of the rendering. Since the value is coming from a fetch call, it takes a bit of time to get the value, so I understand that the child component will receive a 'null' variable. But once the fetch call is completed, the value is updated but the child component still has the original null value.
During my search for a solution, I found that another way was to use Vuex Stores, so I implemented it with the count variable and had a button to call a commit and later dispatch with an action function to the store to increment it's value but when the increment happens, it doesn't show the new value on the screen even though with console logs I confirmed it did change the value when the function was called.
I guess I don't fully understand how to update the value of a variable without reassigning it within it's own component or having to call a separate function manually right after I change the value of a data variable.
App.vue
<template>
<div id="app">
<div id="banner">
<div>Title</div>
</div>
<p>count: {{count}}</p> // a small test i was doing to figure out how to update data values
<button #click="update">Click </button>
<div id="content" class="container">
<CustomDropdown title="Title Test" :valueProps="values" /> // passing the data into child component
</div>
</div>
</template>
<script>
import CustomDropdown from './components/CustomDropdown.vue'
export default {
name: 'App',
components: {
CustomDropdown,
},
data() {
return {
values: null
count: this.$store.state.count
}
},
methods: {
update() {
this.$store.dispatch('increment')
}
},
async created() {
const response = await fetch("http://localhost:3000/getIds", {
method: 'GET',
headers: {
'Accept': 'application/json, text/plain, */*',
'Content-Type': 'application/json'
}
});
const data = await response.json();
this.values = data // This is when I expect the child component to rerender and show the new data. data is an array of objects
console.log("data", data, this.values) // the console log shows both variables have data
}
}
</script>
CustomDropDown.vue
<template>
<div id="dropdown-container" class="">
<b-dropdown class="outline danger" variant="outline-dark" :text="title" :disabled="disabled">
<b-dropdown-item
v-for="value in values"
:key="value.DIV_ID"
href="#">
{{value.name}}
</b-dropdown-item>
</b-dropdown>
</div>
</template>
<script>
export default {
name: 'CustomDropdown',
components: {},
props: {
title: String,
valuesProp: Array,
disabled: Boolean
},
data() {
return {
values: this.valuesProp
}
},
methods: {
},
created() {
console.log("dropdown created")
console.log(this.valuesProp) //Always undefined
}
}
</script>
store.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state() {
return {
count: 0,
divisionIds: []
}
},
mutations: {
increment (state) {
console.log("count", state.count)
state.count++
}
},
actions: {
increment (state) {
console.log("count action", state.count)
state.commit('increment')
}
}
})
data in your child component CustomDropdown.vue is not reactive: therefore the value of this.values is not updated when the prop changes. If you want to alias a prop, use computed instead:
export default {
name: 'CustomDropdown',
components: {},
props: {
title: String,
valuesProp: Array,
disabled: Boolean
},
computed: {
values() {
return this.valuesProp;
}
},
created() {
console.log("dropdown created");
}
}
If you want to console log the most updated values of this.valuesProp, you will need to watch it: the same if you want for this.values.
One thing you can do is to use a v-if in your child component to only render it after you get your result from you api.
It would be something like:
<CustomDropdown title="Title Test" :valueProps="values" v-if="values"/>
This way you would make sure that your child component gets rendered only when values are available.
It would only be a bad solution if this api call took so long and you needed to display the child component data to the user before that.
Hey you can simply watch it your child component
watch: { valuesProp: function(newVal, oldVal) { // watch it if(newVal.length > 0) do something }
it will watch for the value changes and when you get your desired value you can perform whatever hope it will help you you dont need store or conditional binding for it.

Why doesn't a VueJS prop reactively update when it's an argument to a method?

I have this component with two text-fields with two expressions that should evaluate to the same thing.
<template>
<div>
{{ value.flange?value.circle.diameter:413 }}
{{ get(value, 'circle.diameter') }}
</div>
</template>
<script>
import _ from "lodash/fp"
export default {
name: "MyComponent",
props: {
value: {
type: Object,
default: () => ({}),
},
},
methods: {
get (obj, path) {
return _.get(obj, path)
},
},
}
</script>
This is my attempt to guard my page from crashing on property look ups for the brief moment when this.value is undefined. I prefer to use lodash/fp's _.get function since it's a lot more readable and much less tedious for deeper nested objects.
However, when I load this page, I see brief momentary blip of 413 and nothing, which is expected. Since 413 is my placeholder value for the first expression and the second should be undefined. Then when value gets an update, the 413 quickly changes to value.circle.diameter, as it should, but the one wrapped in the get never updates.
Why is a method call blocking propagation and how should I in general guard against my deeply nested objects from crashing on property access when value is undefined?
Note: I don't want to block render of the page when the object is null with, for example, a v-if="value" on my div. This is because I want to be able to edit the value if it's {} and construct that structure as the user edits it.
This is because properties are reactive and function calls are not. However, you can achieve this with computed properties:
<template>
<div>
{{ value.flange?value.circle.diameter:413 }}
{{ diameter }}
</div>
</template>
<script>
import _ from "lodash/fp"
export default {
name: "MyComponent",
props: {
value: {
type: Object,
default: () => ({}),
},
},
computed: {
diameter: function () {
return _.get(this.value, 'circle.diameter');
}
}
}
</script>

How to wrap the slot in a scoped slot at runtime

I have an very unusual scenario.
<WrapperComponent>
<InnerComponent propA="Static"></InnerComponent>
</WrapperComponent>
The WrapperComponent should manage all instances of the InnerComponent. However I don't know what will be compiled into the wrapper component.
Usually I would do something like this:
<WrapperComponent>
<template scoped-slot="{data}">
<InnerComponent v-for="(propB) in data" prop-b="Static" :prop-b="propB"></InnerComponent>
</template>
</WrapperComponent>
But I cannot do this for reasons!
I need to be able to create multiple instances of the slot content at runtime. I have created a code sandbox with what I got so far.
https://codesandbox.io/s/stupefied-framework-f3z5g?file=/src/App.vue:697-774
<template>
<div id="app">
<WrapperComponent>
<InnerComponent propA="Static"></InnerComponent>
</WrapperComponent>
</div>
</template>
<script>
import Vue from "vue";
const WrapperComponent = Vue.extend({
name: "WrapperComponent",
data() {
return {
valueMap: ["Child 1", "Child 2"]
}
},
render(h) {
return h("div", {}, [
this.valueMap.map(val => {
console.log(val);
for (const slot of this.$slots.default) {
// this is a read only slot. I can not change this.
// However, I want multiple instances of the slot
// => Inject a scoped-slot
slot.componentOptions.propsData.propB = val;
}
return h("div", {}, [this.$slots.default]);
})
])
}
});
const InnerComponent = Vue.extend({
name: "InnerComponent",
props: {
// This is a static configuration value.
propA: String,
// This is a runtime value. The parent component manages this component
propB: String
},
template: "<div>A: {{propA}} B: {{propB}}</div>"
});
export default {
name: "App",
components: {
WrapperComponent,
InnerComponent
}
};
</script>
This works perfectly fine with static information only, but I also have some data that differs per slot instance.
Because VNodes are readonly I cannot modify this.$slot.default.componentOptions.propsData. If I ignore the warning the result will just be the content that was passed to last instance of the slot.
<WrapperComponent>
<WrapperSubComponent v-for="(propB) in data" key="probB" :prop-b="prop-b">
<InnerComponent propA="Static"></InnerComponent>
</WrapperSubComponent>
</WrapperComponent>
Works after wrapping the component in another component and only executing the logic once.

Properly alert prop value in parent component?

I am new to Vue and have been very confused on how to approach my design. I want my component FileCreator to take optionally take the prop fileId. If it's not given a new resource will be created in the backend and the fileId will be given back. So FileCreator acts as both an editor for a new file and a creator for a new file.
App.vue
<template>
<div id="app">
<FileCreator/>
</div>
</template>
<script>
import FileCreator from './components/FileCreator.vue'
export default {
name: 'app',
components: {
FileCreator
}
}
</script>
FileCreator.vue
<template>
<div>
<FileUploader :uploadUrl="uploadUrl"/>
</div>
</template>
<script>
import FileUploader from './FileUploader.vue'
export default {
name: 'FileCreator',
components: {
FileUploader
},
props: {
fileId: Number,
},
data() {
return {
uploadUrl: null
}
},
created(){
if (!this.fileId) {
this.fileId = 5 // GETTING WARNING HERE
}
this.uploadUrl = 'http://localhost:8080/files/' + this.fileId
}
}
</script>
FileUploader.vue
<template>
<div>
<p>URL: {{ uploadUrl }}</p>
</div>
</template>
<script>
export default {
name: 'FileUploader',
props: {
uploadUrl: {type: String, required: true}
},
mounted(){
alert('Upload URL: ' + this.uploadUrl)
}
}
</script>
All this works fine but I get the warning below
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:
"fileId"
What is the proper way to do this? I guess in my situation I want the prop to be given at initialization but later be changed if needed.
OK, so short answer is that the easiest is to have the prop and data name different and pass the prop to the data like below.
export default {
name: 'FileCreator',
components: {
FileUploader
},
props: {
fileId: Number,
},
data() {
return {
fileId_: this.fileId, // HERE WE COPY prop -> data
uploadUrl: null,
}
},
created(){
if (!this.fileId_){
this.fileId_ = 45
}
this.uploadUrl = 'http://localhost:8080/files/' + this.fileId_
}
}
Unfortunately we can't use underscore as prefix for a variable name so we use it as suffix.

VueJS Passing computed data as a prop returns undefined

I've tried and tried, but i can't figure it out the problem. From what I could read elsewhere, the variable passed to the child component gets sent as undefined before the data is available in the parent.
Please see here for reference:
the code in codesandbox
<template>
<div id="app">
<child :parentData="data.message"/>
</div>
</template>
<script>
import Child from "./components/Child";
export default {
name: "App",
components: {
Child
},
computed: {
quote() { return 'Better late than never' }
},
data() {
return {
data: { message: this.quote } ,
thisWorks: { message: "You can see this message if you replace what is passed to the child" }
};
}
};
</script>
Then in the child:
<template>
<div>
<h1>I am the Child Component</h1>
<h2> {{ parentData }}</h2>
</div>
</template>
<script>
export default {
name: "Child",
props: {
parentData: { type: String, default: "I don't have parent data" },
},
};
</script>
The answer is, you cannot access the value of this.quote because at the moment the data objectis creating, the computed object actually does not exist.
This is an alternative, we will use the created() lifecycle hook to update the value of data object:
created(){
this.data = {
message: this.quote
}
},
You don't need to change any things, just adding those line of codes is enough.
I've already tested those codes in your CodeSandbox project and it works like a charm.
Hopefully it helps!