Wrapping a ValidationObserver around a v-for loop - vue.js

I have a v-for loop that allows me to dynamically add new fields to my form. This loop is within a tab which I need to validate before I go onto the next section of my form. It seems as though nothing renders when I place the v-for within my validation observer. Is there another way to accomplish this?
I'm using VeeValidate 3
<template>
<div>
<b-card class="mb-3">
<ValidationObserver :ref="'contact_obs'" v-slot="{ invalid }">
<div
v-for="(contact, index) in this.applicant.contacts"
:key="contact.id"
role="tablist"
>
<b-form-row>
<BTextInputWithValidation
rules="required"
class="col-md-4"
:label="
$t('contact_name', { name: applicant.contacts[index].title })
"
:name="$t('contact_name')"
v-model="applicant.contacts[index].name"
description
placeholder
/>
<BTextInputWithValidation
rules
class="col-md-4"
:label="$t('contact_title')"
:name="$t('contact_title')"
v-model="applicant.contacts[index].title"
description
placeholder
/>
<BTextInputWithValidation
rules
class="col-md-3"
:label="$t('contact_email_address')"
:name="$t('contact_email_address')"
v-model="applicant.contacts[index].email"
description
placeholder
/>
<b-button
variant="outline-danger"
class="float-right mt-4 mb-4 ml-3"
v-on:click="deleteContact(index)"
>
<span class="fas fa-user-minus"></span>
</b-button>
</b-form-row>
</div>
</ValidationObserver>
<b-button
variant="outline-success"
class="float-right mt-4 mb-4 ml-3"
v-on:click="addContact"
>
<span class="fas fa-user-plus"></span>
</b-button>
</b-card>
</div>
</template>
<script>
import { ValidationObserver } from 'vee-validate'
import VeeValidate from 'vee-validate'
import BTextInputWithValidation from './inputs/BTextInputWithValidation'
let id = 10
export default {
components: { ValidationObserver, BTextInputWithValidation },
mounted() {},
data: function() {
return {
applicant: {
contacts: [
{
id: '1',
name: '',
title: 'Primary Principal',
email: ''
},
{
id: '2',
name: '',
title: 'Secondary Principal',
email: ''
},
{
id: '3',
name: '',
title: 'Accounts Receivable',
email: ''
}
]
}
}
},
methods: {
addContact: function(params) {
this.applicant.contacts.push({
id: id,
name: '',
title: '',
email: ''
})
id++
},
deleteContact: function(index) {
this.$delete(this.applicant.contacts, index)
},
validate() {
const isValid = this.$refs.contact_obs.validate()
if (isValid) {
this.$emit('on-validate', this.$data, isValid)
}
return isValid
// return true
}
}
}
</script>
<style lang="scss" scoped></style>

I believe the problem is here:
v-for="(contact, index) in this.applicant.contacts"
In general you should avoid using the this. prefix to access properties in templates but usually it does no harm. This is one of those cases where it actually does break something. this does not refer to the correct object inside a scoped slot.
I'm surprised you don't see an error in your console.

Related

Vue Component Select option from list item

I'm new to Vue. I have an autocomplete component that used AXIOS to get the data. When I type in at least 3 characters, it does display the results in a list item, however I can't seem to figure out how to actually select the option and populate the text field.
Here is the code
<template>
<div>
<input type="text" placeholder="Source client" v-model="query" v-on:keyup="autoComplete" #keydown.esc="clearText" class="form-control">
<div class="panel-footer" v-if="results.length">
<ul class="list-group">
<li class="list-group-item" v-for="result in results">
<span> {{ result.name + "-" + result.oid }} </span>
</li>
</ul>
</div>
</div>
</template>
<script>
import axios from 'axios'
export default{
data(){
return {
selected: '',
query: '',
results: []
}
},
methods: {
clearText(){
this.query = ''
},
autoComplete(){
this.results = [];
if(this.query.length > 2){
axios.get('/getclientdata',{params: {query: this.query}}).then(response => {
this.results = response.data;
});
}
}
}
}
</script>
Any help would be appreciated!
Writing an autocomplete is a nice challenge.
You want this component to act like an input. That means that if you'd use your autocomplete component, you will probably want to use it in combination with v-model.
<my-autocomplete v-model="selectedModel"></my-autocomplete>
To let your component work in harmony with v-model you should do the following:
Your component should accept a prop named value, this may be a string, but this may also be an object.
In the example above the value of selectedModel will be available inside your autocomplete component under '{{value}}' (or this.value).
To update the value supplied you $emit an input event with the selected value as second parameter. (this.$emit('input', clickedItem))
Inside your autocomplete you have query, which holds the search term. This is a local variable and it should be. Don't link it to value directly because you only want to change the value when a user selects a valid result.
So to make the example work you could do the following:
<template component="my-autocomplete">
<div>
<input type="text" placeholder="Source client" v-model="query" v-on:keyup="autoComplete" #keydown.esc="clearText" class="form-control">
<div class="panel-footer" v-if="results.length">
<ul class="list-group">
<li class="list-group-item" v-for="result in results" #click="selectValue(result)">
<span> {{ result.name + "-" + result.oid }} </span>
</li>
</ul>
</div>
</div>
</template>
<script>
//import axios from 'axios'
export default{
props: ['value'],
data(){
return {
selected: '',
query: '',
results: []
}
},
methods: {
clearText(){
this.query = ''
},
autoComplete(){
this.results = [];
if(this.query.length > 2){
this.results = [{ name: 'item 1', oid: '1' }, {name: 'item 2', oid: '2'}];
//axios.get('/getclientdata',{params: {query: this.query}}).then(response => {
//this.results = response.data;
//});
}
},
selectValue(selectedValue) {
this.$emit('input', selectedValue);
}
}
}
</script>
I commented out the axios part for demonstration purposes.
To verify the workings:
<template>
<div>Autocomplete demo:
<my-autocomplete v-model="selectedRow">
</my-autocomplete>
Selected value: <pre>{{selectedRow}}</pre>
</div>
</template>
<script>
export default {
data() {
return {
selectedRow: null
}
}
}
</script>
Now the autocomplete does what it should, it allows you to search and pick a valid result from the list.
One problem remains though. How do you display the selected value. Some options are:
Copy the value to query, this works seemlessly if all your options are strings
Copy the value of result.name to query, this would make the solution work
Accept a scoped slot from the parent component which is responsible for displaying the selected item.
I will demonstrate option 2:
<template component="my-autocomplete">
<div>
<input type="text" placeholder="Source client" v-model="query" v-on:keyup="autoComplete" #keydown.esc="clearText" class="form-control">
<div class="panel-footer" v-if="results.length">
<ul class="list-group">
<li class="list-group-item" v-for="result in results" #click="selectValue(result)">
<span> {{ result.name + "-" + result.oid }} </span>
</li>
</ul>
</div>
</div>
</template>
<script>
//import axios from 'axios'
export default{
props: ['value'],
data(){
return {
selected: '',
query: '',
results: []
}
},
mounted() {
if (this.value && this.value.name) {
this.query = this.value.name;
}
},
// Because we respond to changes to value
// we do not have to do any more work in selectValue()
watch: {
value(value) {
if (this.value && this.value.name) {
this.query = this.value.name;
} else {
this.query = '';
}
}
},
methods: {
clearText(){
this.query = ''
},
autoComplete(){
this.results = [];
if(this.query.length > 2){
this.results = [{ name: 'item 1', oid: '1' }, {name: 'item 2', oid: '2'}];
//axios.get('/getclientdata',{params: {query: this.query}}).then(response => {
//this.results = response.data;
//});
}
},
selectValue(selectedValue) {
this.$emit('input', selectedValue);
}
}
}
</script>
A working example can be found here: https://jsfiddle.net/jangnoe/4xeuzvhs/1/

Vuejs - v-bind.sync on resursive components (hierarchical list)

I have a hierarchical list component where child items have checkboxes. Checkbox actions(check/uncheck) must keep the parent component in sync with the checkbox's changed state. I cannot figure out how to achieve this using v-bind.sync recursively. My code is as below:
Menu.vue
This component holds the hierarchical list. (Only relevant code included)
HierarchicalCheckboxList is the component that displays the hierarchical list
Property 'value' holds the check/uncheck value (true/false)
Property 'children' contains the child list items
How do I define the .sync attribute on HierarchicalCheckboxList and with what parameter?
<template>
<div>
<HierarchicalCheckboxList
v-for="link in links"
#checked="primaryCheckChanged"
:key="link.id"
v-bind="link">
</HierarchicalCheckboxList>
</div>
</template>
<script>
import HierarchicalCheckboxList from 'components/HierarchicalCheckboxList'
data () {
return {
links: [{
id: 1,
title: 'Home',
caption: 'Feeds, Dashboard & more',
icon: 'account_box',
level: 0,
children: [{
id: 2,
title: 'Feeds',
icon: 'feeds',value: true,
level: 1,
children: [{
id: '3',
title: 'Dashboard',
icon: 'settings',
value: true,
level: 1
}]
}]
}]
}
},
methods: {
primaryCheckChanged (d) {
// A child's checked state is propogated till here
console.log(d)
}
}
</script>
HierarchicalCheckboxList.vue
This component calls itself recursively:
<template>
<div>
<div v-if="children != undefined && children.length == 0">
<!--/admin/user/user-->
<q-item clickable v-ripple :inset-level="level" :to="goto">
<q-item-section>
{{title}}
</q-item-section>
</q-item>
</div>
<div v-else>
<div v-if="children != undefined && children.length > 0">
<!-- {{children}} -->
<q-expansion-item
expand-separator
:icon="icon"
:label="title"
:caption="caption"
:header-inset-level="level"
default-closed>
<template v-slot:header>
<q-item-section>
{{ title }}
</q-item-section>
<q-item-section side>
<div class="row items-center">
<q-btn icon="add" dense flat color="secondary"></q-btn>
</div>
</q-item-section>
</template>
<HierarchicalCheckboxList
v-for="child in children"
:key="child.id"
#checked="primaryCheckChanged"
v-bind="child">
</HierarchicalCheckboxList>
</q-expansion-item>
</div>
<!-- to="/admin/user/user" -->
<div v-else>
<q-item clickable v-ripple :inset-level="level">
<q-item-section>
<q-checkbox :label="title" v-model="selection" />
</q-item-section>
</q-item>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'HierarchicalCheckboxList',
props: {
id: { type: String, required: true },
title: { type: String, required: false },
caption: { type: String, default: '' },
icon: { type: String, default: '' },
value: { type: Boolean, default: false },
level: { type: Number, default: 0 },
children: { type: Array }
},
data () {
return {
localValue: this.$props.value
}
},
computed: {
selection: {
get: function () {
return this.localValue
},
set: function (newvalue) {
this.localValue = newvalue
this.$emit('checked', this.localValue)
// or this.$emit('checked', {id: this.$props.id, value: this.localValue })
}
}
},
methods: {
primaryCheckChanged (d) {
this.$emit('checked', d)
}
}
}
</script>
What works so far
As a work-around I am able to get the checkbox state emitted with $emit('checked'), which I use to send it to the next process. But the parent's state is not updated until I refresh it back from the database.
How do I update the parent component's state using v-bind.sync recursively?
Appreciate any help!!
UI
Figured out how to do it after I broke the code down from the whole 2000 line code to a separate 'trial-n-error' code of 20 lines and then things became simple and clear.
Menu.vue
A few changes in the parent component in the HierarchicalCheckboxList declaration:
Note the sync property
<HierarchicalCheckboxList
v-for="child in children"
:key="child.id"
:u.sync="link.value"
v-bind="child">
</HierarchicalCheckboxList>
HierarchicalCheckboxList.vue
Change the same line of code in the child component (as its recursive)
<HierarchicalCheckboxList
v-for="child in children"
:key="child.id"
:u.sync="child.value"
v-bind="child">
</HierarchicalCheckboxList>
And in the computed set property, emit as below:
this.$emit('update:u', this.localValue)
That's it - parent n children components now stay in snyc.

Vue.js - Inject el elements to html

I have website for online tests.
One of the question that i have created on the test its topic "Fill in the blank", which means fill in the blank spaces words.
The question comes from the server as a string like that "Today is a [1] day, and i should [2] today".
What i want to do is to get that string and replace all the [] with el-input.
I have done something like that
<template>
<div class="d-flex flex-column mg-t-20 pd-10">
<h6 class="tx-gray-800">Fill in the blank areas the missing words</h6>
<div class="mg-t-20" v-html="generateFillBlankQuestion(question.question)" />
</div>
</template>
<script>
export default {
name: 'FillBlank',
directives: {},
props: [ 'question' ],
components: {
},
computed: {},
data() {
return {
input: ''
}
},
filters: {},
created() {
},
methods: {
generateFillBlankQuestion(question) {
var matches = question.match((/\[\d\]/g))
console.log(matches)
matches.forEach((element) => {
console.log(element)
question = question.replace(element, '<el-input />')
})
console.log(question)
return question
}
}
}
On this line question = question.replace(element, '<el-input />') I'm replacing the [] to input.
For some reason when i try to replace it to <el-input> it doesn't render it.
But if i use <input type='text'> it renders it.
Is it possible to inject el elements?
If you are not using the Vue run-time template compiler you can not render Vue components inside v-html. You should do something like this:
<template>
<div class="d-flex flex-column mg-t-20 pd-10">
<h6 class="tx-gray-800">Fill in the blank areas the missing words</h6>
<div class="mg-t-20">
<template v-for="(word,idx) in wordList">
<el-input v-if="word.blank" v-model="word.value" :key="idx" />
<template v-else>{{ word.text }}</template>
</template>
</div>
</div>
</template>
<script>
export default
{
name: 'FillBlank',
props:
{
question:
{
type: String,
default: ''
}
},
computed:
{
wordList()
{
const words = this.question.split(' ');
return words.map(word =>
({
value: '',
text: word,
blank: /^\[\d+\]$/.test(word),
}));
}
}
}

Vue: data from grandparent to grandchild (search component)

In my Vue app I have a view ('projects.vue') that gets some .json and which has a child component ('subheader.vue') which imports a search/filter mixin. I had this working but I wanted to split out the search elements from the subheader component to its own component, so subheader would hold only the headings and then import the search component. Despite adapting the props and bindings from the working version, my new three-component setup is throwing an error. Here's the setup:
the searchMixin.js:
export default {
computed: {
filteredProjects: function() {
const searchTerm = this.search.toLowerCase();
if (!searchTerm) {
return false;
}
return this.projects.filter((project) => {
return (project.client.toLowerCase().match(searchTerm)) ||
(project.contacts.filter((el) => {
return el.name.toLowerCase().match(searchTerm);
}).length > 0) ||
(project.projectReference.toLowerCase().match(searchTerm));
});
}
}
}
Projects.vue:
<template>
<div class="container" id="projects">
<!-- app-subheader is global component, imported & registered in main.js -->
<app-subheader v-bind:title="title" v-bind:subtitle="subtitle" />
[ snip irrelevant stuff ]
</div>
</template>
<script>
export default {
data () {
return {
title: "Projects",
subtitle: "",
projects: []
}
},
created: function() {
this.$http.get('https://xyz.firebaseio.com/projects.json')
.then(function(data){
return data.json();
})
.then(function(data){
var projectsArray = [];
for (let key in data) {
data[key].id = key;
this.projectID = key;
projectsArray.push(data[key]);
}
this.projects = projectsArray;
})
},
} // export default
</script>
subheader.vue:
<template>
<div class="subheader">
<div class="headings">
<h1 v-html="title"></h1>
<h2 v-html="subtitle"></h2>
<!-- I want to conditionally include <app-search /> according to an ID on an element in the grandparent (e.g., projects.vue) -->
<app-search v-bind:projects="projects" />
</div>
</div>
</template>
<script>
import Search from './search.vue';
export default {
components: {
'app-search': Search
},
props: [ "title", "subtitle", "projects" ],
data() {
return {
search: "",
projects: []
}
},
created: function() {
console.log("created; log projects", this.projects);
},
methods: {},
computed: {}
}
</script>
search.vue:
<template>
<div class="search-wrapper">
<div class="search">
<input type="text" v-model="search" placeholder="search by client, contact name, description, project, source" />
</div>
<div class="search-results-wrapper">
<h3>search-results:</h3>
<span class="results-count" v-if="filteredProjects.length == 0">
no results matching search "{{ search }}":
</span>
<span class="results-count" v-if="filteredProjects.length > 0">
{{ filteredProjects.length }} result<span v-if="filteredProjects.length > 1">s</span> matching search "{{ search }}":
</span>
<ul class="search-results" v-bind:class="{ open: filteredProjects.length > 0 }">
<li v-for="(project, ix) in filteredProjects" :key="ix">
{{ ix + 1 }}:
<router-link v-bind:to="'/project-detail/' + project.id">
{{ project.client }} ({{ project.projectReference }})
</router-link>
</li>
</ul>
</div>
</div><!-- END .search-wrapper -->
</template>
<script>
import searchMixin from '../mixins/searchMixin.js';
export default {
props: [ "projects" ],
data() {
return {
search: "",
}
},
created: function() {
},
mixins: [ searchMixin ],
}
When the search function is invoked, this error is thrown:
[Vue warn]: Error in render: "TypeError: Cannot read property 'filter' of undefined"
found in --->
<AppSearch> at src/components/search.vue
<AppSubheader> at src/components/subheader.vue
<Projects> at src/components/projects.vue
<App> at src/App.vue
... which seems to suggest the search mixin is not getting 'projects'.
Also, in subheader.vue, I get various errors whether I have 'projects' as a prop or 'projects: []' as a data key, and in once case or another I either get no results from the search function or an error, "Property or method "projects" is not defined on the instance but referenced during render".
Obviously I'm lacking clarity on the docs re; grandparent-parent-child data flow. Any help is greatly appreciated.
Whiskey T.

Using a vue-apollo component with different queries on the same page

I would like to make a wrapper component using Apollo, which receives different GraphQL queries and variables within props, queries GraphQL source and and passes response to its child component. I would like to use this component twice on a page with different queries.
But I think I'm stuck at some point. I end up having both instances of the component working with same inputs: query prop of the first dropdown component is being used in all dropdowns on the page. Although they have a different scope, different keywords and items, all dropdowns on the page are using the query of first dropdown.
Here is my DropdownSearch component. It passes keyword from searchbar component to query-list component:
<template>
<div class="dropdown-search">
<div class="dropdown-search-display" #click="toggleDropdown">
<span>{{value[nameProp]}}</span>
<span class="dropdown-search-dropdown-toggle">
<i v-if="!isOpen" class="fa fa-caret-down" aria-hidden="true"></i>
<i v-if="isOpen" class="fa fa-caret-up" aria-hidden="true"></i>
</span>
</div>
<div :class="{ 'dropdown-search-dropdown': true, 'dropdown-search-dropdown--open': isOpen}" >
<search-bar v-model="searchKeyword" class="dropdown-search-searchbar"></search-bar>
<div class="dropdown-search-list">
<query-list
:query="query"
:keyword="searchKeyword"
:listItemComponent="ListItem"
></query-list>
</div>
</div>
</div>
</template>
<script>
const ListItem = {
template: '<div class="dropdown-search-list-item">{{record.name}}</div>',
props: {
record: { required: true },
},
};
export default {
name: 'dropdownSearch',
props: {
name: String,
nameProp: String,
value: Object,
query: Object,
},
components: {
'dropdown-list-item': ListItem,
},
data() {
return {
isOpen: false,
searchKeyword: '',
ListItem,
};
},
methods: {
toggleDropdown() {
this.isOpen = !this.isOpen;
},
},
};
</script>
QueryList component, which is using Apollo to make queries and displays a list of results:
<template>
<div class="">
<loading-indicator :isLoading="loading > 0"></loading-indicator>
<ul class="app__list">
<li class="app__list-item" v-for="item in items" :key="item.id">
<component
:is="listItemComponent"
:record="item"
></component>
</li>
</ul>
</div>
</template>
<script>
export default {
name: 'queryList',
props: {
query: Object,
keyword: String,
listItemComponent: { required: true },
},
data() {
return {
items: [],
loading: 0,
};
},
apollo: {
items: {
query() {
return this.query;
},
variables() {
return {
keyword: this.keyword || '',
};
},
loadingKey: 'loading',
},
},
};
</script>
This is how I use Dropdown Search components on a page:
<label class="edit-record-field">
<span class="edit-record-field-label">Category</span>
<dropdown-search
class="edit-record-field-input"
v-model="record.category"
:nameProp="'name'"
:query="getCategories"
></dropdown-search>
</label>
<label class="edit-record-field">
<span class="edit-record-field-label">Location</span>
<dropdown-search
class="edit-record-field-input"
v-model="record.location"
:nameProp="'name'"
:query="getLocation"
></dropdown-search>
</label>
I'm looking for a solution. I would be grateful if you could help me to make my components working.