Vee-Validate between and data_between not working with object notation - vuejs2

I am trying to validate data between to years using the object notation like followed:
<div class="form-group">
<label for="construction-year">Construction Year</label>
<input v-validate="validations.building.construction_year" data-vv-as="Construction Year" v-model="construction_year" type="text" id="construction-year" class="form-control" name="construction_year" placeholder="Year" maxlength="4">
<div v-show="errors.has('construction_year')" id="construction-year-error" class="msg-error text-danger">{{ errors.first('construction_year') }}</div>
</div>
<script>
data() {
return {
validations: {
building: {
construction_year: {
required: true,
date_format: 'YYYY',
date_between:`1500,${new Date().getFullYear()}`
},
floors_above_ground: {
required: true,
between: '1,11',
},
}
},
}
},
</script>
However, the message that I am getting is:
The Construction Year must be between 1500,2018 and undefined.
How would be the right way to do it? In the documentation, it is not shown the object notation, so I tried to pass a string but it did not work either. The same problem is happening when I am using the validation 'between' as illustrated above.
Thanks for the help in advance.

When I was trying to create a custom validation as a workaround to this error, I realized that the args parameter, which the Custom validator receives, is an array.
So, I tried to use an array to the between and data_between validations in the object notation and it worked as followed.
validations: {
building: {
construction_year: {
required: true,
date_format: 'YYYY',
date_between: ['1500', `${new Date().getFullYear()}`],
},
floors_above_ground: {
required: true,
between: [1, 11],
},
}
},
It would be good to add this information in the Documentation.

Related

Vuelidate $each: How can I validate a nested collection?

I am having a really hard time trying to grasp the likely elementary concept(s). I am passing a location in as a prop. It has a json column to store additionalAttributes. It looks something like this:
"additionalProperties": [
{
"integrations": [
{
"exampleVendor": {
"locationId": 123,
"positionId": 456
}
}
]
}
],
"createdAt": "",
"updatedAt": "",
...
The above is what I've hard-coded into my database (Postgres) to attempt to mock what the data will look like when it comes back.
I am working from the validate collections portion of the vuelidate documentation.
Here is what I am using to attempt to create the validation rule:
validations: {
location: {
additionalProperties: {
$each: {
integrations: {
$each: {
exampleVendor: {
locationId: {required},
positionId: {required},
}
}
}
}
}
}
},
In my template, I'm trying to connect the validations like this:
<select id="my-id"
name="my-id"
class="py-3 px-3 mt-1 block w-full pl-3 pr-10 py-2 text-base sm:text-sm rounded-md"
v-if="locations"
v-model.trim="$v.location.additionalProperties[0].integrations[0].exampleVendor.locationId.$model"
:class="[$v.location.additionalProperties[0].integrations[0].exampleVendor.locationId.$error ?
'focus:ring-red-500 focus:border-red-500 border-red-300' : 'focus:ring-gray-400 focus:border-gray-400 border-gray-300',]"
>
...
</select>
I've been working with this component for quite a while and have already asked a really silly question.
I am also concerned that by setting such a rigid path additionalProperties[0].integrations[0] is really bad.
I fear this one isn't too far behind but it's time to ask for some advice. Thank you for any suggestions!
EDIT
#tony19 made an excellent call about why the array if only the first value is being used. Perhaps there is a better way to do what I'm doing; here is a wider view of what the data in my database could look like. It has additional properties now beyond just integrations. For now, I'm only focused on that though.
"additionalProperties": [
{
"integrations": [
{
"exampleVendor": {
"locationId": 123,
"positionId": 456
},
"anotherVendor": {
"foo": "abc",
"bar": "def"
},
"someOtherVendor": {
"thing": "value"
}
}
],
"anotherAttribute: {
"one": "two"
},
"possibleAttributes": [...]
}
],
As you commented it's possible to have more array values in additionalProperties and integrations, it makes more sense to iterate those properties rather than hard-coding access to the first element only.
The Vuelidate docs for collections you linked shows iterating the array with $each.$iter, so I would use <template v-for="ARRAY.$each.$iter"> for each level of nesting:
<template v-for="(addtlProperty, i) in $v.location.additionalProperties.$each.$iter">
<template v-for="(integration, j) in addtlProperty.integrations.$each.$iter">
<select
:key="`${i}-${j}`"
v-model.trim="integration.exampleVendor.locationId.$model"
:class="[
integration.exampleVendor.locationId.$error
? 'focus:ring-red-500 focus:border-red-500 border-red-300'
: 'focus:ring-gray-400 focus:border-gray-400 border-gray-300',
]"
>
...
</select>
</template>
</template>
demo
There are quite a few things I've learned while working through this. One of the more important being how to troubleshoot what vuelidate thought it was getting.
I created an change handler to provide insight to what the $model value was. Here is an example:
<select #change="onChange"...">...</select>
...
// start with what I know to be true.
onChange() {
console.log($v.location.additionalProperties);
}
Using the above object structure, I'd then move into the object until I ended up with this:
console.log($v.location.additionalProperties.$each[0].integrations.$each[0]. exampleVendor.locationId.$model; // 12345
Now that I had the "path" to the model, I could update my <select> element:
<select id="my-locationId" name="my-locationId" class="py-3 px-3 mt-1 block w-full pl-3 pr-10 py-2 text-base sm:text-sm rounded-md"
v-model.trim="$v.location.additionalProperties.$each[0].integrations .$each[0].exampleVendor.locationId.$model"
:class="[
$v.location.additionalProperties.$each[0].integrations.$each[0].exampleVendor.locationId.$error
? 'focus:ring-red-500 focus:border-red-500 border-red-300'
: 'focus:ring-gray-400 focus:border-gray-400 border-gray-300',
]"
>
<option selected="selected" value="">Select</option>
<option
v-for="location in myLocations"
:key="location.id"
:value="location.id"
>
{{ location.name }}
</option>
</select>
Now that the nested path was collecting/setting the data, I could set up the validation rules:
...
data: () => ({...}),
validations: {
location: {
additionalProperties: {
$each: {
integrations: {
$each: {
exampleVendor: {
locationId: { required },
positionId: { required },
},
},
},
},
},
},
},
...
methods: {
async save() {
this.$v.$touch();
if (this.$v.$invalid) {
this.errors = true;
} else {
try {
const params = {
location: this.location, // location is passed in as props
method: this.location.id ? "PATCH" : "POST",
};
console.log('params: ', params); // {...}
// Save to vuex or ??
} catch (error) {
console.log('there was an error:', error);
}
}
},
}
Hope this helps someone else - it wasn't super straight forward & I'm sure there is a more effective way, but this ended up working for me.
EDIT 2
Please be sure to follow #tony19's suggested answer as well. The solution provided removes the "rigidity" I was speaking about in my question.

Connect v-select with vuex: problem [object object]

I am trying to create a dropdown (v-select/q-select (using quasar)), which allows me to select from an array in my vuex-storage and then eventually save the selected item (content of it) in a variable. Currently I have no problem to access the vuex-storage, but face the problem, that the v-select expects a string and not an object.
My code looks like the following.
// vuex storage:
const state = {
savedsystems:
[
id: "1",
system: {...}
],
[
id: "2",
system: {...}
]
// example of the vuex storage out of my viewdevtools
systemsconstant: Object
savedsystems:Array[2]
0:Object
id:"first"
system:Object
7a73d702-fc28-4d15-a54c-2bb950f7a51c:Object
name:"3"
status:"defined"
88519419-8a81-48f1-a5e6-5da77291b848:Object
name:"5"
status:"not defined"
1:Object
id:"second"
system:Object
7a73d702-fc28-4d15-a54c-2bb950f7a51c:Object
name:"3"
status:"not defined"
88519419-8a81-48f1-a5e6-5da77291b848:Object
name:"9"
status:"defined"
}
// dropdown:
<q-select
outlined
dense
emit-value
:value="currentsystem"
:options="savedsystems"
label="selectsystem" />
// computed to get systems from vuex:
computed: {
savedsystems() {
return this.$store.getters['systemsconstant/getsavedsystems']
}
},
I used the following example https://codepen.io/sagalbot/pen/aJQJyp as inspiration and tried a couple of different setups stringifying resulting in nothing really.
If one would try to apply my case to a similar problem (v-select displays object Object), the mentioned formatlabel would be an object instead of a string.
Question:
How can I modify the (with a getter) imported array of objects "savedsystems", so it can be used both as label to select it and furthermore then to connect it properly to the values, so I can save the selected as a variable.
Or can I change something in my v-select, e.g. varying what comes behind :options/options?
I'd appreciate any help!
You should use the property option-label
<div id="q-app">
<div class="q-pa-md" style="max-width: 300px">
<div class="q-gutter-md">
<q-badge color="secondary" multi-line>
Model: "{{ model }}"
</q-badge>
<q-select filled v-model="model" :options="options" label="Standard" option-label="description"></q-select>
{{ model }}
</div>
</div>
</div>
JS:
new Vue({
el: '#q-app',
data () {
return {
model: null,
options: [
{
label: 'Google',
value: 'Google',
description: 'Search engine',
category: '1'
},
{
label: 'Facebook',
value: 'Facebook',
description: 'Social media',
category: '1'
},
{
label: 'Twitter',
value: 'Twitter',
description: 'Quick updates',
category: '2'
},
]
}
}
})
https://codepen.io/reijnemans/pen/bGpqJYx?editors=1010

Vue-MultiSelect: Can't seem to get property from an object during select event

I am using the Vue-Multiselect plugin for an input drop down box and trying to get the customer's last name when they fill out a form. For some reason I am just getting an object as the value of the issue.customer_last_name property (see screenshot below). I want to get a string Doe, instead.
Anyone have any ideas ? My goal is to eventually submit this data (POST request) via a submit button to a remote api.
Link to codesandbox replicating the issue: https://codesandbox.io/s/vue-template-9z1wd?fontsize=14&module=%2Fsrc%2Fcomponents%2FTest.vue
Full code:
<template>
<div>
<label for="customer_last_name_input">Last Name:</label>
<multiselect
id="customer_last_name_input"
v-model="issue.customer_last_name"
:options="customers"
placeholder="Select an existing customer or type to add a new one"
:custom-label="customerSelectName"
label="lastname"
track-by="uid"
:close-on-select="true"
#select="onSelect"
></multiselect>
<br>
<!-- <input id="customer_last_name_input" type="text" class="form-control" v-model="issue.customer_last_name" placeholder required /> -->
<div>
<label for="customer_first_name_input">First Name:</label>
<input
id="customer_first_name_input"
type="text"
v-model="issue.customer_first_name"
placeholder
required
>
</div>
</div>
</template>
<script>
import Multiselect from "vue-multiselect";
export default {
name: "test",
components: {
Multiselect
},
data() {
return {
customers: [
{
uid: "1",
firstname: "John",
lastname: "Doe",
email: "johndoe#aol.com",
phone: null,
c_organization: "ACME",
c_title: null
},
{
uid: "2",
firstname: "Mary",
lastname: "Smith",
email: "msmith#aol.com",
phone: null,
c_organization: "NBA",
c_title: "Miss"
}
],
issue: {
customer_first_name: "",
customer_last_name: ""
//customer_email: ""
}
};
},
// async created() {
// await this.getCustomers()
// },
methods: {
customerSelectName(option) {
//return `${option.lastname}, ${option.firstname}`
return `${option.lastname}, ${option.firstname}`;
},
onSelect(option) {
console.log(option.lastname);
this.issue.customer_last_name = option.lastname;
this.issue.customer_first_name = option.firstname;
// this.issue.customer_email = option.email
}
}
};
</script>
The problem is the v-model="issue.customer_last_name" on the multi-select itself. That's setting the selection of the multi-select component to the whole object that the user selects, not just the last name.
Since you're using #select="onSelect" to do all the updating necessary, just remove the v-model (line 6 in the code sandbox) and you'll be all set.
Edited to add:
When you're seeing unhelpful [object Object], it can be useful to render that somewhere else, where it's more likely to be fully expanded. It's all somewhat browser dependent, but adding <div>{{issue.customer_last_name}}</div> was a helpful debugging tool.

Dynamic form problem in vue2: [TypeError: Cannot read property '_withTask' of undefined]

I have to create a dynamic form in vue2. I want to save the values of the dynamic fields in an named object so that I can pass them along on submit.
The following code is working fine except I get an error in the console when I change the input value the first time (value will be propagated correctly though):
[TypeError: Cannot read property '_withTask' of undefined]
Here is how I define the props:
props: {
fields: {
type: Object,
default: {startWord: 'abc'}
},
},
And this is how I populate the model from the input field:
v-model="fields[field.id]"
Here is the entire code:
<template>
<div>
<!-- Render dynamic form -->
<div v-for="(field, key) in actionStore.currentAction.manifest.input.fields">
<!-- Text -->
<template v-if="field.type == 'string'">
<label>
<span>{{key}} {{field.label}}</span>
<input type="text" v-bind:placeholder="field.placeholder"
v-model="fields[field.id]"/>
</label>
</template>
<!-- Footer -->
<footer class="buttons">
<button uxp-variant="cta" v-on:click="done">Done</button>
</footer>
</div>
</template>
<script>
const Vue = require("vue").default;
const {Bus, Notifications} = require('../../Bus.js');
module.exports = {
props: {
fields: {
type: Object,
default: {startWord: 'abc'}
},
},
computed: {
actionStore() {
return this.$store.state.action;
},
},
methods: {
done() {
console.log('fields', this.fields);
Bus.$emit(Notifications.ACTION_INPUT_DONE, {input: this.fields});
}
},
}
</script>
So again, everything is working just fine (showing initial value in input, propagating the new values to the model etc.). But I get this '_withTask' error when I first enter a new character (literally only on the first keystroke). After that initial error it doesn't pop up again.
-- Appendix --
This is what the manifest/fields look like:
manifest.input = {
fields: [
{ id: 'startWord', type: 'string', label: 'Start word', placeholder: 'Enter start word here...' },
{ id: 'startWordDummy', type: 'string', label: 'Start word dummy', placeholder: 'Enter start word here...' },
{ id: 'wordCount', type: 'integer', label: 'Word count' },
{ id: 'clean', type: 'checkbox', label: 'Clean up before' },
]
}
-- Update --
I just discovered that if I set the dynamic field values initially with static values I don't get the error for those fields set this way:
created() {
this.fields.startWord = 'abc1';
},
But this is not an option since it will be a dynamic list of fields. So what is the best way to handle scenarios like this?
From documentation: Due to the limitations of modern JavaScript (and the abandonment of Object.observe), Vue cannot detect property addition or deletion. Since Vue performs the getter/setter conversion process during instance initialization, a property must be present in the data object in order for Vue to convert it and make it reactive.
As I understand it's bad idea create keys of your object by v-model.
What would I do in HTML:
<input type="text"
:placeholder="field.placeholder"
#input="inputHandler(event, field.id)" />
Then in JS:
methods: {
// ...
inputHandler({ target }, field) {
this.fields[field] = target.value;
}
},

vue-chartjs reactive data error

I am trying to use reactive data mixin for vue-chartjs
The mounted function to set the initial data is working and I can see the chart correctly using the API response:
fetchSessionTrends() {
axios.get(endpoint)
.then(({data}) => {
this.sessions = data.map(session => session.sessions);
this.sessionLabels = data.map(label => label.date);
this.loaded = true;
});
},
The data:
data() {
return {
endpoint: 'public/api/sessions',
sessions: [],
sessionLabels: [],
loaded: false,
daysFilter: 14
};
},
I am display the chart with a text field to provide the reactive portion - expectation is that it calls the endpoint again and receives new response
<div class="col-md-2 session-filter">
<div class="input-group">
<input type="text" class="form-control" placeholder="days..." v-model="daysFilter">
<span class="input-group-btn">
<button class="btn btn-secondary" type="button" #click="refreshSessions">Go</button>
</span>
</div>
</div>
<line-chart v-if="loaded" :chart-data="sessions" :chart-labels="sessionLabels"></line-chart>
To test the reactive part however, for now I am simply changing the data arrays directly to see how it works:
refreshSessions() {
this.sessions = [1, 2, 3];
this.sessionlabels = ["june", "july", "august"];
},
Right, so this is giving me the errors
[Vue warn]: Error in callback for watcher "chartData": "TypeError: Cannot read property 'map' of undefined" found in ....
TypeError: Cannot read property 'map' of undefined
LineChart.js is as described in the docs, abbreviated here for space
import { Line, mixins } from 'vue-chartjs';
const { reactiveProp } = mixins
extends: Line,
mixins: [reactiveProp],
props: {
chartData: {
type: Array,
required: true
},
chartLabels: {
type: Array,
required: true
}
},
mounted() {
this.renderChart({
labels: this.chartLabels,
datasets: [
{
label: 'sessions',
data: this.chartData
}
]
}, this.options)
}
So, chart is initially working fine but I can't seem to get the reactive part working.
This will not work. Because the reactiveMixins assume that chartData is the whole chartjs dataset object. With the dataset array, with the labels etc.
But you are splitting it up, this way the reactiveMixins can't work.
Because your chartData is only the pure data of one dataset.
To solve it, you can do two things:
Pass in the whole dataset object
Add own watchers.
I guess the most simple method would be to add two watchers to watch chartData and chartLabel and on a change call this.$data._chart.update()