Vuejs - Child component method run before parent query done running - javascript

I'm working on a basic POC app that has a Solr search function on the front that finds products, and then links in the search results use a route to go to a Product detail page.
index.js
import Vue from 'vue'
import Router from 'vue-router'
import Search from '#/components/Search'
import Product from '#/components/Product'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/search',
name: 'Search',
component: Search
},
{
path: '/product_display/:language/:entity_id',
name: 'product_display',
component: Product
},
]
})
Product.vue
<template>
<section class="hero is-warning">
<div class="hero-body">
<div class="container">
<h1 class="title">
{{ product.ss_field_content_title }}
</h1>
</div>
</div>
<section class="section">
<div class="container">
<productSlideshow :slideshowNid="product.is_product_slideshow"
:language="product.ss_language"></productSlideshow>
</div>
</section>
</section>
</template>
<script>
import axios from 'axios'
import ProductSlideshow from '#/components/ProductSlideshow'
export default {
name: 'Product',
components: {
ProductSlideshow
},
data () {
return {
product: {}
}
},
created () {
this.getProduct()
},
methods: {
getProduct: function () {
const params = new URLSearchParams()
var entityId = this.$route.params.entity_id
var language = this.$route.params.language
params.append('fq', 'bundle:product_display')
params.append('fq', 'entity_id:' + entityId)
params.append('fq', 'ss_language:' + language)
params.append('wt', 'json')
params.append('rows', 1)
axios.get('https://my.solrurl.com/solr/indexname/select', {
params: params
})
.then(response => {
this.product = response.data.response.docs[0]
})
.catch(e => {
this.errors.push(e)
})
}
},
watch: {
'$route': 'getProduct'
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>
Getting to this page works fine. The problem is getting the necessary params to the child ProductSlideshow component.
<template>
<h1>{{ slideshow.label }}</h1>
</template>
<script>
import axios from 'axios'
export default {
name: 'ProductSlideshow',
props: {
slideshowNid: {
type: Number,
required: true
},
language: {
type: String,
required: true
}
},
data () {
return {
slideshow: {},
slides: {}
}
},
created () {
this.getSlideshow()
},
methods: {
getSlideshow: function () {
var language = this.language
// Get slideshow record from Solr.
const params = new URLSearchParams()
params.append('fq', 'bundle:slideshow')
params.append('fq', 'entity_id:' + this.slideshowNid)
params.append('fq', 'ss_language:' + language)
params.append('fq', 'entity_type:node')
params.append('wt', 'json')
params.append('rows', 1)
axios.get('https://my.solrurl.com/solr/indexname/select', {
params: params
})
.then(response => {
this.slideshow = response.data.response.docs[0]
this.slides = this.slideshow.sm_field_slideshow_home
})
}
},
watch: {
'$route': 'getSlideshow'
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>
What I'm seeing is no data returned from the Solr query for the slideshow record, and when inspecting the Solr query, it was because language was undefined in the query, so nothing could be returned. By using various debugger breakpoints, I figured out that what's happening is that getSlideshow() is being called before the results are returned from getProduct() in the Product component. I tried removing the created() method in the ProductDisplay component, but that didn't make a difference.
What do I need to change so that my getSlideshow() method in the ProductSlideshow component doesn't get called until the query from the Product component has been completed?

Your issue connected with the fact, that Vue fires created hooks in sync way, so all children templates are created and mounted before the response from API comes.
The solution is to use Conditional Rendering
In your Product.vue component create some Boolean value like isProductFetched and render the child only when this value will be true.
<template>
<section class="hero is-warning">
<div class="hero-body">
<div class="container">
<h1 class="title">
{{ product.ss_field_content_title }}
</h1>
</div>
</div>
<section class="section">
<div class="container">
<productSlideshow
v-if="isProductFetched"
:slideshowNid="product.is_product_slideshow"
:language="product.ss_language"></productSlideshow>
</div>
</section>
</section>
</template>
<script>
import axios from 'axios'
import ProductSlideshow from '#/components/ProductSlideshow'
export default {
name: 'Product',
components: {
ProductSlideshow
},
data () {
return {
isProductFetched: false,
product: {}
}
},
created () {
this.getProduct()
},
methods: {
getProduct: function () {
const params = new URLSearchParams()
var entityId = this.$route.params.entity_id
var language = this.$route.params.language
params.append('fq', 'bundle:product_display')
params.append('fq', 'entity_id:' + entityId)
params.append('fq', 'ss_language:' + language)
params.append('wt', 'json')
params.append('rows', 1)
axios.get('https://my.solrurl.com/solr/indexname/select', {
params: params
})
.then(response => {
this.product = response.data.response.docs[0]
// At this point, the product is fetched
this.isProductFetched = true
})
.catch(e => {
this.errors.push(e)
})
}
},
watch: {
'$route': 'getProduct'
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>
As a result, the child component won't be rendered until this value becomes true. Also, do not forget to handle the error case.

Related

Vue not reacting to a computed props change

I am using the Vue composition API in one of my components and am having some trouble getting a component to show the correct rendered value from a computed prop change. It seems that if I feed the prop directly into the components render it reacts as it should but when I pass it through a computed property it does not.
I am not sure why this is as I would have expected it to be reactive in the computed property too?
Here is my code:
App.vue
<template>
<div id="app">
<Tester :testNumber="testNumber" />
</div>
</template>
<script>
import Tester from "./components/Tester";
export default {
name: "App",
components: {
Tester,
},
data() {
return {
testNumber: 1,
};
},
mounted() {
setTimeout(() => {
this.testNumber = 2;
}, 2000);
},
};
</script>
Tester.vue
<template>
<div>
<p>Here is the number straight from the props: {{ testNumber }}</p>
<p>
Here is the number when it goes through computed (does not update):
{{ testNumberComputed }}
</p>
</div>
</template>
<script>
import { computed } from "#vue/composition-api";
export default {
props: {
testNumber: {
type: Number,
required: true,
},
},
setup({ testNumber }) {
return {
testNumberComputed: computed(() => {
return testNumber;
}),
};
},
};
</script>
Here is a working codesandbox:
https://codesandbox.io/s/vue-composition-api-example-forked-l4xpo?file=/src/components/Tester.vue
I know I could use a watcher but I would like to avoid that if I can as it's cleaner the current way I have it
Don't destruct the prop in order to keep its reactivity setup({ testNumber }) :
setup(props) {
return {
testNumberComputed: computed(() => {
return props.testNumber;
}),
};
}

In Vue2 tag input, trying to display tag name from tag object in autocomplete

I have a Vue component with a tag input where I make an ajax call to the db to retrieve suggestions as the user is typing. I am using #johmun/vue-tags-input for this. Everything works fine except that instead of the autocomplete listing options including only the tag attribute of the Tag model, it includes the entire object.
I want to list only the tag attribute in the view, but I want to reference the array of entire Tag objects when it comes time to create the association with the user.
This what the current dropdown look like in the browser:
Here is my input component removing the irrelevant parts, so it meets SO's size constraints:
<template>
<div >
<b-container class="mt-8 pb-5">
<b-row class="justify-content-center">
<b-col lg="5" md="7">
<form>
...
<div v-if="step === 3">
<h2><strong>What topics are you interested in?</strong> (e.g tag1, tag2, etc...)</h2>
<h2>A few popular ones:
<button #click.prevent="addToTags(item)" class="btn btn-sm btn-success" v-for="item in existingTags.slice(0, 3)" :key="item.id">
{{ item.tag }}
</button>
</h2>
<vue-tags-input
v-model="tag"
v-on:keyup.native="getTags"
:tags="tags"
:autocomplete-items="filteredItems"
:autocomplete-min-length=3
#tags-changed="confirmedTags"
/>
</div>
...
</form>
</b-col>
</b-row>
</b-container>
</div>
</template>
<script>
import VueTagsInput from '#johmun/vue-tags-input';
import UsersService from '#/services/UsersService'
import TagsService from '#/services/TagsService'
import TagRelationsService from '#/services/TagRelationsService'
export default {
name: 'UserOnboard',
data() {
return {
tag: '',
tags: [],
...
}
};
},
components: {
VueTagsInput
},
computed: {
filteredItems() {
return this.existingTags.filter(i => {
return i.tag.toLowerCase().indexOf(this.tag.toLowerCase()) !== -1;
});
},
...
user() {
return this.$store.state.auth.user
},
existingTags() {
return this.$store.state.tags.existingTags
}
},
...
methods:{
...
},
addToTags(newTag) {
if (!this.tags.includes(newTag)) {
this.tags.push(newTag)
}
// on button click add appropriate tag to tags array
// console.log('tag array is: ',tags)
},
confirmedTags(event) {
this.tags=event
console.log(event)
},
...
getTags() { //debounce need to be inside conditional
console.log('gettin tags')
// if (this.tag.length >2) {
this.$store.dispatch('debounceTags', this.tag)
// }
}
}
}
</script>
Also, here is the debounceTags method which runs via vuex:
import TagsService from '#/services/TagsService'
import { debounce } from "lodash";
export const state = {
existingTags: []
}
export const mutations = {
setTags (state, tags) {
state.existingTags = tags
}
}
export const actions = {
debounceTags: debounce(({ dispatch }, data) => {
console.log("Inside debounced function.");
dispatch("getTags" ,data);
}, 300),
async getTags ({ commit }, data) {
await TagsService.getTags(data)
.then(function (response) {
console.log('before setting tags this is resp.data: ', response)
commit('setTags', response);
});
}
}
export const getters = {
}

Vue.js : Why I can't bind external property to child props component?

I have a question about Vue.js, I'm stuck on something tricky I guess.
I cannot bind passed property as component property to do some stuff on this data, here is my code, the issue is affecting Plot component.
Here is my dashboard component which is the parent :
<template>
<div>
<div v-if="!hasError && countryData">
<div v-if="loading" id="dashboard" class="ui two column grid sdg-dashboard sdg-text-center active loader">
</div>
<div v-else id="dashboard" class="ui two column grid sdg-dashboard">
<div class="column sdg-text-center">
<MapDisplay :country="countryData" :latitude="latData" :longitude="lonData"/>
</div>
<div class="column">
<TopicSelector v-on:topicSelectorToParent="onTopicSelection" :goals="goalsData"/>
</div>
<div class="two segment ui column row sdg-text-center">
<Plot :topic-selection-data="topicSelectionData"/>
</div>
</div>
<sui-divider horizontal><h1>{{ countryData }}</h1></sui-divider>
</div>
<div v-else>
<NotFound :error-type="pageNotFound"/>
</div>
</div>
</template>
<script>
import NotFound from '#/views/NotFound.vue';
import MapDisplay from '#/components/dashboard/MapDisplay.vue';
import TopicSelector from '#/components/dashboard/TopicSelector.vue';
import Plot from '#/components/dashboard/Plot.vue';
const axios = require('axios');
export default {
name: 'Dashboard',
components: {
NotFound,
MapDisplay,
TopicSelector,
Plot
},
props: {
countryCode: String
},
data: function() {
return {
loading: true,
hasError: false,
country: this.countryCode,
//Country, lat, lon
countryData: null,
latData: null,
lonData: null,
//Goals provided to Topic Selector
goalsData: null,
//Selected topic provided by Topic Selector
topicSelection: null,
//Topic Data provided to Plot component
topicData: null, //All topic data
topicSelectionData: null,
pageNotFound: "Error 500 : Cannot connect get remote data."
}
},
created: function() {
const api = process.env.VUE_APP_SDG_API_PROTOCOL + "://" + process.env.VUE_APP_SDG_API_DOMAIN + ":" + process.env.VUE_APP_SDG_API_PORT + process.env.VUE_APP_SDG_API_ROUTE;
axios.get(api + "/countrycode/" + this.countryCode)
.then(response => {
this.countryData = response.data.data.country;
this.latData = response.data.data.coordinates.latitude;
this.lonData = response.data.data.coordinates.longitude;
this.goalsData = response.data.data.goals.map(goal => {
return {
goal_code: goal["goal code"],
goal_description: goal["goal description"]
}
});
this.topicData = response.data.data.goals;
})
.catch(() => this.hasError = true)
.finally(() => this.loading = false);
},
methods: {
onTopicSelection: function(topic) {
this.topicSelection = topic;
this.topicSelectionData = this.topicData.filter(goal => this.topicSelection.includes(goal["goal code"]));
}
}
}
</script>
<style scoped lang="scss">
#dashboard {
margin-bottom: 3.1vh;
margin-left: 1vw;
margin-right: 1vw;
}
</style>
Here is the Plot component which his the child :
<template>
<div id="plot">
topic data : {{ topicData }}<br>
topicSelectionData : {{ topicSelectionData }}
</div>
</template>
<script>
export default {
name: 'Plot',
props: {
topicSelectionData: Array
},
data: function() {
return {
topicData: this.topicSelectionData //This line is not working =(
}
}
}
</script>
<style scoped lang="scss">
</style>
I can see my data in {{ topicSelectionData }} but when I bind it to topicData, I cannot retrieve the data using {{ topicData }} or doing some stuff inside a method using topicData as input.
Could you provide me some help ?
Regards
You need to assign the value on mounted as follows:
data() {
return {
topicData: ''
}
},
mounted(){
this.topicData = this.topicSelectionData;
}
And in order to update the child when the value changes in the parent:
watch: {
topicSelectionData: function(newValue, oldValue) {
this.topicData = newValue;
}
},

Problems with data communication between components

I had a page on which there was a header with an input that was a search engine, a list of posts, and pagination. I decided to move the header from this file to a separate component in a separate vue file. After I did this, the search for posts by title stopped working, and I can’t add a post now either. I think that I need to import my posts into a new file for my newly created component but how to do it.
My code when it worked(before my changes)
My code is not working after the changes:
The file in which my posts situated:
<template>
<div class="app">
<ul>
<li v-for="(post, index) in paginatedData" class="post" :key="index">
<router-link :to="{ name: 'detail', params: {id: post.id, title: post.title, body: post.body} }">
<img src="src/assets/nature.jpg">
<p class="boldText"> {{ post.title }}</p>
</router-link>
<p> {{ post.body }}</p>
</li>
</ul>
<div class="allpagination">
<button type="button" #click="page -=1" v-if="page > 0" class="prev"><<</button>
<div class="pagin">
<button class="item"
v-for="n in evenPosts"
:key="n.id"
v-bind:class="{'selected': current === n.id}"
#click="page=n-1">{{ n }} </button>
</div>
<button type="button" #click="page +=1" class="next" v-if="page < evenPosts-1">>></button>
</div>
</div>
</template>
<script>
import axios from 'axios';
export default {
name: 'Pagination',
data () {
return {
search: '',
current: null,
page: 0,
posts: [],
createTitle: '',
createBody: '',
visiblePostID: '',
}
},
watch: {
counter: function(newValue, oldValue) {
this.getData()
}
},
created(){
this.getData()
},
computed: {
evenPosts: function(posts){
return Math.ceil(this.posts.length/6);
},
paginatedData() {
const start = this.page * 6;
const end = start + 6;
return this.posts.filter((post) => {
return post.title.match(this.search);
}).slice(start, end);
},
},
methods: {
getData() {
axios.get(`https://jsonplaceholder.typicode.com/posts`).then(response => {
this.posts = response.data
})
},
}
}
</script>
Header vue:
AddPost
<script>
import axios from 'axios';
export default {
name: 'Pagination',
data () {
return {
search: '',
current: null,
posts: [],
createTitle: '',
createBody: '',
}
},
created(){
this.getData()
},
methods: {
getData() {
axios.get(`https://jsonplaceholder.typicode.com/posts`).then(response => {
this.posts = response.data
})
},
addPost() {
axios.post('http://jsonplaceholder.typicode.com/posts/', {
title: this.createTitle,
body: this.createBody
}).then((response) => {
this.posts.unshift(response.data)
})
},
}
}
</script>
App.vue:
<template>
<div id="app">
<header-self></header-self>
<router-view></router-view>
</div>
</template>
<script>
export default {
components: {
name: 'app',
}
}
</script>
You have a computed property paginatedData in your "posts" component that relies a variable this.search:
paginatedData () {
const start = this.page * 6;
const end = start + 6;
return this.posts.filter((post) => {
return post.title.match(this.search);
}).slice(start, end);
},
but this.search value is not updated in that component because you moved the search input that populates that value into the header component.
What you need to do now is make sure that the updated search value is passed into your "posts" component so that the paginatedData computed property detects the change and computes the new paginatedData value.
You're now encountering the need to pass values between components that may not have a parent/child relationship.
In your scenario, I would look at handling this need with some Simple State Management as described in the Vue docs.
Depending on the scale of you app it may be worth implementing Vuex for state management.

Vue.js passing store data as prop causes mutation warnings

I'm passing store data (Vuex) as a property of component but it's giving me mutation errors even though I'm not changing the data.
Edit: Codepen illustrating error: https://codesandbox.io/s/v8onvz427l
Input
<template>
<div>
<input type="text" class="form-control" ref="input" />
<div style="padding-top: 5px">
<button #click="create" class="btn btn-primary btn-small">Create</button>
</div>
{{ example }}
</div>
</template>
<script>
import store from "#/store"
export default {
props: {
"example": {
}
},
data() {
return {
store
}
},
methods: {
create() {
store.commit("general_set_creation_name", {name: this.$refs.input.value})
}
}
}
</script>
Modal.vue
<template src="./Modal.html"></template>
<script>
import $ from 'jquery'
import store from '#/store'
export default {
props: {
"id": String,
"height": {
type: String,
default: "auto"
},
"width": {
type: String,
default: "40vw"
},
"position": {
type: String,
default: "absolute"
},
"component": {
default: null
},
"global": {
default: true
}
},
data () {
return {
store: store
}
},
computed: {
body () {
return store.state.General.modal.body
},
props () {
return store.state.General.modal.props
},
title () {
return store.state.General.modal.title
},
},
methods: {
close_modal (event) {
if (event.target === event.currentTarget) {
this.$refs.main.style.display = "none"
}
}
}
}
</script>
<style scoped lang="scss" src="./Modal.scss"></style>
Modal.html
<div
:id="id"
class="main"
ref="main"
#click="close_modal"
>
<div ref="content" class="content" :style="{minHeight: height, minWidth: width, position}">
<div ref="title" class="title" v-if="title">
{{ title }}
</div>
<hr v-if="title" />
<div ref="body" class="body">
<slot></slot>
<component v-if="global" :is="body" v-bind="props"></component>
</div>
</div>
</div>
Changing store data with this line in a third component:
store.commit("general_set_modal", {body: Input, title: "New "+page, props: {example: "example text 2"})
I'm quite sure you should not put a vue component on the state. If you are supposed to do that then I don't think the creators of vuex understand how an event store works.
In the documentation it also says you need to initialize your state with values and you don't do that.
Your sandbox works fine when removing the vue component from the state (state should contain data but vue components are objects with both data and behavior).
index.js in store:
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export default new Vuex.Store({
state: {
modal: {
body: {},
title: "",//det it to something
props: {}
},
creationName: null
},
mutations: {
general_set_creation_name(state, payload) {
state.creationName = payload.name;
},
general_set_modal(state, payload) {
state.modal.title = payload.title;
state.modal.props = payload.props;
console.log("we are fine here");
}
},
strict: process.env.NODE_ENV !== "production"
});
For whatever reason, changing the way I import the class removes the warning
const test = () => import('./Test')
Details:
https://forum.vuejs.org/t/getting-vuex-mutation-error-when-im-only-reading-the-data/27655/11

Categories

Resources