-
Notifications
You must be signed in to change notification settings - Fork 0
/
forms.js
236 lines (186 loc) · 6.71 KB
/
forms.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
'use strict';
/**
* Utilities for Form components
* @class
*/
class Forms {
/**
* The Form constructor
* @param {Object} form The form DOM element
*/
constructor(form = false) {
this.FORM = form;
this.strings = Forms.strings;
this.submit = Forms.submit;
this.classes = Forms.classes;
this.markup = Forms.markup;
this.selectors = Forms.selectors;
this.attrs = Forms.attrs;
this.FORM.setAttribute('novalidate', true);
return this;
}
/**
* Map toggled checkbox values to an input.
* @param {Object} event The parent click event.
* @return {Element} The target element.
*/
joinValues(event) {
if (!event.target.matches('input[type="checkbox"]'))
return;
if (!event.target.closest('[data-js-join-values]'))
return;
let el = event.target.closest('[data-js-join-values]');
let target = document.querySelector(el.dataset.jsJoinValues);
target.value = Array.from(
el.querySelectorAll('input[type="checkbox"]')
)
.filter((e) => (e.value && e.checked))
.map((e) => e.value)
.join(', ');
return target;
}
/**
* A simple form validation class that uses native form validation. It will
* add appropriate form feedback for each input that is invalid and native
* localized browser messaging.
*
* See https://developer.mozilla.org/en-US/docs/Learn/HTML/Forms/Form_validation
* See https://caniuse.com/#feat=form-validation for support
*
* @param {Event} event The form submission event
* @return {Class/Boolean} The form class or false if invalid
*/
valid(event) {
let validity = event.target.checkValidity();
let elements = event.target.querySelectorAll(this.selectors.REQUIRED);
for (let i = 0; i < elements.length; i++) {
// Remove old messaging if it exists
let el = elements[i];
this.reset(el);
// If this input valid, skip messaging
if (el.validity.valid) continue;
this.highlight(el);
}
return (validity) ? this : validity;
}
/**
* Adds focus and blur events to inputs with required attributes
* @param {object} form Passing a form is possible, otherwise it will use
* the form passed to the constructor.
* @return {class} The form class
*/
watch(form = false) {
this.FORM = (form) ? form : this.FORM;
let elements = this.FORM.querySelectorAll(this.selectors.REQUIRED);
/** Watch Individual Inputs */
for (let i = 0; i < elements.length; i++) {
// Remove old messaging if it exists
let el = elements[i];
el.addEventListener('focus', () => {
this.reset(el);
});
el.addEventListener('blur', () => {
if (!el.validity.valid)
this.highlight(el);
});
}
/** Submit Event */
this.FORM.addEventListener('submit', (event) => {
event.preventDefault();
if (this.valid(event) === false)
return false;
this.submit(event);
});
return this;
}
/**
* Removes the validity message and classes from the message.
* @param {object} el The input element
* @return {class} The form class
*/
reset(el) {
let container = (this.selectors.ERROR_MESSAGE_PARENT)
? el.closest(this.selectors.ERROR_MESSAGE_PARENT) : el.parentNode;
let message = container.querySelector('.' + this.classes.ERROR_MESSAGE);
// Remove old messaging if it exists
container.classList.remove(this.classes.ERROR_CONTAINER);
if (message) message.remove();
// Remove error class from the form
container.closest('form').classList.remove(this.classes.ERROR_CONTAINER);
// Remove dynamic attributes from the input
el.removeAttribute(this.attrs.ERROR_INPUT[0]);
el.removeAttribute(this.attrs.ERROR_LABEL);
return this;
}
/**
* Displays a validity message to the user. It will first use any localized
* string passed to the class for required fields missing input. If the
* input is filled in but doesn't match the required pattern, it will use
* a localized string set for the specific input type. If one isn't provided
* it will use the default browser provided message.
* @param {object} el The invalid input element
* @return {class} The form class
*/
highlight(el) {
let container = (this.selectors.ERROR_MESSAGE_PARENT)
? el.closest(this.selectors.ERROR_MESSAGE_PARENT) : el.parentNode;
// Create the new error message.
let message = document.createElement(this.markup.ERROR_MESSAGE);
let id = `${el.getAttribute('id')}-${this.classes.ERROR_MESSAGE}`;
// Get the error message from localized strings (if set).
if (el.validity.valueMissing && this.strings.VALID_REQUIRED)
message.innerHTML = this.strings.VALID_REQUIRED;
else if (!el.validity.valid &&
this.strings[`VALID_${el.type.toUpperCase()}_INVALID`]) {
let stringKey = `VALID_${el.type.toUpperCase()}_INVALID`;
message.innerHTML = this.strings[stringKey];
} else
message.innerHTML = el.validationMessage;
// Set aria attributes and css classes to the message
message.setAttribute('id', id);
message.setAttribute(this.attrs.ERROR_MESSAGE[0],
this.attrs.ERROR_MESSAGE[1]);
message.classList.add(this.classes.ERROR_MESSAGE);
// Add the error class and error message to the dom.
container.classList.add(this.classes.ERROR_CONTAINER);
container.insertBefore(message, container.childNodes[0]);
// Add the error class to the form
container.closest('form').classList.add(this.classes.ERROR_CONTAINER);
// Add dynamic attributes to the input
el.setAttribute(this.attrs.ERROR_INPUT[0], this.attrs.ERROR_INPUT[1]);
el.setAttribute(this.attrs.ERROR_LABEL, id);
return this;
}
}
/**
* A dictionairy of strings in the format.
* {
* 'VALID_REQUIRED': 'This is required',
* 'VALID_{{ TYPE }}_INVALID': 'Invalid'
* }
*/
Forms.strings = {};
/** Placeholder for the submit function */
Forms.submit = function() {};
/** Classes for various containers */
Forms.classes = {
'ERROR_MESSAGE': 'error-message', // error class for the validity message
'ERROR_CONTAINER': 'error', // class for the validity message parent
'ERROR_FORM': 'error'
};
/** HTML tags and markup for various elements */
Forms.markup = {
'ERROR_MESSAGE': 'div',
};
/** DOM Selectors for various elements */
Forms.selectors = {
'REQUIRED': '[required="true"]', // Selector for required input elements
'ERROR_MESSAGE_PARENT': false
};
/** Attributes for various elements */
Forms.attrs = {
'ERROR_MESSAGE': ['aria-live', 'polite'], // Attribute for valid error message
'ERROR_INPUT': ['aria-invalid', 'true'],
'ERROR_LABEL': 'aria-describedby'
};
export default Forms;