1 |
|
2 | import * as defaultValidators from'./validators';
|
3 | import util from './util';
|
4 |
|
5 | export default (extraValidators = {}) => {
|
6 | const validators = {
|
7 | ...defaultValidators,
|
8 | ...extraValidators
|
9 | }
|
10 |
|
11 | const validationStyles = {
|
12 | afterBlur: 'after-blur',
|
13 | afterChange: 'after-change',
|
14 | afterSubmit: 'after-submit'
|
15 | }
|
16 |
|
17 | const validClass = 'field-validation-valid';
|
18 | let validatorGroup = null;
|
19 |
|
20 | return {
|
21 | name: 'vue-dotnet-validator',
|
22 | props: {
|
23 |
|
24 | value: {
|
25 | default: ''
|
26 | },
|
27 |
|
28 | extraErrorMessage: {
|
29 | default: ''
|
30 | },
|
31 | validationStyle: {
|
32 | default: validationStyles.afterBlur
|
33 | }
|
34 | },
|
35 | data() {
|
36 | return {
|
37 | validators: [],
|
38 | blurred: false,
|
39 | hasValidationError: false,
|
40 |
|
41 | localInputValue: this.value,
|
42 | isTwoWayBind: false,
|
43 | hasChanged: false,
|
44 | hasForced: false,
|
45 | hasBlurred: false,
|
46 | name: '',
|
47 | field: null
|
48 | };
|
49 | },
|
50 | updated() {
|
51 | this.initialize();
|
52 | },
|
53 | mounted() {
|
54 | validatorGroup = util.findValidatorGroup(this);
|
55 | validatorGroup.addValidator(this);
|
56 | this.initialize();
|
57 | },
|
58 | destroyed() {
|
59 | this.$nextTick(() => {
|
60 | validatorGroup.removeValidator(this);
|
61 | });
|
62 | },
|
63 | methods: {
|
64 | initialize() {
|
65 |
|
66 | if (this.field) {
|
67 | return;
|
68 | }
|
69 |
|
70 | this.field = this.resolveField(this);
|
71 |
|
72 | if(!this.field) {
|
73 | if (process.env.NODE_ENV !== 'production') {
|
74 | console.warn('Field is missing. This could be an error or it will resolve if the input is mounted async.', this.$el);
|
75 | }
|
76 | return;
|
77 | }
|
78 |
|
79 | this.name = this.field.name;
|
80 |
|
81 |
|
82 |
|
83 |
|
84 | this.isTwoWayBind = this.$options._parentListeners && !!this.$options._parentListeners.input;
|
85 |
|
86 | this.findValidators();
|
87 | this.addAriaDescribedBy();
|
88 |
|
89 | if(this.$refs.message.innerText) {
|
90 |
|
91 |
|
92 | this.hasValidationError = true;
|
93 | this.blurred = true;
|
94 | } else {
|
95 | this.$refs.message.classList.add(validClass);
|
96 | }
|
97 |
|
98 | if(!this.isCheckbox && !this.isRadio) {
|
99 | this.field.addEventListener('blur', this.blurField);
|
100 | }
|
101 | this.field.addEventListener('change', this.changeField);
|
102 | this.field.addEventListener('input', this.changeField);
|
103 | },
|
104 | resolveField(component) {
|
105 | if(!component) {
|
106 | return null;
|
107 | }
|
108 |
|
109 | if(component.$refs.field) {
|
110 | return component.$refs.field;
|
111 | }
|
112 |
|
113 | if(component.$children.length > 0) {
|
114 | return component.$children.map(child => this.resolveField(child)).filter(result => !!result)[0];
|
115 | }
|
116 |
|
117 | return null;
|
118 | },
|
119 | blurField(event) {
|
120 | if(event && event.target.value !== '') {
|
121 | this.val = event.target.value;
|
122 | }
|
123 | this.blurred = true;
|
124 | this.hasBlurred = true;
|
125 | this.$emit('blur-field', this);
|
126 | this.showValidationMessage(false);
|
127 | },
|
128 | changeField(event) {
|
129 | if(event) {
|
130 | if(this.isCheckbox || this.isRadio) {
|
131 | this.blurred = true;
|
132 | this.val = this.isCheckbox
|
133 | ? event.target.checked
|
134 | : event.target.checked ? event.target.value : '';
|
135 | } else {
|
136 | this.val = event.target.value;
|
137 | }
|
138 | }
|
139 | this.hasChanged = true;
|
140 | this.$emit('change-field', this);
|
141 | this.showValidationMessage(false);
|
142 | },
|
143 |
|
144 | findValidators() {
|
145 | const dataAttributes = this.field.dataset;
|
146 | const validatorKeys = Object.keys(validators);
|
147 |
|
148 | validatorKeys.forEach(validatorKey => {
|
149 | const sanitzedKey = validatorKey.charAt(0).toUpperCase() + validatorKey.slice(1).toLowerCase();
|
150 | const validationMessage = dataAttributes['val' + sanitzedKey];
|
151 |
|
152 | if(!validationMessage) {
|
153 |
|
154 | return;
|
155 | }
|
156 | this.validators.push(new validators[validatorKey](validationMessage, dataAttributes, validatorGroup));
|
157 | });
|
158 | },
|
159 | showValidationMessage(forced = false) {
|
160 | if (!forced && !this.shouldValidate) {
|
161 | return;
|
162 | }
|
163 |
|
164 | if (!this.$refs.message) {
|
165 | return;
|
166 | }
|
167 |
|
168 | this.$refs.message.innerHTML = this.validationMessage;
|
169 |
|
170 | if(this.validationMessage) {
|
171 | this.hasValidationError = true;
|
172 | return this.$refs.message.classList.remove(validClass);
|
173 | }
|
174 | this.hasValidationError = false;
|
175 | return this.$refs.message.classList.add(validClass);
|
176 | },
|
177 | addAriaDescribedBy() {
|
178 |
|
179 |
|
180 | const id = `vue-validator-${parseInt(Math.random()*100)}-${this._uid}`;
|
181 | this.$refs.message.id = id;
|
182 | this.field.setAttribute('aria-describedby', id);
|
183 | this.$refs.message.setAttribute('role', 'alert');
|
184 | }
|
185 | },
|
186 | computed: {
|
187 | shouldValidate() {
|
188 | if (!this.field && this._isMounted) {
|
189 | console.warn('Tring to validate without a field');
|
190 | }
|
191 |
|
192 | if (this.validationStyle === validationStyles.afterBlur && !this.hasBlurred) {
|
193 | return false;
|
194 | }
|
195 |
|
196 | if (this.validationStyle === validationStyles.afterSubmit && !this.hasForced) {
|
197 | return false;
|
198 | }
|
199 |
|
200 | if (this.validationStyle === validationStyles.afterChange && !this.hasChanged) {
|
201 | return false;
|
202 | }
|
203 |
|
204 | return true;
|
205 | },
|
206 | isValid() {
|
207 | if (!this.field) {
|
208 | return false;
|
209 | }
|
210 |
|
211 | return this.validators.filter(validator => {
|
212 | return validator.isValid(this.val);
|
213 | }).length === this.validators.length && !this.extraErrorMessage && !this.hasValidationError;
|
214 | },
|
215 |
|
216 | validationMessage() {
|
217 | if (!this.field) {
|
218 | return 'The field was not ready yet, please try again.';
|
219 | }
|
220 |
|
221 | let message = '';
|
222 | this.validators.forEach(validator => {
|
223 | const valid = validator.isValid(this.val);
|
224 | if(!valid && !message) {
|
225 | message = validator.getMessage();
|
226 | }
|
227 | });
|
228 |
|
229 | return message || this.extraErrorMessage;
|
230 | },
|
231 |
|
232 | val: {
|
233 | get() {
|
234 | if(this.isTwoWayBind) {
|
235 | return this.value;
|
236 | }
|
237 | return this.localInputValue;
|
238 | },
|
239 | set(value) {
|
240 | this.hasChanged = true;
|
241 |
|
242 | if(this.isTwoWayBind) {
|
243 |
|
244 | return this.$emit('input', value);
|
245 | }
|
246 | return this.localInputValue = value;
|
247 | }
|
248 | },
|
249 | isCheckbox() {
|
250 | return this.field && this.field.type == 'checkbox';
|
251 | },
|
252 | isRadio() {
|
253 | return this.field && this.field.type == 'radio';
|
254 | }
|
255 | },
|
256 | watch: {
|
257 | isValid() {
|
258 | if(this.field && this.shouldValidate) {
|
259 | this.field.setAttribute('aria-invalid', !this.isValid);
|
260 | }
|
261 | },
|
262 | validationMessage() {
|
263 | this.showValidationMessage();
|
264 | }
|
265 | }
|
266 | }
|
267 | };
|