Use component based translations in child components in vue-i18n - vue.js

I'm using vue-i18n to translate messages in my vue app. I have some global translations that are added in new VueI18n(...) as well as some component based translations in a component named c-parent. The component contains child components named c-child. Now, I would like to use the component based translations of c-parent also in c-child.
I made a small example in this fiddle: https://jsfiddle.net/d80o7mpL/
The problem is in the last line of the output: The message in c-child is not translated using the component based translations of c-parent.
Since global translations are "inherited" by all components, I would expect the same for component based translations (in their respective component subtree). Is there a way to achieve this in vue-i18n?

Well, you need to pass the text to child component using props.
Global translations are "inherited" by all components. But you're using local translation in child.
const globalMessages = {
en: { global: { title: 'Vue i18n: usage of component based translations' } }
}
const componentLocalMessages = {
en: { local: {
title: "I\'m a translated title",
text: "I\'m a translated text"
}}
}
Vue.component('c-parent', {
i18n: {
messages: componentLocalMessages
},
template: `
<div>
<div>c-parent component based translation: {{ $t('local.title') }}</div>
<c-child :text="$t('local.title')"></c-child>
</div>
`
})
Vue.component('c-child', {
props: ['text'],
template: `
<div>c-child translation: {{ text }}</div>
`
})
Vue.component('app', {
template: '<c-parent />'
})
const i18n = new VueI18n({
locale: 'en',
messages: globalMessages
})
new Vue({
i18n,
el: "#app",
})
body {
background: #20262E;
padding: 20px;
font-family: Helvetica;
}
#app {
background: #fff;
border-radius: 4px;
padding: 20px;
}
h5 {
margin: 1em 0 .5em 0;
}
<script src="https://unpkg.com/vue"></script>
<script src="https://unpkg.com/vue-i18n"></script>
<div id="app">
<h2>{{ $t('global.title') }}</h2>
We define two Vue components: <code><c-child/></code> contained in <code><c-parent/></code>.
<code><c-parent/></code> defines some component based translations. We would like to use the
parent's translations in the child but it does not work.
<h5>Example:</h5>
<app />
</div>

What I'm doing is using i18n.mergeLocaleMessage in router.ts to merge a particular .i18n.json translation file (by setting a meta.i18n property) for each route:
const router = new Router({
[...]
{
path: '/settings',
name: 'settings',
component: () => import('./views/Settings.vue'),
meta: {
i18n: require('./views/Settings.i18n.json'),
},
},
[...]
});
router.beforeEach((to, from, next) => {
// load view-scoped translations?
if (!!to.meta.i18n) {
Object.keys(to.meta.i18n).forEach((lang) => i18n.mergeLocaleMessage(lang, to.meta.i18n[lang]));
}
next();
});
With Settings.i18n.json being like:
{
"en":
{
"Key": "Key"
},
"es":
{
"Key": "Clave"
}
}
That way, all child components will use the same translation file.
In case you can't use vue-router, maybe you can do it in the parent component's mounted() hook (haven't tried that)

I had the same situation with i18n.
Let's say we have a "card" object prop which it includes the needed language ( was my case) that we'll use in a CardModal.vue component which will be the parent.
So what i did was get the needed locale json file ( based on the prop language) and adding those messages within the card prop.
So in the parent component we'll have:
<template>
<div id="card-modal">
<h1> {{ card.locales.title }} </h1>
<ChildComponent card="card" />
</div>
</template>
<script>
export default {
name: 'CardModal',
props: {
card: {
type: Object,
required: true,
}
}
data() {
return {
locale: this.card.language, //'en' or 'es'
i18n: {
en: require('#/locales/en'),
es: require('#/locales/es'),
},
}
},
created() {
this.card.locales = this.i18n[this.locale].card_modal
}
}
</script>
Notice that we are not relying in the plugin function anymore ( $t() ) and we are only changing the locale in the current component. I did it in this way cause i didn't want to use the "i18n" tag in each child component and wanted to keep all the locales messages in one single json file per language. I was already using the card prop in all child components so that's why i added the locales to that object.
If you need a way to change the locale using a select tag in the component, we can use a watcher for the locale data property like the docs shows

Related

How to properly work with v-model and the Composition API :value="modelValue" syntax? Converting Vue2 custom input into Vue3

I have a useful little custom input I've been using in all of my Vue2 projects (allows you to customize with autofocus, autogrow and debounce), but now that I am working in Vue3, I've been trying to create an updated version.
I've not completed it yet, but I've come across some trouble with the Vue3's composition API :value="modelValue" syntax. In my CodeSandbox I have two inputs, one using the new syntax and the other just straight up using v-model. The later works, while the :value="valueInner" throws Extraneous non-props attributes errors.
What am I doing wrong here and how can I get this to work with that :value="modelValue" syntax?
Cheers!
Link to CodeSandbox
NOTE:
I still need to add autogrow and add all the usecases in App.vue.
CInput
<template>
<input
ref="inputRef"
data-cy="input-field"
v-if="type !== 'textarea'"
:disabled="disabled"
:type="type"
:placeholder="placeholder"
:readonly="readonly"
:required="required"
:autofocus="autofocus"
:debounce="debounce"
:value="valueInner"
/>
<!-- <input
ref="inputRef"
data-cy="input-field"
v-if="type !== 'textarea'"
:disabled="disabled"
:type="type"
:placeholder="placeholder"
:readonly="readonly"
:required="required"
:autofocus="autofocus"
:debounce="debounce"
v-model="valueInner"
/> -->
</template>
<script>
import { defineComponent, ref, onMounted, nextTick, watch } from "vue";
export default defineComponent({
props: {
/** HTML5 attribute */
disabled: { type: String },
/** HTML5 attribute (can also be 'textarea' in which case a `<textarea />` is rendered) */
type: { type: String, default: "text" },
/** HTML5 attribute */
placeholder: { type: String },
/** HTML5 attribute */
readonly: { type: Boolean },
/** HTML5 attribute */
required: { type: Boolean },
/** v-model */
modelValue: { type: [String, Number, Date], default: "" },
autofocus: { type: Boolean, default: false },
debounce: { type: Number, default: 1000 },
},
emits: ["update:modelValue"],
setup(props, { emit }) {
const inputRef = ref(null);
const timeout = ref(null);
const valueInner = ref(props.modelValue);
if (props.autofocus === true) {
onMounted(() => {
// I don't know we need nexttick
nextTick(() => {
inputRef.value.focus();
// setTimeout(() => inputRef.value.focus(), 500);
});
});
}
watch(valueInner, (newVal, oldVal) => {
const debounceMs = props.debounce;
if (debounceMs > 0) {
clearTimeout(timeout.value);
timeout.value = setTimeout(() => emitInput(newVal), debounceMs);
console.log(newVal);
} else {
console.log(newVal);
emitInput(newVal);
}
});
function emitInput(newVal) {
let payload = newVal;
emit("update:modelValue", payload);
}
// const onInput = (event) => {
// emit("update:modelValue", event.target.value);
// };
return { inputRef, valueInner };
},
});
</script>
App.vue
<template>
<CInput :autofocus="true" v-model.trim="inputValue1" />
<CInput :autofocus="false" v-model.trim="inputValue2" />
<pre>Input Value 1: {{ inputValue1 }}</pre>
<pre>Input Value 2: {{ inputValue2 }}</pre>
</template>
<script>
import { ref } from "vue";
import CInput from "./components/CInput.vue";
export default {
name: "App",
components: { CInput },
setup() {
const inputValue1 = ref("");
const inputValue2 = ref("");
return { inputValue1, inputValue2 };
},
};
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
The full warning is:
[Vue warn]: Extraneous non-props attributes (modelModifiers) were passed to component but could not be automatically inherited because component renders fragment or text root nodes.
CInput has more than one root node (i.e., the <input> and the comment node), so the component is rendered as a fragment. The .trim modifier on CInput is normally passed onto root node's v-model, as seen in this demo. Since the actual code is a fragment, Vue can't decide for you where to pass on the modelModifiers prop, leading to the warning you observed.
However, declaring a modelModifiers prop to receive the modifiers is enough to resolve the problem:
// CInput.vue
export default {
props: {
modelModifiers: {
default: () => ({})
}
}
}
demo
So far I have only built one App using Vue 3 + Vite.
I went with the <script setup> method only and stuck with it, this is how that looks when defining props:
<script setup>
import { onMounted } from 'vue'
const props = defineProps({
readonly: { type: Boolean },
required: { type: Boolean },
})
onMounted(() => {
console.dir(props.readonly)
})
</script>
In the Vite setup, defineProps is globally registered, seems to be the only method that does not require an import, I would not know if this is true of any other compiler methods.
v-model in Vue3 pops out as modelValue not value, maybe you can defineProp the modelValue unless its there by default always?
There is some explanation here: https://v3-migration.vuejs.org/breaking-changes/v-model.html#_2-x-syntax
Hope this applies to you and helps.

Dynamic components based on route params in vue-router

Is it possible in Vue to dynamically use components based on the value of a route parameter?
Here's an example of what I'm trying to do:
path: '/model/:modelName',
components: {
default: ModelDefinition.models.find((model) => model.key === $route.params.modelName),
},
The problem is: $route is not available here. Neither is modelName.
What might be nice if default could also be a function (similar to props):
path: '/model/:modelName',
components: {
default: (modelName) => (ModelDefinition.models.find((model) => model.key === $route.params.modelName)),
},
This function actually gets called to my surprise, but the argument seems to be some sort of function, and certainly not the modelName I'm hoping for.
The reason I need this is: there's actually a lot more configuration for this route, specifying lots of components and values for various things. If I have to specify that for every model, it's going to be long and repetitive, and would have to be expanded for every model we add to our system.
I'm not sure you have to "do magic" with vue-router - why don't you just handle the parameter inside your component (or create a container-component, and handle the parameter there)?
The snippet below shows that you can use external data to control what component should be displayed. If you pass your model parameter down, then any existing (and registered) component-name can be displayed.
const CompA = {
template: `<div class="border-blue">COMP-A</div>`
}
const CompB = {
template: `<div class="border-red">COMP-B</div>`
}
new Vue({
el: "#app",
components: {
CompA,
CompB
},
data: {
selected: ''
}
})
[class^="border-"] {
padding: 10px;
}
.border-blue {
border: 1px solid blue;
}
.border-red {
border: 1px solid red;
}
<script src="https://cdn.jsdelivr.net/npm/vue#2.x/dist/vue.js"></script>
<div id="app">
<select v-model="selected">
<option disabled value="">Please select one</option>
<option>CompA</option>
<option>CompB</option>
</select>
<br />
<span>Selected: {{ selected }}</span>
<br />
<br /> Here's a variable component - based on external data:
<component :is="selected"></component>
</div>
In components declaration, this is not available and hence you can not access the props and other instance data.
What you can do is create a computed property that returns a component. Like this:
<template>
<component :is="componentInstance" />
</template>
<script>
export default {
props: {
componentName: String,
},
computed: {
componentInstance() {
return () => import(`./modules/${this.componentName}/template.vue`);
},
},
};
</script>

Vue - Render an element out of string

I would like to create a vue element from a string from my database.
In this case, it should be a message with a smiley emoji.
I actually save it like: Some text with Emoji: :santa::skin-tone-3:, and replace all valid string between '::' with the <Emoji emoji=':santa::skin-tone-3:' :size='16' />
<template>
<span class=message v-html=convertedMessage></div>
</template>
<script>
import { Emoji } from 'emoji-mart-vue'
export default {
components: {
Emoji
},
computed:{
convertedMessage(){
return "Some text with Emoji: "+"<Emoji emoji=':santa::skin-tone-3:' :size='16' />"
}
}
}
</script>
But instead of the rendered element which should be something like:
<span data-v-7f853594="" style="display: inline-block; width: 32px; height: 32px; background-image: url("https://unpkg.com/emoji-datasource-apple#4.0.4/img/apple/sheets/64.png"); background-size: 5200%; background-position: 15.6863% 41.1765%;"></span>
I only get:
<emoji emoji=":santa::skin-tone-3:" :size="16"></emoji>
What is the best possibility to render this Element like intended?
Here are some much easier ways to do what you generally want. If you give more specifics, your right direction may be a strategy pattern before one of these solutions, but one of these solutions is probably what you want:
1) Vue lets you dynamically define components right out of the box, so this single line:
<component v-for="(component, index) in components" :key="'component'+index" :is="component.name" v-bind="component.props" />
...would draw a bunch of components in an array of objects like this (for example): {name: 'myComponentName', props: {foo: 1, bar: 'baz'}}.
2) Vue lets you inject HTML into components by simply adding v-html="variable"
For example, here is a component that creates dynamic SVG icons, where the contents of the SVG is dynamically injected from JavaScript variables...
<template>
<svg xmlns="http://www.w3.org/2000/svg"
:width="width"
:height="height"
viewBox="0 0 18 18"
:aria-labelledby="name"
role="presentation"
>
<title :id="name" lang="en">{{name}} icon</title>
<g :fill="color" v-html="path">
</g>
</svg>
</template>
<script>
import icons from '../common/icons'
export default {
props: {
name: {
type: String,
default: 'box'
},
width: {
type: [Number, String],
default: 18
},
height: {
type: [Number, String],
default: 18
},
color: {
type: String,
default: 'currentColor'
}
},
data () {
return {
path: icons[this.name]
}
},
created () {
console.log(icons)
}
}
</script>
<style scoped>
svg {
display: inline-block;
vertical-align: baseline;
margin-bottom: -2px;
}
</style>
3) Vue lets you dynamically define your component template through this.$options.template:
export default {
props: ['name', 'props'],
template: '',
created(){
this.$options.template = `<component :is="name" ${props.join(' ')} ></component>`
},
}
4) Vue lets you define a render function, so proxy components or other advanced shenanigans are trivial:
Vue.component('component-proxy', {
props: {
name: {
type: String,
required: true
},
props: {
type: Object,
default: () => {}
}
},
render(h) {
// Note the h function can render anything, like h('div') works too.
// the JS object that follows can contain anything like on, class, or more elements
return h(this.name, {
attrs: this.props
});
}
});
A smart genius wrote a jsbin for this here: http://jsbin.com/fifatod/5/edit?html,js,output
5) Vue allows you to create components with Vue.extend or even passing in raw JavaScript objects into a page or apps components section, like this, which creates a component named "foo" from a simple string for the template and an array for props, you could also extend the data, created, on, etc. the same way using the JS object alone:
new Vue({
el: '#app',
data: {
foo: 'bar',
props: {a: 'a', b: 'b'}
},
components: {
foo: {
template: '<p>{{ a }} {{ b }}</p>',
props: ['a', 'b']
}
}
})
What i figured out now:
convertedMessage(){
let el = Vue.compile("<Emoji emoji=':santa::skin-tone-3:' :size='16' />")
el = new Vue({
components: {
Emoji
},
render: el.render,
staticRenderFns: el.staticRenderFns
}).$mount()
return "Some text with Emoji: "+el.$el.innerHTML
}
Maybe there is still a better solution to handle this?
Here's how I went about when I needed to do something similar.
I rendered the component, say, <my-component> normally, but since I only needed rendered HTML, I wrapped it inside a <div class="hidden"> like so:
<div class="hidden">
<my-component />
</div>
With CSS:
.hidden {
display: none;
}
This way, I can refer to the element through $refs or you could get the element from the DOM using document.querySelector() while keeping it invisible to the end users.
So in the above example, to get the rendered HTML, You'd only need to do this:
let el = document.querySelector('.hidden');
let renderedHTMLString = el.children[0].outerHTML;
This way, you get the rendered HTML, without any overhead costs that's associated with Vue.compile or any other plugin. Render it normally. Hide it. Access it's outerHTML.
v-html only render plain HTML, see https://v2.vuejs.org/v2/guide/syntax.html#Raw-HTML
In your case you should probably take a look at render functions and JSX. I'm not an expert but it seems that some people are acheiving what you want with the dangerouslySetInnerHTML JSX function. Take a look at this thread : How do I convert a string to jsx?
I know sometimes we have no choice but if you can I think the best solution could be to avoid generating the template from the backend as it breaks separation of concern (and also, probably, security issues).

Reusable component to render button or router-link in Vue.js

I'm new using Vue.js and I had a difficulty creating a Button component.
How can I program this component to conditional rendering? In other words, maybe it should be rendering as a router-link maybe as a button? Like that:
<Button type="button" #click="alert('hi!')">It's a button.</Button>
// -> Should return as a <button>.
<Button :to="{ name: 'SomeRoute' }">It's a link.</Button>
// -> Should return as a <router-link>.
You can toggle the tag inside render() or just use <component>.
According to the official specification for Dynamic Components:
You can use the same mount point and dynamically switch between multiple components using the reserved <component> element and dynamically bind to it's is attribute.
Here's an example for your case:
ButtonControl.vue
<template>
<component :is="type" :to="to">
{{ value }}
</component>
</template>
<script>
export default {
computed: {
type () {
if (this.to) {
return 'router-link'
}
return 'button'
}
},
props: {
to: {
required: false
},
value: {
type: String
}
}
}
</script>
Now you can easily use it for a button:
<button-control value="Something"></button-control>
Or a router-link:
<button-control to="/" value="Something"></button-control>
This is an excellent behavior to keep in mind when it's necessary to create elements that may have links or not, such as buttons or cards.
You can create a custom component which can dynamically render as a different tag using the v-if, v-else-if and v-else directives. As long as Vue can tell that the custom component will have a single root element after it has been rendered, it won't complain.
But first off, you shouldn't name a custom component using the name of "built-in or reserved HTML elements", as the Vue warning you'll get will tell you.
It doesn't make sense to me why you want a single component to conditionally render as a <button> or a <router-link> (which itself renders to an <a> element by default). But if you really want to do that, here's an example:
Vue.use(VueRouter);
const router = new VueRouter({
routes: [ { path: '/' } ]
})
Vue.component('linkOrButton', {
template: `
<router-link v-if="type === 'link'" :to="to">I'm a router-link</router-link>
<button v-else-if="type ==='button'">I'm a button</button>
<div v-else>I'm a just a div</div>
`,
props: ['type', 'to']
})
new Vue({ el: '#app', router })
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue-router/3.0.1/vue-router.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.9/vue.js"></script>
<div id="app">
<link-or-button type="link" to="/"></link-or-button>
<link-or-button type="button"></link-or-button>
<link-or-button></link-or-button>
</div>
If you're just trying to render a <router-link> as a <button> instead of an <a>, then you can specify that via the tag prop on the <router-link> itself:
Vue.use(VueRouter);
const router = new VueRouter({
routes: [ { path: '/' } ]
})
new Vue({ el: '#app', router })
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue-router/3.0.1/vue-router.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.9/vue.js"></script>
<div id="app">
<router-link to="/">I'm an a</router-link>
<router-link to="/" tag="button">I'm a button</router-link>
</div>
You can achieve that through render functions.
render: function (h) {
if(this.to){ // i am not sure if presence of to props is your condition
return h(routerLink, { props: { to: this.to } },this.$slots.default)
}
return h('a', this.$slots.default)
}
That should help you start into the right direction
I don't think you'd be able to render a <router-link> or <button> conditionally without having a parent element.
What you can do is decide what to do on click as well as style your element based on the props passed.
template: `<a :class="{btn: !isLink, link: isLink}" #click="handleClick"><slot>Default content</slot></a>`,
props: ['to'],
computed: {
isLink () { return !!this.to }
},
methods: {
handleClick () {
if (this.isLink) {
this.$router.push(this.to)
}
this.$emit('click') // edited this to always emit
}
}
I would follow the advice by #Phil and use v-if but if you'd rather use one component, you can programmatically navigate in your click method.
Your code can look something like this:
<template>
<Button type="button" #click="handleLink">It's a button.</Button>
</template>
<script>
export default {
name: 'my-button',
props: {
routerLink: {
type: Boolean,
default: false
}
},
methods: {
handleLink () {
if (this.routerLink) {
this.$router.push({ name: 'SomeRoute' })
} else {
alert("hi!")
}
}
}
}
</script>

Vue.js - set slot content programmatically

is there any way how can I set/override slot's content from inside the component? Like
Template:
<div>
<slot></slot>
</div>
JS:
export default {
...
mounted() {
this.$slot.render("<button>OK</button>");
}
...
}
I know I can use v-html on my element to dynamically push content into component template, but I mean not just pure HTML I mean HTML with Vue directives. Like:
JS:
export default {
...
mounted() {
this.$slot.default.render('<button #click="submit">OK</button>');
},
methods: {
submit() {
// Here I want to get :)
}
}
...
}
Basically I want Vue to render (like parse and render, not like innerHTML) certain string in scope of my component and put in on certain spot in my component. The reason is I get the new content from server via AJAX.
I'm sorry but I can't still get my head around after 2 days of googling.
Thanks a lot!
According to this you can instantiate a component programmatically and insert slot content as well.
import Button from 'Button.vue'
import Vue from 'vue'
var ComponentClass = Vue.extend(Button)
var instance = new ComponentClass({
propsData: { type: 'primary' }
})
instance.$slots.default = [ 'Click me!' ]
instance.$mount() // pass nothing
this.$refs.container.appendChild(instance.$el)
I think this is your best chance: building your component on the fly.
Here in the example I use a simple placeholder (with v-cloak).
You may obtain a better result using vue-router to add your newly created component during the execution, with router.addRoutes so your app doesn't have to wait to instantiate the whole vue.
function componentFactory(slot) {
return new Promise((resolve, reject) => {
window.setTimeout(() => { // Asynchronous fetching
resolve({ // Return the actual component
template: `<div>
${slot}
</div>`,
methods: {
submit() {
console.log('hello');
}
}
});
}, 1000);
});
}
componentFactory('<button #click="submit">OK</button>') // Build the component
.then(component => {
new Vue({ // Instantiate vue
el: '#app',
components: {
builtComponent: component
}
});
});
[v-cloak], .placeholder {
display: none;
}
[v-cloak] + .placeholder {
display: block;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.4/vue.min.js"></script>
<div id='app' v-cloak>
<built-component></built-component>
</div>
<div class="placeholder">
This is a placeholder, my vue app is loading...
</div>