@action-land/component
Version:
Basic interface for a component
293 lines (292 loc) • 10.7 kB
JavaScript
"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;