Skip to content

Commit dbef221

Browse files
committed
feat(QInput): async validation rules support & loading prop
1 parent c969cd3 commit dbef221

File tree

10 files changed

+373
-55
lines changed

10 files changed

+373
-55
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<template>
2+
<div class="q-pa-md">
3+
<div class="q-gutter-y-md column" style="max-width: 300px">
4+
<q-input
5+
:loading="loadingState"
6+
filled
7+
v-model="text"
8+
label="Label"
9+
/>
10+
<q-toggle v-model="loadingState" label="Loading state" />
11+
</div>
12+
</div>
13+
</template>
14+
15+
<script>
16+
export default {
17+
data () {
18+
return {
19+
text: '',
20+
loadingState: false
21+
}
22+
}
23+
}
24+
</script>
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<template>
2+
<div class="q-pa-md" style="max-width: 300px">
3+
<q-input
4+
ref="input"
5+
filled
6+
v-model="model"
7+
label="Required Field *"
8+
:rules="[ myRule ]"
9+
/>
10+
11+
<q-btn class="q-mt-sm" label="Reset" @click="reset" color="primary"/>
12+
</div>
13+
</template>
14+
15+
<script>
16+
export default {
17+
data () {
18+
return {
19+
model: ''
20+
}
21+
},
22+
23+
methods: {
24+
myRule (val) {
25+
// simulating a delay
26+
27+
return new Promise((resolve, reject) => {
28+
setTimeout(() => {
29+
// call
30+
// resolve(true)
31+
// --> content is valid
32+
// resolve(false)
33+
// --> content is NOT valid, no error message
34+
// resolve(error_message)
35+
// --> content is NOT valid, we have error message
36+
resolve(!!val || '* Required')
37+
38+
// calling reject(...) will also mark the input
39+
// as having an error, but there will not be any
40+
// error message displayed below the input
41+
// (only in browser console)
42+
}, 1000)
43+
})
44+
},
45+
46+
reset () {
47+
this.$refs.input.resetValidation()
48+
}
49+
}
50+
}
51+
</script>

docs/src/pages/vue-components/input.md

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ The role of debouncing is for times when you watch the model and do expensive op
9090

9191
<doc-example title="Debounce model" file="QInput/Debouncing" />
9292

93+
### Loading state
94+
95+
<doc-example title="Loading state" file="QInput/LoadingState" />
96+
9397
## Mask
9498

9599
You can force/help the user to input a specific format with help from `mask` prop.
@@ -132,10 +136,6 @@ value => value.includes('Hello') || 'Field must contain word Hello'
132136

133137
You can reset the validation by calling `resetValidation()` method on the QInput.
134138

135-
::: warning
136-
Rules are not asynchronous and need to return immediately.
137-
:::
138-
139139
<doc-example title="Basic" file="QInput/ValidationRequired" />
140140

141141
<doc-example title="Maximum length" file="QInput/ValidationMaxLength" />
@@ -146,6 +146,15 @@ If you set `lazy-rules`, validation starts after first blur.
146146

147147
<doc-example title="Form validation" file="QInput/ValidationForm" />
148148

149+
#### Async rules
150+
Rules can be async too, by using async/await or by directly returning a Promise.
151+
152+
::: tip
153+
Consider coupling async rules with `debounce` prop to avoid calling the async rules immediately on each keystroke, which might be detrimental to performance.
154+
:::
155+
156+
<doc-example title="Async rules" file="QInput/ValidationAsync" />
157+
149158
### External validation
150159

151160
You can also use external validation and only pass `error` and `error-message` (enable `bottom-slots` to display this error message).

quasar/build/build.api.js

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,6 @@ const
1111
dest = path.join(root, 'dist/api'),
1212
extendApi = require(resolve('src/api.extends.json'))
1313

14-
function getWithoutExtension (filename) {
15-
const insertionPoint = filename.lastIndexOf('.')
16-
return filename.slice(0, insertionPoint)
17-
}
18-
1914
function getMixedInAPI (api, mainFile) {
2015
api.mixins.forEach(mixin => {
2116
const mixinFile = resolve('src/' + mixin + '.json')

quasar/dev/components/form/input-validate.vue

Lines changed: 167 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,132 @@
4949
hint="Validation starts after first blur"
5050
:rules="[
5151
val => !!val || '* Required',
52-
val => val.length < 2 || 'Please use maximum 1 character',
52+
val => val.length < 2 || 'Please use maximum 1 character'
5353
]"
5454
lazy-rules
5555
/>
5656

57+
<q-input
58+
ref="input3"
59+
v-bind="{[type]: true}"
60+
v-model="model4"
61+
label="Required, Len > 1, Len > 2"
62+
counter
63+
hint="Multiple"
64+
:rules="[
65+
val => !!val || '* Required',
66+
val => val.length > 1 || 'Please use min 1 characters',
67+
val => val.length > 2 || 'Please use min 2 characters'
68+
]"
69+
/>
70+
71+
<q-input
72+
ref="input1"
73+
v-bind="{[type]: true}"
74+
v-model="model5"
75+
label="Multiple - call stack test *"
76+
:rules="[
77+
callRule1,
78+
void 0,
79+
callRule2
80+
]"
81+
/>
82+
83+
<q-input
84+
ref="input1"
85+
v-bind="{[type]: true}"
86+
v-model="model6"
87+
label="Multiple - async call stack test *"
88+
:rules="[
89+
asyncCallRule1,
90+
asyncCallRule2
91+
]"
92+
/>
93+
94+
<div class="text-h6 q-mt-xl">
95+
Async rules
96+
</div>
97+
<q-input
98+
ref="input1"
99+
v-bind="{[type]: true}"
100+
v-model="model7"
101+
label="Only async *"
102+
:rules="[
103+
asyncRule
104+
]"
105+
/>
106+
107+
<q-input
108+
ref="input1"
109+
v-bind="{[type]: true}"
110+
v-model="model7"
111+
label="Multiple async *"
112+
:rules="[
113+
asyncRule,
114+
secondAsyncRule
115+
]"
116+
/>
117+
118+
<q-input
119+
ref="input1"
120+
v-bind="{[type]: true}"
121+
v-model="model7"
122+
label="Loading slot *"
123+
:rules="[
124+
asyncRule
125+
]"
126+
>
127+
<template v-slot:loading>
128+
<q-spinner-gears color="purple" />
129+
</template>
130+
</q-input>
131+
132+
<q-input
133+
ref="input1"
134+
v-bind="{[type]: true}"
135+
v-model="model7"
136+
debounce="1000"
137+
label="X Mixed *"
138+
:rules="[
139+
asyncRule,
140+
val => val.length > 2 || 'Please use min 3 characters'
141+
]"
142+
/>
143+
144+
<q-input
145+
ref="input1"
146+
v-bind="{[type]: true}"
147+
v-model="model7"
148+
debounce="1000"
149+
label="Debounced input *"
150+
:rules="[
151+
asyncRule
152+
]"
153+
/>
154+
155+
<q-input
156+
ref="input1"
157+
v-bind="{[type]: true}"
158+
v-model="model7"
159+
label="Mixed, Lazy *"
160+
lazy-rules
161+
:rules="[
162+
asyncRule,
163+
val => val.length > 2 || 'Please use min 3 characters'
164+
]"
165+
/>
166+
167+
<q-input
168+
ref="input1"
169+
v-bind="{[type]: true}"
170+
v-model="model7"
171+
label="Lazy async *"
172+
lazy-rules
173+
:rules="[
174+
asyncRule
175+
]"
176+
/>
177+
57178
<div class="text-h6 q-mt-xl">
58179
External validation
59180
</div>
@@ -95,7 +216,7 @@
95216
<script>
96217
export default {
97218
data () {
98-
const n = 3
219+
const n = 7
99220
100221
const data = {
101222
n,
@@ -117,6 +238,50 @@ export default {
117238
for (let i = 1; i <= this.n; i++) {
118239
this.$refs['input' + i].resetValidation()
119240
}
241+
},
242+
243+
async asyncRule (val) {
244+
return new Promise((resolve, reject) => {
245+
setTimeout(() => {
246+
resolve(!!val || '* Required')
247+
}, 1000)
248+
})
249+
},
250+
251+
async secondAsyncRule (val) {
252+
return new Promise((resolve, reject) => {
253+
setTimeout(() => {
254+
resolve((val && val.length > 2) || 'Min 3 characters')
255+
}, 1000)
256+
})
257+
},
258+
259+
callRule1 (val) {
260+
console.log('call 1')
261+
return false
262+
},
263+
264+
callRule2 (val) {
265+
console.log('call 2')
266+
},
267+
268+
async asyncCallRule1 (val) {
269+
console.log('call async 1')
270+
return new Promise((resolve, reject) => {
271+
setTimeout(() => {
272+
reject(new Error('some err'))
273+
// resolve(!!val || '* Required 1')
274+
}, 1000)
275+
})
276+
},
277+
278+
async asyncCallRule2 (val) {
279+
console.log('call async 2')
280+
return new Promise((resolve, reject) => {
281+
setTimeout(() => {
282+
resolve(!!val || '* Required 2')
283+
}, 1000)
284+
})
120285
}
121286
}
122287
}

quasar/src/components/field/QField.js

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Vue from 'vue'
22

33
import QIcon from '../icon/QIcon.js'
4+
import QSpinner from '../spinner/QSpinner.js'
45

56
import ValidateMixin from '../../mixins/validate.js'
67
import slot from '../../utils/slot.js'
@@ -29,6 +30,8 @@ export default Vue.extend({
2930

3031
square: Boolean,
3132

33+
loading: Boolean,
34+
3235
bottomSlots: Boolean,
3336
rounded: Boolean,
3437
dense: Boolean,
@@ -40,7 +43,11 @@ export default Vue.extend({
4043

4144
data () {
4245
return {
43-
focused: false
46+
focused: false,
47+
48+
// used internally by validation for QInput
49+
// or menu handling for QSelect
50+
innerLoading: false
4451
}
4552
},
4653

@@ -154,6 +161,17 @@ export default Vue.extend({
154161
}, [ h(QIcon, { props: { name: this.$q.iconSet.type.warning, color: 'negative' } }) ])
155162
: null,
156163

164+
this.loading === true || this.innerLoading === true
165+
? h('div', {
166+
staticClass: 'q-field__append q-field__marginal row no-wrap items-center q-anchor--skip',
167+
key: 'inner-loading-append'
168+
}, (
169+
this.$scopedSlots.loading !== void 0
170+
? this.$scopedSlots.loading()
171+
: [ h(QSpinner, { props: { color: this.color } }) ]
172+
))
173+
: null,
174+
157175
this.__getInnerAppend !== void 0
158176
? h('div', {
159177
staticClass: 'q-field__append q-field__marginal row no-wrap items-center q-anchor--skip',

quasar/src/components/field/__QField.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@
4848
"extends": "dark"
4949
},
5050

51+
"loading": {
52+
"type": "Boolean",
53+
"desc": "Signals the user a process is in progress by displaying a spinner; Spinner can be customized by using the 'loading' slot."
54+
},
55+
5156
"filled": {
5257
"type": "Boolean",
5358
"desc": "Use 'filled' design for the field"
@@ -131,6 +136,10 @@
131136

132137
"counter": {
133138
"desc": "Slot for counter text; Enabled only if 'bottom-slots' prop is used; Suggestion: <div>"
139+
},
140+
141+
"loading": {
142+
"desc": "Override default spinner when component is in loading mode; Use in conjunction with 'loading' prop"
134143
}
135144
}
136145
}

0 commit comments

Comments
 (0)