What is an appropriate way to achieve a nested "n" levels of routes based on currently selected category ?
I would like to properly access the routes, so my URL would look like this:
/categories
/categories/outfit/
/categories/outfit/jackets/
/categories/outfit/jackets/mountains/
/categories/outfit/jackets/mountains/you_get_the_idea
I have a list of categories like so:
const categories = [
{
name: 'outfit',
childs: [
{ name: 'jackets',
childs: [
{ name: 'mountains' }
]
},
{ name: 'pants', childs: ['many subcategories'] },
{ name: 'boots', childs: ['many subcategories'] }
]
},
{
name: 'knives',
childs: [
{ name: 'hunting' },
{ name: 'souvenirs' },
{ name: 'kitchen' },
{ name: 'skinning' },
{ name: 'throwing' }
]
}
]
I have a main page of Categories (TOP LEVEL):
<div class="col-6" v-for="category in categories" :key="category.id">
<div class="block q-pa-md">
<router-link :to="'/categories/' + category.slug">
<h4>{{ category.name }}</h4>
</router-link>
</div>
</div>
And there is a nested page for 1st level of childs:
<h1>{{ category.name }}</h1>
<q-img :src="category.image"/>
<p>{{ category.description }}</p>
<div class="row q-col-gutter-md">
<div class="col-md-4 col-xs-6" v-for="subcategory in category.childs" :key="subcategory.id">
<div class="block q-pa-md">
<router-link :to="'/categories/' + subcategory.id">
<h4>{{ subcategory.name }}</h4>
</router-link>
</div>
</div>
</div>
How would I do some repeating nested childs ?
Dynamic Route Matching feature of Vue router allows to define a dynamic parameter which captures more than one segment of the path by defining your route as /categories/:categoryId+ (router uses path-to-regex to match)
When defined this way, route as /categories/outfit/jackets/mountains will have a categoryId parameter with value outfit/jackets/mountains, which can be passed into the component, easily parsed and worked with...
See my example below:
Vue.config.devtools = false
Vue.config.productionTip = false
const categories = [{
name: 'outfit',
childs: [{
name: 'jackets',
childs: [{
name: 'mountains'
}]
},
{
name: 'pants',
childs: [{
name: 'short'
}, {
name: 'long'
}]
},
{
name: 'boots',
childs: [{
name: 'barefoot'
}, {
name: 'mountains'
}]
}
]
},
{
name: 'knives',
childs: [{
name: 'hunting'
},
{
name: 'souvenirs'
},
{
name: 'kitchen'
},
{
name: 'skinning'
},
{
name: 'throwing'
}
]
}
]
const cat = Vue.component('categories', {
data: function() {
return {
categories: categories // in real life, this data is shared for example using Vuex
}
},
template: `
<div>
<template v-for="cat in categories">
<router-link :to="'/categories/'+cat.name" :key="cat.name"> {{ cat.name }} </router-link>
</br>
</template>
</div>
`
})
const catView = Vue.component('category-view', {
data: function() {
return {
categories: categories // in real life, this data is shared for example using Vuex
}
},
props: ['categoryId'],
template: `
<div>
<hr>
<router-link :to="parentCategoryPath"> <- Back </router-link>
<div>Params: {{ categoryId }}</div>
<hr>
<template v-if="categoryDefinition && categoryDefinition.childs">
<template v-for="cat in categoryDefinition.childs">
<router-link :to="$route.path+ '/' +cat.name" :key="cat.name"> {{ cat.name }} </router-link>
</br>
</template>
</template>
</div>
`,
computed: {
categoryDefinition() {
let subCategory = this.categories.find(cat => cat.name === this.categoryId[0]);
for (i = 1; i < this.categoryId.length; i++) {
subCategory = subCategory.childs.find(cat => cat.name === this.categoryId[i])
}
return subCategory
},
parentCategoryPath() {
return '/categories/' + this.categoryId.slice(0, -1).join('/')
}
}
})
const router = new VueRouter({
base: '/js',
mode: 'history',
routes: [{
path: '/',
redirect: '/categories',
},
{
path: '/categories',
component: cat,
},
{
path: '/categories/:categoryId+',
component: catView,
props: route => ({
categoryId: route.params.categoryId.split('/')
})
}
]
})
const vm = new Vue({
el: '#app',
router,
data: function() {
return {}
}
})
hr {
border: none;
border-top: 3px double #333;
color: #333;
overflow: visible;
text-align: center;
height: 5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue-router/3.5.1/vue-router.min.js" integrity="sha512-c5QVsHf336TmWncPySsNK2ncFiVsPEWOiJGDH/Le/5U6q1hU6F5B7ziAgRwCokxjmu4HZBkfWsQg/D/+X3hFww==" crossorigin="anonymous"></script>
<div id="app">
{{ $route.path }}
<router-view></router-view>
</div>
Related
My hunch is that there is some hydration mismatch where the FontAwesomeIcon was not rendered on the server (only the span) and then on the client both child nodes of the NuxtLink were rendered (the svg and the span), prompting Nuxt to render the span twice.
The console does not return an error, though.
Any thoughts on how to debug this?
This is the Vue component:
<template>
<ul v-if="routes.length > 0" class="col-span-2 flex flex-col">
<li v-for="(item, i) in routes" :key="item.name">
<NuxtLink :to="item.path" target="_blank">
<FontAwesomeIcon :icon="item.icon" class="mr-3" fixed-width />
<span>{{ item.title }}</span>
</NuxtLink>
</li>
</ul>
</template>
<script lang="ts">
export default defineComponent({
props: {
links: {
type: Array,
default: () => ["instagram", "facebook", "email"],
},
},
computed: {
routes() {
return [
{
name: "instagram",
path: "https://www.instagram.com/insta.name/",
title: "Instagram",
icon: ["fab", "instagram"],
},
{
name: "facebook",
path: "https://www.facebook.com/fb.name",
title: "Facebook",
icon: ["fab", "facebook"],
},
{
name: "email",
path: "mailto:hello#example.com",
title: "Email",
icon: ["fas", "envelope"],
},
].filter((e) => this.links.includes(e.name));
},
},
});
</script>
Using Vue2 I have an array of objects in data which have an html string rendered in a v-for loop. Part of each string is a prop, which renders correctly initially. However, when the prop value is updated with v-model the data in the v-for loop is not updated.
jsfiddle: When the input is changed from "Bob" to "Sally" all instances should change, but those in the for-loop do not.
html
<div id="app">
<h2>Testing</h2>
<ul>
<li v-for="statement in statements" v-html="statement.text"></li>
</ul>
<input v-model="name" placeholder="edit name">
<p>Name is: {{ name }}</p>
<p class="italic">Outside loop: <b>{{name}}</b> likes dogs.</p>
</div>
vue
new Vue({
el: "#app",
data: function() {
return {
statements: [
{
id: 'food',
text: '<b>'+ this.name + '</b> likes to eat ice cream.',
},
{
id: 'fun',
text: 'Running is the favorite activity of <b>'+ this.name + '</b>',
},
],
}
},
props: {
name: {
type: String,
default: 'Bob',
},
},
})
The code has been simplified - the actual HTML strings have ~3 variables each that need to update, and are at different locations in each string, so I can't think of another way to replace the values when they are updated, while preserving the html tags. This is intended to be a single-page vue application, but is using Laravel and blade for some general page formatting.
name should be in data, not in props (it's not coming from a parent, it's just reactive data, which needs to be tracked for changes internally - inside this component).
statements should be in computed, because you want Vue to update it whenever its reactive references (e.g: this.name) change. Besides, this is not what you think it is inside the data function.
See it working:
new Vue({
el: "#app",
data: () => ({
name: 'Bob'
}),
computed: {
statements() {
return [
{
id: 'food',
text: '<b>'+ this.name + '</b> likes to eat ice cream.',
},
{
id: 'fun',
text: 'Runing is the favorite activity of <b>'+ this.name + '</b>',
},
]
}
},
})
body {
background: #20262E;
padding: 20px;
font-family: Helvetica;
}
#app {
background: #fff;
border-radius: 4px;
padding: 20px;
}
li {
margin: 8px 0;
}
h2 {
font-weight: bold;
margin-bottom: 15px;
}
p.italic {
font-style: italic;
}
<script src="https://v2.vuejs.org/js/vue.min.js"></script>
<div id="app">
<h2>Testing</h2>
<ul>
<li v-for="(statement, key) in statements" v-html="statement.text" :key="key"></li>
</ul>
<input v-model="name" placeholder="edit name">
<p>Name is: {{ name }}</p>
<p class="italic">Outside loop: <b>{{name}}</b> likes dogs.</p>
</div>
If you're trying to create a reusable component which takes in a person (with some values) and creates the statements based on those values and also allows editing the person's values, here's how to do it:
Vue.component('person-editor', {
template: '#person-editor-tpl',
props: {
person: {
type: Object,
default: () => ({})
}
},
data: () => ({
details: [
{ name: 'name', placeholder: 'Name' },
{ name: 'fun', placeholder: 'Running', label: 'Favorite activity' },
{ name: 'food', placeholder: 'pizza', label: 'Favorite food'}
]
}),
methods: {
update(payload) {
this.$emit('update:person', { ...this.person, ...payload })
},
getDetailLabel(d) {
return d.label || (d.name[0].toUpperCase() + d.name.slice(1))
}
}
});
Vue.component('person-details', {
template: '#person-details-tpl',
props: {
person: {
type: Object,
default: () => ({})
}
},
data: () => ({
statements: [
{ id: 'food', text: p => `<b>${p.name}</b> likes to eat ${p.food}.` },
{ id: 'fun', text: p => `${p.fun} is the favorite activity of <b>${p.name}</b>` }
]
})
})
new Vue({
el: "#app",
data: () => ({
persons: [
{ name: 'Jane', food: 'apples', fun: 'Hiking' },
{ name: 'Jack', food: 'pizza', fun: 'Sleeping' }
]
}),
methods: {
updatePerson(key, value) {
this.persons.splice(key, 1, value);
}
}
})
label {
display: flex;
}
label > span {
width: 150px;
}
<script src="https://v2.vuejs.org/js/vue.min.js"></script>
<div id="app">
<template v-for="(person, key) in persons">
<hr v-if="key" :key="`hr-${key}`">
<person-details :person="person"
:key="`details-${key}`"
></person-details>
<person-editor :person.sync="person"
:key="`editor-${key}`"
#update:person="updatePerson(key, person)"></person-editor>
</template>
</div>
<template id="person-editor-tpl">
<div>
<template v-for="detail in details">
<label :key="detail.name">
<span v-text="getDetailLabel(detail)"></span>
<input :value="person[detail.name]"
#input="e => update({ [detail.name]: e.target.value })">
</label>
</template>
</div>
</template>
<template id="person-details-tpl">
<ul>
<li v-for="(statement, key) in statements" v-html="statement.text(person)"></li>
</ul>
</template>
I separated the editor and the display in two separate components.
Because I had to define the components on the Vue instance it's a bit crammed in this example, but it looks a lot more elegant when using sfcs (and each component is a standalone .vue file).
I have an object in Portfolio.vue like this:
data() {
return {
portfolio: {
front: [
{ description: '1. project ', src: require("../assets/sample.jpg"), slug: 'first'},
{ description: '2. project', src: require("../assets/sample.jpg"), slug: 'second' },
{ description: '3. project', src: require("../assets/sample.jpg"), slug: 'third' },
]
}
}
}
Portfolio.vue:
<div class="">
<div v-for="(data,index) in portfolio.front" :key="index">
<router-link :to="'/portfolio/'+data.slug"> <div class="element" :data-description="data.description">
<img :src="data.src " alt="">
</div>
</router-link>
</div>
</div>
PortfolioProduct.vue
<template>
<div>
<p>I want to take data to here. In here, i have to reach the data like this: portfolio.front.description</p>
</div>
</template>
<script>
export default {
props: {
},
}
</script>
My routes:
const routes = [
{
path: "/portfolio",
name: "Portfolio",
component: Portfolio,
},
{
path: "/portfolio/:id",
name: "PortfolioProduct",
component: PortfolioProduct,
props: true,
},
];
I want to take data from Portfolio.vue to PortfolioProduct.vue , i couldn't solve. I'm using vue js3 , if you help me i will be glad. Thank you
You could probably use the template literal, changing the router-link would be like so:
<router-link :to="`/portfolio/${data.slug}`"> <div class="element" :data-description="data.description">
Can anyone help with this problem? How can I emit two properties from child component to parent on select input change? I can submit the value, see below, but would like to emit the value and the name property of segmentLocations object. This is the child component:
<template>
<div class="container">
<div>
<select v-model="selectedSegmentValue" v-on:change="$emit('selectLocation', $event.target.value)">
<option selected value="">Choose your location...</option>
<option v-for="segmentLocation in segmentLocations"
:value="segmentLocation.value"
:key="segmentLocation.value">
{{ segmentLocation.name }}>
</option>
</select>
</div>
</div>
</template>
<script>
export default {
data() {
return {
segmentLocations: [
{ value: "Residential", name: 'Residential building' },
{ value: "Workplace", name: 'Workplace' },
{ value: "Hospitality", name: 'Hospitality or Retail' },
{ value: "Real Estate", name: 'Real Estate' },
{ value: "Commercial Parking", name: 'Commercial Parking' },
{ value: "Fleets", name: 'Fleets' },
{ value: "Cities & Governments", name: 'Cities & Governments' },
{ value: "Corridor", name: 'Highway, Corridor or Petrol Station' }
],
}
}
};
</script>
And this is the parent:
<template>
<Segments
v-on:selectLocation="quote.selectedSegmentValue = $event"
:selectedValue="quote.selectedSegmentValue">
</Segments>
</template>
<script>
export default {
data() {
return {
quote: {
selectedSegmentValue: "",
selectedSegmentName: ""
},
};
},
</script>
I think the existing answers and mine share a similar technique, but I created a couple of simplified sample components based on your components.
Child component:
<template>
<div class="emit-two-properties">
<div class="form-group">
<label for="segment-location">Segment Location</label>
<select class="form-control" id="segment-location"
v-model="segmentLocation" #change="selectSegmentLocation">
<option v-for="(segLoc, index) in segmentLocations" :key="index"
:value="segLoc">{{ segLoc.name }}</option>
</select>
</div>
</div>
</template>
<script>
export default {
data() {
return {
segmentLocation: {},
segmentLocations: [
{ value: "Residential", name: 'Residential building' },
{ value: "Workplace", name: 'Workplace' },
{ value: "Hospitality", name: 'Hospitality or Retail' },
{ value: "Real Estate", name: 'Real Estate' },
{ value: "Commercial Parking", name: 'Commercial Parking' },
{ value: "Fleets", name: 'Fleets' },
{ value: "Cities & Governments", name: 'Cities & Governments' },
{ value: "Corridor", name: 'Highway, Corridor or Petrol Station' }
],
}
},
methods: {
selectSegmentLocation() {
this.$emit('select-segment-location-event', this.segmentLocation);
}
}
}
</script>
Parent component:
<template>
<div class="parent">
<h4>Parent.vue</h4>
<div class="row">
<div class="col-md-6">
<form #submit.prevent="submitForm">
<emit-two-properties #select-segment-location-event="updateSegmentLocation" />
<button class="btn btn-secondary">Submit</button>
</form>
<p><span>Selected Segment Location Value:</span>{{ segmentLocation.value }}</p>
<p><span>Selected Segment Location Name:</span>{{ segmentLocation.name }}</p>
</div>
</div>
</div>
</template>
<script>
import EmitTwoProperties from './EmitTwoProperties'
export default {
components: {
EmitTwoProperties
},
data() {
return {
segmentLocation: {}
}
},
methods: {
updateSegmentLocation(segLoc) {
this.segmentLocation = segLoc;
}
}
}
</script>
you can create a method to get name and value from event.target (remove value from the end of child emit):
changeSelectedSegment(selected){
this.selectedSegmentName = selected.name
this.selectedSegmentValue = selected.value
}
in the parent change v-on:selectLocation to v-on:selectLocation="changeSelectedSegment($event)"
you can define a method like this (this method emit an object with name and value properties to parent
)
methods: {
selectLocation(event){
if(event.target.value !== ''){
const item = this.segmentLocations.find( item => item.value === event.target.value)
this.$emit('selectLocation', {
name: item.name,
value: event.target.value
})
}
}
},
and change this line :
<select v-model="selectedSegmentValue" v-on:change="$emit('selectLocation', $event.target.value)">
to this:
<select v-model="selectedSegmentValue" v-on:change="selectLocation">
I have an array of names that I loop over using v-for I'm trying to filter these results when a user starts typing in a search box.
I've added my code below for reference if I do my loop as v-for="entry in entries" then it output the array but doesn't work with the computed and filteredList function
<template>
<div class="container-flex">
<div class="entries">
<div class="entries__header">
<div class="entries__header__title">
<p>Entries</p>
</div>
<div class="entries__header__search">
<input
type="text"
name="Search"
class="input input--search"
placeholder="Search..."
v-model="search">
</div>
</div>
<div class="entries__content">
<ul class="entries__content__list">
<li v-for="entry in filteredList">
{{ entry.name }}
</li>
</ul>
</div>
</div>
</div>
</template>
<script>
import addEntry from '#/components/add-entry.vue'
export default {
name: 'entry-list',
search: '',
components: {
addEntry
},
data: function() {
return {
entries: [
{
name: 'Paul'
},
{
name: 'Barry'
},
{
name: 'Craig'
},
{
name: 'Zoe'
}
]
}
},
computed: {
filteredList() {
return this.entries.filter(entry => {
return entry.name.toLowerCase().includes(this.search.toLowerCase())
})
}
}
}
Try to move the search prop in to data option like this:
export default {
name: 'entry-list',
components: {
addEntry
},
data: function() {
return {
search: '',
entries: [
{
name: 'Paul'
},
{
name: 'Barry'
},
{
name: 'Craig'
},
{
name: 'Zoe'
}
]
}
},
computed: {
filteredList() {
if(this.search === '') return this.entries
return this.entries.filter(entry => {
return entry.name.toLowerCase().includes(this.search.toLowerCase())
})
}
}
}
Also add a check if the search prop is empty to return the full entries list.
Demo fiddle