From 5a1e511a6db52cae5df975bf25fc350f8644339d Mon Sep 17 00:00:00 2001
From: Matei Adriel <rafaeladriel11@gmail.com>
Date: Tue, 12 May 2020 12:34:14 +0300
Subject: [PATCH] typescript(monadic): feat: component builders

Signed-off-by: prescientmoon <git@moonythm.dev>
---
 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<TemplateResult>());
 
 type TodoState = {
   name: string;
@@ -8,26 +23,31 @@ type TodoState = {
 
 type TodoAction = 'complete';
 
-const todo = makeComponent(
-  ({ name, done }: TodoState, { dispatch }) => {
-    return html`
-      <div>
-        Name: ${name} Completed: ${done}
-        <button @click=${() => dispatch('complete')}>Complete todo</button>
-      </div>
-    `;
-  },
-  (action, state) => {
-    if (action === 'complete') {
-      return {
-        ...state,
-        done: true,
-      };
-    }
+const todo = pipe(
+  baseSpec,
+  withAction<TodoAction>(),
+  withState<TodoState>(),
+  buildSpec(
+    ({ name, done }, { dispatch }) => {
+      return html`
+        <div>
+          Name: ${name} Completed: ${done}
+          <button @click=${() => dispatch('complete')}>Complete todo</button>
+        </div>
+      `;
+    },
+    (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<TodoListAction>, child) => {
-    return html`
-      <div>
-        <input
-          value="${state.inputValue}"
-          @input=${({ value }) => dispatch({ label: 'setInput', value })}
-        />
-        <button @click=${() => dispatch('create')}>Add todo</button>
+const todoList = pipe(
+  emptySpec,
+  withAction<TodoListAction>(),
+  withState<TodoListState>(),
+  withChild<'todo', number, TodoState, null>(),
+  buildSpec(
+    (state, { dispatch, child }) => {
+      return html`
         <div>
-          ${state.todos.map((_, index) => child('todo', String(index), index))}
+          <input
+            value="${state.inputValue}"
+            @input=${({ value }) => dispatch({ label: 'setInput', value })}
+          />
+          <button @click=${() => dispatch('create')}>Add todo</button>
+          <div>
+            ${state.todos.map((_, index) =>
+              child('todo', String(index), index)
+            )}
+          </div>
         </div>
-      </div>
-    `;
-  },
-  (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<N>
+>(
+  render: ComponentConfig<T, S, A, O, N, C>['render'],
+  handleAction: ComponentConfig<T, S, A, O, N, C>['handleAction'],
+  children: ComponentConfig<T, S, A, O, N, C>['children']
+) => ComponentConfig<T, S, A, O, N, C>;
+
+/**
+ * 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 = <T, PS, PA, S, I, O>(
+  lens: (input: I) => GenericLens<PS, S>,
+  component: ComponentConfig<T, S, unknown, O, string, {}>,
+  handleOutput?: (input: I, output: O) => O.Option<PA>
+) => ChildTemplate<T, PS, PA, S, I, O>;
+
+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<N>
+> = [T, S, A, O, N, C];
+
+export const withChild = constant(identity) as <
+  N1 extends keyof any,
+  I,
+  S,
+  O
+>() => <T, PS, A, PO, N0 extends string, C extends ChildrenConfigs<N0>>(
+  spec: ComponentSpec<T, PS, A, PO, N0, C>
+) => ComponentSpec<T, PS, A, PO, N1 & N0, C & Record<N1, Child<I, S, O>>>;
+
+export const withAction = constant(identity) as <A1>() => <
+  T,
+  S,
+  A0,
+  O,
+  N extends string,
+  C extends ChildrenConfigs<N>
+>(
+  spec: ComponentSpec<T, S, A0, O, N, C>
+) => ComponentSpec<T, S, A1, O, N, C>;
+
+export const withState = constant(identity) as <S1>() => <
+  T,
+  S0,
+  A,
+  O,
+  N extends string,
+  C extends ChildrenConfigs<N>
+>(
+  spec: ComponentSpec<T, S0, A, O, N, C>
+) => ComponentSpec<T, S1, A, O, N, C>;
+
+export const withTemplate = constant(identity) as <T1>() => <
+  T0,
+  S,
+  A,
+  O,
+  N extends string,
+  C extends ChildrenConfigs<N>
+>(
+  spec: ComponentSpec<T0, S, A, O, N, C>
+) => ComponentSpec<T1, S, A, O, N, C>;
+
+export const buildSpec = (constant(makeComponent) as any) as <
+  T,
+  S,
+  A,
+  O,
+  N extends string,
+  C extends ChildrenConfigs<N>
+>(
+  render: RenderFunction<T, S, A, O, N, C>,
+  handleAction: ComponentConfig<T, S, A, O, N, C>['handleAction'],
+  children: ComponentConfig<T, S, A, O, N, C>['children']
+) => (
+  spec: ComponentSpec<T, S, A, O, N, C>
+) => ComponentConfig<T, S, A, O, N, C>;
+
+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<N>
+> = (state: S, communicate: Communicate<T, A, O, N, C>) => T;
+
 export type ComponentConfig<
   T,
   S,
@@ -28,37 +37,40 @@ export type ComponentConfig<
   N extends string,
   C extends ChildrenConfigs<N>
 > = {
-  render: (state: S, communicate: Communicate<T, A, O, N, C>) => T;
+  render: RenderFunction<T, S, A, O, N, C>;
   handleAction: (action: A, state: S) => S;
-  children: ChildrenTemplates<T, S, A, O, N, C>;
+  children: ChildrenTemplates<T, S, A, N, C>;
 };
 
-type GenericLens<T, U> = {
-  get: (v: T) => U;
-  set: (v: T, n: U) => T;
-};
-
-type Child<I = unknown, S = unknown, O = unknown> = {
+export type Child<I = unknown, S = unknown, O = unknown> = {
   input: I;
   state: S;
   output: O;
 };
 
-type ChildrenConfigs<N extends string> = Record<N, Child>;
+export type ChildrenConfigs<N extends string> = Record<N, Child>;
+
+export type ChildTemplate<T, PS, PA, S, I, O> = {
+  lens: (input: I) => GenericLens<PS, S>;
+  component: ComponentConfig<T, S, unknown, O, string, {}>;
+  handleOutput: (input: I, output: O) => O.Option<PA>;
+};
 
 type ChildrenTemplates<
   T,
   S,
   A,
-  O,
   N extends string,
   C extends ChildrenConfigs<N>
 > = {
-  [K in N]: {
-    lens: (input: C[K]['input']) => GenericLens<S, C[K]['state']>;
-    component: ComponentConfig<T, C[K]['state'], O, unknown, string, {}>;
-    handleOutput: (input: C[K]['input'], output: C[K]['output']) => O.Option<A>;
-  };
+  [K in N]: ChildTemplate<
+    T,
+    S,
+    A,
+    C[K]['state'],
+    C[K]['input'],
+    C[K]['output']
+  >;
 };
 
 type Children<T, S, N extends string, C extends ChildrenConfigs<N>> = {
@@ -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<N>
->(
-  render: ComponentConfig<T, S, A, O, N, C>['render'],
-  handleAction: ComponentConfig<T, S, O, A, N, C>['handleAction'],
-  children: ComponentConfig<T, S, A, O, N, C>['children']
-) => ({ render, handleAction, children });
-
-export const mkChild = <T, PS, A, I, S, O>(
-  lens: (input: I) => GenericLens<PS, S>,
-  component: ComponentConfig<T, S, A, O, string, {}>,
-  handleOutput: (input: I, output: O) => O.Option<A> = 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 <T extends keyof object, U>(
   object: Record<T, U>
 ) => U[];
+
+/**
+ * Library agnostic way of expressing a lens
+ */
+export type GenericLens<T, U> = {
+  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';