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