Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Javelin Reactor, a Functional Reactive Programming library

Summary:
A Functional Reactive Programming library for Javelin,
inspired by Flapjax's library mode.
 * DOM manipulation, coding style, infrastructure and utilities provided by
Javelin
 * Reverse post-order traversal instead of priority queue for event propagation
to deal with cycles
 * Flapjax's "behaviors" are called dynamic values, or "DynVals", to avoid
conflicting with Javelin Behaviors
 * Flapjax's "event streams" are called reactor nodes, or "ReactorNodes", to
avoid conflicting with browser Events

Test Plan: Example page to be updated and released later.

Reviewers: epriestley

Reviewed By: epriestley

CC: aran, epriestley

Differential Revision: 848
  • Loading branch information...
commit 14d3ba10a7f885639cd679d3f7ffd59b68a472d1 1 parent 0cdaf85
adonohue authored
View
48 src/ext/reactor/core/DynVal.js
@@ -0,0 +1,48 @@
+/**
+ * @provides javelin-dynval
+ * @requires javelin-install
+ * javelin-reactornode
+ * javelin-util
+ * javelin-reactor
+ * @javelin
+ */
+
+JX.install('DynVal', {
+ members : {
+ _lastPulseVal : null,
+ _reactorNode : null,
+ getValueNow : function() {
+ return this._lastPulseVal;
+ },
+ getChanges : function() {
+ return this._reactorNode;
+ },
+ forceValueNow : function(value) {
+ this.getChanges().forceSendValue(value);
+ },
+ transform : function(fn) {
+ return new JX.DynVal(
+ this.getChanges().transform(fn),
+ fn(this.getValueNow())
+ );
+ },
+ calm : function(min_interval) {
+ return new JX.DynVal(
+ this.getChanges().calm(min_interval),
+ this.getValueNow()
+ );
+ }
+ },
+ construct : function(stream, init) {
+ this._lastPulseVal = init;
+ this._reactorNode =
+ new JX.ReactorNode([stream], JX.bind(this, function(pulse) {
+ if (this._lastPulseVal == pulse) {
+ return JX.Reactor.DoNotPropagate;
+ }
+ this._lastPulseVal = pulse;
+ return pulse;
+ }));
+ }
+});
+
View
92 src/ext/reactor/core/Reactor.js
@@ -0,0 +1,92 @@
+/**
+ * @provides javelin-reactor
+ * @requires javelin-install
+ * javelin-dynval
+ * javelin-reactornode
+ * javelin-util
+ * @javelin
+ */
+
+JX.install('Reactor', {
+ statics : {
+ /**
+ * Return this value from a ReactorNode transformer to indicate that
+ * its listeners should not be activated.
+ */
+ DoNotPropagate : {},
+ /**
+ * For internal use by the Reactor system.
+ */
+ propagatePulse : function(start_pulse, start_node) {
+ var reverse_post_order =
+ JX.Reactor._postOrder(start_node).reverse();
+ start_node.primeValue(start_pulse);
+
+ for (var ix = 0; ix < reverse_post_order.length; ix++) {
+ var node = reverse_post_order[ix];
+ var pulse = node.getNextPulse();
+ if (pulse === JX.Reactor.DoNotPropagate) {
+ continue;
+ }
+
+ var next_pulse = node.getTransformer()(pulse);
+ var sends_to = node.getListeners();
+ for (var jx = 0; jx < sends_to.length; jx++) {
+ sends_to[jx].primeValue(next_pulse);
+ }
+ }
+ },
+ /**
+ * For internal use by the Reactor system.
+ */
+ _postOrder : function(node, result, pending) {
+ if (typeof result === "undefined") {
+ result = [];
+ pending = {};
+ }
+ pending[node.getGraphID()] = true;
+
+ var nexts = node.getListeners();
+ for (var ix = 0; ix < nexts.length; ix++) {
+ var next = nexts[ix];
+ if (pending[next.getGraphID()]) {
+ continue;
+ }
+ JX.Reactor._postOrder(next, result, pending);
+ }
+
+ result.push(node);
+ return result;
+ },
+
+ // Helper for lift.
+ _valueNow : function(fn, dynvals) {
+ var values = [];
+ for (var ix = 0; ix < dynvals.length; ix++) {
+ values.push(dynvals[ix].getValueNow());
+ }
+ return fn.apply(null, values);
+ },
+
+ /**
+ * Lift a function over normal values to be a function over dynvals.
+ * @param fn A function expecting normal values
+ * @param dynvals Array of DynVals whose instaneous values will be passed
+ * to fn.
+ * @return A DynVal representing the changing value of fn applies to dynvals
+ * over time.
+ */
+ lift : function(fn, dynvals) {
+ var valueNow = JX.bind(null, JX.Reactor._valueNow, fn, dynvals);
+
+ var streams = [];
+ for (var ix = 0; ix < dynvals.length; ix++) {
+ streams.push(dynvals[ix].getChanges());
+ }
+
+ var result = new JX.ReactorNode(streams, valueNow);
+ return new JX.DynVal(result, valueNow());
+ }
+ }
+});
+
View
97 src/ext/reactor/core/ReactorNode.js
@@ -0,0 +1,97 @@
+/**
+ * @provides javelin-reactornode
+ * @requires javelin-install
+ * javelin-reactor
+ * javelin-util
+ * javelin-reactor-node-calmer
+ * @javelin
+ */
+
+JX.install('ReactorNode', {
+ members : {
+ _transformer : null,
+ _sendsTo : null,
+ _nextPulse : null,
+ _graphID : null,
+
+ getGraphID : function() {
+ return this._graphID || this.__id__;
+ },
+
+ setGraphID : function(id) {
+ this._graphID = id;
+ return this;
+ },
+
+ setTransformer : function(fn) {
+ this._transformer = fn;
+ return this;
+ },
+
+ /**
+ * Set up dest as a listener to this.
+ */
+ listen : function(dest) {
+ this._sendsTo[dest.__id__] = dest;
+ return { remove : JX.bind(null, this._removeListener, dest) };
+ },
+ /**
+ * Helper for listen.
+ */
+ _removeListener : function(dest) {
+ delete this._sendsTo[dest.__id__];
+ },
+ /**
+ * For internal use by the Reactor system
+ */
+ primeValue : function(value) {
+ this._nextPulse = value;
+ },
+ getListeners : function() {
+ var result = [];
+ for (var k in this._sendsTo) {
+ result.push(this._sendsTo[k]);
+ }
+ return result;
+ },
+ /**
+ * For internal use by the Reactor system
+ */
+ getNextPulse : function(pulse) {
+ return this._nextPulse;
+ },
+ getTransformer : function() {
+ return this._transformer;
+ },
+ forceSendValue : function(pulse) {
+ JX.Reactor.propagatePulse(pulse, this);
+ },
+ // fn should return JX.Reactor.DoNotPropagate to indicate a value that
+ // should not be retransmitted.
+ transform : function(fn) {
+ return new JX.ReactorNode([this], fn);
+ },
+
+ /**
+ * Suppress events to happen at most once per min_interval.
+ * The last event that fires within an interval will fire at the end
+ * of the interval. Events that are sandwiched between other events
+ * within an interval are dropped.
+ */
+ calm : function(min_interval) {
+ var result = new JX.ReactorNode([this], JX.id);
+ var transformer = new JX.ReactorNodeCalmer(result, min_interval);
+ result.setTransformer(JX.bind(transformer, transformer.onPulse));
+ return result;
+ }
+ },
+ construct : function(source_streams, transformer) {
+ this._nextPulse = JX.Reactor.DoNotPropagate;
+ this._transformer = transformer;
+ this._sendsTo = {};
+ for (var ix = 0; ix < source_streams.length; ix++) {
+ source_streams[ix].listen(this);
+ }
+ }
+});
+
View
48 src/ext/reactor/core/ReactorNodeCalmer.js
@@ -0,0 +1,48 @@
+/**
+ * @provides javelin-reactor-node-calmer
+ * @requires javelin-install
+ * javelin-reactor
+ * javelin-util
+ * @javelin
+ */
+
+JX.install('ReactorNodeCalmer', {
+ properties : {
+ lastTime : 0,
+ timeout : null,
+ minInterval : 0,
+ reactorNode : null,
+ isEnabled : true
+ },
+ construct : function(node, min_interval) {
+ this.setLastTime(-min_interval);
+ this.setMinInterval(min_interval);
+ this.setReactorNode(node);
+ },
+ members: {
+ onPulse : function(pulse) {
+ if (!this.getIsEnabled()) {
+ return pulse;
+ }
+ var current_time = new Date().getTime();
+ if (current_time - this.getLastTime() > this.getMinInterval()) {
+ this.setLastTime(current_time);
+ return pulse;
+ } else {
+ clearTimeout(this.getTimeout());
+ this.setTimeout(setTimeout(
+ JX.bind(this, this.send, pulse),
+ this.getLastTime() + this.getMinInterval() - current_time
+ ));
+ return JX.Reactor.DoNotPropagate;
+ }
+ },
+ send : function(pulse) {
+ this.setLastTime(new Date().getTime());
+ this.setIsEnabled(false);
+ this.getReactorNode().forceSendValue(pulse);
+ this.setIsEnabled(true);
+ }
+ }
+});
+
View
406 src/ext/reactor/dom/RDOM.js
@@ -0,0 +1,406 @@
+/**
+ * Javelin Reactive functions to work with the DOM.
+ * @provides javelin-reactor-dom
+ * @requires javelin-dom
+ * javelin-dynval
+ * javelin-reactornode
+ * javelin-install
+ * javelin-util
+ * @javelin
+ */
+JX.install('RDOM', {
+ statics : {
+ _time : null,
+ /**
+ * DynVal of the current time in milliseconds.
+ */
+ time : function() {
+ if (JX.RDOM._time === null) {
+ var time = new JX.ReactorNode([], JX.id);
+ window.setInterval(function() {
+ time.forceSendValue(new Date().getTime());
+ }, 100);
+ JX.RDOM._time = new JX.DynVal(time, new Date().getTime());
+ }
+ return JX.RDOM._time;
+ },
+
+ /**
+ * Given a DynVal[String], return a DOM text node whose value tracks it.
+ */
+ $DT : function(dyn_string) {
+ var node = document.createTextNode(dyn_string.getValueNow());
+ dyn_string.transform(function(s) { node.data = s; });
+ return node;
+ },
+
+ _recvEventPulses : function(node, event) {
+ var reactor_node = new JX.ReactorNode([], JX.id);
+ var no_path = null;
+ JX.DOM.listen(
+ node,
+ event,
+ no_path,
+ JX.bind(reactor_node, reactor_node.forceSendValue)
+ );
+
+ reactor_node.setGraphID(JX.DOM.uniqID(node));
+ return reactor_node;
+ },
+
+ _recvChangePulses : function(node) {
+ return JX.RDOM._recvEventPulses(node, 'change').transform(function() {
+ return node.value;
+ });
+ },
+
+
+ /**
+ * Sets up a bidirectional DynVal for a node.
+ * @param node :: DOM Node
+ * @param inPulsesFn :: DOM Node -> ReactorNode
+ * @param inDynValFn :: DOM Node -> ReactorNode -> DynVal
+ * @param outFn :: ReactorNode -> DOM Node
+ */
+ _bidi : function(node, inPulsesFn, inDynValFn, outFn) {
+ var inPulses = inPulsesFn(node);
+ var inDynVal = inDynValFn(node, inPulses);
+ outFn(inDynVal.getChanges(), node);
+ inDynVal.getChanges().listen(inPulses);
+ return inDynVal;
+ },
+
+ /**
+ * ReactorNode[String] of the incoming values of a radio group.
+ * @param Array of DOM elements, all the radio buttons in a group.
+ */
+ _recvRadioPulses : function(buttons) {
+ var ins = [];
+ for (var ii = 0; ii < buttons.length; ii++) {
+ ins.push(JX.RDOM._recvChangePulses(buttons[ii]));
+ }
+ return new JX.ReactorNode(ins, JX.id);
+ },
+
+ /**
+ * DynVal[String] of the incoming values of a radio group.
+ * pulses is a ReactorNode[String] of the incoming values of the group
+ */
+ _recvRadio : function(buttons, pulses) {
+ var init = '';
+ for (var ii = 0; ii < buttons.length; ii++) {
+ if (buttons[ii].checked) {
+ init = buttons[ii].value;
+ break;
+ }
+ }
+
+ return new JX.DynVal(pulses, init);
+ },
+
+ /**
+ * Send the pulses from the ReactorNode[String] to the radio group.
+ * Sending an invalid value will result in a log message in __DEV__.
+ */
+ _sendRadioPulses : function(rnode, buttons) {
+ return rnode.transform(function(val) {
+ var found;
+ if (__DEV__) {
+ found = false;
+ }
+
+ for (var ii = 0; ii < buttons.length; ii++) {
+ if (buttons[ii].value == val) {
+ buttons[ii].checked = true;
+ if (__DEV__) {
+ found = true;
+ }
+ }
+ }
+
+ if (__DEV__) {
+ if (!found) {
+ throw new Error("Mismatched radio button value");
+ }
+ }
+ });
+ },
+
+ /**
+ * Bidirectional DynVal[String] for a radio group.
+ * Sending an invalid value will result in a log message in __DEV__.
+ */
+ radio : function(input) {
+ return JX.RDOM._bidi(
+ input,
+ JX.RDOM._recvRadioPulses,
+ JX.RDOM._recvRadio,
+ JX.RDOM._sendRadioPulses
+ );
+ },
+
+ /**
+ * ReactorNode[Boolean] of the values of the checkbox when it changes.
+ */
+ _recvCheckboxPulses : function(checkbox) {
+ return JX.RDOM._recvChangePulses(checkbox).transform(function(val) {
+ return Boolean(val);
+ });
+ },
+
+ /**
+ * DynVal[Boolean] of the value of a checkbox.
+ */
+ _recvCheckbox : function(checkbox, pulses) {
+ return new JX.DynVal(pulses, Boolean(checkbox.checked));
+ },
+
+ /**
+ * Send the pulses from the ReactorNode[Boolean] to the checkbox
+ */
+ _sendCheckboxPulses : function(rnode, checkbox) {
+ return rnode.transform(function(val) {
+ if (__DEV__) {
+ if (!(val === true || val === false)) {
+ throw new Error("Send boolean values to checkboxes.");
+ }
+ }
+
+ checkbox.checked = val;
+ });
+ },
+
+ /**
+ * Bidirectional DynVal[Boolean] for a checkbox.
+ */
+ checkbox : function(input) {
+ return JX.RDOM._bidi(
+ input,
+ JX.RDOM._recvCheckboxPulses,
+ JX.RDOM._recvCheckbox,
+ JX.RDOM._sendCheckboxPulses
+ );
+ },
+
+ /**
+ * ReactorNode[String] of the changing values of a text input.
+ */
+ _recvInputPulses : function(input) {
+ // This misses advanced changes like paste events.
+ var live_changes = [
+ JX.RDOM._recvChangePulses(input),
+ JX.RDOM._recvEventPulses(input, 'keyup'),
+ JX.RDOM._recvEventPulses(input, 'keypress'),
+ JX.RDOM._recvEventPulses(input, 'keydown')
+ ];
+
+ return new JX.ReactorNode(live_changes, function() {
+ return input.value;
+ });
+ },
+
+ /**
+ * DynVal[String] of the value of a text input.
+ */
+ _recvInput : function(input, pulses) {
+ return new JX.DynVal(pulses, input.value);
+ },
+
+ /**
+ * Send the pulses from the ReactorNode[String] to the input
+ */
+ _sendInputPulses : function(rnode, input) {
+ var result = rnode.transform(function(val) {
+ input.value = val;
+ });
+ result.setGraphID(JX.DOM.uniqID(input));
+ return result;
+ },
+
+
+ /**
+ * Bidirectional DynVal[String] for a text input.
+ */
+ input : function(input) {
+ return JX.RDOM._bidi(
+ input,
+ JX.RDOM._recvInputPulses,
+ JX.RDOM._recvInput,
+ JX.RDOM._sendInputPulses
+ );
+ },
+
+ /**
+ * ReactorNode[String] of the incoming changes in value of a select element.
+ */
+ _recvSelectPulses : function(select) {
+ return JX.RDOM._recvChangePulses(select);
+ },
+
+ /**
+ * DynVal[String] of the value of a select element.
+ */
+ _recvSelect : function(select, pulses) {
+ return new JX.DynVal(pulses, select.value);
+ },
+
+ /**
+ * Send the pulses from the ReactorNode[String] to the select.
+ * Sending an invalid value will result in a log message in __DEV__.
+ */
+ _sendSelectPulses : function(rnode, select) {
+ return rnode.transform(function(val) {
+ select.value = val;
+
+ if (__DEV__) {
+ if (select.value !== val) {
+ throw new Error("Mismatched select value");
+ }
+ }
+ });
+ },
+
+ /**
+ * Bidirectional DynVal[String] for the value of a select.
+ */
+ select : function(select) {
+ return JX.RDOM._bidi(
+ select,
+ JX.RDOM._recvSelectPulses,
+ JX.RDOM._recvSelect,
+ JX.RDOM._sendSelectPulses
+ );
+ },
+
+ /**
+ * ReactorNode[undefined] that fires when a button is clicked.
+ */
+ clickPulses : function(button) {
+ return JX.RDOM._recvEventPulses(button, 'click').transform(function() {
+ return null;
+ });
+ },
+
+ /**
+ * ReactorNode[Boolean] of whether the mouse is over a target.
+ */
+ _recvIsMouseOverPulses : function(target) {
+ var mouseovers = JX.RDOM._recvEventPulses(target, 'mouseover').transform(
+ function() {
+ return true;
+ });
+ var mouseouts = JX.RDOM._recvEventPulses(target, 'mouseout').transform(
+ function() {
+ return false;
+ });
+
+ return new JX.ReactorNode([mouseovers, mouseouts], JX.id);
+ },
+
+ /**
+ * DynVal[Boolean] of whether the mouse is over a target.
+ */
+ isMouseOver : function(target) {
+ // Not worth it to initialize this properly.
+ return new JX.DynVal(JX.RDOM._recvIsMouseOverPulses(target), false);
+ },
+
+ /**
+ * ReactorNode[Boolean] of whether an element has the focus.
+ */
+ _recvHasFocusPulses : function(target) {
+ var focuses = JX.RDOM._recvEventPulses(target, 'focus').transform(
+ function() {
+ return true;
+ });
+ var blurs = JX.RDOM._recvEventPulses(target, 'blur').transform(
+ function() {
+ return false;
+ });
+
+ return new JX.ReactorNode([focuses, blurs], JX.id);
+ },
+
+ /**
+ * DynVal[Boolean] of whether an element has the focus.
+ */
+ _recvHasFocus : function(target) {
+ var is_focused_now = (target === document.activeElement);
+ return new JX.DynVal(JX.RDOM._recvHasFocusPulses(target), is_focused_now);
+ },
+
+ _sendHasFocusPulses : function(rnode, target) {
+ rnode.transform(function(should_focus) {
+ if (should_focus) {
+ target.focus();
+ } else {
+ target.blur();
+ }
+ return should_focus;
+ });
+ },
+
+ /**
+ * Bidirectional DynVal[Boolean] of whether an element has the focus.
+ */
+ hasFocus : function(target) {
+ return JX.RDOM._bidi(
+ target,
+ JX.RDOM._recvHasFocusPulses,
+ JX.RDOM._recvHasFocus,
+ JX.RDOM._sendHasFocusPulses
+ );
+ },
+
+ /**
+ * Send a CSS class from a DynVal to a node
+ */
+ sendClass : function(dynval, node, className) {
+ return dynval.transform(function(add) {
+ JX.DOM.alterClass(node, className, add);
+ });
+ },
+
+ /**
+ * Dynamically attach a set of DynVals to a DOM node's properties as
+ * specified by props.
+ * props: {left: someDynVal, style: {backgroundColor: someOtherDynVal}}
+ */
+ sendProps : function(node, props) {
+ var dynvals = [];
+ var keys = [];
+ var style_keys = [];
+ for (var key in props) {
+ keys.push(key);
+ if (key === 'style') {
+ for (var style_key in props[key]) {
+ style_keys.push(style_key);
+ dynvals.push(props[key][style_key]);
+ node.style[style_key] = props[key][style_key].getValueNow();
+ }
+ } else {
+ dynvals.push(props[key]);
+ node[key] = props[key].getValueNow();
+ }
+ }
+
+ return JX.Reactor.lift(JX.bind(null, function(keys, style_keys, node) {
+ var args = JX.$A(arguments).slice(3);
+
+ for (var ii = 0; ii < args.length; ii++) {
+ if (keys[ii] === 'style') {
+ for (var jj = 0; jj < style_keys.length; jj++) {
+ node.style[style_keys[jj]] = args[ii];
+ ii++;
+ }
+ ii--;
+ } else {
+ node[keys[ii]] = args[ii];
+ }
+ }
+ }, keys, style_keys, node), dynvals);
+ }
+ }
+});
+
+
Please sign in to comment.
Something went wrong with that request. Please try again.