FieldState

Download Download from Github

FieldState

Download Download from Github

FieldState is a visual state handler for form fields where the required/available state of a field depends on other factors - for example the values/selections of other fields.

At its heart FieldState saves you writing lots of code to control which fields suddenly become available/required based on user input in other fields. FieldState handles this via a simple, HTML5 data attributes-based API (with a JavaScript API for deeper usage).

Formore complex evaluations, FieldState supports regular expressions, callbacks, multi-field conditional chaining and various types of event.

FieldState is not a form validator! Always validate input on the server not (or not only) via JavaScript.

FieldState works in all good browsers plus IE8 and above.

Examples 🔗

Example 1 - basic: The dropdown field is required, but the postcode field is required only if UK was selected:

<label>Country</label> <select id=country> <option>Please select...</option> <option>Zambia</option> <option>UK</option> <!-- other options... --> </select> <label>UK postcode</label> <input type=text data-req='if:("#country" == "UK")' data-unreq-state=disabled />

Example 2 - REGEXP: The submit button becomes available only when a password has been chosen that comprises 8-15 alphanumeric characters.

<label>Choose a password</label> <input type=password id=pass data-req=true /> <input type=submit value=Register data-avail='if:("#pass" /^\w{8,15}$/)' data-unavail-state=disabled />

Example 3 - checkboxes: The field becomes available only when two or more checkboxes are checked; otherwise it is hidden.

<label>Some checkboxes</label> <input type=checkbox /> <input type=checkbox /> <input type=checkbox /> <input type=checkbox /> <label>A field</label> <input type=text data-avail='if:("#demo3 [type=checkbox]" :2+_checked)' data-unavail-state=hidden />

Example 4 - chained requirements: The passenger 1 field is always required, but the passenger 2 field becomes available only after passenger 1 acquires a value. Similarly, the passenger 3 field becomes available only once the passenger 2 field acquires a value. See multiple field chaining.

<label>Passenger 1 name</label> <input type=text id=passenger_1 data-req=true /> <label>Passenger 2 name</label> <input type=text id=passenger_2 data-avail='if:("#passenger_1" /.+/)' data-unavail-state=hidden /> <label>Passenger 3 name</label> <input type=text id=passenger_3 data-avail='if:("#passenger_2" /.+/)' data-unavail-state=hidden />

Usage 🔗

Data attributes 🔗

FieldState defines five data attributes, applied to the field whose required/available state is contingent on other field(s):

  • data-req - an expression stipulating a requirement state for the field.
  • data-avail - an expression stipulating an availability state for the field
  • data-unreq-state - the state the field should be in ("hidden" or "disabled") any time it is in an unrequired state
  • data-unavail-state - the state the field should be in ("hidden" or "disabled") any time it is in an unavailable state
  • data-and-cntr - If true, the state toggle applies not only to the field and its label but also its parent container element

You can avoid having to repeat the data-unavail-state and data-unreq-state attributes for multiple fields by instead using the JavaScript API's setDefaultState() method to set default unavailabe/unrequired states.

A sixth attribute, data-fieldState, allows fields to import their FieldState profiles from other fields. It takes the syntax as:selector, so data-fieldState="as:#foo" would means the field should import its FieldState profile from the element with ID "foo".

Expressions 🔗

Requirement/availability is stipulated via expressions. There are five kinds:

  • Fixed, where a field should be always required or available ("true"), or, conversely, always not required or available ("false") e.g. data-req="true".
  • Conditional on the value of another field - if:("selector" operator "comparison"). See example 1. Valid operators are ==, !=, > (greater than) and < (less than).
  • Conditional on the value of another field when subjected to a regular expression - if:("selector" /regex/). See example 2. The pattern can be negated by prefixing it with !, i.e. !/my_pattern/.
  • Conditional on the checked state(s) of other fields - if:("selector" checked_state). Valid checked state flags are :checked, :all_checked, :any_checked, :none_checked, :3_checked, :3+_checked (N or more) and :3-_checked (N or fewer). See example 3.
  • Conditional on the value of another field when subjected to a callback function - if("selector" callback:my_callback). Callbacks must first be registered with FieldState via the fs.addCallback() method (see JavaScript API) and will be executed in the context of the element. Callbacks are automatically passed two arguments: a nodeset of the contingent field(s) (i.e. the field(s) matched by the selector) and an array of the fields' current values or, for checkboxes or radios, their checked state (true or false). Your callback should return a boolean denoting the new un/required or un/available state of the affected field.

In each case, the selector part of an expression should be a CSS selector (which is compatible with document.querySelector()) targeting a field or fields, on whose value(s) or checked state(s) the field is contingent. See the examples to see some of these situations in action.

Note that the condition will fail if the field(s) you are checking against are themselves currently disabled/hidden. In the case of checking against multiple fields (see above), this applies only if all of them are disabled/hidden.

Expressions matching multiple fields 🔗

Most of the time your fields' available/required states will be contingent on single other fields - at least when checking against string values (rather than checked states, which is a more common use of setting dependencies on multiple fields, as shown in example 3.)

It is possible, however, to set dependencies on multiple fields. The key here is to note that the comparison will be made against the concatenation of those fields' values.

So for example, recall in example 1 we made the postcode field available only once the user had selected "UK" from the countries dropdown. Let's extend it to also require that "house" is selected from a second, "property type" dropdown:

<label>Country</label> <select id='country'> <option>Please select...</option> <option>UK</option> </select> <label>Property type</label> <select id='prop_type'> <option>Please select...</option> <option>house</option> </select> <label>UK postcode</label> <input type='text' data-req='if:("#country, #prop_type" == "UKhouse")' data-unreq-state='disabled' />

Take a look at how the expression has changed. We now target two fields, not one (#country, #prop_type) and set the comparison as the concatenation of the two values we're after, "UKhouse".

CSS classes 🔗

Any field that uses FieldState has a class that reports its state at any given time - and so does its corresponding label, if it has one. This allows you to track state changes easily in your JavaScript or CSS.

Valid FieldState classes are:

  • fs-required - the field is currently required
  • fs-available - the field is currently available but not required
  • fs-disabled - the field is currently disabled
  • fs-hidden - the field is currently hidden

One of the first two classes gets added any time a field is, or becomes, required or available, depending on its expression, and one of the latter two classes gets added any time this changes.

JavaScript API 🔗

Though interfaced primarily through data attributes, FieldState also exports a small JavaScript API for more advanced usage. This lives on the global fs namespace.

A common and simple use of the JS API is to set default states for fields, via the setDefaultState() method, to save having to repeatedly specify them.

Available methods are:

  • fs.addCallback(name, callback) - register a callback function, callback (function), for use with callback expressions. name (string) is a name given to the callback that callback expressions should reference.
  • fs.initialise([elements, context]) - initialises or reinitialises FieldState. Useful for adding FieldState behaviour to elements that were injected into the DOM later, after FieldState did its initial onload sweep (see Chronology). elements and context are both optional, and both accept an element reference or selector to be passed which, respectively, limit the operation to certain fields and/or within a certain container.
  • fs.onFieldStateChange(element, callback) - register a callback function, callback (function), to fire when the field(s) denoted by element (an element reference or selector string) change state. The callback is automatically passed three arguments:
  • fs.toggle(element[, direction]) - manually toggle the state of field(s) denoted by element (an element reference or selector string). Beacuse this constitutes an override to the state FieldState thinks the field should be in, the field will no longer respond to the changing values/states of any other field(s) it is contingent upon. The new state can be forced via direction (boolean) - true, putting the field into its required or available state, and false for its unrequired or unavailable state. If omitted, the value is toggled from its current state rather than forced.
  • fs.setFieldValue(element, val[, context]) - set a value, val (string/int), on field(s) denoted by element (an HTML element reference or a selector string) . Doing it via FieldState rather than directly means is that it will trigger a input/change event, triggering changes to any other fields dependent on. Additional, an optional context may be forced (see initialise()).
  • fs.setDefaultState(which, state) - set a default unrequired/unavailable state, state (string - "disabled" or "hidden"), so you don't have to keep providing data-un*-state attributes on elements. which (string) is either "req" or "avail".
  • fs.reset([elements, context]) - resets fields to their starting field states, pre-FieldState. This may be limited to specific elements or within a specific context (see initialise()).

Chaining requirements 🔗

FieldState can handle chained requirements amongst fields, demonstrated in example 4. That is to say, if a state change to field A triggers a state change to field B, any other fields (C and D) contingent on the state of field B will in turn be updated also.

In this way, it is not necessary to make C and D directly contingent on the state of field A, but only on the state of field B. This form of delegation keeps logic to a minimum.

Chronology 🔗

On DOM load, FieldState makes an initial sweep of the DOM and locates any form inputs that have FieldState data attributes.

For any elements created then injected into the DOM, FieldState will not be aware of these until you notify it via the fs.initialise() method (see JavaScript API). For example:

//create new input var new_el = document.createelement('input'); new_el.type = 'text'; new_el.id = 'new_input'; new_el.setattribute('data-req', true); //and new label var label = document.createelement('label'); label.textcontent = 'some field'; //append both to form and register input with fieldstate var form = document.queryselector('form'); form.appendchild(label); form.appendchild(new_el); fs.initialise(new_el); //could also do fs.initialise("#new_input");

When creating several elements, instead of manually notifying FieldState of each one separately we can just tell it to look at all inputs within a container context:

//iteratively create inputs and labels and append to form var form = document.queryselector('form'); for (var i=0; i<10; i++) { var new_el = document.createelement('input'); new_el.type = 'text'; new_el.setattribute('data-req', true); var label = document.createelement('label'); label.textcontent = 'input '+(i+1); form.appendchid(label); form.appendchid(new_el); } //register all inputs with fieldstate, by passing the form context fs.initialise('input', form);

Notes / issues 🔗

  • The current state of a field at any given time can be read from its fs-N CSS class, where N is either 'required', 'available', 'hidden' or 'disabled' - see CSS classes.
  • It is possible to affect the visibility state of not only the field but also its parent container - see Usage > Data attributes.
  • Though perhaps not overly intuitive as a concept, it is possible to combine required and available states on the same field, so that it is available (but not required) in certain cases but becomes required in others.
  • The data-un*-state attributes are useful only where you also provide a data-req or data-avail attribute containing an expression
  • For any callbacks or elements created after DOM load, you will need to notify FieldState of these. See Chronology.
  • Regular expressions should use single backslash escaping, not double, i.e. \.
  • Disabled fields, and their corresponding label (which is assumed to be a preceding sibling of the field, though not necessarily the immediately preceding sibling), are given a class, fs-disabled. This class is toggled should the field become enabled later.
  • Labels of fields in a required state are appended a span containing *. This is removed should the field lose required status later.
  • Except when checking against check states, you will ordinarily want to ensure selectors in expressions match just one field. Should they match multiple, the condition will be made against the concatenated values of those fields. See Usage.

Download Download from Github