Passing to v-model a composed value - vue.js

I'm using Quasar v.2 with VueJs 3.
I'm using the q-select component in order to switch dynamically the language. Here's my code:
<template>
<q-select v-model="$i18n.locale" :options="langs">
</q-select>
</template>
<script lang="ts">
export default defineComponent({
name: 'LanguageSwitch',
setup() {
const langs = ['en', 'jp']
return {langs}
}
</script>
What I want to do now is to keep displaying "en" and "jp", and to concatenate them with another word (to form, for example "en-US") in the moment I pass them to the i18n.locale

You could update langs to include the desired values and labels:
export default defineComponent({
setup() {
const langs = [
{ value: 'en-US', label: 'en' },
{ value: 'jp-JP', label: 'jp' },
]
return { langs }
}
}
Then set <q-select>.optionValue to "value" so it uses the item's value property as the option value, and <q-select>.optionLabel to "label" to use the item's label property as the label:
<q-select v-model="$i18n.locale" :options="langs"
option-value="value"
option-label="label">
</q-select>

Related

<b-form-input> bind value does not update on the second time

I am currently implementing a component that update parent's year[] array when year to / year[1] value is lower than year from / year[0] with <b-input> (Bootstrap Vue Library).
The year to stop updating after the second time.
Code example are as below.
Code equivalent in jsfiddle can be found here.
Parent.vue
<template>
<child :year="year" #update="update">
</template>
<script>
// Import child component here
export default {
name: 'Parent',
components: {
Child,
},
data: () => ({
year: [100, null],
}),
methods: {
update(newYear) {
this.year = newYear;
},
},
</script>
Child.vue
<template>
<div>
From <b-input :value="year[0]" />
To <b-input :value="year[1]" #change="update" />
</div>
</template>
<script>
export default {
name: 'Child',
props: {
year: {
type: Array,
required: true,
}
},
methods: {
update(yearToVal) {
const [yearFrom] = this.year;
let newYear = [...this.year];
if (yearToVal < yearFrom) {
/* Both of this update end up the same */
// newYear[1] = yearFrom;
this.$set(newYear, 1 , yearFrom);
}
this.$emit('update', newYear);
},
},
};
</script>
I had used Vue Dev Tools to check and the child is emitting data correctly to the parent.
The issue happen on the vModalValue and localValue of the <b-input> are not updating on the second time.
What am I doing wrongly or is it a Bootstrap Vue library problem?
Hiws's answer indicate that the this problem does not only happen on <b-form-input> but ordinary <input> with Vue as well.
This happen due to Vue not able to react to changes since the update is happening on child, hence when year to is lower than year from, parent will not detect any changes on the second time as the array pass to Parent.vue will always be [100,100].
The solution will be using watcher on Parent.vue's array to detect the changes, hence both eg: [100, 1] -> [100,100] are both reflected on Parent.vue and most importantly, force the component to re-render.
Without force re-rendering, [100,1] or [100,2]... will always be treated as [100,100], the same value, and Vue will not react or even update to them.
Jsfiddle equivalent solution can be found here
Code sample below:
Parent.vue
<template>
<child :year="year" #update="update">
</template>
<script>
// Import child component here
export default {
name: 'Parent',
components: {
Child,
},
data: () => ({
year: [100, null],
yearKey: 0,
}),
watch: {
year: {
handler(val) {
if (val[1] < val[0]) {
let newYear = [...val];
newYear[1] = val[0];
this.year = newYear;
// key update reference: https://michaelnthiessen.com/force-re-render/
this.yearKey += 1;
}
}
}
},
methods: {
update(newYear) {
this.year = newYear;
},
},
</script>
Child.vue
<template>
<div>
From <b-input :value="year[0]" />
To <b-input :value="year[1]" #change="update" />
</div>
</template>
<script>
export default {
name: 'Child',
props: {
year: {
type: Array,
required: true,
}
},
methods: {
update(yearToVal) {
const [yearFrom] = this.year;
let newYear = [...this.year];
newYear[1] = yearToVal;
this.$emit('update', newYear);
},
},
};
</script>

how to create a dynamic vue component that has a computed template containing another component with Object properties without passing it as string

I have a component like this:
Relation.vue
<template>
<div :is="dynamicRelation"></div>
</template>
<script>
import Entry from '#/components/Entry';
import weirdService from '#/services/weird.service';
export default {
name: 'Relation',
data() {
return {
entry1: { type: 'entity', value: 'foo', entity: {id: 4}},
entry2: { type: 'entity', value: 'bar', entity: {id: 5}},
innerText: '#1 wut #2',
}
},
computed: {
dynamicRelation() {
return {
template: `<div>${this.innerText
.replace('#1', weirdService.entryToHtml(this.entry1))
.replace('#2', weirdService.entryToHtml(this.entry2))}</div>`,
name: 'DynamicRelation',
components: { Entry }
};
}
}
}
</script>
wierd.service.js
export default {
entryToHtml(entry) {
[some logic]
return `<entry entry='${JSON.stringify(entry)}'></entry>`;
// unfortunately I cannot return JSX here: <entry entry={entry}></entry>;
// I get 'TypeError: h is not a function'
// unless there is a way to convert JSX to a pure html string on the fly
}
}
Entry.vue
<template>
<div>{{objEntry.name}}</div>
</template>
<script>
export default {
name: 'Entry',
props: {
entry: String // I need this to be Object
},
computed: {
objEntry() {
return JSON.parse(this.entry);
}
}
}
</script>
The innerText property decides how the components will be rendered and it can be changing all the time by having its # slots in any position.
In this example the result is:
<div>
<div>foo</div>
wut
<div>bar</div>
</div>
This works since Entry component has as a property entry that is of type String but I have to JSON.stringify() the entry object in weirdService side and then in Entry component I have to JSON.parse() the string to get the real object back.
How can I make the above work so that I pass an object directly to a dynamic template so I avoid serialization and deserialization all the time.
btw for this to work runtimeCompiler needs to be enabled in vue.config.js:
module.exports = {
runtimeCompiler: true
}
I know I can use JSX to return components with objects in them but this is allowed only in render() function it seems and not custom ones like mine.
Thanks!!
I was able to do what I wanted by using JSON.stringify still but pass the entry as object :entry
wierd.service.js
export default {
entryToHtml(entry) {
return `<entry :entry='${JSON.stringify(entry)}'></entry>`;
}
}
Entry.vue
<template>
<div>{{entry.name}}</div>
</template>
<script>
export default {
name: 'Entry',
props: {
entry: Object
}
}
</script>

Properly alert prop value in parent component?

I am new to Vue and have been very confused on how to approach my design. I want my component FileCreator to take optionally take the prop fileId. If it's not given a new resource will be created in the backend and the fileId will be given back. So FileCreator acts as both an editor for a new file and a creator for a new file.
App.vue
<template>
<div id="app">
<FileCreator/>
</div>
</template>
<script>
import FileCreator from './components/FileCreator.vue'
export default {
name: 'app',
components: {
FileCreator
}
}
</script>
FileCreator.vue
<template>
<div>
<FileUploader :uploadUrl="uploadUrl"/>
</div>
</template>
<script>
import FileUploader from './FileUploader.vue'
export default {
name: 'FileCreator',
components: {
FileUploader
},
props: {
fileId: Number,
},
data() {
return {
uploadUrl: null
}
},
created(){
if (!this.fileId) {
this.fileId = 5 // GETTING WARNING HERE
}
this.uploadUrl = 'http://localhost:8080/files/' + this.fileId
}
}
</script>
FileUploader.vue
<template>
<div>
<p>URL: {{ uploadUrl }}</p>
</div>
</template>
<script>
export default {
name: 'FileUploader',
props: {
uploadUrl: {type: String, required: true}
},
mounted(){
alert('Upload URL: ' + this.uploadUrl)
}
}
</script>
All this works fine but I get the warning below
Avoid mutating a prop directly since the value will be overwritten
whenever the parent component re-renders. Instead, use a data or
computed property based on the prop's value. Prop being mutated:
"fileId"
What is the proper way to do this? I guess in my situation I want the prop to be given at initialization but later be changed if needed.
OK, so short answer is that the easiest is to have the prop and data name different and pass the prop to the data like below.
export default {
name: 'FileCreator',
components: {
FileUploader
},
props: {
fileId: Number,
},
data() {
return {
fileId_: this.fileId, // HERE WE COPY prop -> data
uploadUrl: null,
}
},
created(){
if (!this.fileId_){
this.fileId_ = 45
}
this.uploadUrl = 'http://localhost:8080/files/' + this.fileId_
}
}
Unfortunately we can't use underscore as prefix for a variable name so we use it as suffix.

How to pass editable data to component?

I'm working on an app that allows you to capture and edit soccer match results.
there is a Matches component that makes an AP call to get the data of multiple matches from a server (match_list) and then renders a bunch of Match components, passing the data as props to these sub-components to fill their initial values.
<component :is="Match" v-for="match in match_list"
v-bind:key="match.id"
v-bind="match"></component>
On the Match component, I accept all the values as props.
But I get a warning that props shouldn't be edited and these should be data elements instead. So I tried passing them to the component data.
export default {
name: "Match",
props: ['local_team', 'visitor_team', 'localScore', 'visitorScore', 'date', 'time', 'location', 'matchId'],
data(){
return{
id: this.id,
local_team: this.local_team,
visitor_team: this.visitor_team,
location: this.location,
date: this.date,
time: this.time,
localScore: this.localScore,
visitorScore: this.visitorScore
}
},
Now I get a warning that editable data shouldn't be based on props.
How can I make the data from the Match component editable so it safely propagates to the parent component?
You need to accept your match object on the component's props, and make a copy of it on data (to be used as a model for your inputs). When your model changes you should emit that change to the parent so that it can change its own data appropriately (which then gets passed and reflected correctly through the child's props):
In this example I watch for any changes to the model and then emit the event directly, you can of course replace that behavior by having a submit button that fires the event upon click or something.
Vue.component('match', {
template: `
<div>
<p>{{match.name}}</p>
<input v-model="matchModel.name" />
</div>
`,
props: ['match'],
data() {
return {
matchModel: Object.assign({}, this.match)
}
},
watch: {
matchModel: {
handler(val) {
this.$emit('match-change', val)
},
deep: true,
}
}
});
new Vue({
el: "#app",
data: {
matches: [{
id: 1,
name: 'first match'
},
{
id: 2,
name: 'second match'
}
]
},
methods: {
onMatchChange(id, newMatch) {
const match = this.matches.find((m) => m.id == id);
Object.assign(match, newMatch);
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.min.js"></script>
<div id="app">
<match v-for="match in matches" :match="match" :key="match.id" #match-change="onMatchChange(match.id, $event)"></match>
</div>

Vue.js single file component 'name' not honored in consumer

Please pardon my syntax, I'm new to vue.js and may not be getting the terms correct.
I've got a single file component (SFC) named CreateTodo.vue. I've given it the name 'create-todo-item' (in the name property). When I import it in my app.vue file, I can only use the component if I use the markup <create-todo>. If I use <create-todo-item>, the component won't render on the page.
I've since learned that I can do what I want if I list the component in my app.vue in the format components: { 'create-todo-item': CreateTodo } instead of components: { CreateTodo }.
My question is this: is there any point to giving the component a name in the name property? It's not being honored in the consumer, and if I leave it empty, the app runs without error.
Also, am I correct in my belief that vue-loader is assigning the kebab-case element name for template use based on the PascalCase import statement?
Bad - component name property
Here's the code where I try to name the SFC (CreateTodo.vue)
<script>
export default {
name: 'create-todo-item',
data() {
return {
titleText: '',
projectText: '',
isCreating: false,
};
},
};
</script>
The name as listed in the component is ignored by my App.vue. The html renders fine even though I have the element <create-todo> instead of <create-todo-item>:
<template>
<div>
<!--Render the TodoList component-->
<!--TodoList becomes-->
<todo-list v-bind:todos="todos"></todo-list>
<create-todo v-on:make-todo="addTodo"></create-todo>
</div>
</template>
<script>
import TodoList from './components/TodoList.vue'
import CreateTodo from './components/CreateTodo.vue'
export default {
name: 'app',
components: {
TodoList,
CreateTodo,
},
// data function avails data to the template
data() {
return {
};
},
methods: {
addTodo(todo) {
this.todos.push({
title: todo.title,
project: todo.project,
done: false,
});
},
}
};
</script>
Good - don't use component name property at all
Here's my CreateTodo.vue without using the name property:
<script>
export default {
data() {
return {
titleText: '',
projectText: '',
isCreating: false,
};
},
};
</script>
And here's my App.vue using the changed component:
<template>
<div>
<!--Render the TodoList component-->
<!--TodoList becomes-->
<todo-list v-bind:todos="todos"></todo-list>
<create-todo-item v-on:make-todo="addTodo"></create-todo-item>
</div>
</template>
<script>
import TodoList from './components/TodoList.vue'
import CreateTodo from './components/CreateTodo.vue'
export default {
name: 'app',
components: {
TodoList,
'create-todo-item': CreateTodo,
},
// data function avails data to the template
data() {
return {
};
},
methods: {
addTodo(todo) {
this.todos.push({
title: todo.title,
project: todo.project,
done: false,
});
},
}
};
</script>
First note that the .name property in a SFC module is mostly just a convenience for debugging. (It's also helpful for recursion.) Other than that, it doesn't really matter when you locally register the component in parent components.
As to the specific details, in the first example, you're using an ES2015 shorthand notation
components: {
TodoList,
CreateTodo,
},
is equivalent to
components: {
'TodoList': TodoList,
'CreateTodo': CreateTodo
},
so that the component that is imported as CreateTodo is given the name 'CreateTodo' which is equivalent to <create-todo>.
In the second example, you give the name explicitly by forgoing the shorthand notation
components: {
TodoList,
'create-todo-item': CreateTodo,
},
That's equivalent, btw to
components: {
TodoList,
'CreateTodoItem': CreateTodo,
},
So you can see, in that case, that you're giving the component the name 'CreateTodoItem' or, equivalently, <create-todo-item>