Strange behaviour of collection-validation in vuelidate - vue.js

I have made the following minimal example, which can also be tested in this fiddle:
HTML:
<div id="app">
<div v-for="(gameV, index) in $v.games.$each.$iter">
<input type="text" :class="{error: gameV.$error, dirty: gameV.$dirty}" v-model="gameV.name.$model" />
<input type="button" value="-" #click="games.splice(index, 1)" style="cursor: pointer;"/>
</div>
<input type="button" value="+" #click="games.push({name: ''})" style="cursor: pointer; margin-top: 5px;"/>
<div v-if="$v.$invalid" style="color: red; margin-top: 1em;">Form invalid</div>
<pre>{{ $v }}</pre>
</div>
JS:
Vue.use(vuelidate.default)
new Vue({
el: "#app",
data: {
games: [{name: "Fallout"}, {name: "WoW"}, {name: ""}]
},
validations: {
games: {
$each: {
name: {
required: validators.required
}
}
}
}
})
Steps to reproduce the error:
Type something in the second line.
Remove second line.
Result:
The former third line (now second) is marked as containing an error, even though it was not touched.
Note
I've also filed an issue on the vuelidate github-repo, but as there are many unanswered issues, I have decided to also ask the question here.

https://jsfiddle.net/jacobgoh101/cqye5van/
You can use $trackBy to solve this.
If you use $trackBy: 'id', the validation will be differentiated based on the id in each game. Each game object in the array would need to have a unique id for this to work.
e.g. games: [{name: "Fallout", id: 1}, {name: "WoW", id: 2}, {name: "", id: 3}]

Related

vue #click not changing class

sorry I'm still new to reactive vue models and updating I'm trying to draw a line through an input element if a box is checked.
so far I have this setup:
data: {
selected: null,
checked: null,
list: [
{
id: 0,
category: 'Bakery',
food: ['bread','muffins','pie']
},
{
id: 1,
category: 'Fruits',
food: ['apple','bananna','orange']
},
{
id: 2,
category: 'Diary',
food: ['cheese','milk','yogurt']
}
],
isHidden: true,
form: {},
},
my html is as follows (this is a single page app)
<li v-for="food in item.food" class="list-group-item">
<input :class="{marked:food == checked}" #click="checked = food" type="checkbox"> {{ food }}</input>
</li>
this is the css i'm trying to implement
.marked{
text-decoration: line-through;
}
I'm not sure what I need to do to my #click to make it work but so far nothing happens and the class is not applied. Can someone give me tips?
An <input> element can't have content.
So you've got this structure:
<input>{{ food }}</input>
But that's misleading as the <input> tag will be closed automatically. What you'll end up with will actually be some text next to a checkbox, not inside it.
You probably want something similar to this:
<label :class="{marked: food === checked}" #click="checked = food">
<input type="checkbox"> {{ food }}
</label>
There are some other problems involving deselection and multiple selections. I've tried to fix those in the example below:
new Vue({
el: '#app',
data: {
checked: [],
list: [
{
id: 0,
category: 'Bakery',
food: ['bread','muffins','pie']
},
{
id: 1,
category: 'Fruits',
food: ['apple','bananna','orange']
},
{
id: 2,
category: 'Diary',
food: ['cheese','milk','yogurt']
}
]
}
})
.marked{
text-decoration: line-through;
}
<script src="https://unpkg.com/vue#2.6.11/dist/vue.js"></script>
<div id="app">
<ul v-for="item in list">
<li v-for="food in item.food" class="list-group-item">
<label :class="{marked: checked.includes(food)}">
<input type="checkbox" v-model="checked" :value="food">{{ food }}
</label>
</li>
</ul>
</div>

In VueJS using v-for, is this structure possible?

Given a collection of objects in the data of a component:
data: function () {
return [
{ id: 1, name: "foo", br: false },
{ id: 1, name: "bar", br: true },
{ id: 1, name: "baz", br: false }
]
}
...is it possible to render a structure like so...
<div id="1">foo</div>
<div id="2">bar</div><div class="break" />
<div id="3">baz</div>
In a nutshell, I need to have another div conditionally rendered at the same level as the items in the list. If it matters or helps, the individual items in the list are also components. I know how to set up the rest of the data and properties - it's just getting that additional HTML rendered in the list that I need to accomplish.
I want to avoid creating another item in the list and additional component to represent the break. No need to add the overhead of the additional Vue objects for the simple HTML div. This list may have > 100 items and "breaks" and it can add up quickly.
Yes. You should loop through the items like so:
<template v-for="item in items">
<div :id="item.id">
{{ item.name }}
</div>
<div class="break" v-if="item.br">
</div>
</template>
You can do it with a normal v-for and a normal v-if for your optional div
<html>
<head>
<script type = "text/javascript" src = "https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.3/vue.min.js">
</script>
</head>
<body>
<div id="app">
<div v-for="item in items">
<div :id="item.id">{{item.name}}</div>
<div v-if="item.br" class="break">
</div>
</div>
<script type = "text/javascript">
var vue_det = new Vue({
el: '#app',
data: {
items: [
{ id: 1, name: "foo", br: false },
{ id: 2, name: "bar", br: true },
{ id: 3, name: "baz", br: false }
]}
});
</script>
</body>
</html>
You should not be afraid of 100 divs or around so, a library like Vue is made to manage efficiently thousands of components

How to display multiple labels in vue-select field using Vue Select Library

I am using vue-select component from Vue Select Library in my html form as shown below and I want to display three value in the label but don't know how to achieve that. i couldn't found any solution in the documentation.
I want to display three value in the label as shown below.
<v-select id="selected_item" name="selected_item" title="Select an item" :options="formfieldsdata.items" :reduce="item_name => item_name.id" label="item_name+'::'+item_code+'::'+item_created_at" v-model="item.selected_item" #input="getSelectedItem" style="width: 100%; height:56px;" />
HTML:
<script src="{{ asset('assets/requiredjs/vue-select.js') }}"></script>
<link href="{{ asset('assets/requiredcss/vue-select.css') }}" rel="stylesheet">
<v-select id="selected_item" name="selected_item" title="Select an item" :options="formfieldsdata.items" :reduce="item_name => item_name.id" label="item_name" v-model="item.selected_item" #input="getSelectedItem" style="width: 100%; height:56px;" />
JS:
Vue.component('v-select', VueSelect.VueSelect);
var app = new Vue({
el: '#app',
data: {
formfieldsdata: {
items: [],
},
item: {
selected_item: 0,
},
}
});
Ref to vue select library documentation: https://vue-select.org/guide/values.html#transforming-selections
Just use template literals, what allow embed expressions in JavaScript strings. And make the label binded :label
<v-select id="selected_item" name="selected_item" title="Select an item" :options="formfieldsdata.items" :reduce="item_name => item_name.id" :label="`${item_name} ${item_code} ${item_created_at}" v-model="item.selected_item`" #input="getSelectedItem" style="width: 100%; height:56px;" />
Update
label can use only for one object property. But you can use scopes for options and selected values. Example on codepen
<v-select id="selected_item" name="selected_item" title="Select an item" :options="formfieldsdata.items" :reduce="item_name => item_name.id" v-model="item.selected_item" #input="getSelectedItem" style="width: 100%; height:56px;" >
<template slot="option" slot-scope="option">
<span :class="option.icon"></span>
{{ option.item_name }} {{option.item_code}} {{option.created_at}}
</template>
<template slot="selected-option" slot-scope="option">
<span :class="option.icon"></span>
{{ option.item_name }} {{option.item_code}} {{option.created_at}}
</template>
</v-select>
Update 2
Multi properties search vue-select
vue-component
<div id="app">
<h1>Vue Select - Multiple properties</h1>
<v-select :options="options" label="item_data"
v-model="selected">
</v-select>
</div>
vue-code
Vue.component('v-select', VueSelect.VueSelect)
new Vue({
el: '#app',
data: {
options: [
{
title: 'Read the Docs',
icon: 'fa-book',
url: 'https://codeclimate.com/github/sagalbot/vue-select'
},
{
title: 'View on GitHub',
icon: 'fa-github',
url: 'https://codeclimate.com/github/sagalbot/vue-select'
},
{
title: 'View on NPM',
icon: 'fa-database',
url: 'https://codeclimate.com/github/sagalbot/vue-select'
},
{
title: 'View Codepen Examples',
icon: 'fa-pencil',
url: 'https://codeclimate.com/github/sagalbot/vue-select'
}
]
}
})

Incorrect order of Vue components after copy operation

I have several accordions (every one is a single Vue component) and they are expanded by default. There's also a 'copy' function allowing to make a duplicate of every component.
Vue.component("Accordion", {
template: "#accordion-template",
data: function () {
return {
open: true
}
},
methods: {
toggle: function () {
this.open = !this.open;
}
}
});
new Vue({
el: '#vue-root',
data: {
devices: [
{
name: "a", description: "first"
},
{
name: "b", description: "second"
},
{
name: "c", description: "third"
}
]
},
methods: {
copy: function (device) {
var index = this.devices.indexOf(device) + 1;
var copy = {
name: device.name + "_copy",
description: device.description + "_copy"
};
this.devices.splice(index, 0, copy);
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.4.4/vue.js"></script>
<div id="vue-root">
<div class="device" v-for="device in devices">
<accordion>
<div slot="acc-head">
<span>{{ device.name }}</span><br/>
<button #click="copy(device)">copy</button>
</div>
<div slot="acc-body">
{{ device.description }}
</div>
</accordion>
</div>
</div>
<script type="text/x-template" id="accordion-template">
<div>
<slot name="acc-head"></slot>
<button #click="toggle">Open: {{ open }}</button>
<div :class="open ? 'active' : 'hidden'">
<slot name="acc-body"></slot>
</div>
</div>
</script>
When all accordions are collapsed (in other words 'open: false') and I try to duplicate an accordion from the middle of list (for example b), I expect appearing of the new component named 'name'_copy and it must be expanded by default. But instead of this, the new component has the same values of all attributes as the duplicated one and the last component in the list becomes expanded.
How can I solve this issue?
Fiddle: https://jsfiddle.net/j3ydt1m7/
Short answer
Add a key in your v-for loop: v-for="device in devices" :key="{something here}". Your key must be unique and identify each device, even after device copy
Code
Please check: https://jsfiddle.net/Al_un/9cradxvp/. For debugging purpose, I changed few things:
I put device as props of <accordion> so that I can use device properties in console.log
Copying device is now emitted from <accordion>. Vue doc on listening to child component events
I have added mounted() and updated() hooks. More about Lifecycle hooks
Each device has an ID
Long answer
About list rendering
If key is not provided in v-for loop, Vue does not know how to update a List. From Vue documentation:
To give Vue a hint so that it can track each node’s identity, and thus reuse and reorder existing elements, you need to provide a unique key attribute for each item.
Let's consider your list (I have added one element)
[
{id: 1, name: "a"},
{id: 2, name: "b"},
{id: 3, name: "c"},
{id: 4, name: "d"},
]
Now, let's copy node "b". Without :key="device.id", the console output is
4: d is mounted
3: c is updated
5: b_copy is updated
With :key="device.id", the console output is only:
5: b_copy is mounted
Basically, without keys, there are:
two updates: c becomes b_copy, d becomes c
one insert: d is created
Consequently, the last element is recreated every time you proceed to a copy. As open default value is true, obviously, d has open = true.
If each element has a :key="device.id", then only element b_copy is created
To check that, remove the :key="device.id" from my fiddle and see what happens in the console
Selecting a key
As the key must uniquely identify your device, you should not use index as a key as device index in the array changes whenever you copy a device
Additionally, an ID field is preferred because there is no guarantee that your devices names are unique. What if you initialise the list with
[
{ name: "a"},
{ name: "b"},
{ name: "a"}
]
From a functional point of view, this is correct.
When working with Vue and lists you should add a key prop to the element with v-for. Using the key like this, let's Vue know that you mean a specific element.
<div class="device" v-for="device in devices" :key="device.name">
I believe the reason for this is that due to performance reasons Vue by default adds a new element as the last element and then updates the data in the other nodes. Thus, the new element that you add is actually the last one in the list which has open set as true.
A little addition to your code:
Instead of providing the "device" object and searching for its index you could just pass the index directly.
This is what i mean: jsFiddle
Vue.component("Accordion", {
template: "#accordion-template",
data: function() {
return {
open: true
}
},
methods: {
toggle: function() {
this.open = !this.open;
}
}
});
new Vue({
el: '#vue-root',
data: {
devices: [{
name: "a",
description: "first"
},
{
name: "b",
description: "second"
},
{
name: "c",
description: "third"
},
]
},
methods: {
copy: function(index) {
var device = this.devices[index];
var copy = {
name: device.name + "_copy",
description: device.description + "_copy"
};
this.devices.splice(index + 1, 0, copy);
},
remove: function(index) {
this.devices.splice(index, 1);
}
}
});
.device {
margin: 10px 0;
}
.active {
display: block;
}
.hidden {
display: none;
}
div.device {
border: 1px solid #000000;
box-shadow: 1px 1px 2px 1px #a3a3a3;
width: 300px;
padding: 5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="vue-root">
<div class="device" v-for="(device, index) in devices" :key="device.name">
<accordion>
<div slot="acc-head">
<span>{{ device.name }}</span><br/>
<button #click="copy(index)">copy</button>
<button #click="remove(index)">remove</button>
</div>
<div slot="acc-body">
{{ device.description }}
</div>
</accordion>
</div>
</div>
<script type="text/x-template" id="accordion-template">
<div>
<slot name="acc-head"></slot>
<button #click="toggle">Open: {{ open }}</button>
<div :class="open ? 'active' : 'hidden'">
<slot name="acc-body"></slot>
</div>
</div>
</script>

How to use query parameter in Vue search box?

I have a page with a search box on it using Vue. What I want to do is this: when a user comes from another page with a parameter in the URL (e.g., myurl.com/?windows), I capture the parameter and populate the search field to run the search on that string when the page loads. If there's no parameter, nothing happens.
I'm capturing the string from the URL with JavaScript, but don't see how to get it in the input to run the search.... I created a method but don't see how to apply it.
<div id="app">
<input type="text" v-model="search" placeholder="Search Articles" />
<div v-for="article in filteredArticles" v-bind:key="article.id" class="container-fluid to-edges section1">
<div class="row">
<div class="col-md-4 col-sm-12 section0">
<div class="section0">
<a v-bind:href="article.url" v-bind:title="toUppercase(article.title)">
<img class="resp-img expand section0"
v-bind:src="article.src"
v-bind:alt="article.alt"/>
</a>
</div>
<div>
<h3 class="title-sec">{{ article.title }}</h3>
<p>{{ article.description }}</p>
</div>
</div>
</div>
</div>
</div>
<script type="text/javascript">
var pgURL = window.location.href;
var newURL = pgURL.split("?")[1];
console.log(newURL);
</script>
// Filters
Vue.filter('to-uppercase', function(value){
return value.toUpperCase();
});
new Vue({
el: "#app",
data: {
articles: [
{ id: 1, title: 'Trend Alert: Black Windows', category: 'Windows', description: 'Timeless, elegant, and universally flattering, black is an excellent color to add to any wardrobe – or any window. Get in the black with this chic design trend.', src: 'http://i1.adis.ws/i/stock/Trending_Polaroid_Black_Windows_2018_1?$trending-mobile$', url: '/{StorefrontContextRoot}/s/trending/trend-alert-black-windows', alt: 'Pantone Colors image' },
{ id: 2, title: 'Benefits of a Pass-Through Window', category: 'Windows', description: 'Whether you’re adding a pass-through window in order to enjoy an al fresco aperitif or for easier access to appetizers in the kitchen, we’re big fans of bringing the outdoors in.', src: 'http://i1.adis.ws/i/stock/polaroid_benefitsofapassthroughwindow655x536?$trending-mobile$', url: '/{StorefrontContextRoot}/s/trending/kitchen-pass-through-bar-window', alt: 'Benefits of a Pass-Through Window image' }, etc....
],
search: ''
},
methods: {
toUppercase: function(title){
return title.toUpperCase();
},
urlSearch: function(newURL) {
if (newURL) {
return this.search = newURL;
}
}
},
computed: {
filteredArticles: function() {
// returning updated array based on search term
return this.articles.filter((article) => {
return article.category.match(new RegExp(this.search, "i"));
});
}
}
})
You can call the urlSearch method during the mounted hook:
mounted() {
this.urlSearch(newURL)
},
methods: {
urlSearch(url) {
return this.search = url
}
},