furls v0.11.0
furls: Synchronize form state (and other state) with URL
The furls library makes it easy to synchronize form state (e.g. checkboxes, radio buttons, and text/textarea inputs) with the query part of the page's URL. This makes it easy to bookmark/share/link the current state of a web app, and makes the browser's back button act as an undo action. The library can also synchronize the classes of a particular element to represent the form state, making it easy to customize styles in response to form state. For examples of furls in action, see the font-webapp library which builds upon it.
Basic Usage
To define a global window.Furls, include <script src="furls.js"></script>
in your HTML, via either:
- Local
npm install furlsand use<script src="node_modules/furls/furls.js></script> - CDN
<script src="https://cdn.jsdelivr.net/npm/furls/furls.js"></script>
If you're using a build system supporting NPM modules via require,
use Furls = require('furls').
Simple example of usage in CoffeeScript:
update = (changed) ->
## @ is the furls instance
if changed.foo
console.log "foo changed from #{changed.foo.oldValue} to #{changed.foo.value}"
for name, value of @getState() # mapping of names/ids to values
console.log "#{name} is currently #{value}"
furls = new Furls() # create input handler
.addInputs() # auto-add all inputs
.on 'stateChange', update # call update(changed) when any input changes
.syncState() # auto-keep URL's search in sync with form state
.syncClass() # auto-keep <html>'s class in sync with form stateAPI
Furls objects supports the following API:
Inputs
<input> elements can generally be specified by string ID, DOM element,
or furls' internal representation of the input (see below).
.addInputs(query = 'input, select, textarea'): Start tracking all inputs matching the specified query selector (a valid input todocument.querySelectorAll). The defaultqueryincludes all<input>,<select>, and<textarea>elements in the document..configInput(input, options): Modify theencodeanddecodemethods, orminorattribute, as described under Input Objects, for an already added input. Useful after bulk.addInputs()to configure a specific input..addInput(input, options = {}): Start tracking the specified input. Optionally, you can specify manualencodeanddecodemethods, orminorattribute, as described under Input Objects. Degenerates to.configInputifinputis already tracked..removeInput(input): Stop tracking the specified input..removeInputs(query): Stop tracking all matching inputs. Sometimes it's easier to specify what not to track than what to track..clearInputs(): Stop tracking all inputs..get(input): Get the value ofinput(as convenient short-hand)..set(input, value): Set the value ofinputtovalueas if the user did, triggering change events if appropriate. (Note that manually setting a DOM'svalueattribute does not trigger events, so use this instead.).maybeChange(input): Check whether the value ofinputhas changed, and trigger change events if appropriate. (In case the DOM'svalueattribute changed manually without calling.set.).findInput(input): Get the internal representation of the specified input..getInputEvents(input): Returns which events to monitor for inputinput. Defaults to['input', 'change'], which should cover all input types on all browsers, but you could override this function to listen for custom DOM events. Redundant events are coalesced so they generate only one furlsinputChangeevents..isCommitChange(input): Detect whether the last change event for inputinputis a'change'(commit) event rather than an'input'input, for inputs supporting both events (andtrueotherwise). If you add events to.getInputEvents, you might need to change this too.
States / URL Synchronization
.syncState(history = 'auto', loadNow = true): Automatically keep the document's URL's search component in sync with the state of the inputs. When a form changes input, the new URL either gets "pushed" (whenhistoryis'push', so the back button returns to the previous state) or "replaced" (whenhistoryis'replace', so the back button leaves the page). See the difference betweenpushStateandreplaceState. Whenhistoryis the default setting of'auto', toggling checkboxes etc. pushes the URL, while editing text fields pushes only after the user defocuses the textbox (thereby "committing" the change).loadNowspecifies whether to immediately set the inputs' state according to the current URL's search component (defaulttrue)..loadURL(url = document.location, trigger = true): Manually set the inputs' state according tourl's search component, and trigger change events iftriggeristrue(default yes). If you've called.syncState, this gets automatically called duringpopstateevents, but this can be useful if you want to load a stored state of some kind..setURL(history = 'push', force = false): Manually set the document's URL's search component to match the state of the inputs.historycan bepushorreplaceas in.syncState(). If you want to push the current state even if the state hasn't changed, setforcetotrue..getState(): Return objectstatewith attributestate[name]for each input with thatnameequal to the value of the input (from thecheckedorvalueattribute). For each group of radio buttons, this object stores a single mapping from the group'snameto the selected button'svalue(like HTML forms)..getSearch(): Return state in URL search format (?key=value&...)..getRelativeURL: Return URL to self (document.location.pathname) with search given by.getSearch().
Class Synchronization
.syncClass(query = ':root', prefix = '', updateNow = true): Synchronizes theclassListof the specified DOMquery(which can also be just a DOM element or an array of DOM elements) to match the state of all "discrete" tracked inputs. Classes are of the formNAME-VALUE, prefixed byprefix; for example, a checkbox with nameboxcan be tested with CSS queries.box-trueand.box-false..discreteValue(input): Returns whether the given input has a "discrete" value, and thus.syncClasswill synchronize its class. By default, this includes all inputs of type"checkbox"and"radio", but you could override it to include a different subset of inputs.
Events
.on(event, listener): Calllistenerwheneventoccurs..off(event, listener): Stop callinglistenerwheneventoccurs..trigger(event, ...): Forceeventto occur with specified arguments. (You probably shouldn't need this.)
There are currently three types of events that occur:
'inputChange': An input changed in value. (Null changes don't count.) Argument is the internal representation of the input (see below).'stateChange': One or more inputs changed in value, aggregating together potentially several'inputChange'events (e.g. when loading from URL). (Null changes don't count.) Argument is an objectchangedwith an attributechanged[name]for each changed input with thatname, giving the internal representation of the input (see below). When a radio button changes,changed[name]will be the newly selected radio button (excluding all other buttons with the samenamei.e. group). Triggered after individualinputChangeevents.'loadURL': All input values were just loaded from the URL (caused bysyncStatefrom browser navigation or initial loading on startup, or from callingloadURLmanually). Argument is the URL's search component. Trigger afterinputChangeandstateChangeevents.
Helpers
You're unlikely to need these functions, unless you're being clever. You can override them, however, to get custom behaviors.
getParameterByName(name, search = window.location.search): Returns the value from anyname=valuein the specified URL search string. In most cases, you should useloadURLwhich calls this repeatedly.getInputValue(input): Given aninputobject, computes its currentvaluein the format described under Input Objects. In most cases, you should use.getto get the current value.getInputDefaultValue(input): Given aninputobject, computes its defaultvaluein the format described under Input Objects. In most cases, you should use.findInputand.defaultValue.setInputValue(input, value): Given aninputobject, sets itsvalueaccording to a given value in the format described under Input Objects, without triggering any events. In most cases, you should use.setto set the value of an input, which also triggers the relevant events.queueMicrotask(task): Schedules to calltaskbefore next browser render, ifwindow.queueMicrotaskis available. As a fallback, runstaskafter the next browser render viasetTimeout(task, 0).
Input Objects
The internal representation of an input (as returned by e.g. findInput)
is an object with (at least) the following attributes:
.id:idattribute of the<input>element (should be unique).type:typeattribute of the<input>element, or"textarea"in the case of<textarea>elements..name:nameattribute of the<input>element, or else itsid(differs fromidfor radio buttons, wherenamedefines groups). This is the key for the state object returned by.getState(), and what ends up in the URL..dom: The DOM object of the<input>element.defaultValue: The specified default value of the<input>element (from thedefaultCheckedordefaultValueattribute).value: Current value of the<input>element (from thecheckedorvalueattribute). For checkboxes, this istrueorfalse. For radio buttons, this is thevalueattribute if selected, andundefinedif not selected. Fortype=numberandtype=rangeinputs, this is automatically parsed into aNumber. For<select multiple>, this is an array of<option>value strings; for a single-value<select>, this is a single<option>value string..oldValue: The previous value of the<input>element (in particular during change events).lastEvent: Last DOM event for the change of this input (as limited bygetInputEvents), or'set'if it was last changed by.set(), or'load'if it was last changed by.loadURL()(including browser navigation).
In addition, you can add the following attributes, via configInput or
when calling addInput:
.encode(value): Encode the specified value into a string for the URL. For example, you can reduce number precision, or replace characters that encode verbosely into characters that encode more succinctly. Don't worry about URL encoding; whatever you return will be further encoded viaencodeURIComponentand mapping space to+. This method gets called withthisset to the input object..decode(value): Decode the specified string encoding from the URL into a value for this input. For example, you can undo character encodings you did in.encode. If your.encodedoesn't need special decoding (e.g. it reduced number precision), then you don't need to specify.decode. Don't worry about URL encoding; thevalueargument will already be decoded viadecodeURIComponentand mapping+to space. This method gets called withthisset to the input object..minor: Boolean specifying whether changes to this input should be considered "minor". If all changed fields are minor, then thehistorymode is forced to be'replace'.
You can make your custom encoding and decoding methods depend on the state of
other inputs (using e.g. furls.get() or furls.getState()), provided those
inputs do not also have custom encodings (otherwise the decoding order would
be unclear). Specifically, when loading an entire URL (via loadURL or on
startup with syncState), all inputs without custom decoding are loaded
first, so that the inputs with custom decoding can depend on them.
For example, here is how you could add optional ROT47 encoding/decoding
to a text input:
furls.configInput 'text',
encode: rot47 = (s) ->
return s unless furls.get 'rot'
s.split ''
.map (c) =>
code = c.charCodeAt(0)
return c unless 33 <= code <= 126
String.fromCharCode 33 + (code + 14) % 94
.join ''
decode: rot47