redux-actuator
Advanced tools
Comparing version 1.0.0 to 2.0.1
@@ -7,3 +7,3 @@ 'use strict'; | ||
it('should create flux standard action with type ACTUATE', function () { | ||
var action = (0, _actions.actuate)(); | ||
var action = (0, _actions.actuate)('f00z'); | ||
expect(action.type).toBe(_actions.ACTUATE); | ||
@@ -13,10 +13,10 @@ expect(action.payload).toMatchObject({}); | ||
it('creates ACTUATE action on a default channel', function () { | ||
var action = (0, _actions.actuate)(); | ||
expect(action.payload.channel).toBe('default'); | ||
it('uses first arg as a name of the channel', function () { | ||
var action = (0, _actions.actuate)('foo'); | ||
expect(action.payload.channel).toBe('foo'); | ||
}); | ||
it('takes the name of the event as a first arg', function () { | ||
it('puts an empty array if args are not specified', function () { | ||
var action = (0, _actions.actuate)('foo'); | ||
expect(action.payload.event.type).toBe('foo'); | ||
expect(action.payload.args).toEqual([]); | ||
}); | ||
@@ -26,4 +26,4 @@ | ||
var action = (0, _actions.actuate)('foo', 'what', 'the', 'hell'); | ||
expect(action.payload.event.args).toEqual(['what', 'the', 'hell']); | ||
expect(action.payload.args).toEqual(['what', 'the', 'hell']); | ||
}); | ||
}); |
'use strict'; | ||
require('raf/polyfill'); | ||
var _enzyme = require('enzyme'); | ||
var _enzyme2 = _interopRequireDefault(_enzyme); | ||
var _react = require('react'); | ||
@@ -13,2 +17,6 @@ | ||
var _enzymeAdapterReact = require('enzyme-adapter-react-16'); | ||
var _enzymeAdapterReact2 = _interopRequireDefault(_enzymeAdapterReact); | ||
var _index = require('../index'); | ||
@@ -20,2 +28,4 @@ | ||
_enzyme2.default.configure({ adapter: new _enzymeAdapterReact2.default() }); | ||
var delay = function delay(ms) { | ||
@@ -36,3 +46,3 @@ return new Promise(function (resolve) { | ||
describe('Actuator component', function () { | ||
it('catches events on default channel', function () { | ||
it('catches an event via on prop', function () { | ||
var store = createStore(); | ||
@@ -44,3 +54,3 @@ var eventHandler = jest.fn(); | ||
{ store: store }, | ||
_react2.default.createElement(_index.Actuator, { events: { foo: eventHandler } }) | ||
_react2.default.createElement(_index.Actuator, { channel: 'foo', on: eventHandler }) | ||
)); | ||
@@ -59,3 +69,3 @@ | ||
{ store: store }, | ||
_react2.default.createElement(_index.Actuator, { events: { foo: eventHandler } }) | ||
_react2.default.createElement(_index.Actuator, { channel: 'foo', on: eventHandler }) | ||
)); | ||
@@ -79,3 +89,3 @@ | ||
{ store: store }, | ||
_react2.default.createElement(_index.Actuator, { events: { foo: eventHandler } }) | ||
_react2.default.createElement(_index.Actuator, { channel: 'foo', on: eventHandler }) | ||
)); | ||
@@ -97,3 +107,3 @@ | ||
{ store: store }, | ||
_react2.default.createElement(_index.Actuator, { events: { baz: eventHandler }, deltaError: 40 }) | ||
_react2.default.createElement(_index.Actuator, { channel: 'baz', on: eventHandler, deltaError: 100 }) | ||
)); | ||
@@ -115,3 +125,3 @@ | ||
{ store: store }, | ||
_react2.default.createElement(_index.Actuator, { events: { bar: eventHandler }, deltaError: 25 }) | ||
_react2.default.createElement(_index.Actuator, { channel: 'bar', on: eventHandler, deltaError: 25 }) | ||
)); | ||
@@ -122,2 +132,29 @@ | ||
}); | ||
it('allows to subscribe to multiple chans at once', function () { | ||
var store = createStore(); | ||
var handlerA = jest.fn(); | ||
var handlerB = jest.fn(); | ||
var handlerC = jest.fn(); | ||
(0, _enzyme.mount)(_react2.default.createElement( | ||
_reactRedux.Provider, | ||
{ store: store }, | ||
_react2.default.createElement(_index.Actuator, { | ||
on: { | ||
foo: handlerA, | ||
bar: handlerB, | ||
baz: handlerC | ||
} | ||
}) | ||
)); | ||
store.dispatch((0, _index.actuate)('foo')); | ||
store.dispatch((0, _index.actuate)('bar')); | ||
expect(handlerA).toHaveBeenCalled(); | ||
expect(handlerB).toHaveBeenCalled(); | ||
expect(handlerC).not.toHaveBeenCalled(); | ||
}); | ||
}); |
@@ -22,3 +22,3 @@ 'use strict'; | ||
type: _actions.ACTUATE, | ||
payload: { channel: 'm00t', event: { timestamp: 1337 } } | ||
payload: { channel: 'm00t', timestamp: 1337 } | ||
}); | ||
@@ -25,0 +25,0 @@ |
@@ -6,3 +6,3 @@ 'use strict'; | ||
}); | ||
exports.actuateChannel = exports.actuate = exports.ACTUATE = undefined; | ||
exports.actuate = exports.ACTUATE = undefined; | ||
@@ -14,26 +14,32 @@ var _currentTimestamp = require('./utils/currentTimestamp'); | ||
var actuateFactory = function actuateFactory(channel) { | ||
return function (eventType) { | ||
for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { | ||
args[_key - 1] = arguments[_key]; | ||
} | ||
var clock = 0; | ||
var timestamp = (0, _currentTimestamp.currentTimestamp)(); | ||
var genKey = function genKey(ts, clock) { | ||
return ts + '::' + clock; | ||
}; | ||
return { | ||
type: ACTUATE, | ||
payload: { | ||
channel: channel, | ||
event: { | ||
type: eventType, | ||
timestamp: timestamp, | ||
args: args | ||
} | ||
} | ||
}; | ||
var actuate = exports.actuate = function actuate(channel) { | ||
for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { | ||
args[_key - 1] = arguments[_key]; | ||
} | ||
// channel has to be specified | ||
if (!channel) { | ||
var warning = 'Triggering actions without a channel is no longer ' + 'supported by `redux-actuator`. These actions will be ' + 'ingored by the actuator.'; | ||
console.warn(warning); | ||
} | ||
var timestamp = (0, _currentTimestamp.currentTimestamp)(); | ||
clock += 1; | ||
return { | ||
type: ACTUATE, | ||
payload: { | ||
key: genKey(timestamp, clock), | ||
channel: channel, | ||
timestamp: timestamp, | ||
args: args | ||
} | ||
}; | ||
}; | ||
// Action creators | ||
var actuate = exports.actuate = actuateFactory('default'); | ||
var actuateChannel = exports.actuateChannel = actuateFactory; | ||
}; |
@@ -6,5 +6,8 @@ 'use strict'; | ||
}); | ||
exports.Actuator = undefined; | ||
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; | ||
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; | ||
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); | ||
var _react = require('react'); | ||
@@ -20,36 +23,169 @@ | ||
var _ActuatorInner = require('./ActuatorInner'); | ||
var _currentTimestamp = require('../utils/currentTimestamp'); | ||
var _ActuatorInner2 = _interopRequireDefault(_ActuatorInner); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; } | ||
function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } | ||
var Actuator = function Actuator(_ref) { | ||
var event = _ref.event, | ||
events = _ref.events, | ||
props = _objectWithoutProperties(_ref, ['event', 'events']); | ||
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } | ||
return _react2.default.createElement(_ActuatorInner2.default, _extends({ event: event, handlers: events }, props)); | ||
}; | ||
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } | ||
var mapStateToProps = function mapStateToProps(state, ownProps) { | ||
var channels = state.actuator || {}; | ||
var event = channels[ownProps.channel]; | ||
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } | ||
return { event: event }; | ||
}; | ||
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } | ||
var WrappedActuator = (0, _reactRedux.connect)(mapStateToProps)(Actuator); | ||
var Actuator = exports.Actuator = function (_React$Component) { | ||
_inherits(Actuator, _React$Component); | ||
WrappedActuator.propTypes = { | ||
function Actuator(props, context) { | ||
_classCallCheck(this, Actuator); | ||
// Used to store last visible keys on per-channel basis | ||
// format channel -> last key | ||
var _this = _possibleConstructorReturn(this, (Actuator.__proto__ || Object.getPrototypeOf(Actuator)).call(this, props, context)); | ||
_this._keyCache = {}; | ||
return _this; | ||
} | ||
_createClass(Actuator, [{ | ||
key: 'getChanHandlers', | ||
value: function getChanHandlers() { | ||
var _props = this.props, | ||
on = _props.on, | ||
channel = _props.channel; | ||
if (typeof on === 'function') { | ||
return _defineProperty({}, channel, on); | ||
} | ||
return on; | ||
} | ||
}, { | ||
key: 'getObservedChannels', | ||
value: function getObservedChannels() { | ||
return Object.keys(this.getChanHandlers()); | ||
} | ||
}, { | ||
key: 'onStateChanged', | ||
value: function onStateChanged() { | ||
var state = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; | ||
state = state || this.context.store.getState(); | ||
if (!state.actuator) return null; | ||
var channels = this.getObservedChannels(); | ||
for (var _len = arguments.length, restOpts = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { | ||
restOpts[_key - 1] = arguments[_key]; | ||
} | ||
for (var i = 0; i < channels.length; ++i) { | ||
var chan = channels[i]; | ||
var event = state.actuator[chan]; | ||
if (!event) continue; | ||
this.checkEvent.apply(this, [chan, event].concat(restOpts)); | ||
} | ||
} | ||
}, { | ||
key: 'callChanHandler', | ||
value: function callChanHandler(channel, args) { | ||
var handlers = this.getChanHandlers(); | ||
handlers[channel] && handlers[channel].apply(handlers, _toConsumableArray(args)); | ||
} | ||
}, { | ||
key: 'checkEvent', | ||
value: function checkEvent(channel, event) { | ||
var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; | ||
// Compare event keys (they have to be unique) | ||
var _prevKey = this._keyCache[channel]; | ||
if (_prevKey && _prevKey === event.key) { | ||
return; | ||
} | ||
this._keyCache[channel] = event.key; | ||
// Special case when the event was dispatched before | ||
// the mount. If you need to handle cases like | ||
// this one, pass `deltaError` prop, which literally means: | ||
// "handle missed events on mount if it's younger than `deltaError`" | ||
var shouldHandleOnMount = false; | ||
var deltaError = this.props.deltaError; | ||
if (options.onMount && deltaError) { | ||
var now = (0, _currentTimestamp.currentTimestamp)(); | ||
var delta = Math.abs(now - event.timestamp); | ||
shouldHandleOnMount = delta <= deltaError; | ||
} | ||
if (options.onMount && !shouldHandleOnMount) { | ||
return; | ||
} | ||
this.callChanHandler(channel, event.args); | ||
} | ||
}, { | ||
key: 'componentDidMount', | ||
value: function componentDidMount() { | ||
var _this2 = this; | ||
var store = this.context.store; | ||
if (store) { | ||
this._sub = store.subscribe(function () { | ||
return _this2.onStateChanged(); | ||
}); | ||
// trigger on mount check | ||
var state = store.getState(); | ||
this.onStateChanged(state, { onMount: true }); | ||
} | ||
} | ||
}, { | ||
key: 'componentWillUnmount', | ||
value: function componentWillUnmount() { | ||
this._sub && this._sub(); | ||
} | ||
}, { | ||
key: 'render', | ||
value: function render() { | ||
if (!this.props.children) return null; | ||
return this.props.children; | ||
} | ||
}]); | ||
return Actuator; | ||
}(_react2.default.Component); | ||
Actuator.propTypes = { | ||
channel: _propTypes2.default.string, | ||
events: _propTypes2.default.object.isRequired | ||
on: _propTypes2.default.oneOfType([ | ||
// Can be simple event handler of a map of handlers | ||
// e.g. { chanOne: ..., chanTwo: ... } | ||
_propTypes2.default.func, _propTypes2.default.objectOf(_propTypes2.default.func)]).isRequired, | ||
deltaError: _propTypes2.default.number, | ||
// Always require `channel` prop, unless the map | ||
// of listeners wasn't passed with `on` prop. | ||
_requireChannel: function _requireChannel(props) { | ||
var isMapOfHandlers = props.on !== null && _typeof(props.on) === 'object'; | ||
if (!props.channel && !isMapOfHandlers) { | ||
return new Error('channel is required'); | ||
} | ||
} | ||
}; | ||
WrappedActuator.defaultProps = { | ||
channel: 'default' | ||
Actuator.contextTypes = { | ||
store: _propTypes2.default.object | ||
}; | ||
exports.default = WrappedActuator; | ||
exports.default = Actuator; |
@@ -25,10 +25,10 @@ 'use strict'; | ||
var event = rest.event || {}; | ||
// Ignore mailformed actions | ||
// Channel isn't given, ignore | ||
if (!channel) { | ||
if (!channel || !rest) { | ||
return state; | ||
} | ||
return _extends({}, state, _defineProperty({}, channel, event)); | ||
return _extends({}, state, _defineProperty({}, channel, rest)); | ||
} | ||
@@ -35,0 +35,0 @@ return state; |
@@ -6,4 +6,20 @@ "use strict"; | ||
}); | ||
var currentTimestamp = exports.currentTimestamp = function currentTimestamp() { | ||
return new Date().getTime(); | ||
}; | ||
var currentTimestamp = function () { | ||
var _window = window, | ||
performance = _window.performance; | ||
if (performance && performance.now) { | ||
return function () { | ||
return performance.now(); | ||
}; | ||
} | ||
// use a fallback if `performance.now` | ||
// isn't supported by the browser. | ||
return function () { | ||
return new Date().getTime(); | ||
}; | ||
}(); | ||
exports.currentTimestamp = currentTimestamp; |
@@ -5,3 +5,3 @@ { | ||
"license": "MIT", | ||
"version": "1.0.0", | ||
"version": "2.0.1", | ||
"repository": "molefrog/redux-actuator", | ||
@@ -28,15 +28,15 @@ "description": "Communicate between components through Redux store", | ||
"babel-plugin-transform-object-rest-spread": "^6.20.2", | ||
"babel-preset-env": "^1.6.1", | ||
"babel-preset-react": "^6.16.0", | ||
"babel-preset-env": "^1.6.1", | ||
"enzyme": "^2.7.0", | ||
"enzyme": "^3.2.0", | ||
"enzyme-adapter-react-16": "^1.1.0", | ||
"jest": "^18.1.0", | ||
"react": "^15.4.1", | ||
"react-addons-test-utils": "^15.4.1", | ||
"react-dom": "^15.4.1", | ||
"react-redux": "^5.0.1", | ||
"redux": "^3.6.0" | ||
"raf": "^3.4.0", | ||
"react": "^16.2.0", | ||
"react-dom": "^16.2.0", | ||
"react-redux": "^5.0.6", | ||
"redux": "^3.7.2" | ||
}, | ||
"peerDependencies": { | ||
"react": ">= 0.14.0", | ||
"react-redux": ">= 4.0.0", | ||
"redux": "^2.0.0 || ^3.0.0" | ||
@@ -43,0 +43,0 @@ }, |
@@ -45,2 +45,3 @@ # Redux Actuator | ||
npm install --save redux-actuator | ||
# or yarn add redux-actuator | ||
``` | ||
@@ -70,3 +71,3 @@ | ||
In order to do that simply put `<Actuator>` component and specify how events should be handled: | ||
In order to do that simply put `<Actuator>` component with channel provided. | ||
@@ -85,3 +86,3 @@ ```JavaScript | ||
return ( | ||
<Actuator events={{ say: (phrase) => this.saySomething(phrase) }}> | ||
<Actuator channel="talking-guy" on={(phrase) => this.saySomething(phrase)}> | ||
<div> | ||
@@ -95,3 +96,3 @@ ... | ||
How we are ready to trigger actions from the bussiness-logic part of the app: | ||
Now let's trigger actions from the bussiness-logic part of the app: | ||
@@ -103,27 +104,15 @@ ```JavaScript | ||
// the rest is interpreted as event arguments | ||
store.dispatch(actuate('say', 'Hola!') | ||
store.dispatch(actuate('say', 'Hello!') | ||
store.dispatch(actuate('talking-guy', 'Hola!') | ||
store.dispatch(actuate('talking-guy', 'Hello!') | ||
``` | ||
## Using Channels | ||
In the next example we now have 4 talking heads and we would like to trigger | ||
`'say'` event for a specific head: | ||
**Subscribing to multiple channels.** You can also subscribe to a multiple channels at once using this form: | ||
![](assets/talking-guys.gif) | ||
Actuator provides support for channels. Channels can be useful if you'd like to | ||
distinguish similar events passed for different components: | ||
```JavaScript | ||
// If you don't specify a channel, 'default' will be used | ||
<Actuator | ||
channel='gustav' | ||
events={{ say: (phrase) => this.saySomething(phrase) }} /> | ||
import { actuateChannel } from 'redux-actuator' | ||
// actuateChanell is an action creator-creator | ||
const actuateGustav = actuateChannel('gustav') | ||
store.dispatch(actuateGustav('say', 'Goeiedag!')) | ||
// Note: no need to pass a `channel` prop | ||
// Format is { chanOne: handler, chanTwo: ... } | ||
<Actuator on={{ | ||
instantNotification: () => ..., | ||
focusToolbar: () => ... | ||
}} /> | ||
``` | ||
@@ -130,0 +119,0 @@ |
@@ -5,3 +5,3 @@ import { ACTUATE, actuate } from '../actions' | ||
it('should create flux standard action with type ACTUATE', () => { | ||
const action = actuate() | ||
const action = actuate('f00z') | ||
expect(action.type).toBe(ACTUATE) | ||
@@ -11,10 +11,10 @@ expect(action.payload).toMatchObject({}) | ||
it('creates ACTUATE action on a default channel', () => { | ||
const action = actuate() | ||
expect(action.payload.channel).toBe('default') | ||
it('uses first arg as a name of the channel', () => { | ||
const action = actuate('foo') | ||
expect(action.payload.channel).toBe('foo') | ||
}) | ||
it('takes the name of the event as a first arg', () => { | ||
it('puts an empty array if args are not specified', () => { | ||
const action = actuate('foo') | ||
expect(action.payload.event.type).toBe('foo') | ||
expect(action.payload.args).toEqual([]) | ||
}) | ||
@@ -24,4 +24,4 @@ | ||
const action = actuate('foo', 'what', 'the', 'hell') | ||
expect(action.payload.event.args).toEqual([ 'what', 'the', 'hell' ]) | ||
expect(action.payload.args).toEqual(['what', 'the', 'hell']) | ||
}) | ||
}) |
@@ -1,9 +0,15 @@ | ||
import { mount } from 'enzyme' | ||
import 'raf/polyfill' | ||
import Enzyme, { mount } from 'enzyme' | ||
import React from 'react' | ||
import { createStore as createReduxStore, combineReducers } from 'redux' | ||
import { Provider } from 'react-redux' | ||
import Adapter from 'enzyme-adapter-react-16' | ||
import 'raf/polyfill' | ||
Enzyme.configure({ adapter: new Adapter() }) | ||
import createEngine, { actuate, Actuator } from '../index' | ||
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms)) | ||
const delay = ms => new Promise(resolve => setTimeout(resolve, ms)) | ||
@@ -19,3 +25,3 @@ const createStore = () => { | ||
describe('Actuator component', () => { | ||
it('catches events on default channel', () => { | ||
it('catches an event via on prop', () => { | ||
const store = createStore() | ||
@@ -26,4 +32,5 @@ const eventHandler = jest.fn() | ||
<Provider store={store}> | ||
<Actuator events={{foo: eventHandler}} /> | ||
</Provider>) | ||
<Actuator channel="foo" on={eventHandler} /> | ||
</Provider> | ||
) | ||
@@ -40,4 +47,5 @@ store.dispatch(actuate('foo', 1, 2, 3)) | ||
<Provider store={store}> | ||
<Actuator events={{foo: eventHandler}} /> | ||
</Provider>) | ||
<Actuator channel="foo" on={eventHandler} /> | ||
</Provider> | ||
) | ||
@@ -59,4 +67,5 @@ store.dispatch(actuate('foo')) | ||
<Provider store={store}> | ||
<Actuator events={{foo: eventHandler}} /> | ||
</Provider>) | ||
<Actuator channel="foo" on={eventHandler} /> | ||
</Provider> | ||
) | ||
@@ -76,4 +85,5 @@ expect(eventHandler).not.toHaveBeenCalled() | ||
<Provider store={store}> | ||
<Actuator events={{baz: eventHandler}} deltaError={40} /> | ||
</Provider>) | ||
<Actuator channel="baz" on={eventHandler} deltaError={100} /> | ||
</Provider> | ||
) | ||
@@ -93,4 +103,5 @@ expect(eventHandler).toHaveBeenCalled() | ||
<Provider store={store}> | ||
<Actuator events={{bar: eventHandler}} deltaError={25} /> | ||
</Provider>) | ||
<Actuator channel="bar" on={eventHandler} deltaError={25} /> | ||
</Provider> | ||
) | ||
@@ -100,2 +111,29 @@ expect(eventHandler).not.toHaveBeenCalled() | ||
}) | ||
it('allows to subscribe to multiple chans at once', () => { | ||
const store = createStore() | ||
const handlerA = jest.fn() | ||
const handlerB = jest.fn() | ||
const handlerC = jest.fn() | ||
mount( | ||
<Provider store={store}> | ||
<Actuator | ||
on={{ | ||
foo: handlerA, | ||
bar: handlerB, | ||
baz: handlerC | ||
}} | ||
/> | ||
</Provider> | ||
) | ||
store.dispatch(actuate('foo')) | ||
store.dispatch(actuate('bar')) | ||
expect(handlerA).toHaveBeenCalled() | ||
expect(handlerB).toHaveBeenCalled() | ||
expect(handlerC).not.toHaveBeenCalled() | ||
}) | ||
}) |
@@ -13,7 +13,9 @@ import createReducer from '../reducer' | ||
it('should handle ACTUATE action when channel is given', () => { | ||
const state = reducer({}, | ||
const state = reducer( | ||
{}, | ||
{ | ||
type: ACTUATE, | ||
payload: { channel: 'm00t', event: { timestamp: 1337 } } | ||
}) | ||
payload: { channel: 'm00t', timestamp: 1337 } | ||
} | ||
) | ||
@@ -20,0 +22,0 @@ expect(state.m00t).toBeTruthy() |
@@ -6,21 +6,29 @@ import { currentTimestamp } from './utils/currentTimestamp' | ||
const actuateFactory = (channel) => | ||
(eventType, ...args) => { | ||
const timestamp = currentTimestamp() | ||
let clock = 0 | ||
return { | ||
type: ACTUATE, | ||
payload: { | ||
channel, | ||
event: { | ||
type: eventType, | ||
timestamp, | ||
args | ||
} | ||
} | ||
const genKey = (ts, clock) => `${ts}::${clock}` | ||
export const actuate = (channel, ...args) => { | ||
// channel has to be specified | ||
if (!channel) { | ||
const warning = | ||
'Triggering actions without a channel is no longer ' + | ||
'supported by `redux-actuator`. These actions will be ' + | ||
'ingored by the actuator.' | ||
console.warn(warning) | ||
} | ||
const timestamp = currentTimestamp() | ||
clock += 1 | ||
return { | ||
type: ACTUATE, | ||
payload: { | ||
key: genKey(timestamp, clock), | ||
channel, | ||
timestamp, | ||
args | ||
} | ||
} | ||
// Action creators | ||
export const actuate = actuateFactory('default') | ||
export const actuateChannel = actuateFactory | ||
} |
@@ -5,26 +5,126 @@ import React from 'react' | ||
import { connect } from 'react-redux' | ||
import ActuatorInner from './ActuatorInner' | ||
import { currentTimestamp } from '../utils/currentTimestamp' | ||
const Actuator = ({ event, events, ...props }) => ( | ||
<ActuatorInner event={event} handlers={events} {...props} /> | ||
) | ||
export class Actuator extends React.Component { | ||
constructor(props, context) { | ||
super(props, context) | ||
const mapStateToProps = (state, ownProps) => { | ||
const channels = state.actuator || {} | ||
const event = channels[ownProps.channel] | ||
// Used to store last visible keys on per-channel basis | ||
// format channel -> last key | ||
this._keyCache = {} | ||
} | ||
return { event } | ||
getChanHandlers() { | ||
const { on, channel } = this.props | ||
if (typeof on === 'function') { | ||
return { [channel]: on } | ||
} | ||
return on | ||
} | ||
getObservedChannels() { | ||
return Object.keys(this.getChanHandlers()) | ||
} | ||
onStateChanged(state = null, ...restOpts) { | ||
state = state || this.context.store.getState() | ||
if (!state.actuator) return null | ||
const channels = this.getObservedChannels() | ||
for (let i = 0; i < channels.length; ++i) { | ||
const chan = channels[i] | ||
const event = state.actuator[chan] | ||
if (!event) continue | ||
this.checkEvent(chan, event, ...restOpts) | ||
} | ||
} | ||
callChanHandler(channel, args) { | ||
const handlers = this.getChanHandlers() | ||
handlers[channel] && handlers[channel](...args) | ||
} | ||
checkEvent(channel, event, options = {}) { | ||
// Compare event keys (they have to be unique) | ||
const _prevKey = this._keyCache[channel] | ||
if (_prevKey && _prevKey === event.key) { | ||
return | ||
} | ||
this._keyCache[channel] = event.key | ||
// Special case when the event was dispatched before | ||
// the mount. If you need to handle cases like | ||
// this one, pass `deltaError` prop, which literally means: | ||
// "handle missed events on mount if it's younger than `deltaError`" | ||
let shouldHandleOnMount = false | ||
const { deltaError } = this.props | ||
if (options.onMount && deltaError) { | ||
const now = currentTimestamp() | ||
const delta = Math.abs(now - event.timestamp) | ||
shouldHandleOnMount = delta <= deltaError | ||
} | ||
if (options.onMount && !shouldHandleOnMount) { | ||
return | ||
} | ||
this.callChanHandler(channel, event.args) | ||
} | ||
componentDidMount() { | ||
const store = this.context.store | ||
if (store) { | ||
this._sub = store.subscribe(() => this.onStateChanged()) | ||
// trigger on mount check | ||
const state = store.getState() | ||
this.onStateChanged(state, { onMount: true }) | ||
} | ||
} | ||
componentWillUnmount() { | ||
this._sub && this._sub() | ||
} | ||
render() { | ||
if (!this.props.children) return null | ||
return this.props.children | ||
} | ||
} | ||
const WrappedActuator = connect(mapStateToProps)(Actuator) | ||
Actuator.propTypes = { | ||
channel: PropTypes.string, | ||
WrappedActuator.propTypes = { | ||
channel: PropTypes.string, | ||
events: PropTypes.object.isRequired | ||
on: PropTypes.oneOfType([ | ||
// Can be simple event handler of a map of handlers | ||
// e.g. { chanOne: ..., chanTwo: ... } | ||
PropTypes.func, | ||
PropTypes.objectOf(PropTypes.func) | ||
]).isRequired, | ||
deltaError: PropTypes.number, | ||
// Always require `channel` prop, unless the map | ||
// of listeners wasn't passed with `on` prop. | ||
_requireChannel: props => { | ||
const isMapOfHandlers = props.on !== null && typeof props.on === 'object' | ||
if (!props.channel && !isMapOfHandlers) { | ||
return new Error('channel is required') | ||
} | ||
} | ||
} | ||
WrappedActuator.defaultProps = { | ||
channel: 'default' | ||
Actuator.contextTypes = { | ||
store: PropTypes.object | ||
} | ||
export default WrappedActuator | ||
export default Actuator |
import { ACTUATE } from './actions' | ||
const createReducer = (engine) => | ||
(state = {}, action) => { | ||
if (action.type === ACTUATE) { | ||
const { channel, ...rest } = action.payload || {} | ||
const event = rest.event || {} | ||
const createReducer = engine => (state = {}, action) => { | ||
if (action.type === ACTUATE) { | ||
const { channel, ...rest } = action.payload || {} | ||
// Channel isn't given, ignore | ||
if (!channel) { | ||
return state | ||
} | ||
// Ignore mailformed actions | ||
if (!channel || !rest) { | ||
return state | ||
} | ||
return { | ||
...state, | ||
[channel]: event | ||
} | ||
return { | ||
...state, | ||
[channel]: rest | ||
} | ||
return state | ||
} | ||
return state | ||
} | ||
export default createReducer |
@@ -0,2 +1,13 @@ | ||
const currentTimestamp = (() => { | ||
const { performance } = window | ||
export const currentTimestamp = () => (new Date()).getTime() | ||
if (performance && performance.now) { | ||
return () => performance.now() | ||
} | ||
// use a fallback if `performance.now` | ||
// isn't supported by the browser. | ||
return () => new Date().getTime() | ||
})() | ||
export { currentTimestamp } |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
155190
3
747
0
14
28
123