This contains the core reactive programming primitives. These are the core data structures:
ObsBase: base class for all other observable objectsObsCell: observable cell base classSrcCell: a source cell, one that can be directly mutatedDepCell: a dependent cell, one whose value is some function of another observable cell
ObsArray: observable array base classSrcArrayDepArray
ObsSet: observable Set base classSrcSetDepSet
ObsMap: observable Map base classSrcMapDepMap
Ev: an event node; serves as a pub-sub node for events of a certain type
cell(value): return aSrcCellinitialized to the given value (optional; defaults toundefined)cell.from(value): castsvalueto aDepCell, or ifvalueis already anObsCell, returnvalue.array(value[, diff]): return aSrcArrayinitialized to the given array (optional; defaults to[]) and the givendifffunction for computing/propagating the minimal set of change events (defaults torx.basicDiff()).array.from(value): castsvalueto aDepArray, or ifvalueis already anObsArray, returnvalue.set(value): constructs aSrcSetinitialized to the given value. If value is not already a Set, will attempt to cast it to one.set.from(value): castsvalueto aDepSet, or ifvalueis already anObsSet, returnvalue.map(value): constructs aSrcMapinitialized to the given value. If value is not already a Map, will attempt to cast it to one. In particular, to preserve backwards compatibility,valuehere can be a regularObject.map.from(value): castsvalueto aDepMap, or ifvalueis already anObsMap, returnvalue.cellToArray(cell[, diff]): given anObsCellof an array, return aDepArraythat issues the minimal set of change events by diffing the contents of the array to determine what has changed. Similar in mechanism toSrcArray.update.diffis an optional diff function (seeSrcArray.updatedocumentation) and defaults torx.basicDiff().cellToSet(cell): given anObsCellof a primitive JS object, return aDepSetthat, in response to future new values of the object, issues the minimal set of events by diffing the contents of the object to determine what has changed.cellToMap(cell): given anObsCellof a primitive JS object, return aDepMapthat, in response to future new values of the object, issues the minimal set of events by diffing the contents of the object to determine what has changed.cast(...): two forms:rx.cast(opts, types): convenience utility for use in preparing arguments for a component.optsis the arguments object mapping, andtypesis an object mapping fields ofoptsto eithercellorarray.castreturns a copy ofoptsbut with the fields casted based on the specified desiredtypes(yielding observable cells and arrays).rx.cast(data, type): lift the given datum to the given type; acts as identity function if data is already of the proper type.
bind(fn): given a 0-ary function, return aDepCellwhose value is bound to the result of evaluating that function. The function is immediately evaluated once, and each time it's evaluated, for any accesses of observables, theDepCellsubscribes to the corresponding events, which may subsequently trigger future re-evaluations.promiseBind(init, fn, [catchFn]): given a 0-ary functionfnwhich returns aPromise, returns aDepCellwhose value is initialized toinit, executefnand record dependencies, and update the value of the cell when thePromiseresolves. If the Promise fails, use a 0- or 1-arycatchFnto update the value of the cell based on the rejection reason. The defaultcatchFnis() => null--that is, if acatchFnis not specified and the Promise rejects, the cell's value will be reset tonull.snap(fn): evaluate the given 0-ary function while insulating it from the enclosing bind. This will be evaluated just once and prevents subscriptions to any observables contained therein.transaction(f): run the given function in a transaction, during which updates to observables will not emit change events to their subscribers. Only once the transaction ends will all events be fired (at which time the state of the source cells will no longer be in flux, and thus a consistent view of the universe can always be maintained).asyncBind(init, fn): create an asynchronous bind. The givenfnis expected to callthis.record(f)up to at most one time at some point during its potentially asynchronous duration before callingthis.done(result). The call tothis.record(f)evaluatesfwhile recording the implicit subscriptions triggered by accesses to observables. (You can calldonefrom withinrecordbut the result won't be set on this cell until afterrecordfinishes.) The cell's initial value isinit. Note that the use ofthismeans thatfncannot be an arrow function.lagBind(lag, init, fn): same asbindbut waits a 500ms delay after a dependency changes (or after initialization) before theDepCellrefreshes. For the first 500ms, the cell's value isinit; you can set this to e.g.snap(fn)if you want to immediately populate it with the result of evaluating your givenfn.postLagBind(init, fn): immediately evaluatesfn, which is expected to return a{val, ms}, wheremsindicates how long to wait before publishingvalas the new value of thisDepCell. Until the first published value, the cell is initialized toinit.onDispose(callback): add a (0-ary) cleanup listener, which gets fired whenever the current (enclosing) bind is removed or refreshed. This is useful for proper resource disposal, such as closing a connection or removing an interval timer.skipFirst(f): wrap a 1-ary function such that the first invocation is simply dropped. This is useful for subscribing an event listener but suppressing the initial invocation at subscription time.autoSub(ev, listener): Subscribes a listener to an event but such that if this is called from within abindcontext, the listener will be properly cleaned up (unsubscribed) on context disposal. Returns the subscription ID.subOnce(ev, listener): Subscribes a listener to an event, suppresses the initial invocation of the event at subscription. The listener is unsubscribed after the first time it is called.hideMutationWarnings(f): Run the given function, but suppress console logging of mutation warnings and firing of theonMutationWarningsevent. This is useful when you need to mutate a cell when already within a bind context, but you know that doing so is safe.
sub(listener): subscribes a listener function for events from this event node and returns a unique ID for this subscription, which can be used as the handle by which to unsubscribe later. The listener takes a single argument, whose type depends on the event, and is called every time an event is fired on this event node. NB: you are almost certainly better off using therx.autoSubutility method, which ensures that subscriptions are garbage collected.pub(data): publish an event, described by the data indata.datais what will be passed to all the subscribed listeners.unsub(subscriptionId): detaches a listener from this event.scoped(listener, contextFn): subscribes to the listener, runscontextFn(), and unsubs the listener. Returns the result ofcontextFn().observable: the observable object to which theEvis attached.
ObsCell, ObsArray, ObsSet, and ObsMap all extend the ObsBase class, which provides a few common methods:
flatten(): returnsrx.flatten(this).raw(): returns the current value of the reactive object without subscribing. Short forrx.snap(() => this.all()).all(): returns current value of the reactive object, and subscribes to any changes to it. Abstract method implemented by all subclasses.readonly(): returns aDepversion of the current object. AnObsCellreturns aDepCell, anObsMapreturns aDepMap, and so forth. Abstract method implemented by all subclasses. Subtly different from calling e.g.new SrcCell(42).toCell(), sincereadonlyalways returns aDepversion of the current object, while casting an object to its own type viacast,.from, orto<type>will simply return the pre-existing object..toCell(): returns aDepCellwhose value is bound to that of this object. unless the object is anObsCell, in which case it returns the object itself. Equivalent torx.cell.from(this)..toArray(): returns aDepArraywhose value is bound to that of this object. unless the object is anObsArray, in which case it returns the object itself. Equivalent torx.array.from(this)..toSet(): returns aDepSetwhose value is bound to that of this object. unless the object is anObsSet, in which case it returns the object itself. Equivalent torx.set.from(this)..toMap(): returns aDepMapwhose value is bound to that of this object. unless the object is anObsMap, in which case it returns the object itself. Equivalent torx.map.from(this).
In general, you should not instantiate Obs objects. They cannot be subscribed to other objects, nor do they come
with mutator functions. As a result, they are really only useful as base classes.
While Src objects do not share a common mixin, they do all provide an update method, in addition to the built in
ObsBase methods and those particular to each implementation.
update(x): updates the object's value, emits change events, and returns its old value.
With the exception of DepCell (see below), Dep objects inherit their Obs parents, and provide no new methods.
Their constructors all follow the same pattern:
constructor(f): Constructs an observable whose value is bound to f. Runs f. Use this.record to subscribe to
dependencies, and this.done to publish a new value. If any of recorded dependencies change, f will be re-run, its
dependencies cleared and re-transcribed, and the value of this observable will be updated.
Note that DepArrays optionally take a second argument, diff, to their constructor. This argument is used to compute
the diffs
get(): returns current value of the cellonSet: the event that is fired after the value of the cell is changed. The event data is an array of two elements,[oldVal, newVal].
set(x): alias forupdate, included to maintain backwards compatibility and consistency.
Note: these methods are for advanced use only, and are key to the internal workings of Bobtail.
disconnect: unsubscribes this cell from its dependencies and recursively disconnects all nested binds; useful for manual disposal ofbindsrefresh: this method recalculates the value of a cell, as well as its dependencies, and then publishes itsonSetevent.
ObsArray implements most of the non-mutating methods of the native JS Array class, in reactive form.
-
onChange: the event that is fired after any part of the array is changed. The event data is an array of three elements:[index, added, removed], whereindexis the index where the change occurred,addedis the sub-array of elements that were added, andremovedis the sub-array of elements that were removed. -
get(i): returns element ati -
at(i): alias forget. Included for backwards compatibility. -
all(): returns array copy of all elements -
raw(): returns raw array of all elements; this is unsafe since mutations will violate the encapsulated invariants, but serves as a performance escape hatch aroundall() -
length(): returns size of the array -
transform(f): returns aDepArraywith value bound tof(this.all()). -
indexed(): returns aDepArraythat mirrors this array, but whosemapmethod passes in the index as well. The mapper function takes(x,i)wherexis the current element andiis a cell whose value always reflects the index of the current element. -
map(fn): returnsDepArrayof given function mapped over this array. For legacy reasons, not quite analagous toArray.map, in that fn is only a 1-ary function, as opposed to the 3-ary(currentValue, index, array)signature ofArray.prototype.map. -
filter(f): returns aDepArraywith value boundthis.all().filter(f). analagous toArray.prototype.filter. -
slice(x, y): returns aDepArraywith value boundthis.all().slice(x, y). analagous toArray.prototype.slice. -
reduce(f): returns aDepArraywith value boundthis.all().reduce(f). analagous toArray.prototype.reduce. -
reduceRight(f): returns aDepArraywith value boundthis.all().reduceRight(f). Analagous toArray.prototype.reduceRight. -
every(f): returnsthis.all().every(f). -
some(f): returnsthis.all().some(f). -
indexOf(val, from): returnsthis.all().indexOf(val, from). -
lastIndexOf(val, from): returnsthis.all().lastIndexOf(val, from). -
join(separator): returnsthis.all().join(separator). -
first(): returns the first element in theObsArray. -
last(): returns the last element in theObsArray.
ObsArray implements most of the mutator methods of the native JS Array class, in reactive form. None of the mutator
functions specified here should cause events to subscribe.
SrcArray([xs[, diff]]): constructor that initializes the content array toxs(note: keeps a reference and does not create a copy) and the diff function forupdatetodiff(defaults torx.basicDiff()).splice(index, count, additions...): replacecountelements starting atindexwithadditionsinsert(x, i): insert valuexat indexiremove(x): find and remove first occurrence ofxremoveAt(i): remove element at indexipush(x): appendxto the end of theObsArraypop(): removes and returns the last element of theObsArrayunshift(x): prependxto the front of theObsArrayshift(): removes and returns the first element of theObsArrayreverse(): reverses the order of elements of theObsArrayand returns its new value, without subscribing.put(i, x): replace elementiwith valuexreplace(xs): replace entire array with raw arrayxsupdate(xs[, diff]): replace entire array with raw arrayxs, but apply thediffalgorithm to determine the minimal set of changes for which to emit events. This enables flexible updating of the array, representing arbtirary transformations, while at the same time minimizing the downstream recomputation necessary (particularly DOM manipulations).move(src, dest): Moves the element atsrcto beforedestas a single mutation.swap(i1, i2): Swaps the elements at i1 and i2 as a single mutation.- inherits
ObsArray
A reactive version of the Set object introduced by ES2015.
has(key): returns ifkeyis in the set.keys(): returns a copy of the underlyingSet. alias for.all, to match the ES2015SetAPI.values(): returns a copy of the underlyingSet. alias for.all, to match the ES2015SetAPI.entries(): returns a copy of the underlyingSet. alias for.all, to match the ES2015SetAPI.size(): returns the number of elements in theObsSet.union(other): returns a newDepSetwhose value is bound to the union ofthisandother, which can be either a primitive orObsBase, and which will be cast to a Set.intersection(other): returns a newDepSetwhose value is bound to the intersection ofthisandother, which can be either a primitive orObsBase, and which will be cast to a Set.difference(other): returns a newDepSetwhose value is bound to the difference ofthisandother, which can be either a primitive orObsBase, and which will be cast to a Set.symmetricDifference(other): returns a newDepSetwhose value is bound to the symmetric difference ofthisDepSet andother, which can be either a primitive orObsBase, and which will be cast to a Set.
put(item): Putsitemto the Set.add(item): Alias forput.delete(item): Deletesitemfrom the Set.remove(item): alias fordelete.clear(): removes all entries from the Set.
A reactive version of the Map object introduced by ES2015.
onAdd: the event that is fired after an element is added. The event data is a map of added keys to their corresponding values.onRemove: the event that is fired after an element is removed. The event data is a map of removed keys to their original values.onChange: the event that is fired after a key's value is changed. The event data is a map of changed keys to a two-element list of[oldValue, newValue].get(k): returns value associated with keykhas(k): returns whether keykis in the Mapsize(): returns the number of entries in the Map.
put(k, v): associates valuevwith keykand returns any prior value associated withkset(k, v): alias forput.delete(k): removes the entry associated at keykremove(k): alias for delete.clear(): removes all entries from the set.- inherits
ObsMap
rx.flatten(xs): given an array of eitherArrays,ObsArrays,ObsCells,ObsSets, primitives, or objects, flatten them into one dependent array, which will be able to react to changes in any of the observables. It furthermore strips outundefined/nullelements (for convenient conditionals in the array). This operation is a "deep flatten"---it will recursively flatten nested arrays. Furthermore, reactive objects which themselves return reactive objects are also recursively flattened:bind(() => [bind(() => [42]).toArray())will resolve to42. AnyFunctions found in the recursive descent are first bound, and the resulting cell then flattened.
This function exists primarily to support the templating DSL. All non-primitive contents passed to an rxt.tags
function are run through rx.flatten to convert it into something we can use to construct HTML tags. The reasoning
behind this is that, when writing templates, nested structures can easily develop. We would prefer not to have to wrap
these in semantically meaningless div or span tags (especially problematic when, say, working with tables). This
means that, unlike in older versions of Bobtail and Reactive Coffee, you do not have to explicitly call flatten to
simplify complex templating code; we do it for you automatically.
concat(arrays...): returns a dependent array that is the result of concatenating multipleObsArrays (efficiently maintained).basicDiff([key]): returns a diff function that takes two arrays and uses the given key function, which defaults torx.smartUidify, to efficiently hash-compare elements between the two arrays. Used byrx.cellToArray.smartUidify(x): returns a unique identifier for the given object. If the item is a scalar, then return its JSON string. If the item is an object/array/etc., create a unique ID for it, insert it (this is a standard "hashing hack" for JS) as a non-enumerable property__rxUid, and return it (or just return any already-existing__rxUid). I.e., this implements reference equality. Used byrx.basicDiff.
lift(x[, spec]): convert an objectxcontaining regular non-observable fields to one with observable fields instead.specis a mapping from field name to either'cell','array', or'map', based on what kind of observable data structure we want. By default this is supplied byrx.liftSpec.liftSpec(x): given an objectx, return a spec forrx.liftwhere any field that is an array is to be stored in anrx.array, and otherwise in anrx.cell.reactify(obj, spec): mutates an object to replace the specified fields with ES5 getters/setters powered by an observable cell/array, and returns the object.specis a mapping from field name to{type, val}, wheretypeis'array'or'cell'andvalis an optional initial value.autoReactify(obj): mutates an object toreactifyall fields (specifyingarrayfor arrays andcellfor everything else), and returns the object
This contains the template DSL constructs. The main thing here is the tag function, which is what constructs a DOM element.
mktag(tag): returns a tag function of the given tag name. The various tags likedivandh2are simply aliases; e.g.,div = mktag('div'). Tag functions themselves take(attrs, contents), both optional, whereattrsis a JavaScript object of HTML attributes and/or special attributes
andcontentsis an array (ObsArrayor regularArray) of child nodes (either raw elements,RawHtml, or jQuery objects) and/or strings (for text nodes). You can also pass in a singular such node without the array. The function returns an instance of the specified element wrapped in a jQuery object. SeespecialAttrsbelow for more on special attributes.rxt.tags.<tagName>(opts, children): We generate a function usingmktagfor each of the standard HTML tags. See the "Supported Tags" section below for the full list of tag functions.rawHtml(html): returns aRawHtmlwrapper for strings that tags won't escape when rendering; example:div {}, [rawHtml('<span>hello</span>')]specialChar(code, tag): Creates atagwrapping the single special character&<code>;unicodeChar(code, tag): Creates atagwrapping the single unicode character\u<code>;rxt.smushClasses(classes): convenience utility for programmatically constructing strings suitable for use as aclassattribute. Takes an array of strings orundefineds (useful for conditionally including a class), and filters out thenull/undefined.
rx(property): lazily create (or take the cached instance) and return aSrcCellwhose value is maintained to reflect the desired property on the element. If the cell's value is changed, the property on the element will update, and vice versa.propertycan be'checked','focused', or'val'.checkedandvalupdate on theonchangedevent, whilefocus(naturally) updates on theonfocusandonblurevents. This means that, by default, calling$elem.val(newValue)will not cause the cell to update its value, because doing so does not fire theonchangedevent. You must do so manually if you want the cell to update. Note also that radio buttons do not fire anonchangedevent when they are deselected. But see below.
In addition to the normal tags.input function, we take advantage of the fact that JavaScript
functions are also objects to add shorthand methods for each input type:
rxt.tags.input.<type>(attrs, children): Shorthand fortags.input(_.extend(attrs, {type}), children). In addition, for checkboxes and radio buttons, we patch the jquerypropfunction so that using it to programmatically set checked state updates therx('checked')cell. For all other inputs, we similarly patch the jqueryvalfunction to update therx('val')cell.
Special attributes are handled differently from normal. The built-in special attributes are primarily events like
click:
button {click: -> console.log('hello')}, 'Hello'
Special attributes are really just a convenient short-hand for running the handler functions after constructing object. The above example is equivalent to:
$button = button 'Hello'
$button.click -> console.log('hello')
The built-in special attributes available include:
-
init: which is a 0-ary function that is run immediately upon instantiation of the element. -
style: if the value is an -
class: automatically transforms compacts, flattens, and joins the given value if it's an array, then strips any extra whitespace -
events like
click,focus,mouseleave,keyup,load, etc.,: these are available for all jQuery events. The handlers are similar to those you would pass to a jQuery event method?they take a jQuery event and return false to stop propagation?but the context is the jQuery-wrapped element instead of the raw element. The full list:blurchangeclickdblclickerrorfocusfocusinfocusouthoverkeydownkeypresskeyuploadmousedownmouseentermouseleavemousemovemouseoutmouseovermouseupreadyresizescrollselectsubmittoggleunload
specialAttrs: an object mapping special attribute names to the functions that handle them. When an element specifies
a special attribute, this handler function is invoked. The handler function takes (element, value, attrs, contents),
where:
elementis the element being operated onvalueis the value of this special attribute forelementattrsis the full map of attributes forelementcontentsis the content/children forelement(which can be a string,RawHtml, array,ObsCell, orObsArray)
So for instance, we can create a special attribute drag that just forwards to the jQuery [jdragdrop] plug-in:
rxt.specialAttrs.drag = (elt, fn, attrs, contents) -> elt.drag(fn)
and then use it like so in your templates:
div {class: 'block', drag: -> $(this).css('top', e.offsetY)}
colorcheckboxdatedatetimedatetimeLocalemailfilehiddenimagemonthnumberpasswordradiorangeresetsearchsubmitteltexttimeurlweek
a, abbr, address, area, article, aside, audio, b, base, bdi, bdo, blockquote, body, br, button, canvas, caption,
cite, code, col, colgroup, data, datalist, dd, del, details, dfn, div, dl, dt, em, embed, fieldset, figcaption, figure,
footer, form, h1, h2, h3, h4, h5, h6, head, header, hr, html, i, iframe, img, input, ins, kbd, keygen, label, legend,
li, link, main, map, mark, math, menu, menuitem, meta, meter, nav, noscript, object, ol, optgroup, option, output, p,
param, pre, progress, q, rp, rt, ruby, s, samp, script, section, select, slot, small, source, span, strong, style, sub,
summary, sup, svg, table, tbody, td, template, textarea, tfoot, th, thead, time, title, tr, track, u, ul, var, video,
wbr

