Router links and language changes - vuejs2

I have a list of router links of the form
<router-link :to="'/' + $store.getters.lang" tag='v-btn' class="flat" active-class="active" exact>{{$t('home')}}</router-link>
<router-link :to="'/' + $store.getters.lang + '/pest'" tag='v-btn' class="flat">{{$t('factsheets')}}</router-link>
<router-link :to="'/' + $store.getters.lang + '/control'" tag='v-btn' class="flat" active-class="active">{{$t('control')}}</router-link>
which I want to re-format in the form of ...
<router-link
v-for="(item,key) in items"
:key="key"
#click=""
:to="'/' + $store.getters.lang + item.path"
tag='v-btn' class="flat" active-class="active" exact
**** v-html="item.title"> ****
<!--$t('{{item.title}}')-->
</router-link>
My problem is at the **** line where the only way I can get the title of the link to display is via the v-html directive, but in doing so, I lose the ability to have that label change if the site's language option is subsequently switched (say, from EN to ES).
How can I integrate the $t(...) function into the v-for loop?

v-html="$t(item.title)" should work, simple example:
Vue.locale('en', {
foo: 'foo 1',
bar: 'bar 1',
baz: 'baz 1'
})
Vue.locale('el', {
foo: 'foo 2',
bar: 'bar 2',
baz: 'baz 2'
})
new Vue({
el: '#demo',
data() {
return {
items: ['foo', 'bar', 'baz'] //object name
}
},
methods: {
change() {
let current = this.$lang.lang
//toggle lang
this.$lang.lang = (current === 'en') ? 'el' : 'en'
}
}
})
<div id="demo">
<button #click="change">change</button>
<div v-for="item in items">
<p v-html="$t(item)"></p>
</div>
</div>
Working JSFIddle

Related

How can I modify template variables declared with :set?

I have the following code (vue v3.2.33)
<template v-for="route of routes" :key="route.name">
<li :set="open = false">
<div class="collapse"
#click="open = !open"
:class="[open ? 'collapse-is-open' : '']">
</div>
</li>
</template>
Basically each li has a collapse and opens with the class collapse-is-open.
I tried to use a variable with the :set attribute and modify that variable but it doesn't seem to work.
If that can't be working, what would be the other way of doing this ? An object based on the value I set in :key that keeps track of all the states?
:set is syntax for the v-bind directive. It binds a property/attribute on the element, but there is no such set property for <li>. I think you're trying to create an ad-hoc variable named open for each <li>, but that feature doesn't exist in Vue (perhaps you're thinking of petite-vue's v-scope).
One solution is to create a separate data structure that contains the open-state of each <li>, and use that to update the class binding:
export default {
data() {
return {
routes: [⋯],
open: {} // stores open-state of each route item
}
}
}
<template v-for="route of routes" :key="route.name">
<li>
<div class="collapse"
#click="open[route.name] = !open[route.name]"
:class="[open[route.name] ? 'collapse-is-open' : '']">
</div>
</li>
</template>
demo 1
Alternatively, you could add that property to each array item in routes:
export default {
data() {
return {
routes: [
{
name: 'Route 1',
open: false,
},
{
name: 'Route 2',
open: false,
},
{
name: 'Route 3',
open: false,
},
],
}
},
}
<template v-for="route of routes" :key="route.name">
<li>
<div class="collapse"
#click="route.open = !route.open"
:class="[route.open ? 'collapse-is-open' : '']">
</div>
</li>
</template>
demo 2

Vue Component Select option from list item

I'm new to Vue. I have an autocomplete component that used AXIOS to get the data. When I type in at least 3 characters, it does display the results in a list item, however I can't seem to figure out how to actually select the option and populate the text field.
Here is the code
<template>
<div>
<input type="text" placeholder="Source client" v-model="query" v-on:keyup="autoComplete" #keydown.esc="clearText" class="form-control">
<div class="panel-footer" v-if="results.length">
<ul class="list-group">
<li class="list-group-item" v-for="result in results">
<span> {{ result.name + "-" + result.oid }} </span>
</li>
</ul>
</div>
</div>
</template>
<script>
import axios from 'axios'
export default{
data(){
return {
selected: '',
query: '',
results: []
}
},
methods: {
clearText(){
this.query = ''
},
autoComplete(){
this.results = [];
if(this.query.length > 2){
axios.get('/getclientdata',{params: {query: this.query}}).then(response => {
this.results = response.data;
});
}
}
}
}
</script>
Any help would be appreciated!
Writing an autocomplete is a nice challenge.
You want this component to act like an input. That means that if you'd use your autocomplete component, you will probably want to use it in combination with v-model.
<my-autocomplete v-model="selectedModel"></my-autocomplete>
To let your component work in harmony with v-model you should do the following:
Your component should accept a prop named value, this may be a string, but this may also be an object.
In the example above the value of selectedModel will be available inside your autocomplete component under '{{value}}' (or this.value).
To update the value supplied you $emit an input event with the selected value as second parameter. (this.$emit('input', clickedItem))
Inside your autocomplete you have query, which holds the search term. This is a local variable and it should be. Don't link it to value directly because you only want to change the value when a user selects a valid result.
So to make the example work you could do the following:
<template component="my-autocomplete">
<div>
<input type="text" placeholder="Source client" v-model="query" v-on:keyup="autoComplete" #keydown.esc="clearText" class="form-control">
<div class="panel-footer" v-if="results.length">
<ul class="list-group">
<li class="list-group-item" v-for="result in results" #click="selectValue(result)">
<span> {{ result.name + "-" + result.oid }} </span>
</li>
</ul>
</div>
</div>
</template>
<script>
//import axios from 'axios'
export default{
props: ['value'],
data(){
return {
selected: '',
query: '',
results: []
}
},
methods: {
clearText(){
this.query = ''
},
autoComplete(){
this.results = [];
if(this.query.length > 2){
this.results = [{ name: 'item 1', oid: '1' }, {name: 'item 2', oid: '2'}];
//axios.get('/getclientdata',{params: {query: this.query}}).then(response => {
//this.results = response.data;
//});
}
},
selectValue(selectedValue) {
this.$emit('input', selectedValue);
}
}
}
</script>
I commented out the axios part for demonstration purposes.
To verify the workings:
<template>
<div>Autocomplete demo:
<my-autocomplete v-model="selectedRow">
</my-autocomplete>
Selected value: <pre>{{selectedRow}}</pre>
</div>
</template>
<script>
export default {
data() {
return {
selectedRow: null
}
}
}
</script>
Now the autocomplete does what it should, it allows you to search and pick a valid result from the list.
One problem remains though. How do you display the selected value. Some options are:
Copy the value to query, this works seemlessly if all your options are strings
Copy the value of result.name to query, this would make the solution work
Accept a scoped slot from the parent component which is responsible for displaying the selected item.
I will demonstrate option 2:
<template component="my-autocomplete">
<div>
<input type="text" placeholder="Source client" v-model="query" v-on:keyup="autoComplete" #keydown.esc="clearText" class="form-control">
<div class="panel-footer" v-if="results.length">
<ul class="list-group">
<li class="list-group-item" v-for="result in results" #click="selectValue(result)">
<span> {{ result.name + "-" + result.oid }} </span>
</li>
</ul>
</div>
</div>
</template>
<script>
//import axios from 'axios'
export default{
props: ['value'],
data(){
return {
selected: '',
query: '',
results: []
}
},
mounted() {
if (this.value && this.value.name) {
this.query = this.value.name;
}
},
// Because we respond to changes to value
// we do not have to do any more work in selectValue()
watch: {
value(value) {
if (this.value && this.value.name) {
this.query = this.value.name;
} else {
this.query = '';
}
}
},
methods: {
clearText(){
this.query = ''
},
autoComplete(){
this.results = [];
if(this.query.length > 2){
this.results = [{ name: 'item 1', oid: '1' }, {name: 'item 2', oid: '2'}];
//axios.get('/getclientdata',{params: {query: this.query}}).then(response => {
//this.results = response.data;
//});
}
},
selectValue(selectedValue) {
this.$emit('input', selectedValue);
}
}
}
</script>
A working example can be found here: https://jsfiddle.net/jangnoe/4xeuzvhs/1/

How do I implement breadcrumbs with complete links in vue.js

I have managed to implement breadcrumbs in my vue.js application using npm.
But I'd like my breadcrumb to display a link to previous page, with the link to the current page(id included) next to it. Like this:
Home Page > Details page (as clickeble links)
I've tried following the example from this link https://www.npmjs.com/package/vue-2-breadcrumbs]
this is my index.js file
import Vue from 'vue'
import Details from '#/components/Details'
import Router from 'vue-router'
import HomePage from '#/components/HomePage'
import VueBreadcrumbs from 'vue-2-breadcrumbs'
Vue.use(Router)
Vue.use(VueBreadcrumbs,{
template:
' <ul class="breadcrumb">\n' +
' <li v-for="(crumb, key) in $breadcrumbs" v-if="crumb.meta.breadcrumb" :key="key" class="breadcrumb-item active" aria-current="page">\n' +
' <router-link :to="{ path: getPath(crumb) }">{{ getBreadcrumb(crumb.meta.breadcrumb) }}</router-link>' +
' </li>\n' +
' </ul>\n'
})
export default new Router({
routes: [
{
path: '/',
component: HomePage,
name: 'HomePage',
meta: {
breadcrumb: 'Home Page',
},
},
{
path: '/Details/:id',
component: Details,
meta:{
breadcrumb: function () {
const {name} = this.$route;
return `${name}`;
}
},
name: 'Details',
props: true
}
]
})
It all compiles, and it does render (but only one link, which is to the current page). Now I'd like to add the history link to my breadcrumb (the home page link). How do I do that?
Ok, so I got some inspiration from all of your comments and this is how I fixed it:
So I added one extra <li> to hold the "home page" and the second <li> for looping over the current page... I also wrapped everything inside a root- so no errors about double root elements would occur.
Vue.use(VueBreadcrumbs,{
template:
' <div v-if="$breadcrumbs.length" aria-label="breadcrumb">\n' +
' <ol class="breadcrumb">\n' +
' <li class="breadcrumb-item">Home</li>' +
' <li v-for="(crumb, key) in $breadcrumbs" v-if="crumb.meta.breadcrumb" :key="key" class="breadcrumb-item active" aria-current="page">\n' +
' <router-link :to="{ path: getPath(crumb) }">{{ getBreadcrumb(crumb.meta.breadcrumb) }}</router-link>' +
' </li>\n' +
' </ol>\n' +
' </div>'
})
Thank you everyone for your help!
Add another router-link to your template:
<div id="root">
<router-link :to="/">Home</router-link>
<ul class="breadcrumb">
<li v-for="(crumb, key) in $breadcrumbs" v-if="crumb.meta.breadcrumb" :key="key" class="breadcrumb-item active" aria-current="page">
<router-link :to="{ path: getPath(crumb) }">{{ getBreadcrumb(crumb.meta.breadcrumb) }}</router-link>
</li>
</ul>
</div>
Edit:
Declare your template in this way:
Vue.use(VueBreadcrumbs, {
template: ```
<div id="root">
<router-link :to="/">Home</router-link>
<ul class="breadcrumb">
<li v-for="(crumb, key) in $breadcrumbs" v-if="crumb.meta.breadcrumb" :key="key" class="breadcrumb-item active" aria-current="page">
<router-link :to="{ path: getPath(crumb) }">{{ getBreadcrumb(crumb.meta.breadcrumb) }}</router-link>
</li>
</ul>
</div>
```
})
You can create your own breadcrumb
In file 'index.ts' from router
export let activedRoutes: RouteRecord[] = []
router.beforeEach((to, from, next) => {
activedRoutes = [];
to.matched.forEach((record) => { activedRoutes.push(record) })
next();
});
In route definition
{
path: 'my-path-route',
name: 'MyNameRoute',
component: () => import('./MyComponent/MyComponent.component.vue'),
meta: {
title: 'Title',
icon: 'my-icon',
breadcrumb: { label: 'Label of bradcrumb', routeName: 'MyNameRoute'}
},
}
Create a BreadbrumbComponent
import Vue from 'vue';
import Component from 'vue-class-component';
import { Watch } from 'vue-property-decorator';
import { activedRoutes } from '#/router';
#Component({})
export default class BreadcrumbsComponent extends Vue {
breadcrumbs = []
#Watch('$route')
changeRoute() {
this.breadcrumbs = activedRoutes.map(e => e.meta?.breadcrumb).filter(e => e);
}
created() {
this.changeRoute()
}
}
In html BreadcrumbComponent
<template>
<div class="breadcrumbs" v-if="breadcrumbs.length > 1">
<ul class="v-breadcrumbs">
<template v-for="(item, i) in breadcrumbs">
<li :key="item.label">
<router-link
class="v-breadcrumbs__item"
:to="{ name: item.routeName }"
v-html="item.label"
:class="{ disabled: breadcrumbs.length - 1 == i }"
/>
</li>
<li
v-if="breadcrumbs.length - 1 > i"
class="v-breadcrumbs__divider"
:key="item.label"
>
<v-icon>mdi-chevron-right</v-icon>
</li>
</template>
</ul>
</div>
</template>
In scss of BreadcrumbComponent
.breadcrumbs {
.disabled {
text-decoration: none;
color: rgba(0, 0, 0, 0.38);
cursor: default;
}
}
Now is only use your component where wish
You can use the breadcrumb of vuetify to complement
https://vuetifyjs.com/en/components/breadcrumbs/#large

How to get data from component

I am new to Vue.js.
list.vue:
<template>
<div class="m-products-list">
<ul #mouseover="over">
<Item
v-for="(item,idx) in parentList"
location="item.location"
:key="idx"
:meta="item"/>
</ul>
</div>
</template>
<script>
export default {
...
methods: {
over: function (e) {
let dom = e.target;
let tag = dom.tagName.toLowerCase();
if (tag === 'dd') {
console.log(dom.getAttribute('location'))
}
}
}
}
</script>
The Item is from its parent component. And I want to get item.location in over() when I mouseover an item, but console.log always returns null. Anyone have an idea?
This is technically possible (but there may be a better alternative shown in the next section) by setting a data-* attribute in the Item.
// Item.vue
<li :data-location="location" class="item" ... >
new Vue({
el: '#app',
data() {
return {
items: [
{id: 1, location: 'New York'},
{id: 2, location: 'Los Angeles'},
{id: 3, location: 'Chicago'},
]
}
},
components: {
Item: {
props: ['location'],
template: `<li :data-location="location" class="item">{{location}}</li>`,
}
},
methods: {
over(e) {
console.log(e.target.dataset.location)
}
}
})
<script src="https://unpkg.com/vue#2.6.7/dist/vue.min.js"></script>
<div id="app">
<ul #mouseover="over">
<Item v-for="item in items"
:key="item.id"
:location="item.location" />
</ul>
</div>
A better solution that doesn't require DOM manipulation would be to use the data model in Vue and to move the mouseover event listener to the Item:
Change the argument of over() to the location name (previously the event object):
methods: {
over(location) {
/* ... */
}
}
Move the #mouseover event-listener annotation from ul to the Item in the template, and pass the item.location as an argument:
<ul>
<Item v-for="item in items" #mouseover="over(item.location)" ... />
</ul>
Edit the Item's template to forward its mouseover event to the parent:
// Item.vue
<li #mouseover="$emit('mouseover', $event)" ... >
new Vue({
el: '#app',
data() {
return {
items: [
{id: 1, location: 'New York'},
{id: 2, location: 'Los Angeles'},
{id: 3, location: 'Chicago'},
]
}
},
components: {
Item: {
props: ['location'],
template: `<li #mouseover="$emit('mouseover', $event)" class="item">{{location}}</li>`,
}
},
methods: {
over(location) {
console.log(location)
}
}
})
<script src="https://unpkg.com/vue#2.6.7/dist/vue.min.js"></script>
<div id="app">
<ul>
<Item v-for="item in items"
:key="item.id"
:location="item.location"
#mouseover="over(item.location)" />
</ul>
</div>

Accessing siblings elements in a v-for

New to Vuejs 2, need a bit of help to understand how this would be done.
So, I have a template that looks like the following:
<template>
<div class="wrapper">
<div class="item" v-for="item in items">
<div class="item">
<div class="title">{{ item.title }}</div>
<div class="action">
<a href="#" #click.prevent="editSettings(item, $event)">
Click Me
</a>
</div>
</div>
<div v-if="showSettings" class="settings">
<!-- Settings component for this item. -->
<settings-panel item="item"></settings-panel>
</div>
</div>
</div>
</template>
The scripts looks like this:
export default {
data() {
return {
items: [
// This data is from an API usually and only
// shown here for example purposes
{ id: 1, title: 'Title 1' },
{ id: 2, title: 'Title 2' },
{ id: 3, title: 'Title 3' },
{ id: 4, title: 'Title 4' }
],
showSettings: false
};
},
methods: {
editSettings(item, event) {
// Only show the clicked items settings, not all
this.showSettings = !showSettings;
}
}
}
What is the easiest and cleanest approach to only showing the settings panel for the clicked item? Right now, if I click the Click Me settings, all settings open up. The data listed in the code is only there for example, but is coming from an API so I don't have much to do with the manipulation of that data.
Can anyone help lead me in the right direction?
Instead of using showSettings add a selectedItem.
export default {
data() {
return {
items: [
// This data is from an API usually and only
// shown here for example purposes
{ id: 1, title: 'Title 1' },
{ id: 2, title: 'Title 2' },
{ id: 3, title: 'Title 3' },
{ id: 4, title: 'Title 4' }
],
selectedItem: null
};
},
methods:{
selectItem(item){
if (this.selectedItem === item) this.selectedItem = null
else this.selectedItem = item
}
}
}
And modify your template to set the selectedItem when the item is clicked and show the settings only if the selectedItem is the current item.
<template>
<div class="wrapper">
<div class="item" v-for="item in items">
<div class="item">
<div class="title">{{ item.title }}</div>
<div class="action">
<a href="#" #click.prevent="selectItem(item)">
Click Me
</a>
</div>
</div>
<div v-if="selectedItem === item" class="settings">
<!-- Settings component for this item. -->
<settings-panel item="item"></settings-panel>
</div>
</div>
</div>
</template>
The above will show the settings for the clicked item if the currently selected item is not the item that was clicked. If the currently selected item is the one that was clicked, it will hide the settings.