UNPKG

10.2 kBJavaScriptView Raw
1import 'babel-polyfill';
2
3/**
4 * extractValidity - Extracts the ValidityState information from a given
5 * object into an object suitable for manipulation.
6 *
7 * @param {HTMLElement} el A DOM element containing a ValidityState object.
8 * @return {object} A non-read-only object mimicing the ValidityState
9 * object for the given element.
10 */
11function extractValidity(el) {
12 const validity = el.validity
13
14 // VaidityState.tooShort/minlength polyfill for older browsers.
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
45export 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 // v-form directive.
68 Vue.directive('form', (el, { value }) => {
69
70 if (value instanceof VueForm) {
71
72 // Setup the form object when the directive is first bound to the
73 // form element.
74 if (!value.$el) {
75 value.$el = el
76 value.$el.noValidate = value.$noValidate
77
78 // Pre-populate required fields with an empty object in case they are
79 // dynamically inserted.
80 value.$requiredFields.forEach(field => value[field.name || field] = {})
81
82 // Update the forms $wasSubmitted state and apply the appropriate CSS
83 // class when the forms submit event is triggered.
84 value.$el.addEventListener('submit', () => {
85 value.$wasSubmitted = true
86 value.$el.classList.add(value.$wasSubmittedClass)
87 })
88
89 // Update the form and child field state and remove any corresponding
90 // CSS classes when the forms reset event is triggered.
91 value.$el.addEventListener('reset', () => {
92 value.$wasSubmitted = false
93 value.$el.classList.remove(value.$wasSubmittedClass)
94
95 // Reset $wasFocused property and remove the corresponding class
96 // from each child node.
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 // Go through each field within the form, set up its state within
109 // the form object, and listen to input or change events to keep its
110 // state in sync.
111 for (const $el of el.querySelectorAll('input, textarea, select')) {
112
113 // Only work with elements that belong to the form, have the ability
114 // to be validated, and have and id or name property.
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 // Create the field object and extract its validity state.
123 const field = Object.assign({ $el }, extractValidity($el))
124 Vue.set(value, id, field)
125 value.$updateFormValidity(id)
126
127 // Add wasFocused class to element when focus event is triggered.
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 // On change or input events, update the field and form validity
139 // state.
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 * setCustomValidity - A wrapper for HTML5s setCustomValidity function so that
162 * the end user can trigger a custom error without an error message, the
163 * custom error message is accessible through the form object, and the overall
164 * form validity is updated.
165 *
166 * @param {string} field The identifier for the field you wish to
167 * set the validity for.
168 * @param {boolean|string} invalid Whether the field is invalid (true), or
169 * not (false), or the custom error message
170 * for an invalid field.
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 * updateFormValidity - Updates the overall validity of the form based on the
199 * existing validity state of its fields and the updated validity state of
200 * the given field.
201 *
202 * @param {string} field The identifier for the field whose validity state
203 * has updated and has consequently triggered the update
204 * of the overall forms validity.
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 * $isFieldRequired - Checks if a given named group has been manually
224 * designated as required through the VueForm constructor options.
225 *
226 * @param {string} name The name of the field to be checked.
227 *
228 * @returns {boolean} True if the field is required, false otherwise.
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 * $updateNamedValidity - For the use case of requiring a value for a set of
242 * checkboxes or radio buttons with the same name, VueForm provides the
243 * validity state of the overall group using the name as the identifier. This
244 * function updates this validity state.
245 *
246 * @param {HTMLElement} el The DOM element that may trigger an update to the
247 * validity of the named group.
248 * @param {Vue} Vue The Vue.js instance given when this plugin is
249 * installed.
250 */
251 $updateNamedValidity (el, Vue) {
252
253 // Check if the element has a name
254 if (el.hasAttribute('name')) {
255 const name = el.getAttribute('name')
256
257 // Check if the named group was marked as required.
258 if (this.$isFieldRequired(name)) {
259
260 // Set the validity state of the named group.
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 // Update the forms overall validity.
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}