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
}
]
Related
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 am creating an app that will display products on a page. Each of these products have a list of features such as "battery power", "charge time", and even just a description that will vary per feature. My question is, how can I make a clickable element, that when clicked will find the data associated with that button/icon, then update the content on the page to reflect this? This content may or may not be in some kind of v-for loop.
See the example of what I have and what I want to achieve below.
Child component:
<template>
<li>
<button #click="$emit('changeProductData', feature)">
<img :src="require('../assets/images/' + feature.item.img)" />
</button>
</li>
</template>
<script>
export default {
props: {
feature: Object
}
}
</script>
Parent component:
<template>
<div>
<div v-for="product in getProduct(productId)" :key="product.productId">
{{ product }}
<Halo
:featuresCount="
`circle-container-` + product.features.length.toString()
"
>
<Feature
v-for="(feature, key, index) in product.features"
:key="index"
:feature="feature"
#changeProductData="something" // this is where we call the custom event
></Feature>
</Halo>
<h1>This is where I want to dynamically inject the title for each feature on clicking corresponding feature</h1>
</div>
</div>
</template>
<script>
import Halo from '#/components/ProductHalo.vue'
import Feature from '#/components/ProductFeature.vue'
import json from '#/json/data.json'
export default {
name: 'ProductSingle',
components: {
Halo,
Feature
},
data() {
return {
products: json
}
},
computed: {
productId() {
return this.$route.params.id
}
},
methods: {
getProduct(id) {
let data = this.products
return data.filter(item => item.productId == id)
},
something(e) {
// ideally we have a method here that grabs the corresponding
//feature then displays it on the page
console.log(e.item.text)
}
}
}
</script>
My console.log call does indeed call the correct title from my data.json as seen below:
[
{
"productId": 1,
"name": "Test 1",
"image": "sample.jpg",
"features": [
{
"item": {
"text": "Something else",
"img": "sample.jpg"
}
},
{
"item": {
"text": "Turbo",
"img": "wine.jpg"
}
},
{
"item": {
"text": "Strong",
"img": "sample.jpg"
}
}
]
}
]
So it seems I can access my title based on the click of each respective item, just not sure how I can display that in an arbitrary location! Any amazing vue js'ers out there who can solve this riddle? TIA
Option1: change the child component
Instead of creating a component only just for one feature button, I would embed the whole list of features in a component with h1 where you want to show the selected feature. So, the child component will look like this:
<template>
<div>
<li v-for="(feature, key, index) in features" :key="index">
<button #click="setFeature(feature)">
<img :src="require('../assets/images/' + feature.item.img)" />
</button>
</li>
<h1>{{ clickedFeuatureText }}</h1>
</div>
</template>
<script>
export default {
data() {
return {
clickedFeuatureText:null
}
},
methods: {
setFeature(e){
this.clickedFeuatureText = e.item.text
}
},
props: {
features: Array
}
}
</script>
And here is the parent component with corresponding changes:
<template>
<div>
<div v-for="product in getProduct(productId)" :key="product.productId">
{{ product }}
<Halo
:featuresCount="
`circle-container-` + product.features.length.toString()
"
>
<Feature :features=" product.features"></Feature>
</Halo>
</div>
</div>
</template>
<script>
import Halo from '#/components/ProductHalo.vue'
import Feature from '#/components/ProductFeature.vue'
import json from '#/json/data.json'
export default {
name: 'ProductSingle',
components: {
Halo,
Feature
},
data() {
return {
products: json
}
},
computed: {
productId() {
return this.$route.params.id
}
},
methods: {
getProduct(id) {
let data = this.products
return data.filter(item => item.productId == id)
}
}
}
</script>
Option2: create the new component for one product
Another way is to leave a child component as is, but create the new component representing one product in the list:
<template>
<div>
{{ product }}
<Halo
:featuresCount="
`circle-container-` + product.features.length.toString()
"
>
<Feature
v-for="(feature, key, index) in product.features"
:key="index"
:feature="feature"
#changeProductData="setFeatureText"
></Feature>
</Halo>
<h1>{{ clickedFeature}}</h1>
</div>
</div>
</template>
<script>
import Feature from '#/components/ProductFeature.vue'
import Halo from '#/components/ProductHalo.vue'
export default {
name: 'product',
components: {
Feature,
Halo
},
data () {
return {
clickedFeature: null
}
},
props: {
product: Object
},
methods: {
setFeatureText(e) {
this.clickedFeature = e.item.text
}
}
}
</script>
Then use the new Product component:
<template>
<div>
<product
v-for="product in getProduct(productId)"
:key="product.productId"
:product="product"
></product>
</div>
</template>
<script>
import Product from '#/components/Product.vue'
import json from '#/json/data.json'
export default {
name: 'ProductSingle',
components: {
Product
},
data() {
return {
products: json
}
},
computed: {
productId() {
return this.$route.params.id
}
},
methods: {
getProduct(id) {
let data = this.products
return data.filter(item => item.productId == id)
}
}
}
</script>
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.
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.