Vuelidate-error-extractor nested objects validation - vue.js

I have the following div in my vue file:
<div class="md-layout rate-row" v-for="(rate, index) in serviceDetails.serviceRates" :key="index">
<form-group
name="serviceDetails.serviceRates.$each.0.unitPriceExTax.$model"
label="Unit Price (Ex. Tax)"
attribute="Unit Price"
:messages="localMessages"
class="md-layout-item price"
>
<md-input v-model="rate.unitPriceExTax" />
</form-group>
<form-group
name="serviceDetails.serviceRates.$each.0.unitCostExTax.$model"
label="Unit Cost (Ex. Tax)"
attribute="Unit Cost"
:messages="localMessages"
class="md-layout-item cost"
>
<md-input v-model="rate.unitCostExTax" />
</form-group>
</div>
I also have the following prop inside my component (since as I understood I need an initial setup object):
props: {
serviceDetails: {
type: Object,
default: () => ({
serviceRates: [
{
unitPriceExTax: 0,
unitCostExTax: 0
}
]
})
},
...
...
}
I also have my validations which are like:
validations: {
serviceDetails: {
serviceRates: {
$each: {
unitPriceExTax: {
decimal,
minValue: (value) => !value || value > 0,
maxValue: (value) => !value || value < 100000000
},
unitCostExTax: {
decimal,
minValue: (value) => !value || value > 0,
maxValue: (value) => !value || value < 100000000
},
}
}
...
...
}
},
The localMessages in the data() section are:
data() {
return {
localMessages: {
required: '{attribute} is required.',
decimal: '{attribute} should contain a decimal number.',
minValue: '{attribute} should have a positive value.',
maxValue: '{attribute} should be less than 100000000.'
}
};
},
The form-group component source is the following:
<template>
<md-field :class="{ 'md-invalid': hasErrors }">
<label>{{ label }}</label>
<slot :touch="() => preferredValidator.$touch()" />
<span v-if="hasErrors" class="md-error">{{ firstErrorMessage }}</span>
</md-field>
</template>
<script>
import { singleErrorExtractorMixin } from 'vuelidate-error-extractor';
export default {
extends: singleErrorExtractorMixin
};
</script>
<style lang="scss" scoped>
.md-error {
bottom: -10px !important;
}
</style>
My problem is that I can not make vuelidate & the error extractor validate my properties.
I would expect an error message like:
In reality if I go to the Vue developer tools I can see that the validations occur correctly but I don't see the messages bellow the text fields.
I am almost persuaded that the issue is the wrong name attribute in the form-group but I don't know how to structure it. Also, how to put the index inside there?
For your information, the versions being used are:
"vuelidate": "^0.7.5",
"vuelidate-error-extractor": "^2.4.1",

Related

Vue 2: Value prop does not update input value

So I have encountered a weird issue with my code, that I hope to get some help with.
I have a custom "Input" component where I have a normal HTML input with some styling. I have then called this component with a value and a function to call upon changes. No v-model is used as I have to do some validation on the field. However, it doesn't work. I can see that the value variable in the "Input" component changes correctly, but it does not impact the HTML input element at all, if you enter multiple values into the input field. How can this be?
InputComponent
<template>
<label class="block text-sm flex justify-end lg:justify-start w-28 h-10">
<span class="text-gray-800">{{ label }}</span>
<input
class="block text-black placeholder:text-black placeholder:opacity-40 w-14 lg:w-full rounded-lg text-center"
:placeholder="placeholder"
:type="type"
:value="value"
#input="handleInput($event.target.value)"
/>
</label>
</template>
<script>
export default {
props: {
label: String,
placeholder: String,
type: String,
value: String,
size: String,
},
methods: {
handleInput(value) {
this.$emit('input', value);
}
},
}
</script>
Page component calling Input
<template>
<Input
type="number"
placeholder="0"
size="sm"
:value="test"
#input="changePlannedVacationDay($event)"
/>
</template>
<script>
export default {
data() {
return {
test: ""
};
},
methods: {
changePlannedVacationDay(value) {
let localValue = value;
const maxValue = 5;
if (parseInt(localValue) < 0) {
localValue = "0";
} else if (parseInt(localValue) > maxValue) {
localValue = maxValue.toString();
}
this.test = localValue;
}
},
</script>
You should use a computed property with getter and setter:
<template>
<input v-model="localModel">
</template>
<script>
export default
{
name: 'CustomInputComponent',
props:
{
value:
{
type: String,
default: null,
},
},
computed:
{
localModel:
{
get()
{
return this.value;
},
set(val)
{
this.$emit('input', val);
}
},
},
}
</script>
In the parent component you should use a watcher to detect value changes and act upon them.

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")

How to pass props to input value in Vuejs

I have a parent component as a Cart. Here I defined quantity and I want to pass this quantity to the child component's input value which is Counter. So here how I am passing it and here is my parent component, Cart:
<Counter quantity="item.quantity"/>
And here is my child component, Counter:
<template>
<div id="counter">
<button class="minus" #click="countDown"><i :class="quantity == 0 ? 'disabled' : ''" class="fas fa-minus"></i></button>
<div class="count-number"><input class="counter-content" type="number" v-model="quantity"></div>
<button class="plus" #click="countUp"><i class="fas fa-plus"></i></button>
</div>
</template>
<script>
export default {
props: {
quantity: Number
},
methods: {
countUp() {
this.quantity++;
},
countDown() {
if(this.quantity > 0) {
this.quantity--;
}
},
}
}
</script>
I am quite new in Vue, so maybe I am doing something wrong when I pass the props. So could you help me about this?
Try (with the : colon sign)
<Counter :quantity="item.quantity"/>
Before you were just passing the string "item.quanity"
I see you're modifying your prop directly:
countUp() {
this.quantity++;
},
countDown() {
if(this.quantity > 0) {
this.quantity--;
}
},
This is not how you do it in Vue. You need to use two way binding.
countUp() {
this.$emit('input', this.quantity+1)
}
countDown() {
this.$emit('input', this.quantity-1)
}
and in your parent component:
<Counter :quantity="item.quantity" #input="(payload) => {item.quantity = payload}"/>
By the way, the Vue styleguide recommends to use multi-word component names: https://v2.vuejs.org/v2/style-guide/#Multi-word-component-names-essential (Cart = bad, MyCart = good)
We cannot change the value that we get from props, so I created a variable and put props there when mounting
Try it
<Counter :quantity="item.quantity"/>
and
<template>
<div id="counter">
<button class="minus" #click="countDown"><i :class="sum == 0 ? 'disabled' : ''" class="fas fa-minus"></i></button>
<div class="count-number"><input class="counter-content" type="number" v-model="sum"></div>
<button class="plus" #click="countUp"><i class="fas fa-plus"></i></button>
</div>
</template>
<script>
export default {
props: {
quantity: Number
},
data: () => ({
sum: 0
}),
mounted() {
this.sum = this.quantity;
},
methods: {
countUp() {
this.sum++;
},
countDown() {
if(this.sum > 0) {
this.sum--;
}
},
}
}
</script>

Invalid prop: type check failed error in VueJS

I am trying to use a component named CardRenderer.vue which renders card using array of Objects. I am using the same component again & again to render the data. I am having this error "[Vue warn]: Invalid prop: type check failed for prop "renderData" when I tried passing prop from component.
I tried passing different values and different types but it did'nt work.
Here is the code:
CardRenderer.vue
<template lang="html">
<div>
<b-container class="bv-example-row">
<b-row v-for="(row, i) of rows" v-bind:key="i">
<b-col v-for="(item, j) of row" v-bind:key="j" >
<!-- you card -->
<b-card
:title="item.title"
img-src="item.icon"
img-alt="Image"
img-top
tag="article"
style="max-width: 20rem;"
class="mb-2"
>
<b-card-text>
<h1>{{item.name}}</h1>
<pre>{{item.description}}</pre>
</b-card-text>
<b-button :href="'/dashboard/'+item.name" variant="primary">More</b-button>
</b-card>
</b-col>
</b-row>
</b-container>
</div>
</template>
<script lang="js">
export default {
name: 'CardRenderer',
props: {
renderData: []
},
data() {
return {
rows: null
}
},
mounted() {
const itemsPerRow = 3
let rowss = []
let arr = this.renderData
// eslint-disable-next-line
// console.log(this.renderData)
for (let i = 0; i < arr.length; i += itemsPerRow){
let row = []
for (let z = 0; z < itemsPerRow; z++) {
row.push(arr[z])
}
rowss.push(row)
}
this.rows = rowss
// eslint-disable-next-line
console.log(this.rows)
},
methods: {
},
computed: {
// rows() {
// }
}
}
</script>
<style scoped>
</style>
CardGrouper.vue:
<template lang="html">
<div class = "full" >
<div class="h-50" style=" background-color: #C8544F">
<h1 align="center">{{$store.getters.responseAPI.title}} </h1>
<CardRenderer :renderData=this.$store.getters.responseAPI.apps />
</div>
</div>
</template>
<script>
import CardRenderer from "./CardRenderer.vue"
/* eslint-disable */
export default {
name: 'CardGrouper',
components: {
CardRenderer
},
props: [],
mounted() {
},
data() {
return {
}
},
methods: {
},
computed: {
}
}
</script>
<style scoped >
.full{
width: 100vw;
height: 90vh;
background: linear-gradient(to bottom, Red 30%, white 50%);
}
</style>
Something.vue
<template lang="html">
<!-- <h1>Something</h1> -->
<CardRenderer :renderData=valObj />
</template>
<script lang="js">
import CardRenderer from './CardRenderer'
export default {
name: 'something',
components: {
CardRenderer
},
props: [],
data() {
return {
valObj: []
}
},
mounted() {
let key = this.findUrl()
let value = this.$store.getters.responseAPI.apps.filter((elem) => {
if(elem.name == key) return elem.apps
})
if (value.length > 0)
this.valObj = value[0].apps
//eslint-disable-next-line
console.log(this.valObj)
},
methods: {
findUrl() {
let url = window.location.pathname.split("/").slice(-1)[0];
return url
}
},
computed: {
}
}
</script>
<style scoped >
.something {
}
</style>
I am having this error.
It looks like this.
This is the data I am passing as a prop from Something.vue
This is how value looks like
Error is being generated somewhere from Something.vue.
I am passing array of objects as a prop.
How do i rectify this error, to make it work.
Set the renderData type as Array and default value to []:
props: {
renderData: {
type: Array,
deafult: () => []
}
}
It appears that you are defining your renderData prop as an array [] but probably are passing an object to it or something.
Either simplify it and do...
props: ['renderData']
Or if you are passing an object to it do..
props: {
renderData: {
type: Object,
}
}
If it is an array of objects do..
props: {
renderData: {
type: Array,
default: () => [{}];
}
just for doc.
// Object with a default value
propE: {
type: Object,
// Object or array defaults must be returned from
// a factory function
default: function () {
return { message: 'hello' }
}
},
This is in vue prop documentation

vue.js doesn't update on mounted() hook

Edit : Solved, see answer.
So i'm new to vue.js and quasar so it is probably a rookie mistake about vue lifecycle hooks and vue reactivity.
I want to resize an element based on the browser's size. The height of this element is calculated using the height of other elements. I use $refs to get the other element's height and I capture the onResize() event launched by a quasar component.
This event is launched once when the page is loading, and my element's sizes are all 0 because I guess they are not rendered in the DOM yet. I have a method to refresh my calculated height, which I call when "onResize" event is captured, and also at the "mounted()" vue.js hook.
My problem is :
the first onResize() calls the method but all elements have 0px in height.
mounted() calls the method again and here all elements have their height calculated. The results are good, but it does not show on the display, see screenshot #1 : resize events and sizes logged in the console, note that the size is calculated twice, once on onResize() and once on mounted() . the one on mounted() has the good value but it does not show in the DOM.
after I resize the window once, then everything is ok and i don't have any problem anymore. (screenshots #2 (window mode) and #3 (fullscreen again))
My question is : why the height is not update in the DOM when mounted() hook is called even if it is calculated correctly ? (everything is in the same .vue file)
My code :
My problem is with the height of the div that has the "tableRow" ref
<template>
<q-page>
<div class="row" :style="'height: '+pageSize.height*0.95+'px;'">
<div class="col-6 q-pa-lg">
<div class="row" ref="actionsRow">
<div class="col-6 q-mb-sm">
<q-search hide-underline v-model="filter" />
</div>
<div class="col-6">
</div>
</div>
<div class="row" ref="tableHeaderRow">
<q-table class="col-12" :selection="selectionMode" :selected.sync="selectedRows" :data="templateTableData" :columns="templateColumns"
row-key="slug" :pagination.sync="pagination" dense hide-bottom>
<q-tr slot="body" slot-scope="props" :props="props">
</q-tr>
</q-table>
</div>
<div class="row" ref="tableRow" :style="'height: '+tableHeight+'px;'">
<q-scroll-area style="height: 100%" class="col-12 q-mt-sm shadow-3">
<q-table :selection="selectionMode" :selected.sync="selectedRows" :data="templateTableData" :columns="templateColumns" row-key="slug"
:filter="filter" :pagination.sync="pagination" dense hide-bottom hide-header>
<q-tr slot="body" slot-scope="props" :props="props" #click.native="onRowClick(props.row)" class="cursor-pointer">
<q-td auto-width>
<q-checkbox color="primary" v-model="props.selected" />
</q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
</q-tr>
</q-table>
</q-scroll-area>
</div>
</div>
<router-view class="col-6 q-pa-lg">
</router-view>
</div>
<q-window-resize-observable #resize="onResize" />
</q-page>
</template>
Script:
var data = []
for (var i = 1; i <= 100; i++) {
data.push({
id: i,
name: 'Template ' + i,
slug: 'template' + i,
active: true
})
}
import { mapActions, mapGetters } from 'vuex'
export default {
data: () => ({
pageSize: {
height: 0,
width: 0
},
tableHeight: 0,
templateColumns: [
{
name: 'templateName',
required: true,
label: 'Name',
align: 'left',
field: 'name',
sortable: true
},
{
name: 'templateSlug',
label: 'Slug',
align: 'left',
field: 'slug',
sortable: true
},
{
name: 'templateActive',
label: 'Active',
align: 'left',
field: 'active',
sortable: true,
sort: (a, b) => {
if ((a && b) || (!a && !b)) {
return 0
} else if (a) {
return 1
} else {
return -1
}
}
}
],
selectionMode: 'multiple',
selectedRows: [],
pagination: {
sortBy: null, // String, column "name" property value
descending: false,
page: 1,
rowsPerPage: 0 // current rows per page being displayed
},
templateTableData: data,
filter: ''
}),
computed: {
...mapGetters('appUtils', [
'getPageTitle',
'allConst'
])
},
methods: {
...mapActions('appUtils', [
'setPageTitle',
'deletePageTitle'
]),
onResize (size) {
this.pageSize.height = size.height - this.getPageTitle.height
this.resizeTable()
console.log('ON RESIZE EVENT:')
console.log('tableHeaderRow:'+
this.$refs.tableHeaderRow.clientHeight)
console.log('actionsRow:' + this.$refs.actionsRow.clientHeight)
console.log('table:' + this.tableHeight)
},
onRowClick (row) {
this.$router.push('/templates/' + row.slug)
},
resizeTable () {
this.tableHeight = this.pageSize.height - this.$refs.actionsRow.clientHeight -
this.$refs.tableHeaderRow.clientHeight - this.getPageTitle.height -
this.allConst.templatePageHeaderMargins
}
},
mounted () {
console.log('MOUNT TEMPLATES')
this.setPageTitle({ text: 'Manage templates', height: this.allConst.titleHeight })
this.resizeTable()
console.log('tableHeaderRow:' + this.$refs.tableHeaderRow.clientHeight)
console.log('actionsRow:' + this.$refs.actionsRow.clientHeight)
console.log('table:' + this.tableHeight)
},
destroyed () {
console.log('DESTROY TEMPLATES')
this.deletePageTitle()
}
}
Another one of my variables (titleSize) was 0, like the other sizes, during the first onResize() event, and I did not take that into account to correct it during the mounted() Hook.