FormManager – Developer Documentation

Detect fields, validate with callbacks, restore No/Yes controls, and set date values. Source: resources/form_manager.js (no version declaration).

Overview

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.

Load and initialize

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.

FormManager

Initialization

// 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.

Properties

PropertyTypeDescription
namestringFrom 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.
fieldsFieldManager[]Managed field objects for the form.
formHTMLFormElementThe underlying form DOM element.
additional_validation_funcfunctionFinal whole-form check; receives the manager and must return true to pass.

Methods

TypeMethodSignatureDescription
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.

Usage tip

// Preferred lookup:
const fm = FormManager.get_form('enroll_form');

// Legacy/indexed:
const fallback = FormManager.forms[0];

FieldManager

Each discovered control becomes a FieldManager. Its classes determine type and rules.

Supported classes

ClassApply ToBehavior
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 requiredAny managed field/containerMust be non-empty for the manager’s required check to pass; see submission behavior below.

Field API

MemberType / SignatureDescription
namestringFrom the underlying element’s name, or the No/Yes container ID.
field / formDOM element / FormManagerThe value-bearing element (hidden input for No/Yes) and its owning manager.
typestring|undefinedSemantic type detected from CSS classes at construction.
requiredbooleantrue if it has fm-required or the native required property.
validatorfunctionOptional validator receiving the underlying DOM element (see below).
validate()() => true | false | stringRuns only the field validator if present. Required-field and whole-form checks belong to FormManager.check().
get_value()() => stringReturns current value.
set_value(value)(any) => stringSets the value, synchronizes No/Yes UI, and formats Date/timestamp values for date inputs.
get_type()() => string|undefinedReturns the detected semantic type.

Field Classes (What to add to your markup)

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-yesContainer with name and idCreates a hidden input (same name/id) with value "No" or "Yes"; renders No/Yes buttons.
fm-required or native requiredAny managed fieldRequires a non-empty value when check() runs.

Validation callbacks

Form callbacks (assign on the FormManager instance)

CallbackSignatureWhen it runsTypical 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|otherAfter every field passes.Cross-field checks such as matching confirmation values.

Field validator

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.';
};

Examples

Quick markup

<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>

Hook up callbacks & use get_form

document.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 values and restore No/Yes fields

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.';

Add a field after initialization

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();

Form and field object relationships

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 / functionArguments and returnWhat 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.

Form workflows

1. Cross-field confirmation with visible feedback

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();
}

2. Restore saved field values and collect a payload

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.

3. Create a managed form dynamically

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();

Validation behavior

TypeCurrent validator behavior
fm-phoneExtracts digits, requires exactly ten when non-empty, and formats (555) 123-4567.
fm-emailChecks 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-ssnFormats nine digits and rejects the implemented zero/666/9-prefix cases. This is format validation, not identity verification.
fm-numberAccepts digits and commas, removes commas, and adds grouping separators. Decimal points, signs, and currency symbols are rejected.
fm-street-addressChecks 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.