1
Fork 0

typescript(monadic): feat: component builders

Signed-off-by: prescientmoon <git@moonythm.dev>
This commit is contained in:
Matei Adriel 2020-05-12 12:34:14 +03:00 committed by prescientmoon
parent 9e3da82f0d
commit 5a1e511a6d
Signed by: prescientmoon
SSH key fingerprint: SHA256:UUF9JT2s8Xfyv76b8ZuVL7XrmimH4o49p4b+iexbVH4
6 changed files with 264 additions and 111 deletions

View file

@ -4,6 +4,7 @@
"adriel",
"esnext",
"matei",
"pipeable",
"todos",
"tslib"
]

View file

@ -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({

View 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,
{}
>;

View file

@ -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 });

View file

@ -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;
};

View file

@ -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';