typescript(monadic): feat: component builders
Signed-off-by: prescientmoon <git@moonythm.dev>
This commit is contained in:
parent
9e3da82f0d
commit
5a1e511a6d
1
typescript/monadic/.vscode/settings.json
vendored
1
typescript/monadic/.vscode/settings.json
vendored
|
@ -4,6 +4,7 @@
|
|||
"adriel",
|
||||
"esnext",
|
||||
"matei",
|
||||
"pipeable",
|
||||
"todos",
|
||||
"tslib"
|
||||
]
|
||||
|
|
|
@ -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({
|
||||
|
|
123
typescript/monadic/src/Builder.ts
Normal file
123
typescript/monadic/src/Builder.ts
Normal file
|
@ -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,
|
||||
{}
|
||||
>;
|
|
@ -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 });
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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';
|
||||
|
|
Loading…
Reference in a new issue