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.
Related
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;
}),
};
}
I'm building an application to power the backend of a website for a restaurant chain. Users will need to edit page content and images. The site is fairly complex and there are lots of nested pages and sections within those pages. Rather than hardcode templates to edit each page and section, I'm trying to make a standard template that can edit all pages based on data from the route.
I'm getting stuck on the v-model for my text input.
Here's my router code:
{
path: '/dashboard/:id/sections/:section',
name: 'section',
component: () => import('../views/Dashboard/Restaurants/Restaurant/Sections/Section.vue'),
meta: {
requiresAuth: true
},
},
Then, in my Section.vue, here is my input with the v-model. In this case, I'm trying to edit the Welcome section of a restaurant. If I was building just a page to edit the Welcome text, it would work no problem.:
<vue-editor v-model="restInfo.welcome" placeholder="Update Text"></vue-editor>
This issue is that I need to reference the "welcome" part of the v-model dynamically, because I've got about 40 Sections to deal with.
I can reference the Section to edit with this.$route.params.section. It would be great if I could use v-model="restInfo. + section", but that doesn't work.
Is there a way to update v-model based on the route parameters?
Thanks!
Update...
Here is my entire Section.vue
<template>
<div>
<Breadcrumbs :items="crumbs" />
<div v-if="restInfo">
<h3>Update {{section}}</h3>
<div class="flex flex-wrap">
<div class="form__content">
<form #submit.prevent>
<vue-editor v-model="restInfo.welcome" placeholder="Update Text"></vue-editor>
<div class="flex">
<button class="btn btn__primary mb-3" #click="editText()">
Update
<transition name="fade">
<span class="ml-2" v-if="performingRequest">
<i class="fa fa-spinner fa-spin"></i>
</span>
</transition>
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex'
import { VueEditor } from "vue2-editor"
import Loader from '#/components/Loader.vue'
import Breadcrumbs from '#/components/Breadcrumbs.vue'
export default {
data() {
return {
performingRequest: false,
}
},
created () {
this.$store.dispatch("getRestFromId", this.$route.params.id);
},
computed: {
...mapState(['currentUser', 'restInfo']),
section() {
return this.$route.params.section
},
identifier() {
return this.restInfo.id
},
model() {
return this.restInfo.id + `.` + this.section
},
crumbs () {
if (this.restInfo) {
let rest = this.restInfo
let crumbsArray = []
let step1 = { title: "Dashboard", to: { name: "dashboard"}}
let step2 = { title: rest.name, to: { name: "resthome"}}
let step3 = { title: 'Page Sections', to: { name: 'restsections'}}
let step4 = { title: this.$route.params.section, to: false}
crumbsArray.push(step1)
crumbsArray.push(step2)
crumbsArray.push(step3)
crumbsArray.push(step4)
return crumbsArray
} else {
return []
}
},
},
methods: {
editText() {
this.performingRequest = true
this.$store.dispatch("updateRest", {
id: this.rest.id,
content: this.rest
});
setTimeout(() => {
this.performingRequest = false
}, 2000)
}
},
components: {
Loader,
VueEditor,
Breadcrumbs
},
beforeDestroy(){
this.performingRequest = false
delete this.performingRequest
}
}
</script>
Try to use the brackets accessor [] instead of . :
<vue-editor v-model="restInfo[section]"
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;
}
},
I have something like /url/{category}.
This is the code to fetch some of these on the main page:
export default {
data() {
return {
topnews:[],
newsfive:[],
categories: {
tshirts:'',
shirts:'',
shoes:'',
useful:'',
}
}
},
methods() {
async getAll(){
axios.all([
axios.get(`/topnews`),
axios.get(`/news`),
axios.get(`/tshirts`),
axios.get(`/shirts`),
axios.get(`/shoes`),
axios.get(`/useful`)])
.then(axios.spread((topnews, news, tshirts, shirts, shoes, useful) => {
news.data.length = 5;
tshirts.data.length = 5
shirts.data.length = 5
shoes.data.length = 5
useful.data.length = 5
// How to limit these appropriately?
this.newsfive = news.data;
this.topnews = topnews.data;
this.categories = {
tshirts: tshirts.data,
shirts: shirts.data,
shoes: shoes.data,
useful: useful.data,
}
})).catch(error => console.log(error))
}
}
created() {
this.getAll()
}
}
This works, but If I change the route to /tshirts and use browser back to the main page, I get:
typeerror content read-only property
Also is it possible to combine this into a single array instead of creating 7 different divs like:
<div v-for="tshirts,key in categories.tshirts" :key="categories.tshirts.slug">
<img :src="tshirts.thumb" class="img-responsive" width=100%/>
<p>{{tshirts.title}}</p>
</div>
Instead have something like a filtered computed axios response and then just use a single div?
<div v-for="item,key in categories(tshirts)" :key="categories(item.slug)">
How can I limit the axios array response size?
Create Category.vue to render only category content
<template>
<div v-for="(item, key) in category" :key="item.slug">
<img :src="item.thumb" class="img-responsive" width=100% />
<p>{{ item.title }}</p>
</div>
</template>
<script>
export default {
data() {
return {
category: { }
}
},
methods() {
getCategory() {
axios.get(`/${this.$route.params.category}`)
.then((response) => {
this.category = response.data.slice(0, 5);
}).catch(error => console.log(error));
}
}
created() {
this.getCategory()
}
}
</script>
And in App.vue add router-link to all categories
<template>
<nav>
<router-link to="{ name: 'category', params: {category: 'tshirts'} }">T-Shirts</router-link>
<router-link to="{ name: 'category', params: {category: 'shirts'} }">Shirts</router-link>
<!-- and other -->
</nav>
<main>
<router-view></router-view>
</main
</template>
Don't forger about vue-router
import Category from "./Category.vue"
routes = [
{
name: "category",
path: "/:categoryId",
component: Category
}
]
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.