1 | import 'babel-polyfill';
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 | function extractValidity(el) {
|
12 | const validity = el.validity
|
13 |
|
14 |
|
15 | let tooShort = validity.tooShort
|
16 | let valid = validity.valid
|
17 | const minlength = el.getAttribute('minlength')
|
18 | if (minlength && typeof tooShort === 'undefined') {
|
19 | tooShort = el.value.length < minlength
|
20 | if (tooShort) {
|
21 | valid = false
|
22 | el.setCustomValidity(`
|
23 | Please lengthen this text to ${minlength} characters or more (you are
|
24 | currently using ${el.value.length} characters).
|
25 | `.trim())
|
26 | } else {
|
27 | el.setCustomValidity('')
|
28 | }
|
29 | }
|
30 |
|
31 | return {
|
32 | badInput: validity.badInput,
|
33 | customError: validity.customError,
|
34 | patternMismatch: validity.patternMismatch,
|
35 | rangeOverflow: validity.rangeOverflow,
|
36 | rangeUnderflow: validity.rangeUnderflow,
|
37 | stepMismatch: validity.stepMismatch,
|
38 | tooLong: validity.tooLong,
|
39 | tooShort,
|
40 | typeMismatch: validity.typeMismatch,
|
41 | valid,
|
42 | valueMissing: validity.valueMissing,
|
43 | }
|
44 | }
|
45 |
|
46 | export default class VueForm {
|
47 |
|
48 | constructor (options) {
|
49 | const defaults = {
|
50 | wasFocusedClass: 'wasFocused',
|
51 | wasSubmittedClass: 'wasSubmitted',
|
52 | noValidate: true,
|
53 | required: []
|
54 | }
|
55 | Object.assign(defaults, options)
|
56 | this.$noValidate = defaults.noValidate
|
57 | this.$wasFocusedClass = defaults.wasFocusedClass
|
58 | this.$wasSubmittedClass = defaults.wasSubmittedClass
|
59 | this.$requiredFields = defaults.required
|
60 | this.$wasSubmitted = false
|
61 | this.$isInvalid = false
|
62 | this.$isValid = true
|
63 | this.$invalidFields = []
|
64 | }
|
65 |
|
66 | static install (Vue) {
|
67 |
|
68 |
|
69 | Vue.directive('form', (el, { value }) => {
|
70 |
|
71 | if (value instanceof VueForm) {
|
72 |
|
73 |
|
74 |
|
75 | if (!value.$el) {
|
76 | value.$el = el
|
77 | value.$el.noValidate = value.$noValidate
|
78 |
|
79 |
|
80 |
|
81 | value.$requiredFields.forEach(field => value[field.name || field] = {})
|
82 |
|
83 |
|
84 |
|
85 | value.$el.addEventListener('submit', () => {
|
86 | value.$wasSubmitted = true
|
87 | value.$el.classList.add(value.$wasSubmittedClass)
|
88 | })
|
89 |
|
90 |
|
91 |
|
92 | value.$el.addEventListener('reset', () => {
|
93 | value.$wasSubmitted = false
|
94 | value.$el.classList.remove(value.$wasSubmittedClass)
|
95 |
|
96 |
|
97 |
|
98 | for (const id of Object.keys(value)) {
|
99 | if (id.indexOf('$') === -1 && value[id].$el) {
|
100 | value[id].$wasFocused = false
|
101 | value[id].$el.classList.remove(value.$wasFocusedClass)
|
102 | Object.assign(value[id], extractValidity(value[id].$el))
|
103 | value.$updateFormValidity(id)
|
104 | }
|
105 | }
|
106 | })
|
107 | }
|
108 |
|
109 |
|
110 |
|
111 |
|
112 | for (const $el of el.querySelectorAll('input, textarea, select')) {
|
113 |
|
114 |
|
115 |
|
116 | if ($el.form === el && $el.willValidate) {
|
117 | const id = $el.getAttribute('id')
|
118 | const isUnregistered = id && !value[id]
|
119 |
|
120 |
|
121 | if (isUnregistered) {
|
122 |
|
123 |
|
124 | const field = Object.assign({ $el }, extractValidity($el))
|
125 | Vue.set(value, id, field)
|
126 | value.$updateFormValidity(id)
|
127 |
|
128 |
|
129 | $el.addEventListener('focus', ({ target }) => {
|
130 | value[id].$wasFocused = true
|
131 | target.classList.add(value.$wasFocusedClass)
|
132 | })
|
133 |
|
134 | }
|
135 |
|
136 |
|
137 | value.$updateNamedValidity($el, Vue)
|
138 |
|
139 |
|
140 |
|
141 | const type = $el.getAttribute('type')
|
142 | const isCheckable = ['radio', 'checkbox'].indexOf(type) !== -1
|
143 | const eventType = isCheckable ? 'change' : 'input'
|
144 | $el.addEventListener(eventType, ({ target }) => {
|
145 | if (id) {
|
146 | Object.assign(value[id], extractValidity(target))
|
147 | value.$updateFormValidity(id)
|
148 | }
|
149 | value.$updateNamedValidity(target, Vue)
|
150 | })
|
151 | }
|
152 |
|
153 | }
|
154 |
|
155 | }
|
156 |
|
157 | })
|
158 |
|
159 | }
|
160 |
|
161 | |
162 |
|
163 |
|
164 |
|
165 |
|
166 |
|
167 |
|
168 |
|
169 |
|
170 |
|
171 |
|
172 |
|
173 | $setCustomValidity (field, invalid) {
|
174 | if (this[field]) {
|
175 | const isBoolean = typeof invalid === 'boolean'
|
176 | const isNonEmptyString = typeof invalid === 'string' && invalid.length > 0
|
177 | if (invalid && (isBoolean || isNonEmptyString)) {
|
178 | if (isNonEmptyString) {
|
179 | this[field].customMessage = invalid
|
180 | this[field].$el.setCustomValidity(invalid)
|
181 | } else {
|
182 | this[field].$el.setCustomValidity('Error')
|
183 | }
|
184 | } else {
|
185 | delete this[field].customMessage
|
186 | this[field].$el.setCustomValidity('')
|
187 | }
|
188 | Object.assign(this[field], extractValidity(this[field].$el))
|
189 | this.$updateFormValidity(field)
|
190 | }
|
191 | }
|
192 |
|
193 | |
194 |
|
195 |
|
196 |
|
197 |
|
198 |
|
199 |
|
200 |
|
201 |
|
202 | $updateFormValidity (field) {
|
203 | const index = this.$invalidFields.indexOf(field)
|
204 | if (this[field].valid && index !== -1) {
|
205 | this.$invalidFields.splice(index, 1)
|
206 | if (this.$invalidFields.length === 0) {
|
207 | this.$isValid = true
|
208 | this.$isInvalid = false
|
209 | }
|
210 | } else if (!this[field].valid && index === -1) {
|
211 | this.$isValid = false
|
212 | this.$isInvalid = true
|
213 | this.$invalidFields.push(field)
|
214 | }
|
215 | }
|
216 |
|
217 |
|
218 | |
219 |
|
220 |
|
221 |
|
222 |
|
223 |
|
224 |
|
225 |
|
226 | $isFieldRequired(name) {
|
227 | return this.$requiredFields.filter(field => {
|
228 | const isDynamic = field.name && field.name === name && field.required()
|
229 | if (field === name || isDynamic) {
|
230 | return field
|
231 | }
|
232 | }).length > 0
|
233 | }
|
234 |
|
235 |
|
236 | |
237 |
|
238 |
|
239 |
|
240 |
|
241 |
|
242 |
|
243 |
|
244 |
|
245 |
|
246 |
|
247 | $updateNamedValidity (el, Vue) {
|
248 |
|
249 |
|
250 | if (el.hasAttribute('name')) {
|
251 | const name = el.getAttribute('name')
|
252 |
|
253 |
|
254 | if (this.$isFieldRequired(name)) {
|
255 |
|
256 |
|
257 | const valid = this.$getNamedValue(name)
|
258 | const validity = { valid, valueMissing: !valid }
|
259 | if (this[name]) {
|
260 | Object.assign(this[name], validity)
|
261 | } else {
|
262 | Vue.set(this, name, validity)
|
263 | }
|
264 |
|
265 |
|
266 | this.$updateFormValidity(name)
|
267 |
|
268 | }
|
269 | }
|
270 |
|
271 | }
|
272 |
|
273 | $getNamedValue (name) {
|
274 | const elements = this.$el.querySelectorAll(`[name=${name}]`)
|
275 | let value
|
276 | if (elements.length > 1) {
|
277 | for (const el of elements) {
|
278 | if (el.checked) {
|
279 | if (el.type === 'radio') {
|
280 | value = el.value
|
281 | break;
|
282 | } else if (el.type === 'checkbox') {
|
283 | if (value) {
|
284 | value.push(el.value)
|
285 | } else {
|
286 | value = [el.value]
|
287 | }
|
288 | }
|
289 | }
|
290 | }
|
291 | } else if (elements.length === 1) {
|
292 | value = elements[0].value
|
293 | }
|
294 | return value
|
295 | }
|
296 |
|
297 | }
|