Home Reference Source

src/decorators/bindHandlers.js

import {bind} from 'alt-utils/lib/decorators';
import flattenArrays from '../utils/flatten';

/**
 * Decorates a store with any number of action handlers.
 */
export default function bindHandlers(actions, ...handlers) {
    return function decorateStore(storeClass) {
        // attach bound handler methods to the store class for each handler definition
        flattenArrays(handlers).forEach(handler => attachBoundHandler(storeClass, actions[handler.name], handler));
        return storeClass;
    };
}
/**
 * Attaches a single reducer handling to the store.
 *
 * A new handler method will be created on the store class.
 * The handler method invokes the reducer giving it the current store state.
 * It then sets the reducer result as the new store state.
 *
 * If a sideEffect function is defined, it will be called with the signature `{state, prevState, payload}`
 *
 * @param {object} storeClass - The class of the store to be decorated
 * @param {object} action - An altjs action
 * @param {object} handler - An handler object with `{name[, reducer, sideEffect]}`
 */
function attachBoundHandler(storeClass, action, handler) {
    // name is required

    const methodName = `__handle_${handler.name}`;

    if (storeClass.prototype[methodName]) throw new Error(`Duplicate method "${methodName}"`);

    /**
     * Handles an action call and sets the next state of the store.
     *
     * @param {any} payload - the single argument that can be specified when calling an action.
     *                      If you need to use more than one argument, use an object with any properties you need.
     */
    storeClass.prototype[methodName] = function handleAction(payload) {

        const reducer = handler.hasOwnProperty('reducer') && handler.reducer;
        const sideEffect = handler.hasOwnProperty('sideEffect') && handler.sideEffect;

        const currentState = this.state;


        // the actual operation: run the reducer and set its result as state
        let nextState = currentState;
        if (reducer) {
            try {
                nextState = reducer(currentState, payload);
            }
            catch (error) {
                console.error(`Error in reducer (${handler.name}, ${handler.name})`, error);
            }
        }
        if (nextState) {
            this.setState(nextState);
        }
        else if (reducer) {
            console.warn(`reducer "${handler.name}" in call "${handler.name}" did not return a new state.
                Either you forgot to return it, or if no state change is required, maybe you should use a sideEffect instead of a reducer.
            `);
        }



        if (sideEffect) {
            try {
                sideEffect({state: nextState, prevState: currentState, payload});
            }
            catch (error) {
                console.error(`Error in sideEffect (${handler.name}, ${handler.name})`, error);
            }
        }
    };

    const bindhandler = bind(action);

    bindhandler(
        storeClass,
        methodName,
        Object.getOwnPropertyDescriptor(storeClass.prototype, methodName)
    );

};