Using ref with props - vue.js

I have a Component A with a nested Component B. Component A loads a list of strings, Component B has a SELECT element, which is populated when Component A is finished loading. The first option of the SELECT should be selected at this moment. After this moment, when other options are selected, Component B should emit update event with selection data, so that Component A 'knows' what option was selected. Since A should control B, this option is passed back to B with props.
In my implementation below, only the initial value of the selection is received by Component B, and when the list is loaded, it is populated successfully, but the selectedOption value is not changed by the controlling property. So, it doesn't work.
What would be a proper way to implement this?
Component A:
<template>
<component-b :list="list" :selected="selected" #update="onUpdate">
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
export default defineComponent({
setup: () => {
const list = ref<string[]>([]);
const selected = ref('');
load().then(data => { // some load function returning a promise
selected.value = data[0]; // data is never empty
list.value = data;
});
const onUpdate = (data: string) => selected.value = data;
return { list, selected, onUpdate };
}
})
</script>
Component B:
<template>
<select v-model="selectedOption" #change="onChange">
<option v-for="item of props.list" />{{item}</option>
</select>
</template>
<script lang="ts">
import { defineComponent, ref, PropType } from 'vue';
export default defineComponent({
emits: ['update'],
props: {
selected: {
type: String
},
list: {
type: Object as PropType<String[]>
}
}
setup: (props, { emit }) => {
const selectedOption = ref(props.selected); // this doesn't work when loading is finished and props are updated
const onChange = () => emit('update', selectedOption.value);
return { selectedOption, onChange, props }
}
});
</script>
Thank you.

In ComponentB.vue, selectedOption's ref is only initialized to the value of props.selected, but that itself is not reactive (i.e., the ref isn't going to track props.selected automatically):
const selectedOption = ref(props.selected); // not reactive to props.selected
You can resolve this with a watchEffect that copies any new values to selectedOption (which happens automatically whenever props.selected changes):
watchEffect(() => selectedOption.value = props.selected);
Side note: setup() doesn't need to explicitly return props because they're already available to the template by name. Your template could be changed to:
<!-- <option v-for="item of props.list">{{ item }}</option> -->
👇 reference props directly by name
<option v-for="item of list">{{ item }}</option>
demo

In component B, you shouldn't need to add both a v-model and a #change.
<select :value="selected" #change="onChange">
<option v-for="item in list">{{ item }}</option>
</select>
Then in the onChange get the value from the select and emit up:
const onChange = (event) => emit('update', event.target.value);

Related

Passing a value via prop, editing it and saving the new value vue 3

I am trying to pass a value into a child component. The child component will then preform the save operation. The parent doesn't need to know anything about it. I am able to pass in the object but not save its updated form.
Parent
<template>
<div v-show="isOpened">
<EditModal #toggle="closeModal" #update:todo="submitUpdate($event)"
:updatedText="editText" :todo="modalPost" />
</div>
</template>
<script setup lang="ts">
import Post from "../components/Post.vue";
import { api } from "../lib/api";
import { ref } from "vue";
import { onMounted } from "vue-demi";
import EditModal from "../components/EditModal.vue";
const postArr = ref('');
const message = ref('');
let isOpened = ref(false);
let modalPost = ref('');
let editText = ref('');
function closeModal() {
isOpened.value = false
}
function openModal(value: string) {
isOpened.value = true
modalPost.value = value
}
// call posts so the table loads updated item
function submitUpdate(value: any) {
console.log("called update in parent " + value)
editText.value = value
posts()
}
</script>
Child EditModal
<template>
<div>
<div>
<textarea id="updateTextArea" rows="10" :value="props.todo.post"></textarea>
</div>
<!-- Modal footer -->
<div>
<button data-modal-toggle="defaultModal" type="button"
#click="update(props.todo.blogId, props.todo.post)">Save</button>
</div>
</div>
</template>
<script lang="ts" setup>
import { api } from "../lib/api";
import { reactive, ref } from "vue";
const props = defineProps({
todo: String,
updatedText: String,
})
const emit = defineEmits(
['toggle','update:todo']
);
function setIsOpened(value: boolean) {
emit('toggle', value);
}
function update(id: string, value: string) {
console.log('the value ' + value)
try {
api.updateBlog(id, value).then( res => {
emit('update:todo', value)
emit('toggle', false);
})
} catch (e) {
console.log('Error while updating post: '+ e)
}
}
</script>
I know the props are read only, therefore I tried to copy it I can only have one model.
I do not see the reason I should $emit to the parent and pass something to another variable to pass back to the child.
I am trying to pass in text to a modal component where it can edit the text and the child component saves it.
Advice?
First, your component should be named EditTodo no EditModal because you are not editing modal. All Edit components should rewrite props to new local variables like ref or reactive, so you can work on them, they won't be read only any more.
Child EditTodo.vue
<template>
<div>
<div>
<textarea id="updateTextArea" rows="10" v-model="data.todo.title"></textarea>
</div>
<div>
<button data-modal-toggle="defaultModal" type="button" #click="update()">Save</button>
</div>
</div>
</template>
<script setup lang="ts">
import { api } from "../lib/api";
import { reactive } from "vue";
const props = defineProps<{ id: number, todo: { title: string} }>()
const emit = defineEmits(['toggle']);
const data = reactive({ ...props })
// I assuming api.updateBlog will update data in database
// so job here should be done just need toogle false modal
// Your array with todos might be not updated but here is
// no place to do that. Well i dont know how your API works.
// Database i use will automaticly update arrays i updated objects
function update() {
try {
api.updateBlog(data.id, data.todo ).then(res => {
emit('toggle', false);
})
}
}
</script>
Parent
<template>
<div>
<BaseModal v-if="todo" :show="showModal">
<EditTodo :id="todo.id" :todo="todo.todo" #toggle="(value) => showModal = value"></EditTodo>
</BaseModal>
</div>
</template>
<script setup lang="ts">
const showModal = ref(false)
const todo = reactive({ id: 5, todo: { title: "Todo number 5"} })
</script>
I separated a modal object with edit form, so you can create more forms and use same modal. And here is a simple, not fully functional modal.
<template>
<div class="..."><slot></slot></div>
</template>
<script setup>
defineProps(['show'])
defineEmits(['toogle'])
</script>
You might want to close modal when user click somewhere outside of modal.

Attribute :value binding in select tag doesn't update inside Vue 3 component template (Composition API)

I have a drop down menu where options are enumerated and shuffled, so that the selected option becomes the first. This script is working as intended:
<div id="main">
<sub-select :subs="data" #comp-update="onShufflePaths($event)"></sub-select>
</div>
. .
const ui = {
setup() {
let data = ref('first_second_thrid');
const onShufflePaths = (ind) => {
let subs = data.value.match(/[^_]+/g);
const main = subs.splice(ind, 1)[0];
data.value = [main, ...subs].join('_');
}
return {
data, onShufflePaths,
};
},
};
const vueApp = createApp(ui);
vueApp.component('sub-select', {
props: ['subs'],
emits: ['comp-update'],
setup(props, { emit }) {
let subs = computed(() => props.subs.match(/[^_]+/g));
let subpath = computed(() => '0: ' + subs.value[0]);
function onChange(evt) {
emit('comp-update', evt.slice(0,1));
}
return { subs, subpath, onChange };
},
template: `
<select :value="subpath" #change="onChange($event.target.value)">
<option v-for="(v,k) in subs">{{k}}: {{v}}</option>
</select> {{subpath}}`
});
vueApp.mount('#main');
The problem is, if I delete {{subpath}} from the template, the drop down menu comes up with no options selected by default. It looks like :value="subpath" by itself is not enough to update subpath variable when props update, if it's not explicitly mentioned in the template.
How can I make it work?
Basically, I need the first option always to be selected by default.
Thank you!
https://jsfiddle.net/tfoller/uy7k1hvr/26/
So, it looks like it might be a bug in the library.
Solution 1:
wrap select tag in the template in another tag, like this (so it's not the lonely root element in the template):
template: `
<div><select :value="subpath" #change="onChange($event.target.value)">
<option v-for="(v,k) in subs">{{k}}: {{v}}</option>
</select></div>`
Solution 2:
Write a getter/setter to subpath variable, so component definition is as follows:
vueApp.component('sub-select', {
props: ['subs'],
emits: ['comp-update'],
setup(props, { emit }) {
let subs = computed(() => props.subs.match(/[^_]+/g));
let subpath = computed({
get: () => '0: ' + subs.value[0],
set (value) {
emit('comp-update', value.slice(0,1))
}
});
return { subs, subpath };
},
template: `
<select v-model="subpath">
<option v-for="(v,k) in subs">{{k}}: {{v}}</option>
</select>`
});
For having the first option selected by default you need to point to the index 0.
Here your index is the k from v-for
<option v-for="(v,k) in subs" :selected="k === 0">{{k}}: {{v}}</option>
There is no v-model in select, I think that is the main issue. Other is its not clear what you what to do.
please refer the following code and check if it satisfy your need.
// app.vue
<template>
<sub-select v-model="value" :options="options" />
{{ value }}
</template>
<script>
import { ref } from "vue";
import subSelect from "./components/subSelect.vue";
export default {
name: "App",
components: {
subSelect,
},
setup() {
const value = ref(null);
const options = ref(["one", "two", "three"]);
return { value, options };
},
};
</script>
see that I have used v-model to bind the value to sub-select component.
the sub-select component as follows
// subSelect.vue
<template>
<select v-model="compValue">
<template v-for="(option, index) in compOptions" :key="index">
<option :value="option">{{ index }}: {{ option }}</option>
</template>
</select>
</template>
<script>
import { computed } from "vue";
export default {
name: "subSelect",
props: ["modelValue", "options"],
setup(props, { emit }) {
// if value is null then update it to be first option.
if (props.modelValue === null) {
emit("update:modelValue", props.options[0]);
}
const compValue = computed({
get: () => props.modelValue,
set: (v) => emit("update:modelValue", v),
});
// return selected option first in list/Array.
const compOptions = computed(() => {
const selected = props.options.filter((o) => o === compValue.value);
const notSelected = props.options.filter((o) => o !== compValue.value);
return [...selected, ...notSelected];
});
return { compValue, compOptions };
},
};
</script>
in sub-select component i am checking first if modelValue is null and if so set value to be first option.
and also providing compOptions in such sequence that selected options will always be first in list of selection options.
so it satisfies
The first option always to be selected by default.
Selected option will always be first in list of options.
check the code working at codesandbox
edit
jsfiddle as per request
also i suspect that you need options as underscore separated string for that please refer String.prototype.split() for converting it to array and Array.prototype.join() for joining array back to string.
if this is the case please comment so I can update my answer. It should be possible by setting watcher on compOptions and emitting separate event to parent, but I don't think its a good idea!

Vue 3 pass reactive object to component with two way binding

I have an issue in the two way binding of a reactive component in vue 3 using the composition API.
The setup:
The parent calling code is:
<template>
<h1>{{ message.test }}</h1>
<Message v-model="message" />
</template>
<script>
import Message from '#/components/Message.vue';
import { reactive } from 'vue';
export default {
name: 'Home',
components: { Message },
setup() {
const message = reactive({ test: '123' });
return {
message
};
}
};
</script>
The child component code is:
<template>
<label>
<input v-model="message" type="text" />
</label>
</template>
<script>
import { computed } from 'vue';
export default {
props: {
messageObj: {
type: Object,
default: () => {},
},
},
emits: ['update:messageObj'],
setup(props, { emit }) {
const message = computed({
get: () => props.messageObj.test,
set: (value) => emit('update:messageObj', value),
});
return {
message,
};
},
};
</script>
The problem:
When the component is loaded, the default value from the object is shown in the input field.
This is as it should be, however, when I update the value in the input box the H1 in the parent view is not getting updated with the new input box value.
I have searched through the stackoverflow board and google but have not found any hint as to what needs to be done to make the object reactive.
I read through the reactivity documentation but still have not found any solution for my issue.
For testing I have changed message to be a ref and using this single ref value the data remains reactive and everything is working as expected.
Any pointers on what can be the issue with the reactive object not updating?
Here
<div id="app">
<h1>{{ message.test }}</h1>
<child v-model="message"></child>
</div>
const { createApp, reactive, computed } = Vue;
// -------------------------------------------------------------- child
const child = {
template: `<input v-model="message.test" type="text" />`,
props: {
modelValue: {
type: Object,
default: () => ({}),
},
},
emits: ['update:modelValue'],
setup(props, { emit }) {
const message = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val),
});
return { message };
}
};
// ------------------------------------------------------------- parent
createApp({
components: { child },
setup() {
const message = reactive({ test: 'Karamazov' });
return { message };
}
}).mount('#app');
Solution and observations:
In the parent view which is calling the component you can use v-model and add a parameter to that v-model if you need to pass only one of the values in the object.
<template>
<h1>{{ message.test }}</h1>
<!-- <h1>{{ message }}</h1> -->
<Message v-model:test="message" />
</template>
<script>
import Message from '#/components/Message.vue';
import { reactive } from 'vue';
export default {
name: 'Home',
components: { Message },
setup() {
const message = reactive({ test: '123' });
return {
message
};
}
};
</script>
In the receiving component you then register the parameter of the object that was passed in props as an object.
<template>
<label>
<input v-model="message.test" type="text" />
</label>
</template>
<script>
import { computed } from 'vue';
export default {
props: {
test: {
type: Object,
default: () => {}
},
},
emits: ['update:test'],
setup(props, { emit }) {
const message = computed({
get: () => props.test,
set: (value) => emit('update:test', value),
});
return {
message,
};
},
};
</script>
If you need to pass the whole object you need to use as a prop in the component the name modelValue.
Change in parent compared to previous code:
<template>
<h1>{{ message.test }}</h1>
<!-- <h1>{{ message }}</h1> -->
<Message v-model="message" />
</template>
Code of the component:
<template>
<label>
<input v-model="message.test" type="text" />
</label>
</template>
<script>
import { computed } from 'vue';
export default {
props: {
modelValue: {
type: Object,
default: () => {}
},
},
emits: ['update:modelValue'],
setup(props, { emit }) {
const message = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value),
});
return {
message,
};
},
};
</script>
Should be pretty straight forward, and no computed is needed. See example below.
The messageObj was replaced with message in the child component for the emit to work (which would break due to case sensitivity in this demo)
const app = Vue.createApp({
setup() {
const message = Vue.reactive({ test: '123' , foo: "bark"});
return {
message,
};
}
})
app.component('Message', {
props: {
message: {
type: Object,
default: () => {},
},
},
emits: ['update:message'],
setup(props, { emit }) {
const message = props.message;
return { message };
},
template: document.querySelector('#t_child')
})
app.mount('#app')
<script src="https://unpkg.com/vue#3.0.2/dist/vue.global.prod.js"></script>
<fieldset>
<div id="app">
<h1>{{ message.test }} || {{ message.foo }}</h1>
<fieldset><Message v-model:message="message"/></fieldset>
</div>
</fieldset>
<template id="t_child">
<label>
<h4>{{message}}</h4>
<input v-model="message.test" type="text" />
<input v-model="message.foo" type="text" />
</label>
</template>
Your initial problem is quite simple. In Vue 3 v-model defaults to to a prop called modelValue and emits come from update:modelValue. Other answers here have assumed that in their solutions but not directly addressed it.
You can either rename your messageObj prop to use the default prop OR use the multi-model features in Vue 3:
<Message v-model:messageObj="message" />
However our problems run deeper.
All (current) answers will work but aren't quite correct. They all fail the idiomatic "One-way Data Flow" rule.
Consider this JSFiddle, modified from this answer.
const child = {
template: `<input v-model="message.test" type="text" />`,
setup(props, { emit }) {
const message = computed({
get: () => props.modelValue,
// No set() ?
});
return { message };
}
}
In this example, the child component never 'emits' - yet the data is still updating in the parent component. This violates the "One-way" rule. Data must be propagated from child components using only emits and not via prop proxies.
The problem in here is that props.modelValue is reactive when arrives in the child component. One can verify this with the isReactive() helper. When it's passed through the computed() it retains that reactiveness and will continue to proxy updates through itself into the parent component.
A solution:
JSFiddle here
const { createApp, ref, computed } = Vue;
const child = {
template: `<input v-model="message" type="text" />`,
props: {
modelValue: {
type: Object,
default: () => ({}),
},
},
emits: ['update:modelValue'],
setup(props, { emit }) {
const message = computed({
get: () => props.modelValue.test,
set: (test) => emit('update:modelValue', ({...props.modelValue, test })),
});
return { message };
}
};
createApp({
components: { child },
setup() {
const message = ref({ test: 'Karamazov' });
return { message };
}
}).mount('#app');
The solution is three parts:
The computed getter must not return the proxy object from the parent component. Once this happens you're in danger of violating the "one-way" rule [note 1]. In this example props.modelValue.test is a string so we're safe.
The computed setter must emit the whole object, but again it must not be a reactive type. So we clone the modelValue using spread and include in the updated test field. This can also be achieved with Object.assign({}, props.modelValue, {test}) [note 2].
The message variable in the parent component cannot be a reactive() and must be a ref(). When the v-model receives the newly emitted object the message variable is clobbered and no longer reactive [note 3]. Even with refs the props.modelValue will still fully reactive when it arrives in the child component, so the cloning steps are still important.
Alternatively:
I should also mention that values from computed() are not deeply reactive. As in, setting values on a computed object will not trigger the computed setter.
An alternate solution for passing the whole object through to your template:
setup(props, { emit }) {
const message = reactive({...props.modelValue});
watch(message, message => emit('update:modelValue', ({...message})));
return { message };
}
In this, the whole message object will emit whenever the .test field is updated. E.g. <input v-model="message.test" />. This still obeys the "one-way" data rule because emits are the only way data is given to parent component.
Reasoning:
"One-way" data flow is important [4]. Consider this:
<child :modelValue="message"></child>
On a first (and a sensible) glance, this appears to pass data into 'child' but not out of 'child'. But, given a reactive object that is not handled by the child correctly, this will emit changes into my own component.
Observing this code I don't expect this behaviour so it's very important that the child component gets it right.
Notes:
[1]: Testing violations of the "one-way" rule are surprisingly simple. Remove any emit and if the parent receives updates - you've broken it. Or replacing v-model with v-bind also works.
[2]: Object.assign() and {...} spread are indeed different. But shouldn't affect our uses here.
[3]: I haven't found any clear documentation about this behaviour regarding reactive() and v-model. If anyone wants to chime in, that'd be great.
[4]: The Vue docs stress the importance of one-way bind. Evan himself (creator of Vue) even provides examples about how to use v-model with objects (in Vue 2, but the principles still apply).
I feel it's also important to note later in the same thread Evan suggests objects that are nested more than 1-level are considered misuse of v-model.
It turns out that 2 way binding of object properties with Vue 3 is even easier than demonstrated in any of the previous answers.
Parent Code (App.vue):
<script setup>
import Controller from './components/Controller.vue';
import { reactive } from 'vue';
const object1 = reactive({name: "Bruce", age: 38});
const object2 = reactive({name: "Alex", age: 6});
</script>
<template>
<div>
{{object1}}<br/>
{{object2}}
<Controller :my-object="object1"/>
<Controller :my-object="object2"/>
</div>
</template>
Component code (Controller.vue):
<script setup>
import { computed } from 'vue'
const props = defineProps({
myObject: {
type: Object,
default: () => {}
}
})
const name = computed({
get () {
return props.myObject.name
},
set (value) {
props.myObject.name = value
}
})
const age = computed({
get () {
return props.myObject.age
},
set (value) {
props.myObject.age = parseInt(value)
}
})
</script>
<template>
<div>
<input v-model="name"/><br/>
<input v-model="age" type="number"/>
</div>
</template>
Explanation:
The <component :my-object="object1" /> syntax uses a : to tell Vue that we are passing an object (object1), rather than a string to the component and assigning it to property myObject. It turns out that when the child component receives this property, its reactivity is still intact. Therefore, as long as we don't mutate myObject itself, but instead only modify its properties, there is no need to emit any events or even pass it with as a property called v-model (we can call the property whatever we want). Instead the javascript proxy that the reactive keyword creates will do all the work tracking the changes and re-rendering it.
Some testing reveals that it is even possible to add new properties to the object or change deep properties and still maintain reactivity.
I am just a beginner with Vue, so there may be reasons why using this method are an anti-pattern, with unintended future consequences...

how do I pass the value from child to parent with this.$emit

What I trying to achieve here is to pass the const randomNumber inside the child component [src/components/VueForm/FormQuestion.vue] that need to be passed to parent component [src/App.vue]. Therefore I use $emit to pass the date, but since this is my first time working with $emit, I am not really sure how to do that. Could someone help me with this.
In order to run this app, I would add a working code snippet. Click on the start button and fill in the input fields. When the input field validates correctly it will pop up the button and if the user clicks on that is should pass the data to the parent. At the end it should be stored inside the App.vue in localStorage, so therefore I want to receive the randomNumber from that child component.
working code snippet here
// child component
<template>
<div class="vue-form__question">
<span class="question" :class="{ big: !shouldShowNumber }"> {{ getRandomNumber() }} </span>
</div>
</template>
<script>
export default {
methods: {
getRandomNumber() {
const randomNumber = Math.floor((Math.random() * 3) + 1);
const question = this.question.question;
this.$emit('get-random-number', question[randomNumber]);
return question[randomNumber];
}
}
};
// parent component
<template>
<div id="app">
<vue-form
:data="formData"
#complete="complete"
#getRandomNumber="newRandomNumber"
></vue-form>
</div>
</template>
<script>
import VueForm from "#/components/VueForm";
import data from "#/data/demo";
export default {
data() {
return {
formData: data
}
},
components: {
VueForm
},
created() {
this.complete()
},
methods: {
complete(data) {
// Send to database here
// localStorage.setItem('questions', data.map(d => d.question[this.randomNumber] + ': ' + d.answer));
},
}
};
</script>
v-on:get-random-number (or the superior short-hand syntax: #get-random-number). Just like you'd listen to any other event, such as #click or #mouseenter.
Though I don't know off the top of my head if dashes are valid in event names. Might have to camelcase it.

How to catch events across multiple child Vue components

I am building a form framework in vue. I have components for each field type. Each field type component uses this.$emit to communicate changes with the parent component.
I am able to trigger events in the parent component using v-on directives as follows:
<template>
<div v-if="fieldsLoaded">
<form-select :field="fields.title" v-on:updated="validate" ></form-select>
<form-input :field="fields.first_name" v-on:updated="validate" ></form-input>
</div>
</template>
However, I don't want to have to manually specify that every component should trigger the validate method individually.
How can I have the parent component listen for the updated emit across all its child components?
Edit: I'm looking for something like the below, though $on only catches emits that occur within the same component, rather than its children
created: function(){
this.$on('updated',validate)
}
The best way is to use event bus or even better in my opinion vuex.
For the first case take a look here
For the second here
With event bus you can emit an event, and listen to that event whenever you want(at parent,child even in the same component)
Vuex It serves as a centralized store for all the components in an application and you can have properties in that store,and you can use and manipulate them.
Example with event Bus:
main.js:
import Vue from 'vue'
import App from './App.vue'
export const eventBus = new Vue();
new Vue({
el: '#app',
render: h => h(App)
})
User Component
<template>
<button #click="clicked">Click me to create event</button>
</template>
<script>
import { eventBus } from './main'
export default {
name: 'User',
methods: {
clicked() {
eventBus.$emit('customEvent', 'a text to pass')
}
}
}
</script>
Admin component
<template>
<p>The message from event is: {{message}}</p>
</template>
<script>
import { eventBus } from './main'
export default {
name: 'Admin',
data: () => ({
message: ''
})
created() {
eventBus.$on('customEvent', dataPassed => {
this.message = dataPassed
}
}
}
</script>
Take a look to this tutorial to learn Vuex
For your case you can use v-model like following:
<template>
<div v-if="fieldsLoaded">
<form-select v-model="fields.title" :validate="validate" ></form-select>
<form-input v-model="fields.first_name" :validate="validate" ></form-input>
</div>
</template>
v-model is essentially syntax sugar for updating data on user input events.
<input v-model="something">
is just syntactic sugar for:
<input v-bind:value="something" v-on:input="something = $event.target.value">
You can pass a prop : value in the child components, and before changing input field call a function to validate which is also passed as a prop.
Vue.component('form-select', {
props: ['options', 'value', 'onChange', 'validate'], //Added one more prop
template: '#your-template',
mounted: function () {
},
methods: {
change (opt) {
if (this.validate !== undefined) {
var isValid = this.validate(this.value)
if(!isValid) return;
}
this.$emit('input', opt)
},
},
})