Source RapidContext_Widget_FormValidator.js

1/*
2 * RapidContext <https://www.rapidcontext.com/>
3 * Copyright (c) 2007-2024 Per Cederberg. All rights reserved.
4 *
5 * This program is free software: you can redistribute it and/or
6 * modify it under the terms of the BSD license.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
11 *
12 * See the RapidContext LICENSE for more details.
13 */
14
15// Namespace initialization
16if (typeof(RapidContext) == "undefined") {
17    RapidContext = {};
18}
19RapidContext.Widget = RapidContext.Widget || { Classes: {} };
20
21/**
22 * Creates a new form validator widget.
23 *
24 * @constructor
25 * @param {Object} attrs the widget and node attributes
26 * @param {string} attrs.name the form field name to validate
27 * @param {boolean} [attrs.mandatory] the mandatory field flag,
28 *            defaults to `true`
29 * @param {string|RegExp} [attrs.regex] the regular expression to
30 *            match the field value against, defaults to `null`
31 * @param {string} [attrs.display] the validator display setting
32 *            (either "none", "icon", "text" or "both"), defaults
33 *            to "both"
34 * @param {string} [attrs.message] the message to display, defaults
35 *            to the validator function error message
36 * @param {function} [attrs.validator] the validator function
37 * @param {boolean} [attrs.hidden] the hidden widget flag, defaults to `false`
38 *
39 * @return {Widget} the widget DOM node
40 *
41 * @class The form validator widget class. Provides visual feedback on form
42 *     validation failures, using a `<span>` HTML element. It is normally
43 *     hidden by default and may be configured to only modify its related form
44 *     field.
45 * @property {string} name The form field name to validate.
46 * @property {string} message The default validation message.
47 * @property {function} validator The validator function in use.
48 * @extends RapidContext.Widget
49 *
50 * @example <caption>JavaScript</caption>
51 * let field = RapidContext.Widget.TextField({ name: "name", placeholder: "Your Name Here" });
52 * let attrs = { name: "name", message: "Please enter your name to proceed." };
53 * let valid = RapidContext.Widget.FormValidator(attrs);
54 * let exampleForm = RapidContext.Widget.Form({}, field, valid);
55 *
56 * @example <caption>User Interface XML</caption>
57 * <Form id="exampleForm">
58 *   <TextField name="name" placeholder="Your Name Here" />
59 *   <FormValidator name="name" message="Please enter your name to proceed." />
60 * </Form>
61 */
62RapidContext.Widget.FormValidator = function (attrs) {
63    let o = document.createElement("span");
64    RapidContext.Widget._widgetMixin(o, RapidContext.Widget.FormValidator);
65    o.addClass("widgetFormValidator");
66    let defaults = { name: "", mandatory: true, display: "both", message: null, validator: null };
67    o.setAttrs(Object.assign(defaults, attrs));
68    o.fields = [];
69    o.hide();
70    return o;
71};
72
73// Register widget class
74RapidContext.Widget.Classes.FormValidator = RapidContext.Widget.FormValidator;
75
76/**
77 * Updates the widget or HTML DOM node attributes.
78 *
79 * @param {Object} attrs the widget and node attributes to set
80 * @param {string} [attrs.name] the form field name to validate
81 * @param {boolean} [attrs.mandatory] the mandatory field flag
82 * @param {string|RegExp} [attrs.regex] the regular expression to
83 *            match the field value against
84 * @param {string} [attrs.display] the validator display setting
85 *            (either "none", "icon", "text" or "both")
86 * @param {string} [attrs.message] the message to display
87 * @param {function} [attrs.validator] the validator function
88 * @param {boolean} [attrs.hidden] the hidden widget flag
89 */
90RapidContext.Widget.FormValidator.prototype.setAttrs = function (attrs) {
91    attrs = Object.assign({}, attrs);
92    if ("mandatory" in attrs) {
93        attrs.mandatory = RapidContext.Data.bool(attrs.mandatory);
94    }
95    if ("regex" in attrs && attrs.regex && !(attrs.regex instanceof RegExp)) {
96        if (!attrs.regex.startsWith("^")) {
97            attrs.regex = "^" + attrs.regex;
98        }
99        if (!attrs.regex.endsWith("$")) {
100            attrs.regex += "$";
101        }
102        attrs.regex = new RegExp(attrs.regex);
103    }
104    if ("validator" in attrs) {
105        let valid = typeof(attrs.validator) == "function";
106        attrs.validator = valid ? attrs.validator : null;
107    }
108    this.__setAttrs(attrs);
109};
110
111/**
112 * Resets this form validator. This will hide any error messages and mark all
113 * invalidated fields as valid.
114 *
115 * Note that this method is normally not called directly, instead the
116 * validation is reset by the `RapidContext.Widget.Form` widget.
117 *
118 * @see RapidContext.Widget.Form#validateReset
119 */
120RapidContext.Widget.FormValidator.prototype.reset = function () {
121    this.fields.forEach(function (field) {
122        field.classList.remove("invalid");
123    });
124    this.fields = [];
125    this.hide();
126    this.removeAll();
127};
128
129/**
130 * Verifies a form field with this validator. If the form field value doesn't
131 * match this validator, the field will be invalidated until this validator is
132 * reset.
133 *
134 * Note that this method is normally not called directly, instead the
135 * validation is performed by the `RapidContext.Widget.Form` widget.
136 *
137 * @param {Widget|Node} field the form field DOM node
138 * @param {string} [value] the form field value to check
139 *
140 * @return {boolean} `true` if the form validated successfully, or
141 *         `false` if the validation failed
142 *
143 * @see RapidContext.Widget.Form#validate
144 */
145RapidContext.Widget.FormValidator.prototype.verify = function (field, value) {
146    if (!field.disabled) {
147        if (arguments.length == 1 && typeof(field.getValue) == "function") {
148            value = field.getValue();
149        } else if (arguments.length == 1) {
150            value = field.value;
151        }
152        let str = String(value).trim();
153        if (field.validationMessage) {
154            this.addError(field, field.validationMessage);
155            return false;
156        } else if (this.mandatory && str == "") {
157            this.addError(field, "This field is required");
158            return false;
159        } else if (this.regex && str && !this.regex.test(str)) {
160            this.addError(field, "The field format is incorrect");
161            return false;
162        } else if (typeof(this.validator) == "function") {
163            let res = this.validator(value);
164            if (res !== true) {
165                this.addError(field, res || "Field validation failed");
166                return false;
167            }
168        }
169    }
170    return true;
171};
172
173/**
174 * Adds a validation error message for the specified field. If the field is
175 * already invalid, this method will not do anything.
176 *
177 * Note that this method is normally not called directly, instead the
178 * validation is performed by the `RapidContext.Widget.Form` widget.
179 *
180 * @param {Widget|Node} field the field DOM node
181 * @param {string} message the validation error message
182 *
183 * @see RapidContext.Widget.Form#validate
184 */
185RapidContext.Widget.FormValidator.prototype.addError = function (field, message) {
186    if (!field.classList.contains("invalid")) {
187        this.fields.push(field);
188        field.classList.add("invalid");
189        if (this.display !== "none") {
190            message = this.message || message;
191            let span = null;
192            let icon = null;
193            if (!this.display || this.display === "both") {
194                this.addClass("block");
195            }
196            if (this.display !== "icon") {
197                span = document.createElement("span");
198                span.append(message);
199            }
200            if (this.display !== "text") {
201                icon = RapidContext.Widget.Icon({ ref: "ERROR", tooltip: message });
202            }
203            if (!this.childNodes.length) {
204                this.addAll(icon, span);
205            }
206            this.show();
207        }
208    }
209};
210