-
Notifications
You must be signed in to change notification settings - Fork 1
API Documentation
At its core, what Cable does is maintain a dependency graph, and execute
specialized traversals on it. The developer does not have direct access to how
the graph is constructed, but can create nodes in it using the Cable.define
function.
Cable.define
is the main interface for using Cable. It allows the programmer to
define nodes in the dependency graph by constructing special properties in the
nodes
argument. The options argument allows the programmer to control some
aspects of how Cable installs the nodes.
If the reify
option is disabled, Cable will not update the in references of
the dependency graph. This is useful if the nodes specified in the nodes
argument refer to some node which is not yet in the graph.
If the wireup
option is disabled, Cable will not wireup the unwired events in
the graph.
See the section on Node Definitions for more on defining the different kinds of nodes.
There are some restrictions on how nodes can be named. They may not contain an
underscore, and cannot have the same name as any of the following reserved
words: result
, respond
, type
, event
, define
.
-
nodes
: An object where each property is a description of a node to be defined in the dependency graph. -
options
: An optional object that allows the programmer to override some aspects of how Cable loads the nodes in thenodes
argument. By default it is{ reify:true, wireup:true }
.
Straightforward example that logs every change to a textbox.
Cable.define({
text:Cable.textbox("input"),
logger:function(text) { console.log(text()); }
});
Example of two definitions where reification is disabled in the first call because it refers to a node which is defined in the second.
Cable.define(
{ logger:function(text) { console.log(text()); } },
{ reify:false }
);
Cable.define(
{ text:Cable.textbox("input") }
);
Example of a definition where wireup is disabled. This will not do anything
because the text
is an event node and, if it isn't wired up, the logger
node will never execute.
Cable.define(
{
text:Cable.textbox("input"),
logger:function(text) { console.log(text()); }
},
{ wireup:false }
);
The above example can be fixed by running define without disabling wireup.
Cable.define({ });
Give an initial value to a synthetic node. This is a workaround for cyclic references between synthetic nodes. See an example of using this in the flip-flip circuit example.
-
name
: The name of the synthetic node to be given a default value. -
value
: The initial value to give to the synthetic node.
Cable.initialize("norGate", false);
Cable.withArgs
provides a way of explicitly setting the argument names of a
function. For many Cable node definitions, the argument names of a function are
significant, however, in a couple of cases, it's useful to be able to specify
arguments differently. For example, one might want to specify a function's
arguments dynamically. Another example would be when using a minifier such as
Google's Closure Compiler, the argument names of functions often get renamed
automatically. Cable.withArgs
takes an array of strings, and a function, and
returns a function which will appear to have the argument names of the strings.
Note: Cable's build process uses the Closure Compiler. As such, this function is used several times in the cable source code so that the compiled version matches the semantics of the uncompiled.
-
argumentNames
: An array of strings to be the effective argument names for the resulting function. -
func
: The function to be given the new argument names.
function(x, y, z) { /* ... */ }
// Cable observes that the arg names are x, y and z.
Cable.withArgs(["a", "b", "c"], function(x, y, z) { /* ... */ } )
// Cable observes that the arg names are a, b and c.
Get access to the dependency graph Cable for debugging purposes. Normally, the graph itself is hidden inside the cable system, and should not be interacted with directly. However, it can be useful for debugging a Cable application, so this function provides access for that purpose.
As noted in the Interface section, Cable.define
accepts an object of named
node definitions. Each definition is a description for Cable to install in the
graph.
With some node definitions in Cable, the name you give a function argument is significant and defines which other nodes it depends on. These argument names follow a small set of rules.
- In most cases, it's just a name, such as
jsonData
. In this case, it describes a reference to another node called exactlyjsonData
. The node being defined will be updated every time thejsonData
node is updated. - If the argument name starts with an underscore, then it depends on a node
with the same name sans the underscore. For example, if a node has an argument
_mousePosition
, then it depends on a node namedmousePosition
and can use its value, but is not updated whenmousePosition
is updated. - If the node being defined is in a scoped node definition, then the reference
will apply to nodes in the same scope before it applies to nodes in parent
scopes. For example, if there is a node named
x
defined at the top level, and a node namedx
defined in scopey
(which will also have the namey_x
) then a node depending onx
at the top-level will refer to the top-levelx
, but a node inside scopey
depending onx
will depend ony_x
. See the section on Scoped Nodes for more. - The argument names
result
,respond
,event
anddefine
have special meanings as described in the node definition specs. - If an argument refers to a node which does not exist and is not a reserved
word, then an exception will be thrown by
Cable.define
.
Events create streams of data in the dependency graph. For example a button can be represented as a stream of timestamps of when it has been clicked. A textbox can be represented as a stream of its contents.
Events are identified as functions with a single argument named event
. The
function should wireup the event so that the event
argument is executed with
the new value every time the event fires.
The event function itself also has a method named setDefault
which allows the
setting of the default value for the event. Using event.setDefault
inside the
wireup instead of just invoking event
with a value will not trigger the
event's dependent nodes.
-
function(event) { <wireup event to be called> }
: The function definition for an event.
When a node depends on an event node, the event is provided as a function which returns its current value.
Setup an event for when a button is pressed as a stream of timestamps.
Cable.define({
buttonClick:function(event) {
// Wireup the event function:
document.getElementById("the-button").onclick = function() {
event(new Date());
};
}
});
Setup an event on a textbox as a stream of strings representing the textbox value.
Cable.define({
textbox:function(event) {
// Wireup the event function:
document.getElementById("the-textbox").onchange = function() {
event(document.getElementById("the-textbox").value);
};
// Seed it with the current value, triggering dependent nodes.
event(document.getElementById("the-textbox").value);
}
});
Create an event for the mouse position as a stream of coordinates and log the
mouse position as it moves. Note that the logPosition
effect node uses the
mousePosition
event by invoking it as a function to access its value.
Cable.define({
mousePosition:function(event) {
// Wireup the event function:
document.onmousemove = function(e) {
event({ x:e.clientX, y:e.clientY });
};
// Seed it with an initial value:
event.setDefault({ x:0, y:0 });
},
logPosition:function(mousePosition) {
console.log(mousePosition());
}
});
An effect node is written as a function with significant argument names, which
does not have any argument named result
, respond
, event
or define
. This
node type is called an effect because it is effectful. In other words, it does
not consume Cable nodes to produce a new value stream, but instead uses them to
have an effect on the system. For example, it might change the DOM, or log
something to the console.
-
function( <names of dependencies> ) { <use of dependencies> }
: Since effects are defined as functions, they must merely refer to dependencies as arguments, and make use of them in the body. The arguments may not beresult
,respond
,event
ordefine
.
Effects are for producing effects outside the cable system. Thus, they cannot be consumed by other Cable nodes.
Create an effect to consume the mouse position and log its value.
Cable.define({
logPosition:function(mousePosition) {
console.log(mousePosition());
}
});
A synthetic node synthesizes data streams out of other data streams. This is the most powerful kind of node in Cable. In a sense, event and effect nodes together represent an I/O interface (events and effects are like input and output respectively). In the same way, synthetic nodes do the actual computation in between input and output.
A synthetic node is similar to an event node in that it represents a stream of data, but differs from event nodes in that synthetic nodes do not originate data and do not need to be wired up.
A synthetic node is similar to an effect node in that it consumes other nodes to execute, but differs from effect nodes in that, instead of producing an effect on external state (such as DOM) it synthesizes a new stream of data.
Synthetic nodes have an extra feature called coalescence. With coalescence enabled, if the node computes the same result as it previously had, it will not push an update to its dependents. If coalescence is disabled, it will always push a new value to its dependents whether or not the value itself is different. Coalescence is a very important property because it allows Cable to prevent values from being uselessly recomputed, and is one of the key advantages of FRP.
A synthetic node is identified as a function for which exactly one argument is
either result
or respond
. A synthetic function produces the synthesized data
stream by passing the new value to this argument as a function. The difference
between these result
and respond
is that result
uses coalescence, but
respond
does not. In almost all cases, result
is the option that should be
used.
-
function( <names of dependencies> ) { <use of dependencies to compute a result>; }
: Since synthetic nodes are defined as functions, they must refer to dependencies as arguments, and make use them in the body. Exactly one argument must be eitherresult
orrespond
. Whichever is used (result
orrespond
) the body must pass the new value to it as an argument.
Synthetic nodes can be used in the same manner as with events, i.e. their value can be accessed by invoking them as a function.
Create an AND gate (as in circuitry) for two boolean streams of inputs named
a
and b
.
Cable.define({
andGate:function(a, b, result) { result( a() && b() ); }
});
Create a clocked NOR gate. In this case, clk
is an event node for which
invocation represents the circuit clock falling edge. The clocked behavior is
achieved by putting underscores before the names of the input dependencies,
meaning the gate will not be recomputed when they change, and instead, will be
recomputed when the clock falls. More examples of clocked circuitry are
available in the circuits flip-flop example.
Cable.define({
clockedNor:function(_a, _b, clk, result) { result( !( _a() || _b() ) ); }
});
Project a property out from another stream. For example, with the stream of mouse position, we can synthesize a stream of just the x-coordinate.
Cable.define({
xCoord:function(mousePosition, result) { result(mousePosition().x); }
});
Similarly, we can synthesize a stream of the mouse's distance from the origin.
Cable.define({
dist:function(mousePosition, result) {
result(
Math.sqrt(
Math.pow(mousePosition().x, 2) +
Math.pow(mousePosition().y, 2)
)
);
}
});
A data node is a means of storing state information inside the Cable dependency graph. They are nodes which do not depend on anything, but can be read or set by nodes which depend on them.
Note: when a data node is given a new value, it pushes that value out to the data node's dependents. As such, if a node sets a data node, but does not prefix that dependency with an underscore, it will be triggered itself in a loop. Thus, it's adviseable that one only set data nodes from nodes which have it as an underscore-prefixed dependency.
-
type
: Required. Must equal"data"
. -
value
: Required. The initial value for the data node. -
helpers
: Optional. An object of helper methods for interacting with the data node's contents. When a helper is executed,this
will refer to the getter/setter of the data node.
A node which depends on a data node is given a getter/setter function. Calling it without arguments returns its value. Calling it with a single argument reassigns the data value.
If helpers are used, they will appear as methods of the getter/setter.
Create a data node for the state of a computer game character, an effect to level up the player when an event is invoked and a logger for when the level changes.
Cable.define({
player:{
type:"data",
value:{ name:"Joe Adventurer", health:100, level:4, weapon:"sword" }
},
levelUp:function(levelUpEvent, _player) {
var newPlayer = _player();
newPlayer.level++;
_player(newPlayer);
},
playerLevel:function(player, result) { result(player().level); },
logLevel:function(playerLevel) { console.log(playerLevel()); }
});
Create counter for creating unique ids. It stores the current counter value, and
has a helper (next
) to get and store the next id.
Cable.define({
counter:{
type:"data",
value:-1,
helpers:{
next:function() {
var n = this() + 1;
this(n);
return n;
}
}
},
logButton:function(button, _counter) {
console.log("Button Press ID: " + _counter.next());
}
});
A library node represents a JavaScript library (such as jQuery, or any AMD module). If a node depends on it, the library iteslf is supplied as an argument. If the library has been already loaded, it supplies a cached version to the node. If it has not been loaded, then it is loaded first via Ajax.
In a way, this feature behaves like RequireJS and similar tools, except that the libraries are only loaded and consumed by small bits of code rather than large modules. In this way, Cable's module loader works on a finer granularity.
-
type
: Required. Must equal"library"
. -
path
: Required. A url (relative or absolute) to the JavaScript code. -
shim
: Optional. If the code being loaded is not an AMD module, specifies the name of a property to be used as the handle for the library.
A library is provided directly as an argument to dependent nodes (not as a getter like with some other node types). A library cannot trigger its dependents, so it does not make sense to underscore-prefix a library dependency.
Using jQuery to animate a div when some event is triggered.
Cable.define({
$:{ type:"library", path:"jquery.min.js" },
animateDivOnEvent:function($, myEvent) {
$("#some-div").animate({ someProperty:"someValue" });
}
});
Loading an AMD module with dependencies:
Cable.define({
jquery: { type:"library", path:"jquery.min.js" },
underscore: { type:"library", path:"underscore.min.js" }
backbone: { type:"library", path:"backbone.min.js" }
});
Cable provides a simple scoping mechanism for node definitions. Several related nodes can be grouped together in a scope by providing an object without a type property. Within this object, regular nodes can be defined and can refer to each other.
Every node defined inside a scope is defined as a top-level node with its name
prefixed with the scope name. For example: if a scope is defined named foo
,
and it defines scoped nodes named bar
and baz
, then Cable actually creates
nodes named foo_bar
and foo_baz
. However, node dependencies inside a scope
do not need to be prefixed. For example, in this case, foo_bar
can declare a
dependency named baz
and Cable will resolve it to foo_baz
.
Scopes use a special node name: main
. If there is a node named main
inside a
scope named foo
, then that node is defined as a foo
rather than foo_main
.
The two features of scoped dependency naming, and the special meaning of main
allow the definition of clean interfaces to groups of nodes.
A complex example can be seen in the Cable.list
helper. It's used in the TODO
list example code.
Sometimes it's useful to be able to access a library before defining some nodes. For example, one may want to create a node for each result of some jQuery selector. To access a library when defining a node, a subdefinition function can be used.
A subdefinition function can be identified as a function where one argument is
named define
and every other argument is the name of a library.
The value passed as the define
argument is a special version of Cable.define
which can be passed a node definition object and which will install the given
nodes within the same scope.
-
function( <names of libraries and define> ) { <use of library to create definition> }
: Function which describes how libraries are used to create a defininition.
Use jQuery to create a scoped textbox
event for every text input on the page.
Cable.define({
textboxes:function($, define) {
var obj = { };
$(":text").each(function() {
obj[this.id] = Cable.textbox("#" + this.id);
});
define(obj);
}
});
Alias nodes provide a way to create another name for some node. For example, if
some libraries refer to jQuery as $
, and others refer to it as jquery
, one
could create a library named $
and an alias named jquery
to point to $
.
-
type
: Required. Must equal"alias"
. -
reference
: Required. The name of the module being given a new name.
Alias nodes are to be used in the same way as the node they refer to.
The jQuery example as above.
Cable.define({
$:Cable.library("jquery.js"),
jquery:{ type:"alias", reference:"$" }
});
Cable has a unique module system which is separate from the loading of AMD modules via the library node type. A cable module is actually a scoped node definition which can be loaded from a separate file.
The module is defined with a name, as with any other node. When the modules are loaded, the script that the module object points to is loaded via Ajax and its contents are defined as a replacement to that module definition.
Cable modules are loaded automatically when Cable.define
is run unless both
reification and event-wireup are disabled.
-
type
: Required. Must equal"module"
. -
url
: Required. The url of the Cable module to load.
Special events are events provided by the Cable system and do not need to be defined manually.
init
is an event which is fired only once, and it is fired at the first time
that events are wired up. This can be added to a node as a dependency to make
that code run when the page is loaded in addition to when it is triggered by its
dependencies.
Constructors are helper functions for defining nodes. Some of them depend on jQuery to be used. (Note: jQuery is not required for Cable to be used, but it's required to make use of some of the constructors.)
These constructors are defined outside the closure for the Cable core system.
Create a data node with a value. Helpers can be provided as an optional second argument.
Cable.define({
userName:Cable.data("John Smith")
});
Create an event which fires at a given interval. Passing true
as the second
argument will make the interval fire once at load as well.
The period can be defined as either a number of milliseconds to wait between invocations, or a string of the name of a node to use as an interval. (If the latter is used, that node should produce a number of milliseconds. When this node changes, the interval will dynamically change its period.)
Create an event that fires every ten seconds.
Cable.define({
everyTenSeconds:Cable.interval(10000)
});
Create an event that fires on a period specified by another node.
Cable.define({
myInterval:Cale.data(60000),
onMyInterval:Cable.interval("myInterval")
});
Create a library node in Cable. Optionally provide a shim value as a second parameter.
Cable.define({
$:Cable.library("js/jquery.js")
});
Create a module node in Cable.
Cable.define({
foo:Cable.module("js/cable-modules/foo.js")
});
Depends on jQuery.
Create an event for a textbox. Pass a selector for the textbox as argument. The event data is the value of the textbox.
Cable.define({
userName:Cable.textbox("input#user-name")
});
Depends on jQuery.
Create an event for a checkbox. Pass a selector for the checkbox as argument. The event data is a boolean value representing whether the checkbox is checked.
Depends on jQuery.
Create an event for a button. Pass a selector for the button as argument. The event data is a stream of timestamps of moments when the button was pressed.
Depends on jQuery.
Create an event for when the return key is pressed on a textbox. Pass a selector for the textbox as argument. The event data is a stream of timestamps of moments when the key was pressed.
Depends on jQuery.
Define a templating effect for the contents of some element. Provide a selector for the element to be updated. Provide a string template describing how other nodes are to be rendered into the result.
The template must follow a specific format. Dependencies are declared inside of
{{
and }}
brackets. For example, the template {{name}} - {{time}}
will
depend on the name
and time
nodes and render them as strings separated by
-
. This rendered result is used as the innerHTML
of the elements
identified by the selector
argument.
Render a clock:
Cable.define({
timer:Cable.interval(1000), // Every second:
timeString:function(timer, result) {
result(
timer().getHours() + ":" +
timer().getMinutes() + ":" +
timer().getSeconds()
);
}
showTime:Cable.template("#clock-face", "The time is {{timeString}}")
});
Depends on jQuery.
Incorporate remote JSON data into the dependency graph. Provide a synthetic function definition as argument, which produces a URL to get fetched.
Note: Because the contents of a JSON response may change outside the Cable
dependency graph, this is one instance where the programmer may wish to use a
respond
style synthetic instead of a result
style one.
This is a simplified version of the "stock-quote" example in the examples.
Cable.define({
stockSymbol:Cable.textbox("#stock-symbol"),
remoteData:Cable.json(function(stockSymbol, respond) {
respond("http://example.com/stock-api.php?symbol=" + stockSymbol());
})
});
Depends on jQuery.
Load some remote text over Ajax, but only if it is needed. This is useful for loading template files, for example.