import _Symbol from './utils/_Symbol'
const $$initial = _Symbol('initial')
/**
* Utility class for dealing with Redux actions. The instance of the class acts like an identifier
* for action. Under the hood it stil uses string identifiers and follows
* [Flux Standard Action]{@link https://github.com/acdlite/flux-standard-action}.
*
* Actions can have "stages", which are basically separate actions from Redux point of view. They
* can be used to logically group otherwise different actions if they represent the same action,
* but happening in multiple stages (e.g. consider asynchronous request pattern "started" ->
* "success" or "error").
*
* @example
* const todoAdded = new Action("todoAdded")
*
* // Dispatch
* dispatch(todoAdded.action({ text: "Todo text" }))
*
* // Reducer
* const reducer = Action.createReducer(
* Action.initial({ todos: [] }), // default initial value is { }
*
* todoAdded.on((state, item) => ({ todos: [...state.todos, item] }))
* )
*/
class Action {
/**
* @namespace
* @property {string} STARTED - An asynchronous operation was started.
* @property {string} SUCCESS - An asynchronous operation was completed with success.
* @property {string} ERROR - Action resolved to error. Payload contains `{ error: true }`
* and [Error]{@link https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Error}
* object as a payload.
*/
static get Stage () {
return {
STARTED: 'started',
SUCCESS: 'success',
ERROR: 'error'
}
}
/**
* @param {string} baseType Base type identifier for the action.
*/
constructor (baseType) {
this.baseType = baseType
}
/**
* Generates type identifier for specified stage.
* @param {string=} stage Name of the stage.
* @returns {string} Type for the action in specified stage.
*/
type (stage) {
const {baseType} = this
return (typeof baseType === 'function' ? baseType() : baseType) +
(stage ? '.' + stage : '')
}
/**
* Generates type identifier for `STARTED` stage.
* @returns {string} Type for the action in `STARTED` stage.
* @private
*/
typeStarted () {
return this.type(Action.Stage.STARTED)
}
/**
* Generates type identifier for `SUCCESS` stage.
* @returns {string} Type for the action in `SUCCESS` stage.
* @private
*/
typeSuccess () {
return this.type(Action.Stage.SUCCESS)
}
/**
* Generates type identifier for `ERROR` stage.
* @returns {string} Type for the action in `ERROR` stage.
* @private
*/
typeError () {
return this.type(Action.Stage.ERROR)
}
/**
* Creates plain action object which can be passed to `dispatch()` function.
* @param {string=} stage To use when creating the action object.
* @param payload Payload to be attached to the action object.
* @returns {Object} Plain action object.
*/
action () {
var stage = arguments[0]
var payload = arguments[1]
if (arguments.length === 1) {
stage = undefined
payload = arguments[0]
}
return { type: this.type(stage), payload }
}
/**
* Convenience function. Create plain action object with `STARTED` stage.
* See [action()]{@link Action#action}.
* @param payload Payload to be attached to the action object.
* @returns {Object} Plain action object.
*/
started (payload) {
return this.action(Action.Stage.STARTED, payload)
}
/**
* Convenience function. Create plain action object with `SUCCESS` stage.
* See [action()]{@link Action#action}.
* @param payload Payload to be attached to the action object.
* @returns {Object} Plain action object.
*/
success (payload) {
return this.action(Action.Stage.SUCCESS, payload)
}
/**
* Convenience function. Create plain action object with `ERROR` stage.
* See [action()]{@link Action#action}.
* @param payload Payload to be attached to the action object.
* @returns {Object} Plain action object.
*/
error (payload) {
return { type: this.typeError(), payload, error: true }
}
/**
* Wraps the provided handler function into a condition check, so the function is only called
* if it receives the action with type equal to one of this action. Stage is taken into account
* if specified. Reducer function will receive only payload part of the action.
* @param {stage=} stage Process actions of specified stage.
* @param {function(state, payload)} handler Handler function.
* @returns {function} Wrapper handler function.
*/
on () {
var stage = arguments[0]
var handler = arguments[1]
if (arguments.length === 1) {
stage = undefined
handler = arguments[0]
}
return (state, action) => {
if (action.type === this.type(stage)) {
return handler(state, action.payload)
}
}
}
/**
* Convenience function. Wraps handler for processing `STARTED` stage.
* See [on()]{@link Action#on}.
* @param {function(state, payload)} handler Handler function.
* @returns {function} Wrapper handler function.
*/
onStarted (handler) {
return this.on(Action.Stage.STARTED, handler)
}
/**
* Convenience function. Wraps handler for processing `SUCCESS` stage.
* See [on()]{@link Action#on}.
* @param {function(state, payload)} handler Handler function.
* @returns {function} Wrapper handler function.
*/
onSuccess (handler) {
return this.on(Action.Stage.SUCCESS, handler)
}
/**
* Convenience function. Wraps handler for processing `ERROR` stage.
* See [on()]{@link Action#on}.
* @param {function(state, payload)} handler Handler function.
* @returns {function} Wrapper handler function.
*/
onError (handler) {
return this.on(Action.Stage.ERROR, handler)
}
/**
* Pass the result of the call to [createReducer()]{@link Action.createReducer} to define the
* initial value of the resulting reducer.
* @param valueOrFunc If function is passed it will be called when reducer needs initial value.
* Otherwise provided value is used as is by the reducer.
* @example
* Action.createReducer(
* Action.initial({ foo: 'bar' })
* )
*/
static initial (valueOrFunc) {
const func = typeof valueOrFunc === 'function' ? valueOrFunc : () => valueOrFunc
Object.defineProperty(func, $$initial, {
value: true
})
return func
}
/**
* Combines all of the passed handlers to form a single reducer. Handlers can be a wrapped
* handlers from [Action.on()]{@link Action#on} calls as well as regular reducer functions.
*
* The default initial value for reducer is `{ }` (empty object). It can be overridden with
* [Action.initial()]{@link Action.initial}.
*
* If the first argument passed is a string, the resulting reducer will have all of the attached
* handlers operating on the sub key of the passed state rather than on the state itself.
*
* @param {string=} subKey Sub key for handlers to operate on.
* @param {function} args Handler functions to be combined into the single reducer.
* @returns {function} Redux-compatible reducer function.
*/
static createReducer (...args) {
var actionHandlers = args
var key
if (typeof args[0] !== 'function') {
if (typeof args[0] === 'string') {
key = args[0]
}
actionHandlers = args.slice(1)
}
var initial = actionHandlers.find((h) => h[$$initial]) || (() => ({}))
actionHandlers = actionHandlers.filter((h) => !h[$$initial])
return (state, action) => {
let parentState
if (key) {
parentState = state
state = state[key]
}
if (state === undefined) {
state = initial()
} else {
actionHandlers.forEach((actionHandler) => {
const newState = actionHandler(state, action)
if (newState !== undefined) {
state = newState
}
})
}
if (parentState) {
return Object.assign({ }, parentState, { [key]: state })
} else {
return state
}
}
}
}
export default Action