1/*
2 * RapidContext <https://www.rapidcontext.com/>
3 * Copyright (c) 2007-2025 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 const o = document.createElement("span");
64 RapidContext.Widget._widgetMixin(o, RapidContext.Widget.FormValidator);
65 o.addClass("widgetFormValidator");
66 const 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 = { ...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 const 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((f) => f.classList.remove("invalid"));
122 this.fields = [];
123 this.hide();
124 this.removeAll();
125};
126
127/**
128 * Verifies a form field with this validator. If the form field value doesn't
129 * match this validator, the field will be invalidated until this validator is
130 * reset.
131 *
132 * Note that this method is normally not called directly, instead the
133 * validation is performed by the `RapidContext.Widget.Form` widget.
134 *
135 * @param {Widget|Node} field the form field DOM node
136 * @param {string} [value] the form field value to check
137 *
138 * @return {boolean} `true` if the form validated successfully, or
139 * `false` if the validation failed
140 *
141 * @see RapidContext.Widget.Form#validate
142 */
143RapidContext.Widget.FormValidator.prototype.verify = function (field, value) {
144 if (!field.disabled) {
145 if (arguments.length == 1 && typeof(field.getValue) == "function") {
146 value = field.getValue();
147 } else if (arguments.length == 1) {
148 value = field.value;
149 }
150 const str = String(value).trim();
151 if (field.validationMessage) {
152 this.addError(field, field.validationMessage);
153 return false;
154 } else if (this.mandatory && str == "") {
155 this.addError(field, "This field is required");
156 return false;
157 } else if (this.regex && str && !this.regex.test(str)) {
158 this.addError(field, "The field format is incorrect");
159 return false;
160 } else if (typeof(this.validator) == "function") {
161 const res = this.validator(value);
162 if (res !== true) {
163 this.addError(field, res || "Field validation failed");
164 return false;
165 }
166 }
167 }
168 return true;
169};
170
171/**
172 * Adds a validation error message for the specified field. If the field is
173 * already invalid, this method will not do anything.
174 *
175 * Note that this method is normally not called directly, instead the
176 * validation is performed by the `RapidContext.Widget.Form` widget.
177 *
178 * @param {Widget|Node} field the field DOM node
179 * @param {string} message the validation error message
180 *
181 * @see RapidContext.Widget.Form#validate
182 */
183RapidContext.Widget.FormValidator.prototype.addError = function (field, message) {
184 if (!field.classList.contains("invalid")) {
185 this.fields.push(field);
186 field.classList.add("invalid");
187 if (this.display !== "none") {
188 message = this.message || message;
189 let span = null;
190 let icon = null;
191 if (!this.display || this.display === "both") {
192 this.addClass("block");
193 }
194 if (this.display !== "icon") {
195 span = document.createElement("span");
196 span.append(message);
197 }
198 if (this.display !== "text") {
199 icon = RapidContext.Widget.Icon({ ref: "ERROR", tooltip: message });
200 }
201 if (!this.childNodes.length) {
202 this.addAll(icon, span);
203 }
204 this.show();
205 }
206 }
207};
208
RapidContext
Access · Discovery · Insight
www.rapidcontext.com