UNPKG

8.27 kBJavaScriptView Raw
1/* global process */
2import * as defaultValidators from'./validators';
3import util from './util';
4
5export default (extraValidators = {}) => {
6 const validators = {
7 ...defaultValidators,
8 ...extraValidators
9 }
10
11 const validationStyles = {
12 afterBlur: 'after-blur', // default
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 // Value is the value that will be validated
24 value: {
25 default: ''
26 },
27 // This parameter can be used to provide additional complex validation from your app
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 // This variable is used to store the current value if not in two-way mode.
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 // already initialized
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 // We need to know if 2-way binding is being used so we know where to store the adjusted value.
82 // This check is a little bit dirty, but the only thing that works.
83 // Since vue handles 2-way binding through the 'input' event, we can check if there is something listening to it.
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 // When we already have innerText, it means the server has output a validation error.
91 // We need to replace that validation message as soon as the user changes the value of the input
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; // We are not using blur-event on checkbox, so lets force blurred here.
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 // Initializes custom validators by looking at the attributes in the DOM.
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 // Validator should not be activated
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 // Make kind of sure that the id does not exist yet.
179 // No need to force this kind of stuff, in almost any case this will be enough.
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 // Returns the error-message
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 // This is the internally used value
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 // Two-way binding requires to emit an event in vue 2.x
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};