Vue component data not updating from props - vuejs2

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>

Related

How to create a method with props passed to child component (Vue3)

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){...}
....
}

How to update data from vue-tables-2 after action from Template?

I'm using a custom component as a column on vue-tables-2, to do that I'm using a vue-component as described here: vue-components
I've created a button that opens a modal to the user confirm some information, and after that I make a request to the backend and the record is changed on the database.
Now I want to refresh the data on the table, but I don't know how to do that. The documentation said about using the $ref, but this is not an option because my component is not the parent.
How can I do that?
Links to the code:
Component using 'vue-tables-2'
<template>
<div>
<div id="payment">
<input type="checkbox" v-model="onlyPending" #change="filterPay()">Apenas pendentes</input>
<v-server-table url="/api/payments" :columns="columns" :options="options" ></v-server-table>
</div>
</div>
</template>
<script>
import pay from './ModalConfirmPay.vue'
import {Event} from 'vue-tables-2';
export default {
name: "AeraListPayment",
props: ['groupId'],
data: function(){
let groupId = this.groupId;
return {
columns: ['name','value','course','due_date','paid','installment','pay'],
options: {
responseAdapter : function(data) {
data.data = data.data.map(payment => {
payment.paid = payment.paid ? "pago" : "pendente";
return payment;
})
return data;
},
headings: {
installment: 'Parcela',
paid: 'Status',
value: 'Valor',
due_date: 'Vencimento',
pay: 'Ação',
course: 'Curso',
name: 'Nome'
},
templates : {
pay
},
customFilters: ['onlyPending','groupId'],
initFilters:{groupId:groupId,onlyPending:true}
},
onlyPending: true
}
},
methods: {
filterPay(){
Event.$emit('vue-tables.filter::onlyPending', this.onlyPending);
}
}
}
</script>
Component that is being used as a custom column:
<template>
<div>
<button #click.prevent="show">Pagar</button>
<modal :name="modalName">
<p>Confirma o pagamento de {{data.value}} ?</p>
<p>Parcela: {{data.installment}}</p>
<p>Vecimento: {{data.due_date}}</p>
<button #click.prevent="pay">Confirmar</button>
<button #click.prevent="hide">Cancelar</button>
</modal>
</div>
</template>
<script>
import PaymentService from '../../services/PaymentService'
let service = new PaymentService();
export default {
name:"ModalConfirmPay",
props: ["data"],
computed: {
modalName: function () {
// `this` aponta para a instância Vue da variável `vm`
return `confirm-pay-${this.data.clientGroup_id}-${this.data.installment}`
}
},
methods: {
show () {
this.$modal.show(this.modalName);
},
pay ( ) {
service.pay(this.data)
.then(this.hide());
},
hide () {
this.$modal.hide(this.modalName);
}
}
}
</script>
First, defined an EventBus if you don't have
EventBus.vue
import Vue from 'vue'
export default new Vue()
In ListPayment.vue, import EventBus and listen for refresh-table event. Note that I add ref="table" to vue-tables-2 element
<template>
<v-server-table ref="table" ... />
</template>
<script>
import EventBus from './EventBus.vue'
export default {
mounted() {
EventBus.$on('refresh-table', this.refreshTable)
},
beforeDestroy() {
EventBus.$off('refresh-table', this.refreshTable)
},
methods: {
refreshTable() {
this.$refs.table.refresh();
}
}
}
</script>
Finally, emit event in modal
pay() {
service.pay(this.data)
.then(() => {
EventBus.$emit('refresh-table')
})
.then(this.hide());
}

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.

Unable to trigger render when switching between multiple objects containing content in different languages

I read Reactivity in Depth but can't solve the issue.
I'm creating a small single page app that contains images and text.
When the user clicks a button I want the language to change.
Currently I am storing the content in two files that export an object.
export default {
projects: [
{
title: 'Project Title',
year: 2016,
...
},
]
}
and importing that
import contentEn from './assets/content.en.js'
import contentDe from './assets/content.de.js'
new Vue({
el: '#app',
components: { App },
data: {
mainContent: {
content: contentEn
}
},
methods: {
switchToGerman(){
this.mainContent.content = contentDe
}
},
template: '<App :mainData="mainContent"/>',
})
When I assign another object to mainContent.content the rendering is not triggered.
I understand that adding and deleting properties from object don't lead to change detection but I switch out a whole object. I tried assigning it with this.$set with no success.
I also tried this and googled a lot but can't get it work.
Or is my approach just wrong?
Thank you for helping,
best,
ccarstens
EDIT:
See below the code for the App component and the ProjectElement
// App.vue
<template>
<div id="app">
<button #click="switchGerman">Deutsch</button>
<ProjectElement v-for="(project, key) in fullData.content.projects" :key="key" :content="project"/>
</div>
</template>
<script>
import ProjectElement from './components/ProjectElement'
export default {
name: 'App',
props: [
'mainData'
],
data () {
return{
fullData: {}
}
},
methods: {
switchGerman(){
this.$root.switchToGerman()
}
},
created(){
this.fullData = this.$props.mainData
},
watch: {
mainData: {
handler: function(newData){
this.fullData = newData
},
deep: true
}
},
components: {
ProjectElement,
}
}
</script>
And the ProjectElement
//ProjectElement.vue
<template>
<article :class="classObject" v-observe-visibility="{
callback: visibilityChanged,
throttle,
intersection: {
threshold
}
}">
<header v-html="description"></header>
<div class="content">
<carousel :per-page="1" :pagination-enabled="false">
<slide v-for="(slide, index) in projectContent.media" :key="index">
<VisualElement :content="slide" ></VisualElement>
</slide>
</carousel>
</div>
</article>
</template>
<script>
import {Carousel, Slide} from 'vue-carousel'
import VisualElement from './VisualElement'
export default {
name: "ProjectElement",
components: {
Carousel,
Slide,
VisualElement
},
props: [
'content'
],
data () {
return {
projectContent: {},
isVisible: false,
throttle: 300,
threshold: 0.8
}
},
created(){
this.projectContent = this.content
},
methods: {
visibilityChanged(isVisible){
this.isVisible = isVisible
}
},
computed: {
description(){
return `
<p>${ this.projectContent.title } - ${this.projectContent.year}</p>
<p>${ this.projectContent.description }</p>
`
},
classObject(){
return {
visible: this.isVisible,
'project-element': true
}
}
}
}
</script>
Did you try doing deep copy:
switchToGerman () {
const copyContent = JSON.parse(JSON.stringify(this.mainContent))
copyContent.content = contentDe
this.mainContent = copyContent
}
I found the solution (thank you #EricGuan for pointing out that the mistake must lay somewhere else)
As you can see in the original post I created a watcher for the mainData property and expected that this would trigger the re render.
What was missing is, that I didn't watch the content property on the ProjectElement component, thus not triggering a re render there.
I added this to ProjectElement.vue and now it works like a charm:
watch: {
content(newContent){
this.projectContent = newContent
}
},
Thank you everybody for helping me! <3

Fetch data in component on initiation using parameters from Vuex store

I am new to Vue and am trying to build a simple movie app, fetching data from an API and rendering the results. I want to have an incremental search feature. I have an input field in my navbar and when the user types, I want to redirect from the dashboard view to the search results view. I am unsure of how to pass the query params from the navbar to the search results view.
Here is my App.vue component
<template>
<div id="app">
<Navbar></Navbar>
<router-view/>
</div>
</template>
<script>
import Navbar from './components/Navbar.vue'
export default {
name: 'App',
components: {
Navbar
},
}
</script>
And here is my navbar component where I have the input field
<template>
<nav class="navbar">
<h1 class="logo" v-on:click="goToHome">Movie App</h1>
<input class="search-input" v-on:keyup="showResults" v-model="query" type="text" placeholder="Search..."/>
</nav>
</template>
<script>
import router from '../router/index'
export default {
data: function () {
return {
query: this.query
}
},
methods: {
goToHome () {
router.push({name: 'Dashboard'})
},
showResults () {
//here on each key press I want to narrow my results in the SearchedMovies component
}
}
}
</script>
If I use router.push to the SearchedMovies component then I am only able to pass the query as a parameter once. I thought about using Vuex to store the query and then access it from the SearchedMovies component, but surely there is a better way of doing it?
I also read about using $emit but since my parent contains all the routes, I'm not sure how to go about this.
You don't need to redirect user anywhere. I've made a small demo to show how one might do it. I used this navbar component as you described and emit an event from it:
const movies = {
data: [
{
id: 0,
title: 'Eraserhead',
},
{
id: 1,
title: 'Erazerhead',
},
{
id: 2,
title: 'Videodrome',
},
{
id: 3,
title: 'Videobrome',
},
{
id: 4,
title: 'Cube',
},
]
};
Vue.component('navbar', {
template: '<input v-model="filter" #input="onInput" placeholder="search">',
data() {
return {
filter: '',
};
},
methods: {
onInput() {
this.$emit('filter', this.filter);
}
}
});
// this is just a request imitation.
// just waiting for a second until we get a response
// from the datasample
function request(title) {
return new Promise((fulfill) => {
toReturn = movies.data.filter(movie => movie.title.toLowerCase().indexOf(title.toLowerCase()) !== -1)
setTimeout(() => fulfill(toReturn), 1000);
});
}
new Vue({
el: '#app',
data: {
movies: undefined,
loading: false,
filter: '',
lastValue: '',
},
methods: {
filterList(payload) {
// a timeout to prevent
// instant request on every input interaction
this.lastValue = payload;
setTimeout(() => this.makeRequest(), 1000);
},
makeRequest() {
if (this.loading) {
return;
}
this.loading = true;
request(this.lastValue).then((response) => {
this.movies = response;
this.loading = false;
});
}
},
mounted() {
this.makeRequest('');
}
})
<script src="https://unpkg.com/vue"></script>
<div id="app">
<navbar v-on:filter="filterList"></navbar>
<ul v-if="!loading">
<li v-for="movie in movies" :key="movie.id">{{ movie.title }}</li>
</ul>
<p v-else>Loading...</p>
</div>
Also jsfiddle: https://jsfiddle.net/oniondomes/rsyys3rp/
If you have any problem to understand the code above let me know.
EDIT: Fixed some bugs and added a couple of comments
EDIT2(after the comment below):
Here's what you can do. Every time user inputs something inside a navbar you call a function:
// template
<navbar v-on:input-inside-nav-bar="atInputInsideNavBar"></navbar>
// script
methods: {
atInputInsideNavBar(userInput) {
this.$router.push({
path: '/filtred-items',
params: {
value: userInput
}
})
}
}
Then inside you 'searched movies' page component you can access this value so:
this.$route.params.value // returns userInput from root component