UNPKG

9.96 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 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
46export 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 // v-form directive.
69 Vue.directive('form', (el, { value }) => {
70
71 if (value instanceof VueForm) {
72
73 // Setup the form object when the directive is first bound to the
74 // form element.
75 if (!value.$el) {
76 value.$el = el
77 value.$el.noValidate = value.$noValidate
78
79 // Pre-populate required fields with an empty object in case they are
80 // dynamically inserted.
81 value.$requiredFields.forEach(field => value[field.name || field] = {})
82
83 // Update the forms $wasSubmitted state and apply the appropriate CSS
84 // class when the forms submit event is triggered.
85 value.$el.addEventListener('submit', () => {
86 value.$wasSubmitted = true
87 value.$el.classList.add(value.$wasSubmittedClass)
88 })
89
90 // Update the form and child field state and remove any corresponding
91 // CSS classes when the forms reset event is triggered.
92 value.$el.addEventListener('reset', () => {
93 value.$wasSubmitted = false
94 value.$el.classList.remove(value.$wasSubmittedClass)
95
96 // Reset $wasFocused property and remove the corresponding class
97 // from each child node.
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 // Go through each field within the form, set up its state within
110 // the form object, and listen to input or change events to keep its
111 // state in sync.
112 for (const $el of el.querySelectorAll('input, textarea, select')) {
113
114 // Only work with elements that belong to the form, have the ability
115 // to be validated, and have and id or name property.
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 // Create the field object and extract its validity state.
124 const field = Object.assign({ $el }, extractValidity($el))
125 Vue.set(value, id, field)
126 value.$updateFormValidity(id)
127
128 // Add wasFocused class to element when focus event is triggered.
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 // On change or input events, update the field and form validity
140 // state.
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 * setCustomValidity - A wrapper for HTML5s setCustomValidity function so that
163 * the end user can trigger a custom error without an error message, the
164 * custom error message is accessible through the form object, and the overall
165 * form validity is updated.
166 *
167 * @param {string} field The identifier for the field you wish to
168 * set the validity for.
169 * @param {boolean|string} invalid Whether the field is invalid (true), or
170 * not (false), or the custom error message
171 * for an invalid field.
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 * updateFormValidity - Updates the overall validity of the form based on the
195 * existing validity state of its fields and the updated validity state of
196 * the given field.
197 *
198 * @param {string} field The identifier for the field whose validity state
199 * has updated and has consequently triggered the update
200 * of the overall forms validity.
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 * $isFieldRequired - Checks if a given named group has been manually
220 * designated as required through the VueForm constructor options.
221 *
222 * @param {string} name The name of the field to be checked.
223 *
224 * @returns {boolean} True if the field is required, false otherwise.
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 * $updateNamedValidity - For the use case of requiring a value for a set of
238 * checkboxes or radio buttons with the same name, VueForm provides the
239 * validity state of the overall group using the name as the identifier. This
240 * function updates this validity state.
241 *
242 * @param {HTMLElement} el The DOM element that may trigger an update to the
243 * validity of the named group.
244 * @param {Vue} Vue The Vue.js instance given when this plugin is
245 * installed.
246 */
247 $updateNamedValidity (el, Vue) {
248
249 // Check if the element has a name
250 if (el.hasAttribute('name')) {
251 const name = el.getAttribute('name')
252
253 // Check if the named group was marked as required.
254 if (this.$isFieldRequired(name)) {
255
256 // Set the validity state of the named group.
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 // Update the forms overall validity.
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}