I have a form which includes cascader from element ui. But el-cascader is in child component. So here is my parent component:
<el-form :model="file" :rules="rules" ref="form">
<el-form-item prop="select">
<selector v-model="file.select" />
</el-form-item>
</el-form>
rules: function () {
return {
select: [
{
type: 'object',
trigger: 'change',
message: 'form required',
},
{
validator(rule, value) {
return new Promise((resolve, reject) => {
console.log(value);
if (value.length > 0) {
return reject('form required');
}
return resolve();
});
},
},
],
};
},
is my child component and includes el-cascader inside.
<el-cascader
v-model="file"
:options="fileOptionsForCascader"
placeholder="File"
filterable
:filter-method="filterMethod"
:props="{
emitPath: false,
}"
/>
So my validation is wrong. At very first time, when I select the file, the value comes empty (in console.log) and validation fails. If I select it again, then validation is successful. So I wonder why very first time, the value comes emoty and what can I do to solve it?
Related
I am trying to create Quasar custom select component with autocomplete. Everything works fine except the validation error, the validation error is showing only when I click the input box and leave without adding any value. But, the form is submitting even there are any errors.
Component code
<q-select
ref="members"
v-model="sModel"
use-input
:options="filteredOptions"
:multiple="multiple"
:use-chips="useChips"
:label="label"
:option-label="optionLabel"
:option-value="optionValue"
#filter="filterFn"
#input="handleInput"
emit-value
map-options
hint
dense
outlined
lazy-rules
:rules="rules"
>
<template v-slot:prepend>
<q-icon :name="icon" />
</template>
</q-select>
</template>
<script>
export default {
props: {
value: Array,
rules: Array,
icon: String,
label: String,
optionValue: String,
optionLabel: String,
options: Array,
multiple: Boolean,
useChips: Boolean
},
data () {
return {
filteredOptions: this.options,
sModel: this.value,
validationErrors:{
}
}
},
methods: {
filterFn (val, update) {
if (val === '') {
update(() => {
this.filteredOptions = this.options
// with Quasar v1.7.4+
// here you have access to "ref" which
// is the Vue reference of the QSelect
})
return
}
update(() => {
const needle = val.toLowerCase()
const optionLabel = this.optionLabel
this.filteredOptions = this.options.filter(function(v){
// optionLabel
return v[optionLabel].toLowerCase().indexOf(needle) > -1
})
})
},
handleInput (e) {
this.$emit('input', this.sModel)
}
},
}
</script>
In the parent component, this is how I am implementing it,
<AdvancedSelect
ref="members"
v-model="members"
:options="extAuditEmployees"
icon="people_outline"
multiple
use-chips
label="Team Members *"
option-label="formatted_name"
option-value="id"
:rules="[ val => val && val.length && !validationErrors.members > 0 || validationErrors.members ? validationErrors.members : 'Please enter Team members' ]">
</AdvancedSelect>
Try adding this method on select component methods:
validate(...args) {
return this.$refs.members.validate(...args);
}
It worked for me, apparently it sends the validation of the input to the parent
Source consulted: https://github.com/quasarframework/quasar/issues/7305
add ref to the form and try to validate the form.
you can give give props "greedy" to the form.
I'm using dynamic forms to break up a long form. Beneath each dynamic form are two action buttons which, when clicked, send the user back one form set or onto the next one (unless the end of the form has been reached - the 'Next' button is replaced by a 'Submit' button).
As I have three different forms that all use the same action buttons, I decided to create a child component ('ActionButtons') for action buttons - and in doing so I've gone from code that worked to code that doesn't.
Specifically, I'm having problems with ref information involved in controlling the 'Next' button. The error message is Error in nextTick: "TypeError: Cannot read property '$v' of undefined"
The code that worked fine (i.e. before I created 'ActionButtons') is:
<template>
...
<keep-alive>
<component
ref="currentStep"
:is="currentStep"
#update="processStep"
:wizard-data="formvars"
></component>
</keep-alive>
...
<button
#click="nextButtonAction"
:disabled="!canGoNext"
class="btn"
>{{isLastStep ? 'Submit' : 'Next'}}</button>
...
</template>
<script>
import Gap2InputInfo from './Gap2InputInfo'
import Gap2Materials from './Gap2Materials'
export default {
name: 'GAP2Form',
components: {
Gap2InputInfo,
Gap2Materials
},
data () {
return {
currentStepNumber: 1,
canGoNext: false,
wizardData: {},
steps: [
'Gap2InputInfo',
'Gap2Materials',
],
formvars: {
id: 0,
purchase: null,
name: null,
quantity: null,
supplier: null,
nsteps: 0
},
updatedData: null
}
},
computed: {
isLastStep () {
return this.currentStepNumber === this.length
},
length () {
this.formvars.nsteps = this.steps.length;
return this.steps.length
},
currentStep () {
return this.steps[this.currentStepNumber - 1];
},
},
methods: {
nextButtonAction () {
if (this.isLastStep) {
this.submitForm()
} else {
this.goNext()
}
},
processStep (step) {
Object.assign(this.formvars, step.data);
this.canGoNext = step.valid
},
goBack () {
this.currentStepNumber--;
this.canGoNext = true;
},
goNext () {
this.currentStepNumber++;
this.$nextTick(() => {
this.canGoNext = !this.$refs.currentStep.$v.$invalid
})
}
}
}
</script>
In creating the child component, I send the following props to the child
<action-buttons :currentStepNumber="currentStepNumber" :canGoNext="canGoNext" :isLastStep="isLastStep" :currentStep="currentStep"></action-buttons>
and have moved the methods associated with the button actions from the parent to the child component.
My child component is:
<template>
<div>
<div id="actions" class="buttons">
<button
#click="goBack"
v-if="mcurrentStepNumber > 1"
class="btn-outlined"
>{{$t('back')}}
</button>
<button
#click="nextButtonAction"
:disabled="!canGoNext"
class="btn"
>{{isLastStep ? 'Submit' : 'Next'}}</button>
</div>
</div>
</template>
<script>
import {required} from 'vuelidate/lib/validators'
export default {
name: "ActionButtons",
props: ['currentStepNumber','canGoNext','isLastStep','currentStep'],
data: function () {
return {
mcurrentStepNumber: this.currentStepNumber,
mcurrentStep: this.currentStep,
mcanGoNext: this.canGoNext,
misLastStep: this.isLastStep
}
},
methods: {
nextButtonAction () {
if (this.misLastStep) {
this.submitForm()
} else {
this.goNext()
}
},
goBack () {
this.mcurrentStepNumber--;
this.mcanGoNext = true;
},
goNext () {
this.mcurrentStepNumber++;
this.$nextTick(() => {
this.mcanGoNext = !this.$refs.currentStep.$v.$invalid **** Error triggered at this line
})
}
}
}
</script>
Now when I click the 'Next' button, I get the Cannot read property '$v' of undefined error message. If I've interpreted it correctly, it cannot read the $refs data. I tried altering the code in the child component to
this.mcanGoNext = !this.$parent.$refs.currentStep.$v.$invalid
but the outcome remained the same. Where am I going wrong?
Thanks, Tom.
I use v-autocomplete component and updating items list after http request. But browser was freeze when i set items. What's wrong?
{
mixins: [search_field_block],
data: function () {
return {
item_component: {
props: ['item'],
template: '<div v-html="item.full_name"></div>'
}
};
},
methods: {
search: function (text) {
this.search_text = text.trim();
if (this.search_text) {
this.doGET('/api-method/search_place/', {'query': this.search_text}, this.update_items);
}
},
update_items: function (data) {
this.items = data;
}
}
}
I use a mixin for other components. It contained universal template with v-autocomplete:
<field-block :label="label" :error="field_error" :description="item_description">
<v-autocomplete slot="input"
class="page-form__field required"
:class="{ focused: focused, 'not-empty': not_empty, error: field_error != null, 'list-open': is_list_open }"
v-model="value"
ref="autocomplete"
:required="required"
:inputAttrs="{ref: 'input', autocomplete: 'off'}"
:items="items"
:component-item="item_component"
:get-label="getLabel"
:min-len="2"
#update-items="search"
#item-selected="onSelect"
#input="onInput"
#blur="onBlur"
#focus="onFocus">
</v-autocomplete>
</field-block>
I was find v-autocomplete on the github. It contain a v-for block for rendering search results
I found the problem. I have "computed" property and set value to parent app:
computed: {
is_list_open: function () {
this.$parent.list_opened = this.focused && this.items.length > 0 || (this.$refs.autocomplete ? this.$refs.autocomplete.show : false);
return this.$parent.list_opened;
}
},
This is incorrect behavior.
I have the following component:
Vue.component('email-input', {
template: '#tmpl-email-input',
name: 'email-input',
delimiters: ['((', '))'],
props: ['name', 'required', 'value'],
data: () => ({
suggestedEmail: '',
email: '',
}),
methods: {
onInput() {
this.checkEmail();
this.$emit('input', this.email);
},
checkEmail() {
Mailcheck.run({
email: this.email,
suggested: suggestion => {
this.suggestedEmail = suggestion.full;
},
empty: () => {
this.suggestedEmail = '';
},
});
},
confirmSuggestion(confirm) {
if (confirm) this.email = this.suggestedEmail;
this.suggestedEmail = '';
},
},
mounted() {
this.checkEmail = _.debounce(this.checkEmail.bind(this), 1000);
},
});
using this template
<template id="tmpl-email-input">
<div>
<input
type="email"
class="form-control"
:name="name || 'email'"
:required="required"
v-on:input="onInput"
v-model="email"
/>
<small class="email-correction-suggestion" v-if="suggestedEmail">
Did you mean ((suggestedEmail))?
Yes
No
</small>
</div>
</template>
<!-- Lodash from GitHub, using rawgit.com -->
<script src="https://cdn.rawgit.com/lodash/lodash/4.17.4/dist/lodash.min.js"></script>
<!-- Mailcheck: https://github.com/mailcheck/mailcheck -->
<script src="/js/lib/mailcheck.js"></script>
<script src="/js/views/partials/email_input.js"></script>
And I'm calling it using
<email-input name="email" required></email-input>
I'd like to set an initial value for this email input, something like
<email-input name="email" required value="test#test.com"></email-input>
and have that show in the input.
I assumed I could do this by simply setting email to this.value in data but that doesn't help. How can I do this?
There's a value prop but you are not using it at all! So it doesn't really matter which value you pass down as value prop: it won't be used.
I think what you are trying to achieve is expose an API similar to the one exposed by input component. That can be done and it's detailed in the docs.
What Vue does to handle the v-model bindings is assuming the component will emit an input event passing the new value as $event. It will also pass down to the component a value to the value prop. So this 2-way binding is automatically handled by Vue as long as you define a value prop and emit an input event.
The problem is that your component acts as a middleware for the underlying input component but it is passing down a different binding instead of forwarding it.
Translating this into your component, you should not use v-model to pass down email to the input component but a combination of :value and #input bindings: you pass down the value prop of email-input component to the value prop of the input component and as handler of input event of the input component you should just emit another input event with the same $event payload.
Template:
<template id="tmpl-email-input">
<div>
<input
type="email"
class="form-control"
:name="name || 'email'"
:required="required"
:value="value"
#input="onInput($event.target.value)"
/>
<small class="email-correction-suggestion" v-if="suggestedEmail">
Did you mean ((suggestedEmail))?
Yes
No
</small>
</div>
</template>
<!-- Lodash from GitHub, using rawgit.com -->
<script src="https://cdn.rawgit.com/lodash/lodash/4.17.4/dist/lodash.min.js"></script>
<!-- Mailcheck: https://github.com/mailcheck/mailcheck -->
<script src="/js/lib/mailcheck.js"></script>
<script src="/js/views/partials/email_input.js"></script>
Note the change from #input="onInput" to #input="onInput($event.target.value)" so we have access to the new value in onInput method.
Component:
Vue.component('email-input', {
template: '#tmpl-email-input',
name: 'email-input',
delimiters: ['((', '))'],
props: ['name', 'required', 'value'],
data: () => ({
suggestedEmail: ''
}),
methods: {
onInput(newValue) {
this.$emit('input', newValue);
this.checkEmail();
},
checkEmail() {
Mailcheck.run({
email: this.value,
suggested: suggestion => {
this.suggestedEmail = suggestion.full;
},
empty: () => {
this.suggestedEmail = '';
},
});
},
confirmSuggestion(confirm) {
if (confirm) this.$emit('input', this.suggestedEmail);
this.suggestedEmail = '';
},
},
mounted() {
this.checkEmail = _.debounce(this.checkEmail.bind(this), 1000);
},
});
Note the change in onInput method: now it takes a parameter with the new value and emits an input event with that value before checking the email address. It's emitted in that order to ensure we have synced the value of the value binding before checking the address.
Also note the change in confirmSuggestion method: instead of updating email data attribute it just emits an input event.
That's the key to solve this issue: the old implementation forced us to have 2 different variables: one where parent component could pass down a value and another one email-input could modify to store the chosen suggestion.
If we just emit the chosen suggestion as a regular change then we can get rid of the email variable and work with just one binding.
Suggestion totally not related with the issue: you can use debounce directly in methods instead of replacing the method on mounted hook:
Vue.component('email-input', {
template: '#tmpl-email-input',
name: 'email-input',
delimiters: ['((', '))'],
props: ['name', 'required', 'value'],
data: () => ({
suggestedEmail: ''
}),
methods: {
onInput(newValue) {
this.$emit('input', newValue);
this.checkEmail();
},
checkEmail: _.debounce(function () {
Mailcheck.run({
email: this.value,
suggested: suggestion => {
this.suggestedEmail = suggestion.full;
},
empty: () => {
this.suggestedEmail = '';
},
});
}, 1000),
confirmSuggestion(confirm) {
if (confirm) this.$emit('input', this.suggestedEmail);
this.suggestedEmail = '';
},
}
});
Lodash will take care of binding this of the underlying function to the same this that called the debounced function.
I'm using el-select to build a select component. Something like this:
<template>
//omitted code
<el-select v-model="filterForm.client"
filterable
remote
placeholder="Please enter a keyword"
:remote-method="filterClients"
:loading="loading">
<el-option
v-for="item in clientCandidates"
:key="item._id"
:label="item.name"
:value="item._id">
</el-option>
</el-select>
</template>
<scripts>
export default {
data() {
filterForm: {
client: ''
},
clientCandidates: [],
loading: false
},
methods: {
filterClients(query) {
if (query !== '') {
this.loading = true;
setTimeout(() => {
this.loading = false;
this.clientCandidates = [{_id: '1', name: 'foo'}, {_id: '2', name: 'bar'}];
}, 200);
} else {
this.clientCandidates = [];
}
}
}
}
</scripts>
So far so good, but since the component will appear in different pages, so I want to extract a custom component to avoid duplication.
According to the guideline,
v-model="fullName"
is equivalent to
v-bind:value="fullName"
v-on:input="$emit('input', $event)"
So I extracted the select component like this:
<template>
<el-select
v-bind:value="clientId"
v-on:input="$emit('input', $event)"
placeholder="Filter by short name"
filterable="true"
remote="true"
:remote-method="filter"
:loading="loading">
<el-option
v-for="item in clients"
:key="item._id"
:label="item.name"
:value="item._id">
</el-option>
</el-select>
</template>
<scripts>
export default {
props: {
clientId: {
type: String,
required: true
}
},
data() {
return {
clients: [],
loading: false,
}
},
methods: {
filter(query) {
if (query !== '') {
this.loading = true;
setTimeout(() => {
this.loading = false;
this.clients = [{_id: '1', name: 'foo'}, {_id: '2', name: 'bar'}];
}, 200);
} else {
this.clients = [];
}
}
}
}
</scripts>
And the parent component looks like this:
<select-client v-model="filterForm.clientId"></select-client>
The select drop down works fine, but unfortunately, the select does not reveal the option I selected, it remains empty after I choose an option. I suspect that maybe I should switch the v-on:input to 'v-on:change', but it does not work either.
UPDATE
I created a simple example, you can clone it here, please checkout the el-select-as-component branch. Run
npm install
npm run dev
You will see a simple page with 3 kinds of select:
The left one is a custom component written in raw select, it works fine.
The middle one is a custom component written in el-select, the dropdown remains empty but you can see the filterForm.elClientId in the console once you click Filter button. This is why I raise this question.
The right one is a plain el-select, it works fine.
The guideline says v-model is equivalent to v-bind:value and v-on:input but if you look closer, in the listener function, the variable binded is set with the event property. What you do in your exemple isn't the same, in your listener you emit another event. Unless you catch this new event, your value will never be set.
Another thing is you can't modify a props, you should consider it like a read-only variable.
If you want to listen from the parent to the emitted event into the child component, you have to do something like this
<template>
<el-select
:value="selected"
#input="dispatch"
placeholder="Filter by short name"
:filterable="true"
:remote="true"
:remote-method="filter"
:loading="loading">
<el-option
v-for="item in clients"
:key="item._id"
:label="item.name"
:value="item._id">
</el-option>
</el-select>
</template>
<script>
export default {
name: 'SelectClient',
data() {
return {
selected: '',
clients: [],
loading: false,
}
},
methods: {
filter(query) {
if (query !== '') {
this.loading = true;
setTimeout(() => {
this.loading = false
this.clients = [{_id: '1', name: 'foo'}, {_id: '2', name: 'bar'}]
}, 200)
} else {
this.clients = []
}
},
dispatch (e) {
this.$emit('input', e)
this.selected = e
}
}
}
</script>
NB: a v-model + watch pattern will work too. The important thing is to $emit the input event, so the v-model in the parent will be updated.
And in your parent you can use this component like this: <select-client v-model="clientId"/>.
Tips: if you want to modify the same data in different place, you should have a single source of truth and prefer something like vuex. Then your component will be like this
<template lang="html">
<select
v-model="clientId">
<option
disabled
value="">Please select one</option>
<option>A</option>
<option>B</option>
<option>C</option>
</select>
</template>
<script>
export default {
data () {
return {
clientId: ''
}
},
watch: {
clientId (newValue) {
// Do something else here if you want then commit it
// Of course, listen for the 'setClientId' mutation in your store
this.$store.commit('setClientId', newValue)
}
}
}
</script>
Then in your other components, you can listen to $store.state.clientId value.