From 5a1e511a6db52cae5df975bf25fc350f8644339d Mon Sep 17 00:00:00 2001 From: Matei Adriel Date: Tue, 12 May 2020 12:34:14 +0300 Subject: [PATCH] typescript(monadic): feat: component builders Signed-off-by: prescientmoon --- typescript/monadic/.vscode/settings.json | 1 + typescript/monadic/demos/basic/main.ts | 160 +++++++++++++---------- typescript/monadic/src/Builder.ts | 123 +++++++++++++++++ typescript/monadic/src/Component.ts | 70 ++++------ typescript/monadic/src/helpers.ts | 8 ++ typescript/monadic/src/index.ts | 13 +- 6 files changed, 264 insertions(+), 111 deletions(-) create mode 100644 typescript/monadic/src/Builder.ts diff --git a/typescript/monadic/.vscode/settings.json b/typescript/monadic/.vscode/settings.json index 12ddcd5..5ec1b46 100644 --- a/typescript/monadic/.vscode/settings.json +++ b/typescript/monadic/.vscode/settings.json @@ -4,6 +4,7 @@ "adriel", "esnext", "matei", + "pipeable", "todos", "tslib" ] diff --git a/typescript/monadic/demos/basic/main.ts b/typescript/monadic/demos/basic/main.ts index 4b681f4..e66563b 100644 --- a/typescript/monadic/demos/basic/main.ts +++ b/typescript/monadic/demos/basic/main.ts @@ -1,5 +1,20 @@ -import { runUi, makeComponent, Dispatcher, mkChild } from '../../src'; +import { + runUi, + makeComponent, + Dispatcher, + ComponentSpec, + withAction, + mkChild, + buildSpec, + withState, + emptySpec, + withTemplate, + withChild, +} from '../../src'; import { render, html, TemplateResult } from 'lit-html'; +import { pipe } from 'fp-ts/lib/pipeable'; + +const baseSpec = pipe(emptySpec, withTemplate()); type TodoState = { name: string; @@ -8,26 +23,31 @@ type TodoState = { type TodoAction = 'complete'; -const todo = makeComponent( - ({ name, done }: TodoState, { dispatch }) => { - return html` -
- Name: ${name} Completed: ${done} - -
- `; - }, - (action, state) => { - if (action === 'complete') { - return { - ...state, - done: true, - }; - } +const todo = pipe( + baseSpec, + withAction(), + withState(), + buildSpec( + ({ name, done }, { dispatch }) => { + return html` +
+ Name: ${name} Completed: ${done} + +
+ `; + }, + (action, state) => { + if (action === 'complete') { + return { + ...state, + done: true, + }; + } - return state; - }, - {} + return state; + }, + {} + ) ); type TodoListState = { @@ -42,56 +62,64 @@ type TodoListAction = } | 'create'; -const todoList = makeComponent( - (state: TodoListState, dispatch: Dispatcher, child) => { - return html` -
- dispatch({ label: 'setInput', value })} - /> - +const todoList = pipe( + emptySpec, + withAction(), + withState(), + withChild<'todo', number, TodoState, null>(), + buildSpec( + (state, { dispatch, child }) => { + return html`
- ${state.todos.map((_, index) => child('todo', String(index), index))} + dispatch({ label: 'setInput', value })} + /> + +
+ ${state.todos.map((_, index) => + child('todo', String(index), index) + )} +
-
- `; - }, - (action: TodoListAction, state: TodoListState) => { - if (action === 'create') { - return { - inputValue: '', - todos: [ - ...state.todos, - { - name: state.inputValue, - done: false, - }, - ], - }; - } else if (action.label === 'setInput') { - return { - ...state, - inputValue: action.value, - }; - } - - return state; - }, - { - todo: mkChild( - (index: number) => ({ - get: ({ todos }) => todos[index], - set: (state, newTodoState) => ({ + `; + }, + (action: TodoListAction, state: TodoListState) => { + if (action === 'create') { + return { + inputValue: '', + todos: [ + ...state.todos, + { + name: state.inputValue, + done: false, + }, + ], + }; + } else if (action.label === 'setInput') { + return { ...state, - todos: state.todos.map((todoState, currentIndex) => - index === currentIndex ? newTodoState : todoState - ), + inputValue: action.value, + }; + } + + return state; + }, + { + todo: mkChild( + (index: number) => ({ + get: ({ todos }) => todos[index], + set: (state, newTodoState) => ({ + ...state, + todos: state.todos.map((todoState, currentIndex) => + index === currentIndex ? newTodoState : todoState + ), + }), }), - }), - todo - ), - } + todo + ), + } + ) ); runUi({ diff --git a/typescript/monadic/src/Builder.ts b/typescript/monadic/src/Builder.ts new file mode 100644 index 0000000..0109bef --- /dev/null +++ b/typescript/monadic/src/Builder.ts @@ -0,0 +1,123 @@ +import { + ComponentConfig, + ChildrenConfigs, + ChildTemplate, + RenderFunction, + Child, +} from './Component'; +import { GenericLens } from './helpers'; +import { constant, identity } from 'fp-ts/es6/function'; +import * as O from 'fp-ts/es6/Option'; + +type ComponentBuilder = < + T, + S, + A, + O, + N extends string, + C extends ChildrenConfigs +>( + render: ComponentConfig['render'], + handleAction: ComponentConfig['handleAction'], + children: ComponentConfig['children'] +) => ComponentConfig; + +/** + * Create a component + * + * @param render The render function of the component + * @param handleAction Function used to handle actions related to the component. + * @param children Child slots for the component + */ +export const makeComponent: ComponentBuilder = ( + render, + handleAction, + children +) => ({ render, handleAction, children }); + +type ChildBuilder = ( + lens: (input: I) => GenericLens, + component: ComponentConfig, + handleOutput?: (input: I, output: O) => O.Option +) => ChildTemplate; + +export const mkChild: ChildBuilder = ( + lens, + component, + handleOutput = constant(O.none) +) => ({ lens, component, handleOutput }); + +export type ComponentSpec< + T, + S, + A, + O, + N extends string, + C extends ChildrenConfigs +> = [T, S, A, O, N, C]; + +export const withChild = constant(identity) as < + N1 extends keyof any, + I, + S, + O +>() => >( + spec: ComponentSpec +) => ComponentSpec>>; + +export const withAction = constant(identity) as () => < + T, + S, + A0, + O, + N extends string, + C extends ChildrenConfigs +>( + spec: ComponentSpec +) => ComponentSpec; + +export const withState = constant(identity) as () => < + T, + S0, + A, + O, + N extends string, + C extends ChildrenConfigs +>( + spec: ComponentSpec +) => ComponentSpec; + +export const withTemplate = constant(identity) as () => < + T0, + S, + A, + O, + N extends string, + C extends ChildrenConfigs +>( + spec: ComponentSpec +) => ComponentSpec; + +export const buildSpec = (constant(makeComponent) as any) as < + T, + S, + A, + O, + N extends string, + C extends ChildrenConfigs +>( + render: RenderFunction, + handleAction: ComponentConfig['handleAction'], + children: ComponentConfig['children'] +) => ( + spec: ComponentSpec +) => ComponentConfig; + +export const emptySpec = (null as any) as ComponentSpec< + unknown, + unknown, + unknown, + null, + string, + {} +>; diff --git a/typescript/monadic/src/Component.ts b/typescript/monadic/src/Component.ts index 512e5c1..6283468 100644 --- a/typescript/monadic/src/Component.ts +++ b/typescript/monadic/src/Component.ts @@ -1,5 +1,5 @@ import { mapRecord } from './Record'; -import { values } from './helpers'; +import { values, GenericLens } from './helpers'; import O from 'fp-ts/es6/Option'; import { constant } from 'fp-ts/es6/function'; @@ -20,6 +20,15 @@ export type Communicate< raise: (event: O) => void; }; +export type RenderFunction< + T, + S, + A, + O, + N extends string, + C extends ChildrenConfigs +> = (state: S, communicate: Communicate) => T; + export type ComponentConfig< T, S, @@ -28,37 +37,40 @@ export type ComponentConfig< N extends string, C extends ChildrenConfigs > = { - render: (state: S, communicate: Communicate) => T; + render: RenderFunction; handleAction: (action: A, state: S) => S; - children: ChildrenTemplates; + children: ChildrenTemplates; }; -type GenericLens = { - get: (v: T) => U; - set: (v: T, n: U) => T; -}; - -type Child = { +export type Child = { input: I; state: S; output: O; }; -type ChildrenConfigs = Record; +export type ChildrenConfigs = Record; + +export type ChildTemplate = { + lens: (input: I) => GenericLens; + component: ComponentConfig; + handleOutput: (input: I, output: O) => O.Option; +}; type ChildrenTemplates< T, S, A, - O, N extends string, C extends ChildrenConfigs > = { - [K in N]: { - lens: (input: C[K]['input']) => GenericLens; - component: ComponentConfig; - handleOutput: (input: C[K]['input'], output: C[K]['output']) => O.Option; - }; + [K in N]: ChildTemplate< + T, + S, + A, + C[K]['state'], + C[K]['input'], + C[K]['output'] + >; }; type Children> = { @@ -143,29 +155,3 @@ export class Component< return values(this.childrenMap).flatMap(record => values(record)) as any; } } - -/** - * Create a component - * - * @param render The render function of the component - * @param handleAction Function used to handle actions related to the component. - * @param children Child slots for the component - */ -export const makeComponent = < - T, - S, - A, - O, - N extends string, - C extends ChildrenConfigs ->( - render: ComponentConfig['render'], - handleAction: ComponentConfig['handleAction'], - children: ComponentConfig['children'] -) => ({ render, handleAction, children }); - -export const mkChild = ( - lens: (input: I) => GenericLens, - component: ComponentConfig, - handleOutput: (input: I, output: O) => O.Option = constant(O.none) -) => ({ lens, component, handleOutput }); diff --git a/typescript/monadic/src/helpers.ts b/typescript/monadic/src/helpers.ts index e387b60..080ee80 100644 --- a/typescript/monadic/src/helpers.ts +++ b/typescript/monadic/src/helpers.ts @@ -4,3 +4,11 @@ export const values = Object.values as ( object: Record ) => U[]; + +/** + * Library agnostic way of expressing a lens + */ +export type GenericLens = { + get: (v: T) => U; + set: (v: T, n: U) => T; +}; diff --git a/typescript/monadic/src/index.ts b/typescript/monadic/src/index.ts index 3886465..2fd1891 100644 --- a/typescript/monadic/src/index.ts +++ b/typescript/monadic/src/index.ts @@ -1,7 +1,14 @@ export { runUi, EnvConfig } from './environment'; +export { ComponentConfig, Dispatcher } from './Component'; + export { - ComponentConfig, - Dispatcher, makeComponent, mkChild, -} from './Component'; + ComponentSpec, + withAction, + emptySpec, + withState, + buildSpec, + withTemplate, + withChild, +} from './Builder';