UNPKG

@action-land/component

Version:
293 lines (292 loc) 10.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const core_1 = require("@action-land/core"); const standard_data_structures_1 = require("standard-data-structures"); const listComponentState_1 = require("./listComponentState"); const arg2 = (a, b) => b; /** * API for type safe and composable components that are agnostic of view library */ class ComponentNext { /** * @param _init Function which returns initial state of the component * @param _update Function which takes action and state, and returns new state * @param _command Function which takes action and state, and returns new action * @param _view Function which returns view based on state and props */ constructor( // FIXME: Fix typings for _init, _update, _command _init, _update, _command, _view, _children, _iActions) { this._init = _init; this._update = _update; this._command = _command; this._view = _view; this._children = _children; this._iActions = _iActions; } /** * Transform ComponentNext P1 to ComponentNext P2 * @param fn mapper function to transform component */ lift(fn) { return fn(this); } /** * Create new component with provided value as initial state * * ```typescript * import {ComponentNext} from '@action-land/component' * * const component = ComponentNext.lift({count: 100}) * ``` * @param state initial state of component */ static lift(state) { const i = () => state; return new ComponentNext(i, arg2, core_1.Nil, () => undefined, {}, standard_data_structures_1.List.empty()); } /** * Create new component with state as undefined * * ```typescript * import {ComponentNext} from '@action-land/component' * * const component = ComponentNext.empty * ``` */ static get empty() { return ComponentNext.lift(undefined); } /** * Add transformation of component's state for a given action * p.s: Check out test cases for advanced use cases * * ```typescript * import {ComponentNext} from '@action-land/component' * * const component = ComponentNext.lift({count: 100}) * .matchR('add', (value: number, state) => ({count: state.count + value})) * // Adds behaviour to handle action of type Action<number, 'add'> * ``` * @typeparam T Action type to be handled by component * @typeparam V Value of action to be handled by component * @param type Action type for which we want to add behaviour * @param cb Transformation function that returns a new state */ matchR(type, cb) { return new ComponentNext(this._init, (a, s) => { const s2 = this._update(a, s); if (a.type === type) { // this.update args type is Action<unknown> return cb(a.value, s2); } return s2; }, this._command, this._view, this._children, this._iActions.prepend(type)); } /** * Add ability to return new action on matching action * *```typescript * import {ComponentNext} from '@action-land/component' * import {Action} from '@action-land/core' * * const component = ComponentNext.lift({count: 100}) * .matchC('persist', (value: number, state) => (Action.of('writeCache', value))) * // Adds behaviour to handle action of type Action<number, 'persist'> * ``` * @typeparam T Action type to be handled by component * @typeparam V Type of action value to be handled by component * @typeparam T2 Action type fired by cb function * @typeparam V2 value of action fired by cb function * @param type Action type for which we want to add behaviour * @param cb Function that returns new action */ matchC(type, cb) { return new ComponentNext(this._init, this._update, (a, s) => { const a2 = this._command(a, s); if (core_1.isAction(a) && a.type === type) { return core_1.List(a2, cb(a.value, s)); } return a2; }, this._view, this._children, this._iActions.prepend(type)); } /** * Add component as a child of a given component i.e * 1. Forward all actions with type of child's name to child component update function * 2. maintain self-state and child's state separately * * ```typescript * import {ComponentNext} from '@action-land/component' * * const child1 = ComponentNext.lift({c1: 100}) * const child2 = ComponentNext.lift({c2: 200}) * const component = ComponentNext.lift({c: 1000}) * .install( * { * child1, // actions of type `child1` will be forwarded to child1 component * child2 // actions of type `child2` will be forwarded to child2 component * } * ) * * component._init() * //outputs * //{ * // node: {c: 1000}, * // children: { * // child1: {c1: 100}, * // child2: {c2: 200} * // } * //} * * ``` * @param spec key value pair object of child name and child component * */ install(spec) { return new ComponentNext(() => { const node = this._init(); const children = {}; for (let i in spec) { if (spec.hasOwnProperty(i)) { children[i] = spec[i]._init(); } } return { node, children: children }; }, (a, s) => { const node = this._update(a, s.node); const children = s.children; if (spec[a.type]) { return { node, children: Object.assign({}, s.children, { [a.type]: spec[a.type]._update(a.value, s.children[a.type]) }) }; } return { node, children }; }, (a, s) => { const a1 = this._command(a, s.node); if (spec[a.type]) { return core_1.List(a1, core_1.action(a.type, spec[a.type]._command(a.value, s.children[a.type]))); } return a1; }, this._view, spec, Object.keys(spec).reduce((a, b) => a.prepend(b), this._iActions)); } /** * Adds presentation logic to the component * 1. Create view based on props and state * * ```typescript * import {ComponentNext} from '@action-land/component' * * const component1 = ComponentNext.lift(10) * .render((_, props: string) => [props, _.state + 1]) * * component._view({}, component._init(), 'Hello') // output: [Hello, 11] * ``` * 2. Can invoke child component's view * * ```typescript * import {ComponentNext} from '@action-land/component' * * const component = ComponentNext.lift('Hello') * .install({ * child: ComponentNext.lift('World').render((_, p: string) => p) * }) * .render((_, p: string) => [p, _.children.child('World')] * * component._view({}, component._init(), 'Hello') // output: [Hello, World] * ``` * 3. Can emit action * * ```typescript * import {ComponentNext} from '@action-land/component' * * const component = ComponentNext.lift(10) * .matchR('add', (a: number, s) => s + a) * .render(_ => _.actions.add(100)) * * component._view({}, component._init()) // output: Action<100, 'add'> and changes component state to 110 * ``` * @typeparam P View prop type * @typeparam V View representation data structure type * @param cb Function which return view based on props and state */ render(cb) { return new ComponentNext(this._init, this._update, this._command, (e, s, p) => { const children = {}; for (let i in this._children) { if (this._children.hasOwnProperty(i)) { const item = this._children[i]; children[i] = (p) => item._view(e.of(i), s.children[i], p); } } const actions = this._iActions.fold({}, (key, actions) => (Object.assign({}, actions, { [key]: (ev) => e.of(key).emit(ev) }))); return cb({ actions: actions, state: s, children }, p); }, this._children, this._iActions); } /** * Transform initial state of the component * * ```typescript * * import {ComponentNext} from '@action-land/component' * * ComponentNext.lift({count: 100}).configure(iState => ({ * count: iState.count * 2 * })) * * const component._init() // output: {count: 200} * ``` * @typeparam S2 State type post transformation * @param fn function to transform the state * */ configure(fn) { return new ComponentNext(() => fn(this._init()), this._update, this._command, this._view, this._children, this._iActions); } toList(fn) { return new ComponentNext(() => listComponentState_1.ListComponentState.of(this._init), (inputAction, state) => { const listComponentState = state; const updatedState = this._update(inputAction.value, listComponentState.get(inputAction.type).getOrElse(this._init())); return listComponentState.set(inputAction.type, updatedState); }, (inputAction, state) => { return core_1.action(inputAction.type, this._command(inputAction.value, state.get(inputAction.type).getOrElse(this._init()))); }, (e, s, p) => { const key = fn(p); return this._view(e.of(key), s.get(key).getOrElse(this._init()), p); }, /** * @todo: Need to re-look this */ {}, /** * @todo: Need to re-look this */ standard_data_structures_1.List.empty()); } /** * Method to convert old component API to ComponentNext * @typeparam A state * @typeparam V view * @typeparam P prop * @typeparam I init param */ static from(component, ...initParams) { return new ComponentNext(() => component.init(...initParams), component.update, component.command, component.view, {}, standard_data_structures_1.List.empty()); } /** * Method to convert componentNext to old component API */ get component() { return { init: this._init, update: this._update, command: this._command, view: this._view }; } } exports.ComponentNext = ComponentNext;