Vuelidate $each: How can I validate a nested collection? - vue.js

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.

Related

VueJS - Component not displaying unless a selection is made on the drop down menu

I am having an issue whereby my results do not display when the page loads. It will only display if i make a selection on the drop-down menu.
I have tried adding the function to the mounted property but that doesn't seem to work. Any ideas what this might be?
<select v-model="sortResults"
#change="sortalphabetically"
class="col-4 col-lg-5"
aria-label="sortby"
id="sortby">
<option disabled value="" selected>Select</option>
<option value="alpha">Alphabetically</option>
<option value="relevance">Relevance</option>
</select>
methods: {
sortalphabetically() {
switch (this.sortResults) {
case "alpha":
this.theResults = [...this.results].sort((a, b) =>
a.metaData.title > b.metaData.title ? 1 : -1
);
break;
case "relevance":
this.theResults = [...this.results];
break;
}
},
}
data: function () {
return {
sortResults: "relevance"
}
import Result from "#/components/Result.vue";
mounted() {
this.dataFilters;
this.updateURL();
this.theResults();
},
};
I reproduced your code by adding some changes and it works correctly.
First, you do not sort your values in the second switch's case. It maybe is your business :-) It doesn't matter.
Second, in showing results if you are using v-if please do not use i as the index of the result array for its key, It won't work. Vue and even React do not recognise the changing order of an array if its index is being used as the key. So, use the items' unique ids.
<template>
<div>
<select
id="sortby"
aria-label="sortby"
class="col-4 col-lg-5"
v-model="sortResults"
#change="sortalphabetically"
>
<option disabled value="" selected>Select</option>
<option value="alpha">Alphabetically</option>
<option value="relevance">Relevance</option>
</select>
<div style="background-color: aquamarine">
<div
v-for="item in theResults" :key="item.metaData.id"
>
{{item.metaData.title}}
</div>
</div>
</div>
</template>
<script>
export default {
name: 'BaseSelectTest',
data() {
return {
sortResults: 'relevance',
theResults: [],
results: [
{ metaData: { title: 'BBBB', id: 2 } },
{ metaData: { title: 'DDDD', id: 4 } },
{ metaData: { title: 'AAAA', id: 1 } },
{ metaData: { title: 'CCCC', id: 3 } },
{ metaData: { title: 'EEEE', id: 5 } },
],
};
},
methods: {
sortalphabetically() {
switch (this.sortResults) {
case 'alpha':
this.theResults = [...this.results]
.sort((a, b) => (a.metaData.title > b.metaData.title ? 1 : -1));
break;
case 'relevance':
this.theResults = [...this.results] // You may omit the next line
// .sort((a, b) => (a.metaData.title > b.metaData.title ? -1 : 1));
break;
default:
// nothing left to do
}
},
},
mounted() {
this.sortalphabetically(); // It's optional if you ignore sorting for 'relevance'
},
};
</script>
Finally, if your flaw persists, you need to check out the showing result codes. Of course, it's possible to observe data changes using the Vue Dev Tool for sure.

How can I get a specifc selection in select vue.js?

How are you?
I'm studying Vue and I'm stuck on the current task not knowing where to go.
I have a select that when I click I need to show on screen only what corresponds to that selection. For example, when placing the "to do" option in the select, only the tasks with a concluded=false should appear on the screen. I've only gotten this far and I need help to continue. Can you help me? Thanks
This is my App.vue
<template>
<div id="app">
<h1>Lista de Tarefas</h1>
<List :data="list" #remove="handleRemove"/>
<Form #add="addNewTask" #onChange="handleN"/>
</div>
</template>
<script>
import List from "./components/List.vue";
import Form from "./components/Form.vue";
export default {
components: {
List,
Form,
},
data() {
return {
list: [],
};
},
methods: {
addNewTask(newTask) {
this.list.push(newTask);
},
handleRemove(item) {
const index = this.list.findIndex(i => i.id === item.id)
this.list[index].excluded = true
},
handleN(item) {
const index = this.list.findIndex(i => i.id === item.id)
this.list[index].concluded = true
}
},
};
</script>
This is my List.vue
<template>
<ul>
<select v-model="selected" #change="onChange($event)">
<option disabled value="">Escolha a visualização</option>
<option v-for="option in options" :key="option.text">
{{ option.text }}
</option>
</select>
<li v-for="item in itens" :key="item.id">
<input type="checkbox" id="checkbox" v-model="item.concluded" />
<label for="checkbox"> {{ item.description }} </label>
<button #click="() => $emit('remove', item)">Excluir</button>
</li>
</ul>
</template>
<script>
export default {
props: {
data: {
type: Array,
default: () => {},
},
},
data() {
return {
selected: "",
options: [
{ text: "Todos", value: "1" },
{ text: "A fazer", value: "2" },
{ text: "Concluído", value: "3" },
{ text: "Deletado", value: "4" },
],
};
},
computed: {
itens() {
return this.data.filter((item) => item.excluded === false);
},
},
methods: {
onChange(event) {
console.log(event.target.value);
return this.data.filter((item) => item.concluded === false);
},
},
};
</script>
This is my Form.vue
<template>
<form #submit.prevent="handleNewTask">
<input type="text" v-model="newTask" placeholder="Insira a tarefa"/>
<input type="submit" value="Adicionar"/>
</form>
</template>
<script>
import Task from '../types/Task.js'
export default {
data() {
return {
newTask: "",
};
},
methods: {
handleNewTask() {
this.$emit('add', new Task(this.newTask))
this.newTask = ''
}
},
};
</script>
And this is my Task.js
export default class {
constructor(description) {
this.description = description,
this.id = Math.random(),
this.concluded = false,
this.excluded = false
}
}
I watch some tutorials, read the documentation and some StackOverflow questions but I really can't get out of here
Thanks in advance for the help
Based on how you have structured your app, our only concern should be with the List.vue file.
Your goal is to filter the results based on the selection (selected property). However, your issue is that you are not even using that anywhere.
I know you are hard coding the filter on the onChange method but that is, first of all wrong because you aren't really changing anything (you are returning an array), and secondly it's inefficient.
A better way to do it is to update the computed itens function like so:
itens() {
return this.data.filter((item) => {
if (this.selected === '1'){
return item.concluded === false
} else if (this.selected === '2'){
// filter another way
} else if (... // so on and so forth
});
},
Also, I would filter out the excluded items before sending them to the component. If you aren't going to use it, don't send it.
Remove the onChange event on the <select> and the associated method since they are now unused.

Computed properties with v-model by index

Let's say I have the following vuex store...
state: {
someObj: {
someList: [
{ key:'a', someSubList: [] },
{ key:'b', someSubList: [] },
{ key:'c', someSubList: [] },
]
}
}
How would I bind a separate v-model to each someSubList? As an example, after I check some checkboxes, I would expect to see some Ids be populated into the someSubList like this:
someList: [
{ key:'a', someSubList: [1, 13, 17, 19] },
{ key:'b', someSubList: [1, 2, 3, 4] },
{ key:'c', someSubList: [4, 16, 20] },
]
In other words, If I check a checkbox an associated id would be added to someSubList. If I uncheck the box, the id associated with that checkbox would be removed from the someSubList. Keep in mind that each someList has a different someSubList.
I'm thinking it would be similar to below, but I'm not sure what to use for the v-model param and how to pass the index to the set method
ex.
<span v-for="(someListRow.someSubList, index2) in someList" v-bind:key="index2">
<v-checkbox v-model="myModel" />
</span>
computed: {
someList: {
get() {
return this.$store.state.someObj.someList;
},
set(value) {
this.$store.commit('someCommit', value)
}
}
}
UPDATE:
For anyone interested I got it solved using the tips provided in the posts below and ended up doing this:
<v-checkbox #change="myChangeMethod($event, myObj)" label="MyLabel"
:input-value="isMyObjSelected(myObj)" />
myChangeMethod(event, myObj) {
if (event) {
this.$store.commit('AddToMyList', {myObj});
} else {
this.$store.commit('RemoveFromMyList', {myObj});
}
}
isMyObjSelected(myObj){
this.$store.getters.isMyObjSelected(myObj});
}
I believe you want to map your inputs to some value in your store?
For this to work you cannot use v-model. Instead work with a input="updateStore($event, 'pathToStoreField')" (or #change="...") listener and a :value="..." binding. In case of a checkbox you need to use :checked="..." instead of value.
For example:
<input type="checkbox" :checked="isChecked" #input="updateField($event.target.checked, 'form.field')">
...
computed:
...
isChecked() {
return this.$store.state.form.field;
},
...
methods: {
...
updateField(value, path) {
const options = { path, value };
this.$store.commit('setFieldByPath', options);
},
...
},
Then in your store you will need a mutation setFieldByPath that resolves the string-path to a property in the state object (state.form.field) and sets this property to value.
You can also place the updateField() method as setter of a computer property.
There is library that makes this a bit more convenient: https://github.com/maoberlehner/vuex-map-fields
Just look out for checkboxes: to set them checked the checked property needs to be true not the value property.
I think, unsure but Computed values usually become like a local variable available, you need to v-model what other variable is being posted through form, so if myModel is the variable passed through then set the value of the setter once received:
<span v-for="(myList, index2) in someList" v-bind:key="index2">
<v-checkbox v-model="myModel" />
</span>
Then in script
computed: {
myList() {
return this.$store.state.someObj.someList;
}
}
I guess u ask for this:
<span v-for="(someSubList, index2) in someList" v-bind:key="index2">
<v-checkbox v-model="myModel" />
</span>
data() {
return {
myModel: ''
}
},
computed: {
someList: {
get() {
return this.$store.state.someObj.someList;
},
set(value) {
this.$store.commit('someCommit', value)
}
}
}
properties inside data() can be used to bind your v-model's on your template.
For anyone interested I got it solved using the tips provided in the posts below and ended up doing this:
<v-checkbox #change="myChangeMethod($event, myObj)" label="MyLabel"
:input-value="isMyObjSelected(myObj)" />
myChangeMethod(event, myObj) {
if (event) {
this.$store.commit('AddToMyList', {myObj});
} else {
this.$store.commit('RemoveFromMyList', {myObj});
}
}
isMyObjSelected(myObj){
this.$store.getters.isMyObjSelected(myObj});
}

Vue.js binding attribute with v-model [duplicate]

I have form and select components.
In fact things are simple: I need two binding model.
The parent component:
Vue.component('some-form', {
template: '#some-form',
data: function() {
return {
countryNameParent: ''
}
}
});
The child component with items:
Vue.component('countries', {
template: '#countries',
data: function () {
return {
items: {
"0": {
"id": 3,
"name": "Afghanistan"
},
"1": {
"id": 4,
"name": "Afghanistan2"
},
"2": {
"id": 5,
"name": "Afghanistan3"
}
},
countryName: ''
}
},
props: ['countryNameParent'],
created: function() {
var that = this;
this.countryName = this.countryNameParent;
},
methods: {
onChange: function (e) {
this.countryNameParent = this.countryName;
}
}
});
I'm using v-model to incorporate components above.
Templates like this:
<template id="some-form">
{{ countryNameParent }}
<countries v-model="countryNameParent"></countries>
</template>
<template id="countries">
<label for="">
<select name="name" #change="onChange" v-model="countryName" id="">
<option value="0">Select the country!</option>
<option v-for="item in items" v-bind:value="item.name">{{ item.name }}</option>
</select>
</label>
</template>
My target is getting data in parent component to send it to server (real form is much bigger), however I can't get the value of the countryName in countryNameParent. Moreover, Parent should setting data in successor if not empty.
Here you go link where I've been attempting to do it several ways (see commented part of it).
I know that I need to use $emit to set data correctly, I've even implemented model where I get image as base64 to send it by dint of the same form, hence I think solution is approaching!
Also: reference where I've built sample with image.
Here is your countries component updated to support v-model.
Vue.component('countries', {
template: `
<label for="">
<select v-model="countryName">
<option value="0">Select the country!</option>
<option v-for="item in items" v-bind:value="item.name">{{ item.name }}</option>
</select>
</label>
`,
data: function () {
return {
items: {
"0": {
"id": 3,
"name": "Afghanistan"
},
"1": {
"id": 4,
"name": "Afghanistan2"
},
"2": {
"id": 5,
"name": "Afghanistan3"
}
},
}
},
props: ['value'],
computed:{
countryName: {
get() { return this.value },
set(v) { this.$emit("input", v) }
}
},
});
v-model is just sugar for setting a value property and listening to the input event. So to support it in any component, the component needs to accept a value property, and emit an input event. Which property and event are used is configurable (documented here).

add our own function to when something is added to a Vuejs data element

I have a fairly simple Vuejs app and am just learning Vuejs. When I add or delete from a data property, I'd like some other function to happen. The code is like this:
data: {
pricings: null,
},
mounted(){
var somePrice = {"name":"service price", "price":"2.45"}
this.pricings.push(somePrice);
},
methods:{
callMe: function(){
alert("call me");
}
}
I'd like when I add or delete from pricings for some other method (callMe in this case) to be called. I am sure this is possible but am not having luck finding how to do it.
You could use either a computed or a watch property. It really depends on what your use case is.
Take the following example:
Vue.config.productionTip = false;
Vue.config.devtools = false;
new Vue({
el: '#app',
data: {
pricings: [],
},
mounted() {
const somePrices = [{
"name": "Service",
"price": "2.45"
}, {
"name": "Another Service",
"price": "5.25"
}, {
"name": "Service Three",
"price": "1.52"
}];
this.pricings.push(...somePrices);
},
methods: {
callMe: function(newVal) {
// console.log(newVal);
// async or expensive operation ...
console.log("call me");
}
},
computed: {
pricingsSum: function() {
return this.pricings.reduce((sum, item) => sum + Number.parseFloat(item.price), 0);
}
},
watch: {
pricings: function(newVal) {
this.callMe(newVal);
}
}
});
<script src="https://unpkg.com/vue#2.4.3/dist/vue.js"></script>
<div id="app">
<ul>
<li v-for="item in pricings" :key="item.name">
{{ item.name }} ${{ item.price }}</li>
</ul>
<p>Total: ${{ pricingsSum }}</p>
</div>
We used a computed property for complex logic that would prevent the template from being simple and declarative by doing something like:
<p>Total: ${{ this.pricings.reduce((sum, item) => sum + Number.parseFloat(item.price), 0) }}</p>
That would look even worse if you needed to repeat this operation in several parts of your template.
On the other hand, we used a watch property for pricings, which reacts to data changes for pricings.
Quoting the docs:
This is most useful when you want to perform asynchronous or expensive
operations in response to changing data.
Meaning that here you would probably make an asynchronous request to your server or some other complex/expensive operation instead of just manipulating the data like we did with our computed property.
Hope this helps, I recommend reading the full documentation here.
At the end of the day a computed property is just a watcher, you can see this here:
function createComputedGetter (key) {
return function computedGetter () {
var watcher = this._computedWatchers && this._computedWatchers[key];
if (watcher) {
if (watcher.dirty) {
watcher.evaluate();
}
if (Dep.target) {
watcher.depend();
}
return watcher.value
}
}
}
The important distinction is that computed properties are synchronous and must return a value.