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.
To define a global window.Furls
, include <script src="furls.js"></script>
in your HTML, via either:
- Local
npm install furls
and 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 state
Furls
objects supports the following API:
<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 defaultquery
includes all<input>
,<select>
, and<textarea>
elements in the document..configInput(input, options)
: Modify theencode
anddecode
methods, orminor
attribute, 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 manualencode
anddecode
methods, orminor
attribute, as described under Input Objects. Degenerates to.configInput
ifinput
is 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 ofinput
tovalue
as if the user did, triggering change events if appropriate. (Note that manually setting a DOM'svalue
attribute does not trigger events, so use this instead.).maybeChange(input)
: Check whether the value ofinput
has changed, and trigger change events if appropriate. (In case the DOM'svalue
attribute 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 furlsinputChange
events..isCommitChange(input)
: Detect whether the last change event for inputinput
is a'change'
(commit) event rather than an'input'
input, for inputs supporting both events (andtrue
otherwise). If you add events to.getInputEvents
, you might need to change this too.
.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" (whenhistory
is'push'
, so the back button returns to the previous state) or "replaced" (whenhistory
is'replace'
, so the back button leaves the page). See the difference betweenpushState
andreplaceState
. Whenhistory
is 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).loadNow
specifies 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 iftrigger
istrue
(default yes). If you've called.syncState
, this gets automatically called duringpopstate
events, 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.history
can bepush
orreplace
as in.syncState()
. If you want to push the current state even if the state hasn't changed, setforce
totrue
..getState()
: Return objectstate
with attributestate[name]
for each input with thatname
equal to the value of the input (from thechecked
orvalue
attribute). For each group of radio buttons, this object stores a single mapping from the group'sname
to 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()
.
.syncClass(query = ':root', prefix = '', updateNow = true)
: Synchronizes theclassList
of 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 namebox
can be tested with CSS queries.box-true
and.box-false
..discreteValue(input)
: Returns whether the given input has a "discrete" value, and thus.syncClass
will 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.
.on(event, listener)
: Calllistener
whenevent
occurs..off(event, listener)
: Stop callinglistener
whenevent
occurs..trigger(event, ...)
: Forceevent
to 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 objectchanged
with 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 samename
i.e. group). Triggered after individualinputChange
events.'loadURL'
: All input values were just loaded from the URL (caused bysyncState
from browser navigation or initial loading on startup, or from callingloadURL
manually). Argument is the URL's search component. Trigger afterinputChange
andstateChange
events.
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=value
in the specified URL search string. In most cases, you should useloadURL
which calls this repeatedly.getInputValue(input)
: Given aninput
object, computes its currentvalue
in the format described under Input Objects. In most cases, you should use.get
to get the current value.getInputDefaultValue(input)
: Given aninput
object, computes its defaultvalue
in the format described under Input Objects. In most cases, you should use.findInput
and.defaultValue
.setInputValue(input, value)
: Given aninput
object, sets itsvalue
according to a given value in the format described under Input Objects, without triggering any events. In most cases, you should use.set
to set the value of an input, which also triggers the relevant events.queueMicrotask(task)
: Schedules to calltask
before next browser render, ifwindow.queueMicrotask
is available. As a fallback, runstask
after the next browser render viasetTimeout(task, 0)
.
The internal representation of an input (as returned by e.g. findInput
)
is an object with (at least) the following attributes:
.id
:id
attribute of the<input>
element (should be unique).type
:type
attribute of the<input>
element, or"textarea"
in the case of<textarea>
elements..name
:name
attribute of the<input>
element, or else itsid
(differs fromid
for radio buttons, wherename
defines 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 thedefaultChecked
ordefaultValue
attribute).value
: Current value of the<input>
element (from thechecked
orvalue
attribute). For checkboxes, this istrue
orfalse
. For radio buttons, this is thevalue
attribute if selected, andundefined
if not selected. Fortype=number
andtype=range
inputs, 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 viaencodeURIComponent
and mapping space to+
. This method gets called withthis
set 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.encode
doesn't need special decoding (e.g. it reduced number precision), then you don't need to specify.decode
. Don't worry about URL encoding; thevalue
argument will already be decoded viadecodeURIComponent
and mapping+
to space. This method gets called withthis
set to the input object..minor
: Boolean specifying whether changes to this input should be considered "minor". If all changed fields are minor, then thehistory
mode 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