Vue parent component not re-rendering when computed properties are changing - vuejs2

I have a User-Select component that wraps a v-select. This components job is to fetch list of users as user types and allow the user to select one or more users.
Here is the code for this component.
<template>
<div>
<v-select
id="recipients"
name="recipients"
:options="options"
label="name"
multiple
#search="onSearch"
v-model="selectedVal"
/>
</div>
</template>
<script>
import vSelect from 'vue-select'
import axios from 'axios'
import _ from 'lodash'
export default {
name: 'UserSelect',
components: {
'v-select': vSelect
},
props: {
value: {
type: Array
}
},
data() {
return {
options: []
}
},
computed: {
selectedVal: {
get() {
return this.value
},
set(val) {
//this.value = val
this.$emit('input', val)
}
}
},
methods: {
onSearch(search, loading) {
loading(true)
this.search(loading, search, this)
},
setSelected: function(val) {
this.$emit('input', val)
},
search: _.debounce((loading, search, vm) => {
axios
.post('searchPeople', { search }, { useCredentails: true })
.then(res => {
vm.options = res.data
loading(false)
})
}, 350)
}
}
</script>
<style lang="scss" scoped>
#import url('https://unpkg.com/vue-select#latest/dist/vue-select.css');
</style>
As you can see I have a v-model linked to a computed property , which emits input event. Also my property name is value. Hence, I expect the parent component,that is using this UserEvent component to be able to v-model.
In the parent component , i have a computed property to which I have v-modelled the selected value. Here is the code.
<template>
<div>
<b-modal id="editMessage" :title="title" :static="true">
<form id="newMessageForm" class="row">
<div class="col-md-12">
<div class="form-group row">
<label for="to" class="col-sm-3 col-form-label">To:</label>
<user-select
class="col-sm-7"
style="padding-left: 0px; padding-right: 0px"
v-model="editedMessage.recipients"
/>
</div>
<div class="form-group row">
<label for="subject" class="col-sm-3 col-form-label"
>Subject:</label
>
<input
id="subject"
name="subject"
type="text"
class="form-control col-sm-7"
:value="editedMessage.messageSubject"
/>
</div>
<div class="form-group row">
<label for="date" class="col-sm-3 col-form-label"
>Schedule for later :
</label>
<input
type="checkbox"
class="form-control col-sm-7"
v-model="scheduleForLater"
id="scheduleCheckBox"
/>
</div>
<div class="form-group row" v-if="scheduleForLater">
<label for="date" class="col-sm-3 col-form-label"
>Scheduled Date:</label
>
<datetime
v-model="editedMessage.sentDate"
type="datetime"
class="col-sm-17"
input-class="form-control col-sm-15"
input-id="date"
/>
</div>
<div class="form-group row">
<label for="body" class="col-sm-3 col-form-label">Message:</label>
<textarea
id="body"
name="body"
type="text"
rows="10"
class="form-control col-sm-7"
:value="editedMessage.messageBody"
></textarea>
</div>
</div>
</form>
<template v-slot:modal-footer="{ hide }">
<!-- Emulate built in modal footer ok and cancel button actions -->
<b-button size="sm" variant="light" #click="hide()">
Cancel
</b-button>
<b-button
size="sm"
variant="secondary"
#click="
sendMessage(true)
hide()
"
>
Save Draft
</b-button>
<b-button
size="sm"
variant="primary"
#click="
sendMessage(false)
hide()
"
>
Send
</b-button>
</template>
</b-modal>
</div>
</template>
<script>
import { mapState } from 'vuex'
import UserSelect from '#/components/UserSelect.vue'
import axios from 'axios'
export default {
name: 'NewMessage',
components: {
'user-select': UserSelect
},
data() {
return {
options: [],
scheduleForLater: false
}
},
mounted() {},
computed: {
...mapState({
openMessage: state => state.message.openMessage,
messageAction: state => state.message.messageAction
}),
editedMessage: {
get() {
if (this.messageAction === 'new') {
return this.newMessage()
} else if (this.messageAction === 'reply') {
let openMessageClone = Object.assign({}, this.openMessage)
// make sender as the recipient.
return Object.assign(openMessageClone, {
messageSubject: 'RE: ' + this.openMessage.messageSubject,
recipients: [
{
name: this.openMessage.sender.name,
id: this.openMessage.sender.id
}
]
})
} else {
let openMessageClone = Object.assign({}, this.openMessage)
return Object.assign(openMessageClone, {
messageSubject: 'FW: ' + this.openMessage.messageSubject
})
}
},
set(val) {
this.$emit('input', val)
}
},
title() {
if (this.messageAction === 'new') {
return 'New'
} else if (this.messageAction === 'reply') {
return 'Reply'
} else {
return 'Forward'
}
}
},
methods: {
newMessage() {
return {
messageBody: '',
messageSubject: '',
recipients: [],
sender: {}
}
},
sendMessage(saveOrUpdateDraft) {
var url = ''
var message = {
recipients: this.editedMessage.recipients.map(x => x.id),
subject: this.editedMessage.messageSubject,
body: this.editedMessage.messageBody,
sentDate: this.scheduleForLater ? this.editedMessage.sentDate : null,
id: ['editDraft', 'viewScheduled'].includes(this.messageAction)
? this.editedMessage.messageId
: null
}
// id indiciates message id of draft message if one was opened.
if (saveOrUpdateDraft) {
// if no changes have been made to an opened draft, or the draft is completely empty for a new or existing draft , just go back.
if (message.id) {
url = `updateDraft`
} else {
url = 'saveNewDraft'
}
} else {
if (message.id) {
url = `sendSavedMessage`
} else {
url = 'sendMessage'
}
}
axios
.post(`eventdirect/${url}`, message, {
withCredentials: true
})
.then(response => {
if (url.includes('Draft') && this.messageAction === 'viewScheduled') {
this.$store.dispatch('sent/moveToDraft', response.data)
} else if (url.includes('Draft')) {
this.$store.dispatch('drafts/updateDraft', response.data)
} else {
// else part is sending a message or scheduling a draft.
if (this.messageAction === 'editDraft') {
this.$store.dispatch('drafts/deleteDraft', response.data)
}
// if we are sending an existing scheduled messsage , just update the sent vuex store , so that the message moves from scheduled to sent bucket.
if (this.messageAction === 'viewScheduled') {
this.$store.dispatch('sent/updateMessage', response.data)
} else {
this.$store.dispatch('sent/addSentItem', response.data)
}
}
})
.catch(() => {
// TODO , add a qtip here to notify user , this message should be sent later.
// messages in draft store with target , should be posted to the target
this.$store.dispatch('drafts/updateDraft', {
...message,
target: url
})
})
}
}
}
</script>
Now i can see in the vue dev tools the computed values in this NewMessage component gets changed. However this component does not re-render and the selected values are not passed down to UserSelect component until I toggle , schedule for later checkbox , that causes the components data to change and this triggers the Vue component to suddenly start showing the selected values.
Whats going on here. There is something about Vue's reactivity that I am not able to understand here.
Thanks in anticipation. You can try it here, or click on the edit sandbox button above to view and edit the code.
To try it, click on the Vue Message link , then hit reply , then type ‘Test’ and then select ‘Test User’ from the drop down. The selected user wont show until you click the checkbox - Schedule later.
PS: Within the component , UserSelect, In the setter of the computed property selectedVal , if I manually set the value of property value (by simply uncommenting line 39 - which is commented right now) , everything works fine. However , I get a warning that property should not be directly set. Since I am emitting the input event , parent component should change its v-model and re-render , thereby causing child component to re-render. Please correct me , if my understanding is wrong. Problem once again is that the parent component’s v-model changes , but it doesn’t re-render.

Related

V-Model is not Reactive (Vue3)

I have an string input Component, which passes the inputted data back to the parent component, and stores it in a Form Object.
This was working fine, until I began to use a Validation Library (Vee-Validate), I'm trying to retrace my changes, but cannot seem to solve this issue without breaking the validator. I've copied the relevant code below.
Parent Component:
<template>
<form class="form" #submit.prevent="submitFormData">
<BaseInput
v-model="form.salutation.firstName"
type="text"
class="text-class"
label="case_file.table.first_name"
:required="true"
/>
</form>
</template>
const form = reactive({
salutation: {
firstName: ""
},
});
Child Component:
<template>
<div class="form-control">
<label class="label">
<span class="label-text">{{ $t(`${labelName}`) }}</span>
</label>
<input
type="text"
class="input input-bordered"
:value="value"
#input="$emit('update:modelValue', $event.target.value)"
v-on="validationListeners"
/>
<span>{{ errorMessage }}</span>
</div>
</template>
<script setup>
import { useField } from "vee-validate";
import { reactive, computed } from "vue";
import { toRefs } from "#vue/reactivity";
import { ref, watch } from "vue";
import { useParticipantStore } from "#/store/participant";
const participantStore = useParticipantStore();
const props = defineProps({
label: {
type: [String, Boolean],
default: false,
},
modelValue: {
type: String,
default: "",
},
required: {
type: Boolean,
default: false,
},
});
const labelName = ref(props.label);
const localInputValue = ref(props.modelValue);
function validateField(value) {
if (!value && props.required) {
return "This is required";
}
return true;
}
const { errorMessage, value, handleChange } = useField(
"fieldName",
validateField,
{
validateOnValueUpdate: false,
}
);
const validationListeners = computed(() => {
// If the field is valid or have not been validated yet
// lazy
if (!errorMessage.value) {
return {
blur: handleChange,
change: handleChange,
input: (e) => handleChange(e, false),
};
}
// Aggressive
return {
blur: handleChange,
change: handleChange,
input: handleChange, // only switched this
};
});
</script>
With this TextInput component:
value does not need to be a prop
Just emit the update:modelValue event to the parent (this should
be enough)
For the required validation, the browser will help us if we manage the form submit and input attributes properly.
IMHO, other validations should go in the form component rather than the input component. the input should only open the gate for errorMessages to render.
Also, if we have the error messages object (reactive) in the form component, we can determine the submit button state easily.
This does not include vee-validate code but I think you can embed it easily. I feel vee validation should go inside the form component.
<template>
<div class="form-control">
<label class="label">
<span class="label-text">{{ labelName }}</span>
</label>
<input
type="text"
class="input input-bordered"
:value="tv"
:required="required"
#input="updateModelValue"
#change="$emit('change', $event.target.value)"
#focus="$emit('focus', $event.target.value)"
#blur="$emit('blur', $event.target.value)"
#keyup="$emit('keyup', $event.target.value)"
>
<span>{{ errorMessage }}</span>
</div>
</template>
<script setup>
import {ref, defineProps, defineEmits} from "vue"
const emit = defineEmits([
"update:modelValue",
"change",
"blur",
"focus",
"keyup",
])
const tv = ref("")
defineProps({
labelName: {
type: String,
default: ""
},
errorMessage: {
type: String,
default: ""
},
required: {
type: Boolean,
default: false
}
})
const updateModelValue = ($event) => {
tv.value = $event.target.value
emit("update:modelValue", $event.target.value)
}
</script>
And this is in the Parent component:
<template>
<form #submit.prevent="submitForm">
<TextInput
v-model="form.salutation.firstName"
label-name="First Name"
:error-message="formErrors.salutation.firstName"
required
#keyup="validateFirstName"
/>
<button
type="submit"
:disabled="formErrors.salutation.firstName"
>
Submit
</button>
</form>
</template>
<script setup>
import {reactive, watch} from "vue"
const form = reactive({
salutation: {
firstName: "",
}
})
const formErrors = reactive({
salutation: {
firstName: null,
}
})
const validateFirstName = () => {
if (form.salutation.firstName && form.salutation.firstName.length < 3) {
formErrors.salutation.firstName = "First name must be at least 3 characters"
return false
} else {
formErrors.salutation.firstName = null
return true
}
}
const submitForm = () => {
console.log(form.salutation.firstName)
}
</script>
This should make your form:
not submittable when empty (required validation)
show|hide error messages on keydown

v-on does not working why not receive $emit in custom component

I'm create a an component which represents my money field.
My target is on add element in list, set zero on money field to add next element in list...
But, my problem is that not working when send using $emit event to clear input to improve usability.
$emit works as described on image bellow
My money field:
<template>
<div class="input-group" #clear="clearInputField()">
<span>{{ title }}</span>
<input ref="displayMoney" type="text" v-model="displayMoney" #focus="isActive = true" #blur="isActive = false" />
</div>
</template>
<script>
export default {
props: {
title: String,
},
data() {
return {
money: 0,
isActive: false,
};
},
methods: {
clearInputField() {
console.log("Its work event");
this.money = 0;
this.displayMoney = "";
},
},
computed: {
displayMoney: {
get: function () {
if (this.isActive) {
return this.money;
} else {
return this.money.toLocaleString("pt-br", { style: "currency", currency: "BRL" });
}
},
set: function (modifiedMoney) {
let newMoney = parseFloat(modifiedMoney.replace(/[^\d.]/g, "."));
if (isNaN(newMoney) || newMoney.length == 0) {
newMoney = 0;
}
this.$emit("input", newMoney);
return (this.money = parseFloat(newMoney));
},
},
},
};
</script>
My principal component
<template>
<div class="wish-list">
<div class="row">
<div class="input-group">
<span>Digite sua meta: </span>
<input ref="descriptionWish" type="text" v-model="descriptionWish" />
</div>
<MoneyField title="Valor (R$): " v-model="valueWish" #keyup.native.enter="addWish" />
<button id="btnCalculate" #click="addWish()">Adicionar a lista de desejos</button>
</div>
<div class="list-items">
<ul>
<li v-for="wish in wishes" :key="wish">{{ wish }}</li>
</ul>
</div>
</div>
</template>
<script>
import MoneyField from "./Fields/MoneyField";
export default {
components: {
MoneyField,
},
data() {
return {
wishes: [],
valueWish: 0,
descriptionWish: "",
};
},
methods: {
addWish() {
if (!isNaN(this.valueWish) && this.valueWish > 0 && this.descriptionWish.length > 0) {
this.wishes.push(
`${this.descriptionWish} => ${this.valueWish.toLocaleString("pt-BR", { currency: "BRl", style: "currency" })}`
);
this.descriptionWish = "";
console.log("addWish");
this.valueWish = 0;
this.$emit("clear");
this.$refs.descriptionWish.focus();
}
this.valueWish = 0;
},
},
};
</script>
I still don't understand much about vueJS, but I believe it's something related to parent and child elements, but I've done numerous and I can't get my answer.
sorry for my bad english .
The emit sends an event from the child to the parent component not as you've done, to run a method from the child component you could add a ref in the component inside the parent one like :
<MoneyField title="Valor (R$): "
ref="moneyField" v-model="valueWish" #keyup.native.enter="addWish" />
then run this.$refs.moneyField.clearInputField() instead this.$emit("clear")

Vue.js prop sync modifier not updating parent component

I have a property that I need to pass to a child component, but the child component needs to be able to modify the value that was passed. It seems like the .sync modifier is built for this, but I can't seem to get it to work. Here is my code (simplified for this question):
Profile.vue
<template>
<div>
<Avatar :editing.sync="editing"></Avatar>
click to change
...
</div>
</template>
<script>
import Avatar from './profile/Avatar'
export default {
components: { Avatar },
data() {
return {
...,
editing: false,
}
},
methods: {
editAvatar() {
this.editing = true;
}
}
}
</script>
Avatar.vue
<template>
<div>
<template v-if="!editing">
<img class="avatar-preview" :src="avatar">
</template>
<template v-else>
<label v-for="image in avatars">
<img class="avatar-thumb" :src="image">
<input type="radio" name="avatar" :checked="avatar === image">
</label>
<button class="btn btn-primary">Save</button>
</template>
</div>
</template>
<script>
export default {
props: ['editing'],
data() {
return {
avatar: '../../images/three.jpg',
avatars: [
'../../images/avatars/one.jpg',
'../../images/avatars/two.jpg',
'../../images/avatars/three.jpg',
...
]
}
},
methods: {
save() {
axios.put(`/api/user/${ user.id }/avatar`, { avatar: this.avatar }
.then(() => { console.log('avatar saved successfully'); })
.catch(() => { console.log('error saving avatar'); })
.then(() => { this.editing = false; }); // ← this triggers a Vue warning
}
}
}
</script>
You are correct - the .sync modifier is built for cases just like this. However, you are not quite using it correctly. Rather than directly modifying the prop that was passed, you instead need to emit an event, and allow the parent component to make the change.
You can resolve this issue by changing the save() method in Avatar.vue like this:
...
save() {
axios.put(`/api/user/${ user.id }/avatar`, { avatar: this.avatar }
.then(() => { console.log('avatar saved successfully'); })
.catch(() => { console.log('error saving avatar'); })
.then(() => { this.$emit('update:editing', false); });
}
}
...

Send value from API request from a component to another Vue.JS

I have a component which allow to retrieve the datas from a rest API...
My template allow user to enter an input (id) and to find the user associated with the user. I also have a component which is called dynamically.
<template>
<div>
<!-- form -->
<form>
<input type="text" v-model="userId" id="userId">
<button type="submit" class="btn btn-primary" #click="getUser($event); !isExistingUser">Get User</button>
</form>
<!-- result -->
<div v-if="!showComponent">
{{ user["id"] }} {{ user["username"] }} {{ user["email"] }}
<button #click="showComponent = !showComponent">Editer</button>
</div>
<!-- Edit the user -->
<div v-if="showComponent">
<edit-user :toUpdate="updateUser"></edit-user>
</div>
</div>
</template>
In the script part I have datas and methods :
The objective is to send the user that i collect and to send it to the update user. For this I created a data binding.
I also try to set the value of the object in the getUser method. And i can display the value.
<script>
import axios from "axios";
import EditUserForUpdate from "./EditUserForUpdate";
export default {
name: "FindUser",
components: {
"edit-user": EditUserForUpdate
},
data() {
return {
toUpdate: Object,
user: null,
isExistingUser: false,
userId: "",
userEmail:"",
userUsername: "",
showComponent: false
};
},
methods: {
getUser(event) {
axios
.get("http://localhost:4000/api/users/" + this.userId)
.then(response => {
console.log(response);
this.user = response.data.data;
var toUpdate = {};
toUpdate = { upUserName: this.user.username, upUserEmail: this.user.email, upId: this.user.id};
console.log(toUpdate);
});
}
}
};
</script>
Finally in the child component :
<script>
export default {
name: "EditUserForUpdate",
data: function () {
return {
updateUser: ''
}
},
props: {
updateUser: Object
},
methods: {
beforeMount () {
var updateUser = this.updateUser // save props data to itself's data and deal with it
console.log("userToUpdate : " + updateUser);
}
}
}
</script>
My issue is that I don't retrieve the data in the child module for an unknown reason.
The property is named toUpdate and not updateUser.
Update your prop accordingly in the EditUserForUpdate component:
props: {
toUpdate: Object
}
And of course, localize that object for manipulation:
beforeMount() {
this.updateUser = this.toUpdate
}

Validate Child component Vue vee-validate

App (parent)
Hi I have these component (Child)
TextComponent
InfoErrorForm
When I press submit from the parent component App is not validate this form. So I tried to validate with inject $validator in the child component (TextComponent), and provide but not show message error .
If you can help me to validate children form inisde parent component.
This is my code
AppComponent
<template>
<div>
<!-- Form validar numero max input -->
<form :class="{'was-validated': error_in_form_save_progress}" >
<card-shadow v-for="(texto,key) in sections_template.texts" :key="key" >
<texto-component
:orden="key+2"
v-model="sections_template.texts[key].content"
:tituloComponente="texto.title"
:inputName="texto.title" >
<template slot="section_show_error_validate_input">
<info-error-form
:listErrors='errors'
:name_field = "texto.title"
v-show = "error_in_form_save_progress" >
</info-error-form>
</template>
</texto-component>
</card-shadow>
</form>
<div class="row foot_button" >
<div class="offset-md-3 col-md-3">
<button class="btn" #click.prevent="save_progrees"> Guardar Cambios</button>
</div>
</div>
</div>
</template>
<script>
export default {
provide() {
return {
$validator: this.$validator,
};
},
data: function(){
return {
sections_template: {
texts:[
{
section_template_id: 1,
type: "texto",
title: "fundamentacion",
content: ""
},
{
section_template_id: 2,
type: "texto",
title: "sumilla",
content: ""
}
] },
error_in_form_save_progress: true
}
},
methods:{
save_progrees(){
this.$validator.validateAll().then((result) => {
if (result) {
this.error_in_form_save_progress = false;
alert("se guardaran cambios");
return
}
this.error_in_form_save_progress = true;
});
}
}
}
</script>
I found solution with this code. In my parent component i add provide and i send the $validator
export default {
components:{
...
},
provide() {
return {
$validator: this.$validator,
}
},
In my child component i received this
inject: ['$validator'],
In my parent component i add this method to invoque validation
methods:{
save_progrees(){
var validationArray = this.$children.map(function(child){
return child.$validator.validateAll();
});
window.Promise.all(validationArray).then((v) => {
v.some( element => { if ( element == false ) { throw "exists error in child component";} });
this.error_in_form_save_progress = false;
alert("datos guardados");
return
}).catch(() => {
this.show_message_error_validation();
});
},
show_message_error_validation(){
this.error_in_form_save_progress = true;
}
}
Finally to show error in component info-error I use this code
<template>
<div class="row" v-if="errors.items">
<div class="col-md-12">
<template v-for="(e,key) in errors.items" >
<div class="text-danger" v-if="e.field ==name_field" :key="key"> {{e.msg}} </div>
</template>
</div>
</div>
</template>
In your child component do watch for this.errors and in the watch, put this.$emit
Something like this below :
watch: {
errors: function (value) {
this.$emit('TextComponent', value)
}
}
And then catch it on your parent component see it here https://v2.vuejs.org/v2/api/#vm-emit