Detect fields, validate with callbacks, restore No/Yes controls, and set date values. Source: resources/form_manager.js (no version declaration).
FormManager scans a managed form for <input>, <textarea>,
<select>, and .fm-no-yes controls, creates
FieldManager objects, and validates based on CSS classes and native required attributes.
name (from form.name, or auto) and fields (array).valid_func, incomplete_func, invalid_func, and additional_validation_func.Load the hosted script as a classic script before DOMContentLoaded. It has no JavaScript dependencies. Bootstrap CSS supplies the default No/Yes button appearance, or you can style the generated classes yourself.
<script src="https://update.pmblue.us/resources/form_manager/js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const form = FormManager.get_form('demo_form');
if (form) form.check();
});
</script>
The library scans .fm-managed forms once on DOM ready, attaches change/click listeners, then sets global FORM_MANAGER_LOADED = true. It does not initially call check(). Load your initialization callback after the library's callback, and explicitly validate after applying initial values.
// Automatic (recommended): put class="fm-managed" on your <form>.
// The library auto-initializes on DOMContentLoaded.
// Manual: use a form that has not already been initialized.
const fm = new FormManager(document.querySelector('#dynamic-form'));
fm.fields.forEach(initialize_events);
fm.check();
The constructor creates field managers but does not attach their validation listeners. Call initialize_events(field) for each field when initializing manually, including when loading the script after DOM ready. Avoid constructing a second manager for an automatically initialized form.
| Property | Type | Description |
|---|---|---|
name | string | From form.name; the fallback form_1 is used only when that property is null/undefined. An unnamed HTML form usually supplies an empty string, so give each form a unique name. |
fields | FieldManager[] | Managed field objects for the form. |
form | HTMLFormElement | The underlying form DOM element. |
additional_validation_func | function | Final whole-form check; receives the manager and must return true to pass. |
| Type | Method | Signature | Description |
|---|---|---|---|
| Instance | get_field |
get_field(fieldName): FieldManager|null |
Find a field by its name. |
| Instance | check |
check(except_field = null): boolean|undefined |
Validates in field order, skipping a field with the same name as except_field; stops at the first handled failure. Calls callbacks described below. Returns true on success only when valid_func is present. |
| Instance | add_field |
add_field(element): FieldManager |
Adds a dynamically inserted control and attaches its validation events. |
| Static | get_form |
FormManager.get_form(formName): FormManager|null |
Returns the FormManager for a given form.name, or null if not found. |
| Static | forms |
FormManager.forms: FormManager[] |
Array of all instantiated managers, appended by every constructor call. No automatic removal or deduplication is provided. |
// Preferred lookup:
const fm = FormManager.get_form('enroll_form');
// Legacy/indexed:
const fallback = FormManager.forms[0];
Each discovered control becomes a FieldManager. Its classes determine type and rules.
| Class | Apply To | Behavior |
|---|---|---|
| No type class | <input>, <textarea>, <select> | Generic managed field; fm-field is not required by the current scanner. |
fm-phone | <input> | Phone number field/validation. |
fm-email | <input> | Email field/validation. |
fm-ssn | <input> | SSN field/validation. |
fm-number | <input> | Validates and formats a comma-grouped whole number. |
fm-street-address | <input> | Requires a non-empty street address to begin with a house number. |
fm-no-yes |
Container element (e.g., <div>) with its own name and id |
Renders a Bootstrap-style No / Yes button group inside the container and
creates a hidden <input> with the same name and id as the parent container.
The hidden input’s value becomes "No" or "Yes" based on the user’s selection.
|
fm-required or required | Any managed field/container | Must be non-empty for the manager’s required check to pass; see submission behavior below. |
| Member | Type / Signature | Description |
|---|---|---|
name | string | From the underlying element’s name, or the No/Yes container ID. |
field / form | DOM element / FormManager | The value-bearing element (hidden input for No/Yes) and its owning manager. |
type | string|undefined | Semantic type detected from CSS classes at construction. |
required | boolean | true if it has fm-required or the native required property. |
validator | function | Optional validator receiving the underlying DOM element (see below). |
validate() | () => true | false | string | Runs only the field validator if present. Required-field and whole-form checks belong to FormManager.check(). |
get_value() | () => string | Returns current value. |
set_value(value) | (any) => string | Sets the value, synchronizes No/Yes UI, and formats Date/timestamp values for date inputs. |
get_type() | () => string|undefined | Returns the detected semantic type. |
| Class | Use On | Effect |
|---|---|---|
| No type class | <input>, <textarea>, <select> | Managed automatically without special validation. |
fm-phone | <input> | Validates as phone. |
fm-email | <input> | Validates as email. |
fm-ssn | <input> | Validates as SSN. |
fm-number | <input> | Accepts digits and commas and rewrites grouping separators. |
fm-street-address | <input> | Requires the value to start with a digit. |
fm-no-yes | Container with name and id | Creates a hidden input (same name/id) with value "No" or "Yes"; renders No/Yes buttons. |
fm-required or native required | Any managed field | Requires a non-empty value when check() runs. |
| Callback | Signature | When it runs | Typical use |
|---|---|---|---|
valid_func | (formManager) | All fields pass validation. | Enable submit; clear messages. |
incomplete_func | (nextField) | A required field is empty. | Disable submit; show “required” message. |
invalid_func | (invalidField, errCode) | A field validator failed. | Disable submit; show specific error. |
additional_validation_func | (formManager) => true|other | After every field passes. | Cross-field checks such as matching confirmation values. |
Assign a validator function to any field:
// Return values:
// true ? valid
// false ? invalid (unknown reason)
// "msg" ? invalid (specific reason)
const ssn = FormManager.get_form('demo_form').get_field('ssn');
ssn.validator = (input) => {
const digits = (input.value || '').replace(/\D+/g,'');
return /^(?!000|666|9\d\d)\d{3}(?!00)\d{2}(?!0000)\d{4}$/.test(digits)
? true
: 'Enter a valid SSN.';
};
<form class="fm-managed" name="demo_form">
<div class="mb-3">
<label class="form-label">Email</label>
<input name="email" class="form-control fm-required fm-email">
<div class="invalid-feedback"></div>
</div>
<div class="mb-3">
<label class="form-label">Phone</label>
<input name="phone" class="form-control fm-required fm-phone">
<div class="invalid-feedback"></div>
</div>
<div class="mb-3">
<label class="form-label">SSN</label>
<input name="ssn" class="form-control fm-ssn fm-required">
<div class="invalid-feedback"></div>
</div>
<div class="mb-3">
<label class="form-label">Do you agree?</label>
<div class="fm-no-yes fm-required" name="agree" id="agree"></div>
<small class="text-muted">This control creates a hidden input with the same name and id; value will be "No" or "Yes".</small>
<div class="invalid-feedback"></div>
</div>
<button type="submit" class="btn btn-primary" disabled>Submit</button>
</form>
get_formdocument.addEventListener('DOMContentLoaded', () => {
const fm = FormManager.get_form('demo_form');
if (!fm) return;
fm.valid_func = (form) => {
form.fields.forEach(clearError);
const submit = form.form.querySelector('[type=submit]');
if (submit) submit.disabled = false;
};
fm.incomplete_func = (field) => showError(field, 'This field is required.');
fm.invalid_func = (field, msg) => showError(field, msg || 'Invalid value.');
// Initialization does not validate until requested.
fm.check();
fm.form.addEventListener('submit', (event) => {
if (fm.check() !== true) event.preventDefault();
});
function showError(field, message) {
const el = field.get_type() === 'no-yes'
? field.field.parentElement : field.field;
el?.classList?.add('is-invalid');
const fb = el?.nextElementSibling;
if (fb && fb.classList.contains('invalid-feedback')) fb.textContent = message;
const submit = field.form.form.querySelector('[type=submit]');
if (submit) submit.disabled = true;
}
function clearError(field) {
const el = field.get_type() === 'no-yes'
? field.field.parentElement : field.field;
el?.classList?.remove('is-invalid');
const fb = el?.nextElementSibling;
if (fb && fb.classList.contains('invalid-feedback')) fb.textContent = '';
}
});
set_value() returns the stored DOM value. A No/Yes field accepts the exact strings "Yes" and "No", dispatches fm_set_value, and updates active button classes. The container is renamed with an _div suffix while its hidden input receives the original ID and name.
const fm = FormManager.get_form('demo_form');
fm.get_field('agree').set_value('Yes');
// For an input with type="date": numbers are milliseconds since the epoch.
const start = fm.get_field('start_date');
if (start) {
start.set_value(new Date(2026, 8, 20)); // local calendar date
start.set_value('2026-09-20'); // native date-input string
}
fm.check();
For date inputs, Date objects and numeric timestamps are converted with local getFullYear(), getMonth(), and getDate() into YYYY-MM-DD. Other inputs receive the value directly. Setting a value does not trigger the form’s validation change handler; call check() explicitly.
For new No/Yes containers, data-value="Yes" or data-value="1" initializes Yes; any other non-empty data-value initializes No. With no value the hidden input starts empty. Required No/Yes fields consider both Yes and No complete; require consent explicitly with a custom validator when only Yes is allowed.
Existing snapshot markup is reused when the container already contains input.fm-ny-input, .fm-no, and .fm-yes. The saved input value is preserved and an existing _div suffix is normalized. Initial snapshot construction does not synchronize button active classes; call set_value(field.get_value()) on the field after restoring to synchronize its UI.
const agree = fm.get_field('agree');
agree.set_value(agree.get_value());
agree.validator = (input) => input.value === 'Yes'
? true : 'Please select Yes to consent.';
const extra = document.createElement('input');
extra.name = 'alternate_email';
extra.className = 'fm-email';
fm.form.appendChild(extra);
fm.add_field(extra); // constructs the manager and attaches change handling
fm.check();
Each FormManager wraps one form element in form.form and holds its field managers in form.fields. Each FieldManager points back to the owning manager through field.form and to its value-bearing DOM element through field.field. For a No/Yes control that element is the generated or restored hidden input, not the visible container.
| Object / function | Arguments and return | What it changes |
|---|---|---|
new FormManager(element) | Synchronous constructor; expects a form DOM element. | Registers itself in FormManager.forms, scans inputs/textarea/select/No-Yes controls, constructs fields, and sets default callbacks. It does not attach ordinary field events or initially validate; the DOM-ready initializer does the event step separately. |
FormManager.get_form(name) | Returns the first matching manager or null. | Lookup only. Names must be unique for unambiguous lookup; the registry is not keyed by ID and does not deduplicate repeated construction. |
form.get_field(name) | Returns the first matching FieldManager or null. | Lookup only; uses the manager's stored name, not a CSS selector. |
form.add_field(element) | Returns the new FieldManager. | Creates the field, appends to fields, and calls initialize_events(). It does not insert the element into the DOM or check the whole form. |
form.check(except_field=null) | Synchronous; normally true/false, or undefined if valid_func is null on success. | Checks required values and validators in order; may format values through validators. Calls the first matching failure callback, or whole-form validation then the valid callback. The skipped field is matched by name. |
new FieldManager(element, form) | Synchronous constructor. | Sets name, type, required, and default validator; may build or restore No/Yes markup. It does not automatically append itself to the parent fields list. Prefer form.add_field() for normal additions. |
field.get_value() / field.set_value(value) | Return the stored DOM value, usually a string. | Get reads only. Set assigns the value; No/Yes additionally dispatches fm_set_value, and date inputs format Date/numeric millisecond values. Set does not run full validation. |
field.validate() | Returns true, false, or a validator-provided message/value. | Calls the validator with field.field. May format the DOM value. It does not check required state, invoke form callbacks, or call additional validation. |
field.get_type() | Returns the semantic type string or undefined. | Uses the stored type, with fallback detection when unset. Native type="email" alone does not select fm-email validation. |
initialize_events(field) | Global synchronous helper; returns undefined. | Adds change listeners or No/Yes button listeners. Call once per field; repeated calls add duplicate listeners. add_field() and automatic DOM initialization already call it. |
All validators and callbacks are synchronous. Return exactly true for success; a Promise from an async function is not awaited and will be treated as failure. Validators receive DOM elements; form failure callbacks receive FieldManager objects; valid_func and additional_validation_func receive the FormManager.
This runs after initialization of a form named signup containing fields named email and confirm_email. It adds a message element, makes both fields required, and defines the UI behavior for every validation path.
const signup = FormManager.get_form('signup');
if (signup) {
const email = signup.get_field('email');
const confirmation = signup.get_field('confirm_email');
const message = document.createElement('p');
signup.form.appendChild(message);
const submit = signup.form.querySelector('[type="submit"]');
const report = (text, valid) => {
message.textContent = text;
if (submit) submit.disabled = !valid;
};
email.required = true;
confirmation.required = true;
signup.incomplete_func = field => report(`${field.name} is required`, false);
signup.invalid_func = (field, error) => report(error || `${field.name} is invalid`, false);
signup.additional_validation_func = manager => {
if (manager.get_field('email').get_value() !== confirmation.get_value()) {
report('Email addresses must match', false);
return false;
}
return true;
};
signup.valid_func = () => report('', true);
signup.form.addEventListener('submit', event => {
if (signup.check() !== true) event.preventDefault();
});
signup.check();
}This example assumes a managed form named profile with ordinary single-value controls, including a No/Yes field contact_allowed and date input review_date. It skips missing saved fields safely. Each set_value() updates the object and its DOM element before one final check.
const profile = FormManager.get_form('profile');
const saved = {display_name: 'Taylor', contact_allowed: 'No', review_date: '2026-09-20'};
if (profile) {
for (const [name, value] of Object.entries(saved)) {
const field = profile.get_field(name);
if (field) field.set_value(value);
}
profile.check();
profile.form.addEventListener('submit', event => {
event.preventDefault();
if (profile.check() !== true) return;
const payload = {};
for (const field of profile.fields) {
if (field.name) payload[field.name] = field.get_value();
}
console.log(JSON.stringify(payload));
});
}The payload loop is for single-value fields. Handle checked state, repeated names, multi-selects, and file inputs explicitly when your form contains them. FormManager has no built-in network submission API.
This form is added after DOM ready, so it uses manual construction. The input exists before the manager scans it; initialize its events once. Later controls can be added through add_field().
const element = document.createElement('form');
element.name = 'dynamic_contact';
const address = document.createElement('input');
address.name = 'email';
address.className = 'fm-email';
address.required = true;
const button = document.createElement('button');
button.type = 'submit';
button.textContent = 'Save contact';
element.append(address, button);
document.body.appendChild(element);
const contact = new FormManager(element);
contact.fields.forEach(initialize_events);
const notes = document.createElement('textarea');
notes.name = 'notes';
element.insertBefore(notes, button);
contact.add_field(notes);
element.addEventListener('submit', event => {
event.preventDefault();
if (contact.check() === true) {
console.log(contact.get_field('email').get_value());
}
});
contact.check();| Type | Current validator behavior |
|---|---|
fm-phone | Extracts digits, requires exactly ten when non-empty, and formats (555) 123-4567. |
fm-email | Checks a simple non-whitespace address with one @ and a dotted domain. Empty and one-character values pass this validator; use a stronger custom rule when needed. |
fm-ssn | Formats nine digits and rejects the implemented zero/666/9-prefix cases. This is format validation, not identity verification. |
fm-number | Accepts digits and commas, removes commas, and adds grouping separators. Decimal points, signs, and currency symbols are rejected. |
fm-street-address | Checks whether the trimmed non-empty value begins with a digit; does not validate a deliverable address. |
Empty values generally pass type validators; required checks run separately. Native input types do not select these validators: add the corresponding fm-* class. If several type classes are present, the first match wins in this order: email, phone, SSN, No/Yes, number, street address.
change for ordinary controls and on No/Yes clicks, not on every keystroke. The constructor and DOM-ready initialization do not intercept form submission.check(), as shown above, and perform server validation for submitted data.incomplete_func and invalid_func callbacks. Setting one to null suppresses both its UI action and that branch’s failure result. Replace with a no-op function to suppress UI while preserving failure behavior.additional_validation_func must return exactly true. Failure returns false but does not invoke a failure callback or disable submit automatically; the callback must update any required UI.get_value() reads .value, so checkboxes/radio inputs need application logic using .checked. It does not collect all selected values from a multiple select.