How to create a method with props passed to child component (Vue3) - vue.js

I am struggling on the last part of my starter project with outputting the results onto its own component.
I have created a method in the parent component to push the results to an array, and I am then passing that array as props to the child component.
If I just display the array in my child component it works fine, however what I am trying to do is then create a method in my child component based on the results passed via the props.
When I try to do this I am not getting anything outputted, is this something which you can do in Vue?
Parent:
<template>
<button #click="decreaseCount">Decrease</button>
<button #click="increaseCount">Increase</button>
<div class="counterDiv">Counter: {{ count }}</div>
<button #click="calculateResults">Submit</button>
<results v-if="resultsVisible" :results="results"></results>
</template>
<script>
import Results from "./components/Results.vue";
export default {
name: "App",
components: {
Results,
},
data() {
return {
count: 0,
results: [],
resultsVisible: false,
};
},
methods: {
increaseCount() {
this.count += 1;
},
decreaseCount() {
this.count -= 1;
},
calculateResults() {
this.results.push(this.count);
this.resultsVisible = true;
},
},
};
</script>
Child:
<template>
<div class="finalResults">Results: {{ this.resultsText }}</div>
</template>
<script>
export default {
props: ["results"],
data() {
return {
resultsText: "",
};
},
methods: {
returnText() {
if (results < 10) {
this.resultsText = "Below 10";
}
},
},
};
</script>
I have created a very basic example of my problem below

You should emit a custom event from child component which has as handler the parent method:
child :
<template>
<div class="finalResults">Results: {{ this.resultsText }}</div>
</template>
<script>
export default {
props: ["results"],
data() {
return {
resultsText: "",
};
},
methods: {
returnText() {
if (this.results < 10) {
this.resultsText = "Below 10";
this.$emit('push-item', this.resultsText )
}
},
},
};
</script>
in parent component :
<results v-if="resultsVisible" :results="results" #push-item="pushResult"></results>
...
methods:{
pushResult(resulttext){...}
....
}

Related

VueJS child component button changes class of parent

I have a Navigation component with a button to toggle dark mode/light mode. When I click this button, I would like to change a class in the App.vue. How to pass the data from the button to the App.vue?
I believe I have to emit something from the Top Nav and v-bind the class in App.vue, but I don't quite understand how to get it change the class.
When I run this, the button does not change the class on the div.
App.vue
<template>
<div id="app" :class="[isActive ? 'darkmode' : '']>
<header>
<top-nav #change-mode="enableDarkMode"></top-nav>
</header>
...
</div>
</template>
<script>
export default {
name: "App",
components: {
TopNav,
},
props: ['isActive'],
data() {},
methods: {
enableDarkMode(isActive) {
console.log(isActive)
},
},
};
</script>
Top Nav Component
<template>
...
<div>
<button
:class="[isActive ? 'dark' : 'light']"
#click="toggle">
{{ isActive ? "DarkMode" : "LightMode" }}
</button>
</div>
</template>
<script>
export default {
name: "TopNav",
data() {
return {
isActive: false,
};
},
components: {},
methods: {
toggle() {
this.isActive = !this.isActive;
this.$emit('change-mode', this.isActive )
console.log('emit child')
},
},
};
</script>
From the snippet you provided it seems like the App.vue has isActive props instead of data for the method enableDarkMode to manage. Vue's props is not to be updated by the component they belong to because of how the data flow works in Vue props. With the App.vue being the parent of Top Nav component, you probably want it to be like this:
App.vue
<template>
<div id="app" :class="[isActive ? 'darkmode' : '']>
<header>
<top-nav #change-mode="enableDarkMode"></top-nav>
</header>
...
</div>
</template>
<script>
export default {
name: "App",
components: {
TopNav,
},
// this is probably not needed because App.vue is the parent component
// props: ['isActive'],
data() {
return {
isActive: false,
};
},
methods: {
enableDarkMode(isActive) {
// manages the data
this.isActive = isActive;
},
},
};
</script>
Top Nav component
<template>
...
<div>
<button
:class="[isActive ? 'dark' : 'light']"
#click="toggle">
{{ isActive ? "DarkMode" : "LightMode" }}
</button>
</div>
</template>
<script>
export default {
name: "TopNav",
data() {
return {
isActive: false,
};
},
components: {},
methods: {
toggle() {
this.isActive = !this.isActive;
this.$emit('change-mode', this.isActive )
console.log('emit child')
},
},
};
</script>

vue js sync modifier doesn't update the input value

I have a beginner question about sync modifier in vuejs. In my example, i want to change the value of inputs depending on focus event. The value is an Object inputsData and i'm getting it from the app. In parent i'm passing it to the child where it is rendering. I set an timer because i want to emit an server request. As you can see in the handleFocusFromChild Methode it change me the dataToBeChanged with newData (se also log after 4 seconds). As i understood from vue guid it should to update also the input value but it doens't, and i don't understand why, because dataToBeChanged have now the new values from newData. Can someone explain me why and how should i do to make it work?
Here i'm using the the Parent:
import Parent from "./parent.js";
Vue.component("app", {
components: {
Parent
},
template: `
<div>
<parent :inputsData="{
'firstElement':{'firstInputValue':'Hi there'},
'secondElement':{'secondInputValue':'Bye there'}
}"></parent>
</div>
`
});
Here is the Parent:
import Child from "./child.js";
export default {
name: "parent",
components: {
Child
},
props: {
inputsData: Object
},
template: `
<div>
<child #focusEvent="handleFocusFromChild"
:value.sync="inputsData.firstElement.firstInputValue"></child>
<child #focusEvent="handleFocusFromChild"
:value.sync="inputsData.secondElement.secondInputValue"></child>
</div>
`,
computed: {
dataToBeChanged: {
get: function() {
return this.inputsData;
},
set: function(newValue) {
this.$emit("update:inputsData", newValue);
}
}
},
methods: {
handleFocusFromChild: function() {
var newData = {
firstElement: { firstInputValue: "Hi there is changed" },
secondElement: { secondInputValue: "Bye there is changed" }
};
setTimeout(function() {
this.dataToBeChanged = newData;
}, 3000);
setTimeout(function() {
console.log(this.dataToBeChanged);
}, 4000);
}
}
};
Here is the child:
export default {
template: `
<div class="form-group">
<div class="input-group">
<input #focus="$emit('focusEvent', $event)"
v-model="value">
</div>
</div>
`,
props: {
value: String
}
};
you child component should emit "this.$emit('update:value', newValue)" as event
take a look over the docs: https://br.vuejs.org/v2/guide/components-custom-events.html
Also a way to do it is like this:
export default {
template: `
<div class="form-group">
<div class="input-group">
<input #focus="$emit('focusEvent', $event)"
v-model="valueProp">
</div>
</div>
`,
props: {
value: String
},
computed: {
valueProp:{
get(){
return this.value
},
set(val){
return this.$emit("update:value", val);
}
},
}
methods: {
handleFocus() {
this.$emit("focusEvent");
}
}
};

why vue v-for doesn't rerender child component when data change?

parent
export default {
name: "App",
components: {ChildComponent},
data: function() {
return {
itemList: []
};
},
methods: {
fetchData() {
callApi().then(res => { this.itemList= res; })
}
}
};
<template>
<ol>
<template v-for="(item) in itemList">
<li :key="item.id">
<child-component :item="item"></card>
</li>
</template>
</ol>
</template>
child
export default {
name: "ChildComponent",
props: {
item: { type: Object }
},
data: function() {
const {
name,
address,
.......
} = this.item;
return {
name,
address,
......
};
},
};
</script>
child get the item props which is an object.
I'm confused why the itemList point to another array, but the child doesn't update?
is it because key doesnt change? (but other data changed..)
thanks
It is because of this part of your code:
const {
name,
address,
} = this.item;
return {
name,
address,
};
What it does it copies name and address from item and return them.
It happens only once in your code while component is created.
If after that your prop item change, your data doesn't copy it and still returns the first values.
Solution:
If you don't change name or address in a child component, just use a prop
this.item.name in a script or {{ item.name }} in a template
It is already reactive so your component will update when prop changes.
Not sure what you do in your child component but this is enough to do and should react to your parent component changes. Basically the idea is to use the prop. I made some tweaks also.
export default {
name: "App",
components: {ChildComponent},
data: function() {
return {
itemList: []
};
},
methods: {
fetchData() {
callApi().then(res => { this.itemList= res; })
}
}
};
<template>
<ol>
<li v-for="(item) of itemList" :key="item.id">
<card :item="item"></card>
</li>
</ol>
</template>
export default {
name: "ChildComponent",
props: {
item: {
type: Object,
required: true
}
}
};
</script>
<template>
<div>
{{ item.name }} {{ item.address }}
</di>
</template>
It is because the data function is called only once on the creation of the component.
The recommended way to get data that depend on other data is to use computed.
export default {
name: "ChildComponent",
props: {
item: { type: Object }
},
computed: {
name() {
return this.item.name;
},
address() {
return this.item.address;
}
}
};
https://v2.vuejs.org/v2/guide/computed.html#Basic-Example

How to pass and change index of array in vue?

I have the gist of how to do this, but I'm a beginner in vue, and I'm struggling with how to put it together. I need Control.vue to update the index in Exhibitor.vue. I know I'll have an $emit event happening in Control when I click on the button to pass the index data to the parent, and I'd have to use props to pass data from Exhibitor to its children, but how? I can't understand how to pass the index of an array with my code.
Exhibitor.vue
<template>
<div id="exhibitor">
<section class="exhibitor_info">
<h1 class="exhibitor_name">{{ exhibitors[index].firstName }} {{ exhibitors[index].lastName }}</h1>
<h2>Tag Number: {{ exhibitors[index].tagNum }} <em>{{ exhibitors[index].species }}</em></h2>
</section>
<div class="frame"><img :src="getImgUrl(exhibitors[index].picture)" alt="Exhibitor-Picture" class="image"></div>
</div>
</template>
<script>
export default {
name: 'Exhibitor',
data() {
return {
exhibitors: [],
index: 0
}
},
created: function() {
this.fetchExhibitors();
},
methods: {
fetchExhibitors() {
let uri = 'http://localhost:8081/exhibitor'
this.axios.get(uri).then(response => {
this.exhibitors = response.data
})
},
getImgUrl: function(pic) {
return require('../assets/' + pic)
}
}
}
</script>
Display.vue
<template>
<div id="display">
<exhibitor></exhibitor>
<buyer></buyer>
</div>
</template>
<script>
import Exhibitor from './Exhibitor.vue';
import Buyer from './Buyer.vue';
export default {
components: {
'exhibitor': Exhibitor,
'buyer': Buyer
}
}
</script>
Control.vue
<template>
<div id="control">
<display></display>
<button v-on:click="incrementLeft">Left</button>
<button v-on:click="incrementRight">Right</button>
</div>
</template>
<script>
import Exhibitor from './Exhibitor.vue';
import Display from './Display.vue';
export default{
props: ['exhibitors', 'buyers', 'index'],
data() {
return {
index: 0
}
},
methods: {
incrementRight: function() {
// Note that '%' operator in JS is remainder and NOT modulo
this.index = ++this.index % this.exhibitors.length
},
incrementLeft: function() {
// Note that '%' operator in JS is remainder and NOT modulo
if (this.index === 0) {
this.index = this.exhibitors.length - 1
} else {
this.index = --this.index % this.exhibitors.length
}
}
},
components: {
'display': Display
}
}
</script>
So you can get what you want to happen and there are two ways of making it happen that I can think of. First I will just clarify the terms relating to this because you seem to have them the wrong way around. Let's look at you tier structure which is like this:
Control.vue
contains: Display.vue
contains: Exhibitors.vue & Buyers.vue.
Therefore Control.vue is the parent of Display.vue which is the parent of Buyers.vue and Exhibitors.vue.
Anyway, What we need to do is control the array of exhibitors (and I guess buyers but you didn't include them in your code so I'll do likewise) which is in Exhibitors.vue from Control.Vue even though they don't have a direct parent child relationship. What I've done is set a prop that is passed to Display.vue which uses scoped slots to render the exhibitors from Exhibitors.Vue. Because the left and right buttons need to know when to stop going I have emitted the array length from Exhibitors.vue to Display.vue and again to Control.vue. It all works so heres some code.
//Control.vue
<template>
<div class="content">
<display v-on:finalLength="setIndexLimit" :i="index"></display>
<button #click="changeDown">Down</button>
<button #click="changeUp">Up</button>
<p>{{indLimit}}</p>
</div>
</template>
<script>
import Display from '#/components/Display'
export default {
components: {
Display
},
data: () => ({
index: 0,
indLimit: 0
}),
methods: {
changeUp() {
if (this.indLimit === this.index+1) {
this.index=0
}
else {
this.index ++
}
},
changeDown() {
if (this.index === 0) {
this.index = this.indLimit - 1
}
else {
this.index --
}
},
setIndexLimit(e) {
this.indLimit = e
}
}
}
</script>
and
//Display.vue
<template>
<div id="display">
<p>From Display</p>
<exhibitors v-on:ExLength="setLength">
<p>{{i}}</p>
</exhibitors>
<exhibitors>
<p slot-scope="{ exhibitors }">{{exhibitors[i].firstName}}</p>
</exhibitors>
<exhibitors>
<p slot-scope="{ exhibitors }">{{exhibitors[i].lastName}}</p>
</exhibitors>
<exhibitors>
<p slot-scope="{ exhibitors }">{{exhibitors[i].tagNum}}</p>
</exhibitors>
<exhibitors>
<p slot-scope="{ exhibitors }">{{exhibitors[i].species}}</p>
</exhibitors>
</div>
</template>
<script>
import Child from '#/components/admin/Exhibitors'
export default {
components: {
Exhibitors
},
props: [
'i'
],
data: () => ({
exhibitLength: null
}),
methods: {
setLength(e) {
this.exhibitLength = e
this.$emit('finalLength',e)
}
}
}
</script>
And finally:
//Exhibitors.vue
<template>
<div>
<slot :exhibitors="exhibitors"><p>I'm the child component!</p></slot>
</div>
</template>
<script>
export default {
data: () => ({
exhibitors: [
{
firstName: 'Joe',
lastName: 'Burns',
tagNum: 1,
species: 'ant'
},
{
firstName: 'Tom',
lastName: 'Yorke',
tagNum: 2,
species: 'bee'
},
{
firstName: 'Flit',
lastName: 'Argmeno',
tagNum: 3,
species: 'giraffe'
}
],
}),
mounted: function () {
this.$nextTick(function () {
let length = this.exhibitors.length
this.$emit('ExLength', length)
})
}
}
</script>
So as I said all that works, you can click the buttons and it will loop through the array contents. You can style it how you want wherever you like. However, it is a bit messy with props and slots and emits and whatnot just to relay a single index number and it would be far easier in my opinion to have a vuex store where 'index' is stored as a state item and can be used and changed anywhere without all the above carry on.

Vue component data not updating from props

I'm building a SPA with a scroll navigation being populated with menu items based on section components.
In my Home.vue I'm importing the scrollNav and the sections like this:
<template>
<div class="front-page">
<scroll-nav v-if="scrollNavShown" #select="changeSection" :active-section="activeItem" :items="sections"></scroll-nav>
<fp-sections #loaded="buildNav" :active="activeItem"></fp-sections>
</div>
</template>
<script>
import scrollNav from '.././components/scrollNav.vue'
import fpSections from './fpSections.vue'
export default {
data() {
return {
scrollNavShown: true,
activeItem: 'sectionOne',
scrollPosition: 0,
sections: []
}
},
methods: {
buildNav(sections) {
this.sections = sections;
console.log(this.sections)
},
changeSection(e) {
this.activeItem = e
},
},
components: {
scrollNav,
fpSections
}
}
</script>
this.sections is initially empty, since I'm populating this array with data from the individual sections in fpSections.vue:
<template>
<div class="fp-sections">
<keep-alive>
<transition
#enter="enter"
#leave="leave"
:css="false"
>
<component :is="activeSection"></component>
</transition>
</keep-alive>
</div>
</template>
<script>
import sectionOne from './sections/sectionOne.vue'
import sectionTwo from './sections/sectionTwo.vue'
import sectionThree from './sections/sectionThree.vue'
export default {
components: {
sectionOne,
sectionTwo,
sectionThree
},
props: {
active: String
},
data() {
return {
activeSection: this.active,
sections: []
}
},
mounted() {
this.buildNav();
},
methods: {
buildNav() {
let _components = this.$options.components;
for(let prop in _components) {
if(!_components[prop].hasOwnProperty('data')) continue;
this.sections.push({
title: _components[prop].data().title,
name: _components[prop].data().name
})
}
this.$emit('loaded', this.sections)
},
enter(el) {
twm.to(el, .2, {
autoAlpha : 1
})
},
leave(el, done) {
twm.to(el, .2, {
autoAlpha : 0
})
}
}
}
</script>
The buildNav method loops through the individual components' data and pushes it to a scoped this.sections array which are then emitted back to Home.vue
Back in Home.vue this.sections is populated with the data emitted from fpSections.vue and passed back to it as a prop.
When I inspect with Vue devtools the props are passed down correctly but the data does not update.
What am I missing here? The data should react to props when it is updated in the parent right?
:active="activeItem"
this is calld "dynamic prop" not dynamic data. You set in once "onInit".
For reactivity you can do
computed:{
activeSection(){ return this.active;}
}
or
watch: {
active(){
//do something
}
}
You could use the .sync modifier and then you need to emit the update, see my example on how it would work:
Vue.component('button-counter', {
template: '<button v-on:click="counter += 1">{{ counter }}</button>',
props: ['counter'],
watch: {
counter: function(){
this.$emit('update:counter',this.counter)
}
},
})
new Vue({
el: '#counter-sync-example',
data: {
foo: 0,
bar: 0
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.2/vue.min.js"></script>
<div id="counter-sync-example">
<p>foo {{ foo }} <button-counter :counter="foo"></button-counter> (no sync)</p>
<p>bar {{ bar }} <button-counter :counter.sync="bar"></button-counter> (.sync)</p>
</div>