Vue passing data back multiple levels - vue.js

i'm having issues passing a value through multiple components, I think I have got it wrong, can someone help me please:
Vue.component('layout', {
props: [ 'current' ],
template: `<deformer :current="current" #current="$emit('update:current', $event)" />`
});
Vue.component('deformer', {
props: [ 'current' ],
template: `<button #click="$emit('update:current', current + 1)">Increase {{ current }}</button>`
});
new Vue({
data: {
current: 1
},
watch: {
current: function(value) {
// Do something with value
console.log(value);
}
}
}).$mount('#app')
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
{{ current }}
<layout :current="current" />
</div>
So that button should increase the number within the main app, but if this is changed from somewhere else this change should also reflect on the button.

current should be a prop, not data. The emit message will notify the parent component to change the value, and it will trickle back down through the prop.
This design ensures only one component owns the data.

Related

How could I change an image in a child page when pressing a button in its parent page?

I have a DefaultLayout component with a dark mode toggle button which is its own component. One if its children (DefaultLayout's) is About.vue where I want a specific image to change its src depending on a localStorage value that can be set to either 'dark' or 'light'.
I've managed to read the localStorage value but the image does not change unless I refresh the page.
I'm new to Vue so I'm lost on how I can create a method to do this in DefaultLayout and change a variable in its child. I've tried to use an emit with no luck.
Could anyone point me in the right direction?
Yes, the local storage is for keeping data not propagate events.
The simplest way for you is to make a prop in child component and pass the value by this prop. But if you want to implement it as global variable the suggested way is by Pinia.
Below is a simple example
Vue.component('About', {
name: 'About',
template: `<div>
<div v-if="mode==='dark'">Dark</div>
<div v-else>Light</div>
</div>
`,
data() {
return {
mode: 'light',
};
},
mounted() {
this.setMode('white'); // In realtime use `this.getMode()` instead of 'white'
},
methods: {
setMode(val) {
this.mode = val;
},
getMode() {
return JSON.parse(localStorage.getItem('mode'));
}
}
});
var app = new Vue({
el: "#app",
template: `<div>
<input type="checkbox" v-model="toggler" #input="setVal" />
<About ref="about" />
</div>`,
data() {
return {
toggler: false,
};
},
methods: {
setVal() {
const mode = this.toggler === false ? 'dark' : 'light';
// localStorage.setItem('mode', mode); // In realtime uncomment this line
this.$refs.about.setMode(mode);
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
</div>

Vue: Updating a data property triggers re-evaluation of entire data?

I've been working on Vue project for almost a year, and I've just observed unexpected behavior below for the first time today...
Here is a link to code sandbox:
https://codesandbox.io/s/2bnem?file=/src/App.vue
And a code snippet from above link:
<template>
<div>
<div>{{a}}</div>
<div>{{translator(b)}}</div>
<input v-model="a" />
</div>
</template>
<script>
export default {
data() {
return {
a: 'a',
b: 'b',
}
},
computed: {
translator: function() {
return function(value) {
console.log(`translated...: ${value}`)
return value
}
}
}
}
</script>
Now every time I hit the key on input, the translator is triggered.
Is this a correct behavior?
If so, what is the cause of this problem or a background reasoning of this behavior?
Note that my vue version is 2.6.14(latest).
Your original issue is that you were attempting to use a method to render parts of your template. Methods used like this will execute for every update cycle, regardless of what changed.
The better solution is to use a computed property. Here's a somewhat dynamic example that wraps each of your data properties with a computed translator_x property
<template>
<div>
<div>{{ a }}</div>
<div>{{ translator_b }}</div>
<input v-model="a" />
</div>
</template>
<script>
const defaultData = {
a: "a",
b: "b"
}
export default {
data: () => ({ ...defaultData }),
computed: Object.fromEntries(Object.keys(defaultData).map(k => [
`translator_${k}`,
vm => {
console.log("translated...", k)
return vm[k]
}
]))
};
</script>
Each translator_x property will only be evaluated if the underlying data property is changed.

Vue stored valued through props not being reactive

So I pass value using [props] and stored it in child component's data. However, when passing [props] value changes from parent, it's not updating in child component's data. Is there a fix for this..?
Here is the link to w3 test (I tried to clarify the problem as much as possible here)
<div id='app'>
<div id='parent'>
<button #click='current_value()'>Click to see parent value</button>
<br><br>
<button #click='change_value($event)'>{{ txt }}</button>
<br><br>
<child-comp :test-prop='passing_data'></child-comp>
</div>
<br><br>
<center><code>As you can see, this methods is <b>NOT</b> reactive!</code></center>
</div>
<script>
new Vue({
el: "#parent",
data: {
passing_data: 'Value',
txt: 'Click to change value'
},
methods: {
current_value(){
alert(this.passing_data);
},
change_value(e){
this.passing_data = 'New Vaule!!';
this.txt = 'Now click above button again to see new value';
e.target.style.backgroundColor = 'red';
e.target.style.color = 'white';
}
},
components: {
"child-comp": {
template: `
<button #click='test()'>Click here to see child (stored) value</button>
`,
props: ['test-prop'],
data(){
return {
stored_data: this.testProp
}
},
methods: {
test(){
alert(this.stored_data);
}
},
watch: {
stored_data(){
this.stored_data = this.testProp;
}
}
}
}
});
Props have one way data flow, that's why it doesn't react when you update it from the parent component. Define a clone of your prop at data to make it reactive, and then you can change the value within the child component.
Short answer: you don't need stored_data. Use alert(this.testProp) directly.
Long answer: when child component is created, stored_data get it's value from this.testProp. But data is local, it won't change automatically. That's why you need to watch testProp and set it again. But is not working because of a simple mistake, your watch should be:
watch: {
testProp(){ // here was the mistake
this.stored_data = this.testProp;
}
}

Prop passed to child component is undefined in created method

I am using Vue.js 2.
I have a problem with passing value to the child component as a prop. I am trying to pass card to card-component.
In card-component I can access the prop in the Card goes here {{card}} section.
However when I try to access it in created or mounted methods it's undefined.
Parent:
<template>
<div class="container">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<card-component :card="place.card"></card-component>
</div>
</div>
</div>
</template>
<script>
import CostComponent from './CostComponent';
import CardComponent from './CardComponent';
export default {
components: {
CostComponent, CardComponent
},
props: ['id'],
data() {
return {
place: []
}
},
created() {
axios.get('/api/places/' + this.id)
.then(response => this.place = response.data);
}
}
</script>
Child:
<template>
<div class="container">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<ul class="list-unstyled">
Card goes here {{card}}
</ul>
</div>
</div>
</div>
</template>
<script>
import CardItemComponent from './CardItemComponent';
export default {
components: {
CardItemComponent
},
props: ['card'],
created() {
console.log(this.card); // undefined
},
mounted() {
console.log(this.card); // undefined
},
}
</script>
I did a lot of googling but none of the solutions I found have fixed my issue.
This is purely a timing issue. Here's what happens...
Your parent component is created. At this time it has an empty array assigned to place (this is also a problem but I'll get to that later). An async request is started
Your parent component creates a CardComponent instance via its template
<card-component :card="place.card"></card-component>
at this stage, place is still an empty array, therefore place.card is undefined
3. The CardComponent created hook runs, logging undefined
4. The CardComponent is mounted and its mounted hook runs (same logging result as created)
5. Your parent component is mounted
6. At some point after this, the async request resolves and changes place from an empty array to an object, presumably with a card property.
7. The new card property is passed down into your CardComponent and it reactively updates the displayed {{ card }} value in its template.
If you want to catch when the card prop data changes, you can use the beforeUpdate hook
beforeUpdate () {
console.log(this.card)
}
Demo
Vue.component('CardComponent', {
template: '<pre>card = {{ card }}</pre>',
props: ['card'],
created () {
console.log('created:', this.card)
},
mounted () {
console.log('mounted:', this.card)
},
beforeUpdate () {
console.log('beforeUpdate:', this.card)
}
})
new Vue({
el: '#app',
data: {
place: {}
},
created () {
setTimeout(() => {
this.place = { card: 'Ace of Spades' }
}, 2000)
}
})
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<div id="app">
<card-component :card="place.card" />
</div>
See https://v2.vuejs.org/v2/guide/instance.html#Lifecycle-Diagram
If place is meant to be an object, you should not be initialising it as an array. Also, if your CardComponent relies on data being present, you may want to conditionally render it.
For example
data () {
return { place: null }
}
and
<card-component v-if="place" :card="place.card"></card-component>
then CardComponent will only be created and mounted after place has data.
Make sure you have props: true in the router file. It is a simple solution but many of us forget this.
{
path: '/path-to',
name: 'Name To',
component: Component,
props: true
}

Vue component communication

I'm looking for a concise example of two Vue components. The first component should contain a text input or textarea. The second component displays a character counter. I would like the first component to emit change events, and the second component should listen for those events and display its computed values (character count). I'm new to Vue and trying to wrap my head around the best way to implement this functionality. It seems rather straightforward in pure JavaScript but doing it the Vue way is not as clear to me. Thanks.
Here is how I'd do it in JavaScript:
Here's the textarea:
<textarea id="pagetext" name="pagetext"
onChange="characterCount();"
onKeyup="characterCount();">Type here</textarea>
Here's the JavaScript:
function characterCount()
{
var characters=document.myForm.pagetext.value.length;
document.getElementById('charcounter').innerHTML=characters+"";
}
My concern with Vue is passing the entire value around... for performance reasons this seems less than ideal. I may want my text editing Vue component to self-contain the value and emit the stats, ie the value for character count which would then be observed by a text stats component.
You can create a "Model" for value of textarea and provide this model to second component by using following way https://v2.vuejs.org/v2/guide/components-props.html
I've written up a snippet with four examples: your original, a simple Vue app (no components) that does the same thing, and two apps with two components that are coordinated by the parent.
The simple Vue app is actually more concise than the pure JavaScript app, and I think it shows off the reason for having a framework: your view doesn't act as a store for your program data, from which you have to pull it out.
In the final example, the parent still owns pageText, but passes it down to the my-textarea component. I like to hide the emitting behind the abstraction of a settable computed, so that the element can use v-model. Any changes are emitted up to the parent, which changes pageText, which propagates back down to the component.
I think your performance concerns fall into the realm of premature optimization, but it is possible not to use the text content as data at all, and only be concerned with the length. The fourth example does that. emitLength could have used event.target.value.length, but I wanted to use it in the mounted to initialize the length properly, so I used a ref.
function characterCount() {
var characters = document.myForm.pagetext.value.length;
document.getElementById('charcounter').innerHTML = characters + "";
}
new Vue({
el: '#app',
data: {
pageText: 'Type here'
}
});
new Vue({
el: '#app2',
data: {
pageText: 'Type here'
},
components: {
myTextarea: {
props: ['value'],
template: '<textarea name="pagetext" v-model="proxyValue"></textarea>',
computed: {
proxyValue: {
get() {
return this.value;
},
set(newValue) {
this.$emit('input', newValue);
}
}
}
},
textLength: {
props: ['value'],
template: '<div>{{value}}</div>'
}
}
});
new Vue({
el: '#app3',
data: {
textLength: null
},
components: {
myTextarea: {
template: '<textarea ref="ta" name="pagetext" #input="emitLength">Type here</textarea>',
methods: {
emitLength() {
this.$emit('change', this.$refs.ta.value.length);
}
},
mounted() {
this.emitLength();
}
},
textLength: {
props: ['value'],
template: '<div>{{value}}</div>'
}
}
});
<script src="https://unpkg.com/vue#latest/dist/vue.js"></script>
<form name="myForm">
<textarea id="pagetext" name="pagetext" onChange="characterCount();" onKeyup="characterCount();">Type here</textarea>
</form>
<div id="charcounter"></div>
<div id="app">
<h1>Vue (simple)</h1>
<form>
<textarea name="pagetext" v-model="pageText"></textarea>
</form>
<div>{{pageText.length}}</div>
</div>
<div id="app2">
<h1>Vue (with components)</h1>
<form>
<my-textarea v-model="pageText"></my-textarea>
</form>
<text-length :value="pageText.length"></text-length>
</div>
<div id="app3">
<h1>Vue emitting stats</h1>
<form>
<my-textarea #change="(v) => textLength=v"></my-textarea>
</form>
<text-length :value="textLength"></text-length>
</div>