Vue - Translation in single file component - vue.js

I'm using vue-i18n for my translations and it works great! But when I'm using the this.$t() function inside the data function of a single file component the translation is not working.
<template>
<VFooter
app
height="auto"
color="secondary">
<VLayout
justify-center
row
wrap>
<VBtn
v-for="link in links"
:key="link.name"
:to="{ name: link.name }"
flat
round
active-class>
{{ link.label }}
</VBtn>
<VFlex
py-3
text-xs-center
xs12>
©{{ currentYear }} — <strong>{{ $t('site_name') }}</strong>
</VFlex>
</VLayout>
</VFooter>
</template>
<script>
export default {
name: 'TheSiteFooter',
data() {
return {
links: [
{ name: 'what-is-pinshop', label: this.$t('footer.what_is_pinshop') },
{ name: 'contact-us', label: this.$t('footer.contact_us') },
{ name: 'cookie-policy', label: this.$t('footer.cookie_policy') },
{ name: 'privacy-policy', label: this.$t('footer.privacy_policy') },
{ name: 'terms-and-conditions', label: this.$t('footer.terms_and_conditions') },
],
};
},
computed: {
currentYear() {
return new Date().getFullYear();
},
},
};
</script>
But, if I instead change data with only the key of translation and then in my template use e.g {{ $t('footer.what_is_pinshop') }} the translation loaded is correct. Why does this happen? I'm using the beforeEnter router guard to change the translation. I have followed this example.
UPDATE
If I put links as a computed property the translation works. So why it does not happen only in data property? I also tried with this.$i18n.t(), but nothing

This is, because of the vue lifecycle. Push your link data into the array by using the created hook. Keep you data(model) clean and do not call functions in it. You call this up before all events and reactivity mechanisms have ever been registered.
lifecycle: https://v2.vuejs.org/v2/guide/instance.html
if you're interested how it works: https://github.com/kazupon/vue-i18n/tree/dev/src
UPDATE
I just showered and thought again about it. In depth this is because of the reactivity mechanism. You initialize your data with a function and vue cannot detect if the returned value has changed. See how it works: https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty in vue 3 this is replaced by https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Global_Objects/Proxy

Related

2 Way Databind components within Components

I am struggling to reuse my components.
I want to pass the data passed to my component as a prop to another component.
If I do that vue complains about a mutation of the prop.
Example:
I have contacts that I want to show on multiple location of my app.
For that I created a contact component to reuse it:
<template>
<div>
<input :value="contact.firstName" #input="$emit('update:contact', {...contact, firstName: $event.target.value})">
<Mother v-model:mother="contact.mother"/>
</div>
</template>
<script>
import Mother from '#/components/Mother'
export default {
name: 'Contact',
components: {
Mother
},
props: {
contact: Object,
},
emit: ['update:contact'],
methods: {
}
}
</script>
Every contact has a mother, mother are shown in other places not only in the contact component.
That is why I created a mother component, that is used by the contact.
<template>
<div>
<input :value="mother.lastName" #input="$emit('update:mother', {...mother, lastName: $event.target.value})">
</div>
</template>
<script>
export default {
name: 'Mother',
props: {
mother: Object,
},
emit: ['update:mother'],
methods: {
}
}
</script>
Now I want to be able to mutate the contact an the mother as well, and I want to be able to use two contact components on the same site.
If I use it the way explained I get this error:
ERROR Failed to compile with 1 error 09:17:25
error in ./src/components/Contact.vue
Module Error (from ./node_modules/eslint-loader/index.js):
/tmp/vue-example/src/components/Contact.vue
4:27 error Unexpected mutation of "contact" prop vue/no-mutating-props
✖ 1 problem (1 error, 0 warnings)
I have an example project showing my problem:
https://gitlab.com/FirstWithThisName/vue-example.git
Thanks for your help
First I need to assume a few points.
You wanted to use v-model.
You wanted the component to be chained.
Working Example here on Vue SFC Playground.
*Note that the import path is different on the example site.
App.vue
<template>
<Contact v-model="contact" />
{{ contact }}
</template>
... remaining code omitted
Contact.vue
<template>
<div>
<input v-model="localValue"/>
<Mother v-model="childValue" />
</div>
</template>
<script>
import Mother from "./Mother.vue"
export default {
name: "Contact",
components: {
Mother
},
props: {
modelValue: Object,
},
mounted(){
this.childValue = this.modelValue.mother
},
data: () => ({
localValue: "",
childValue: null
}),
watch:{
updatedData(){
this.$emit('update:modelValue', this.updatedData)
}
},
computed: {
updatedData() {
return { firstName: this.localValue, mother: this.childValue };
},
},
};
</script>
Mother.vue
<template>
<div>
<input
v-model="localValue"
#input="$emit('update:modelValue', updatedData)"
/>
</div>
</template>
<script>
export default {
name: "Mother",
props: {
modelValue: Object,
},
data: () => ({
localValue: "",
}),
computed: {
updatedData() {
return { ...this.modelValue, lastName: this.localValue };
},
},
};
</script>
As you might know, props cannot be mutated, so you will need to "make a copy" of the value on each component to process locally.
If Mother component are never going to be used separately, v-model can be split into v-on and v-bind instead.
Lastly, as for recommendation, chaining like this can become very messy if the data starts to grow or the depth level increases. You could just make another Wrapper component that contains Contact and Mother component that scales horizontally instead.
Depends on how complex your application will get.
One option is two-way data-binding as explained here:
https://v3.vuejs.org/guide/component-basics.html#using-v-model-on-components
So you basically emit the changes to the parent.
For more complex applications I wouldn't pass data that are used in multiple components as props, but use a store. Either a simple reactive object; with provide/inject or use something like Vuex.

Is it possible to use dynamic scoped slots to override column values inside <v-data-table>?

I'm trying to create a reusable table component that utilizes Vuetify's v-data-table component in order to group common aspects such as a search bar, table actions (refresh, create, etc.) and other features that all of my tables will have. However, I'm running into issues with implementing dynamic, scoped slots inside the table component to account for custom columns. Think of columns like actions, formatted ISO strings, etc.
Here's a simplified example of what I'm trying currently. In the example, I am passing the array customColumns to CustomDataTable.vue as a prop. customColumns has one element with two properties. The slotName property specifies the name of the slot that I'd like to reference in the parent component. The itemValue property specifies the header value that CustomDataTable.vue should override and replace with a scoped slot. The scoped slot is then used in the parent component to correctly format the date in the 'Created At' column.
Parent Component that is implementing the table component:
<template>
<custom-data-table
:items="items"
:headers="headers"
:customColumns="customColumns"
>
<template v-slot:custom-column="slotProps">
<span>{{ formatDate(slotProps.item.createdAt) }}</span>
</template>
</custom-data-table>
</template>
<script>
import CustomDataTableVue from '#/components/table/CustomDataTable.vue'
export default {
data: () => ({
items: [
{
id: 0,
createdAt: new Date().toISOString(),
...
},
...
],
headers: [
{
text: 'Created At',
value: 'createdAt',
sortable: true
},
...
],
customColumns: [
{
slotName: 'custom-column',
itemValue: 'createdAt'
}
]
})
}
</script>
CustomDataTable.vue
<template>
<v-data-table
:items="items"
:headers="headers"
>
<template v-for="column in customColumns" v-slot:item[column.itemValue]="{ item }">
<slot :name="column.slotName" :item="item"/>
</template>
</v-data-table>
</template>
<script>
export default {
name: 'custom-data-table',
props: {
items: {
type: Array,
required: true
},
headers: {
type: Array,
required: true
},
customColumns: {
type: Array
}
}
}
</script>
Is there a way to achieve this? The example does not override the column values and just displays the createdAt ISO string unformatted. I believe the problem might be coming from how I'm assigning the template's slot in CustomDataTable.vue, but I'm sure how else you could dynamically specify a template's slot. Any ideas?
I think the syntax for dynamic slots should be:
<template
v-for="column in customColumns"
v-slot:[`item.${column.itemValue}`]="{ item }"
>
<slot :name="column.slotName" :item="item"/>
</template>

How to store state of tabs based on route?

Is it possible to store different state of a <v-tab> component based on the route i'm currently on? What i'm basically trying to achieve is, have a single component like the code below that i use in several different routes, but have it remember the tab i'm currently at depending on the route.
<v-layout justify-space-around row>
<v-tab v-for="tab in navbarTabs" :key="tab.id">{{ tab.name }}</v-tab>
</v-layout>
data() {
return {
navbarTabs: [
{
id: 0,
name: "Home"
},
{
id: 1,
name: "Schedule"
},
{
id: 2,
name: "Tech card"
},
{
id: 3,
name: "Specifications"
},
{
id: 4,
name: "Control card"
},
{
id: 5,
name: "Packing"
}
]
}
}
I previously managed to achieve the desired result by having a few identical <v-tab> components that i visualised conditionally based on current route with v-if, but that solutions seems a bit crude and not scalable at all.
Can someone give me some pointers on how to achieve that? Thanks in advance!
The v-tabs component has a value prop which you can use to open a tab with a given key. And the v-tab component has a to prop which you can use to set the route of a tab. So essentially, you can use Vue Router params to select a tab and you can set the route using the v-tab's to prop.
You can define your route like this to pass the selectedTab param to the component:
routes: [
{ path: '/tabs/:selectedTab', component: TabsComponent, props: true },
]
And your TabsComponent can look something like this:
<template>
<v-layout justify-space-around row>
<v-tabs v-model="selectedTab">
<v-tab
v-for="tab in navbarTabs"
:key="tab.id"
:to=`/tabs/${tab.id}`
>{{ tab.name }}</v-tab>
</v-layout>
</template>
<script>
export default {
props: ['selectedTab'],
}
</script>

Why do some of my images placed in a Vue template disappear when content in filtered?

I've been put in charge of a Drupal website that has some content that is placed through a complex set of Vue files. I've never used Vue so I need some help with a critical problem we're facing.
I have a news section on the site that users can filter from a dropdown (by choosing a topic from the menu). This works fine. However, whenever you bring back a previously filtered news story, the featured image fails to load. The Vue template brings back the default image. But this only happens for SOME of the images.
As I mentioned, the Vue.js file is complex - it imports nearly a dozen other files that control other features of the site. This is the code from the the most applicable file:
export default {
props: {
url: {
type: String,
required: true,
},
tag: {
type: String,
required: false,
},
eyebrow: {
type: String,
required: false,
},
title: {
type: String,
required: true,
},
image: {
type: Object,
required: false,
},
},
computed: {
imageUrl() {
return this.image && this.image.url ? this.image.url : '/themes/my-theme/images/no-image.jpg';
}
},
template: `
<a :href="url" class="news-card">
<span class="news-card__image">
<img :src="imageUrl" :alt="image.alt">
</span>
<span class="news-card__text">
<span class="news-card__tag" v-if="tag" v-html="tag">
</span>
<p class="news-card__eyebrow" v-if="eyebrow">
{{ eyebrow }}
</p>
<h2 class="news-card__title">
{{ title }}
</h2>
</span>
</a>
`
};
I would post more, but I don't know what else is needed to diagnose a Vue issue. Sorry, I'm brand new to Vue and it's very confusing!
UPDATE
I've added the view.js code below:
import Vue from '#vue';
import { mapState } from 'vuex';
import ListingGrid from '/components/listing-grid';
import ListingNav from '/components/listing-nav';
import ProgramCard from '/components/program-card';
import ListingCard from '/components/listing-card';
import NewsCard from '/components/news-card';
import ListingFilters from '/components/listing-filters';
import ListingLoader from '/components/listing-loader';
import ListingStickyNav from '/components/listing-sticky-nav';
import TabSelect from '/components/tab-select';
import ajaxStore from './store/ajax';
import staticStore from './store/static';
export default (id) => {
new Vue({
el: `#${id}`,
components: {
ListingFilters,
ListingGrid,
ListingNav,
ProgramCard,
ListingCard,
NewsCard,
ListingLoader,
ListingStickyNav,
TabSelect,
},
data: {
lazy: true
},
store: id === 'listing-static' ? staticStore : ajaxStore,
computed: mapState({
grid() {
return this.$store.getters.filteredGrid;
},
filters: state => state.filters,
tabs: state => state.tabs,
loading: state => state.loading,
showLoader: state => state.infiniteLoading,
cardType: state => state.cardType
}),
image: {
type: Object,
default: function () {
return {
url: '/themes/my-theme/images/no-image.jpg',
alt: 'Default No Image Alt Text'
}
},
},
template: `
<main class="view__content v-listing vue-loaded" id="${id}">
<listing-sticky-nav>
<template slot="tab-content">
<tab-select
:tabs="tabs"
v-if="tabs.items"
>
</tab-select>
</template>
<template slot="panel">
<listing-filters
:filters="filters"
additional-classes="mobile"
>
</listing-filters>
</template>
</listing-sticky-nav>
<listing-filters
:filters="filters"
>
</listing-filters>
<listing-grid
:has-results="grid.length > 0"
:loading="loading"
:lazy="lazy">
<listing-nav
v-if="tabs"
:tabs="tabs"
slot="nav"
>
</listing-nav>
<template slot="grid">
<component
:is="cardType"
v-bind="item"
v-for="item, index in grid"
:key="index">
</component>
</template>
<template slot="empty">
No results found.
</template>
</listing-grid>
<span class="v-listing-footer" v-if="showLoader">
<listing-loader></listing-loader>
<span class="visually-hidden">loading</span>
</span>
</main>
`
});
}
Although I agree with the comments above that there are many lacking elements, I am a huge proponent of VueJS and I would like to help if possible.
First glaring issue I see is that the property image is not required. The computed property imageUrl() is defensive in that it will return a default value of '/themes/my-theme/images/no-image.jpg' if the property image is omitted.
However in your template code in the <img /> you are referencing the object key of 'alt' from the image object which may or may not be present.
I would highly recommend adding a computed property of:
imageAlt() {
return this.image && this.image.alt ? this.image.alt : 'Default No Image Alt Text';
}
Then in your template update the image tag as follows:
<img :src="imageUrl" :alt="imageAlt">
This will harden your code ensuring that error cannot happen. You may want to update the default Alt Text as it was provided for example purposes only.
If this does not work in resolving the issue please comment with the following information for additional assistance.
Are there any JS errors in the console?
Did you download the Vue Devtools extension?
If so what is the computed properties value for imageUrl when working and after you experience this issue?
Update. As an alternative approach (and I prefer this) you could also remove all computed properties and provided a default image object. This would require no observers and looks like this:
image: {
type: Object,
default: function () {
return {
url: '/themes/my-theme/images/no-image.jpg',
alt: 'Default No Image Alt Text'
}
},
}
Your image tag would then look like this:
<img :src="image.url" :alt="image.alt">
For more info visit: https://v2.vuejs.org/v2/guide/components-props.html

Vuetify v-text-field Default Slot Not Working

I am trying to use a custom filter with the Vuetify v-text-field control. I am having trouble getting a value to show using the default slot of the v-text-field control. It is apparently derived from v-input, which seems to work fine.
This does not work:
<v-text-field>
{{ purchasePrice | currency }}
</v-text-field>
This works:
<v-input>
{{ purchasePrice | currency }}
</v-input>
Am I missing a template slot or something? I've been able to successfully use the "append" and "prepend" slots on this control, but not the "default" slot. Any suggestions?
Thanks.
I just ran into this as well, and did some source diving. Documenting my findings below:
As of Vuetify 2.5.8 (most recent version), and any other 2+ version, default slot is ignored on v-text-element.
The relevant part in the source code of VTextField.ts:
genDefaultSlot () {
return [
this.genFieldset(),
this.genTextFieldSlot(),
this.genClearIcon(),
this.genIconSlot(),
this.genProgress(),
]
},
it overrides genDefaultSlot method of VInput.ts, which is included as a mixin in VTextField.ts:
genDefaultSlot () {
return [
this.genLabel(),
this.$slots.default,
]
},
I could only make it work with named slots: (Also, I'm reusing this component, so it accepts another slot on the inside)
<template>
<v-layout>
<v-text-field
:type="type"
v-bind="
// https://vuejs.org/v2/guide/components-props.html#Disabling-Attribute-Inheritance
$attrs
"
#input="$emit('update', $event)"
v-on="
// https://vuejs.org/v2/guide/components-custom-events.html#Binding-Native-Events-to-Components
$listeners
"
>
<!-- ⬇️ HERE ⬇️ -->
<template v-slot:label>
<slot></slot>
</template>
</v-text-field>
</v-layout>
</template>
<script>
import { defaultMaterialTextFiledsProps } from '~/config/inputsStyle'
// See https://github.com/chrisvfritz/vue-enterprise-boilerplate/blob/master/src/components/_base-input-text.vue
export default {
// Disable automatic attribute inheritance, so that $attrs are
// passed to the <input>, even if it's not the root element.
// https://vuejs.org/v2/guide/components-props.html#Disabling-Attribute-Inheritance
inheritAttrs: false,
props: {
type: {
type: String,
default: 'text',
// Only allow types that essentially just render text boxes.
validator(value) {
return [
'email',
'number',
'password',
'search',
'tel',
'text',
'url'
].includes(value)
}
}
}
}
</script>