typescript(monadic): feat: rewrote everything I think
Signed-off-by: prescientmoon <git@moonythm.dev>
This commit is contained in:
parent
db5b68592c
commit
9e6c150e5f
2
typescript/monadic/.vscode/settings.json
vendored
2
typescript/monadic/.vscode/settings.json
vendored
|
@ -1,8 +1,10 @@
|
||||||
{
|
{
|
||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
|
"Typesafe",
|
||||||
"adriel",
|
"adriel",
|
||||||
"esnext",
|
"esnext",
|
||||||
"matei",
|
"matei",
|
||||||
|
"todos",
|
||||||
"tslib"
|
"tslib"
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -1,38 +1,102 @@
|
||||||
import { runUi } from '../../src';
|
import { runUi, makeComponent, Dispatcher, mkChild } from '../../src';
|
||||||
import { render, html, TemplateResult } from 'lit-html';
|
import { render, html, TemplateResult } from 'lit-html';
|
||||||
|
|
||||||
type Action = 'increase' | 'decrease';
|
type TodoState = {
|
||||||
|
name: string;
|
||||||
type State = {
|
done: boolean;
|
||||||
count: number;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
runUi<TemplateResult, State, Action>({
|
type TodoAction = 'complete';
|
||||||
parent: document.getElementById('app'),
|
|
||||||
render,
|
const todo = makeComponent(
|
||||||
initialState: { count: 0 },
|
({ name, done }: TodoState, dispatch: Dispatcher<TodoAction>) => {
|
||||||
component: {
|
|
||||||
render: (state, dispatch) => {
|
|
||||||
return html`
|
return html`
|
||||||
<div>
|
<div>
|
||||||
<div>${state.count}</div>
|
Name: ${name} Completed: ${done}
|
||||||
<button @click=${dispatch('increase')}>Increase</button>
|
<button @click=${() => dispatch('complete')}>Complete todo</button>
|
||||||
<button @click=${dispatch('decrease')}>Decrease</button>
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
},
|
},
|
||||||
handleActions: (action: Action, state) => {
|
(action, state) => {
|
||||||
if (action === 'increase') {
|
if (action === 'complete') {
|
||||||
return {
|
return {
|
||||||
count: state.count + 1,
|
...state,
|
||||||
};
|
done: true,
|
||||||
} else if (action === 'decrease') {
|
|
||||||
return {
|
|
||||||
count: state.count - 1,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
},
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
type TodoListState = {
|
||||||
|
todos: TodoState[];
|
||||||
|
inputValue: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TodoListAction =
|
||||||
|
| {
|
||||||
|
label: 'setInput';
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
| '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>
|
||||||
|
<div>
|
||||||
|
${state.todos.map((_, index) => child('todo', String(index), index))}
|
||||||
|
</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) => ({
|
||||||
|
...state,
|
||||||
|
todos: state.todos.map((todoState, currentIndex) =>
|
||||||
|
index === currentIndex ? newTodoState : todoState
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
todo
|
||||||
|
),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
runUi({
|
||||||
|
parent: document.getElementById('app'),
|
||||||
|
render,
|
||||||
|
initialState: { todos: [], inputValue: '' },
|
||||||
|
component: todoList,
|
||||||
});
|
});
|
||||||
|
|
|
@ -38,5 +38,8 @@
|
||||||
"tsdx": "^0.13.2",
|
"tsdx": "^0.13.2",
|
||||||
"tslib": "^1.11.1",
|
"tslib": "^1.11.1",
|
||||||
"typescript": "^3.8.3"
|
"typescript": "^3.8.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"helpers": "^0.0.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
143
typescript/monadic/src/Component.ts
Normal file
143
typescript/monadic/src/Component.ts
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
import { mapRecord } from './Record';
|
||||||
|
import { values } from './helpers';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper type for dispatcher functions
|
||||||
|
*/
|
||||||
|
export type Dispatcher<A> = (action: A) => void;
|
||||||
|
|
||||||
|
export type ComponentConfig<
|
||||||
|
T,
|
||||||
|
S,
|
||||||
|
A,
|
||||||
|
N extends string,
|
||||||
|
C extends ChildrenConfigs<N>
|
||||||
|
> = {
|
||||||
|
render: (
|
||||||
|
state: S,
|
||||||
|
dispatch: Dispatcher<A>,
|
||||||
|
child: <K extends N>(key: K, name: string, input: C[K]['input']) => T
|
||||||
|
) => T;
|
||||||
|
handleAction: (action: A, state: S) => S;
|
||||||
|
children: ChildrenTemplates<T, S, N, C>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type GenericLens<T, U> = {
|
||||||
|
get: (v: T) => U;
|
||||||
|
set: (v: T, n: U) => T;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Child<I = unknown, S = unknown> = {
|
||||||
|
input: I;
|
||||||
|
state: S;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ChildrenConfigs<N extends string> = Record<N, Child>;
|
||||||
|
|
||||||
|
type ChildrenTemplates<T, S, 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'], unknown, string, {}>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type Children<T, S, N extends string, C extends ChildrenConfigs<N>> = {
|
||||||
|
[K in N]: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
component: Component<T, C[K]['state'], unknown, string, {}>;
|
||||||
|
lens: GenericLens<S, C[K]['state']>;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class Component<
|
||||||
|
T,
|
||||||
|
S,
|
||||||
|
A,
|
||||||
|
N extends string,
|
||||||
|
C extends ChildrenConfigs<N>
|
||||||
|
> {
|
||||||
|
private childrenMap: Children<T, S, N, C>;
|
||||||
|
public constructor(
|
||||||
|
protected state: S,
|
||||||
|
private config: ComponentConfig<T, S, A, N, C>,
|
||||||
|
private pushDownwards: (state: S) => void
|
||||||
|
) {
|
||||||
|
this.childrenMap = mapRecord(this.config.children, () => ({}));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected pushStateUpwards(state: S) {
|
||||||
|
this.state = state;
|
||||||
|
|
||||||
|
for (const { component, lens } of this.children()) {
|
||||||
|
component.pushStateUpwards(lens.set(state, component.state));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getTemplate(): T {
|
||||||
|
return this.config.render(
|
||||||
|
this.state,
|
||||||
|
value => this.dispatch(value),
|
||||||
|
(key, name, input) => {
|
||||||
|
const hasName = Reflect.has(this.childrenMap[key], name);
|
||||||
|
|
||||||
|
if (!hasName) {
|
||||||
|
const lens = this.config.children[key].lens(input);
|
||||||
|
const child = this.childrenMap[key] as Record<string, any>;
|
||||||
|
|
||||||
|
child[name] = {
|
||||||
|
lens,
|
||||||
|
component: new Component(
|
||||||
|
lens.get(this.state),
|
||||||
|
this.config.children[key].component,
|
||||||
|
state => this.pushDownwards(lens.set(this.state, state))
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.childrenMap[key][name].component.getTemplate();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public dispatch(action: A) {
|
||||||
|
const newState = this.config.handleAction(action, this.state);
|
||||||
|
|
||||||
|
this.pushStateUpwards(newState);
|
||||||
|
this.pushDownwards(newState);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a list of all the children of the component
|
||||||
|
*/
|
||||||
|
private children() {
|
||||||
|
return values(this.childrenMap).flatMap(record =>
|
||||||
|
values(record)
|
||||||
|
) as Children<T, S, N, C>[N][string][];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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,
|
||||||
|
N extends string,
|
||||||
|
C extends ChildrenConfigs<N>
|
||||||
|
>(
|
||||||
|
render: ComponentConfig<T, S, A, N, C>['render'],
|
||||||
|
handleAction: ComponentConfig<T, S, A, N, C>['handleAction'],
|
||||||
|
children: ComponentConfig<T, S, A, N, C>['children']
|
||||||
|
) => ({ render, handleAction, children });
|
||||||
|
|
||||||
|
export const mkChild = <T, PS, I, S>(
|
||||||
|
lens: (input: I) => GenericLens<PS, S>,
|
||||||
|
component: ComponentConfig<T, S, unknown, string, {}>
|
||||||
|
) => ({ lens, component });
|
11
typescript/monadic/src/Record.ts
Normal file
11
typescript/monadic/src/Record.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
export const mapRecord = <K extends string | number, V, W>(
|
||||||
|
record: Record<K, V>,
|
||||||
|
mapper: (v: V, k: K) => W
|
||||||
|
): Record<K, W> => {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(record).map(([key, value]) => [
|
||||||
|
key,
|
||||||
|
mapper(value as V, key as K),
|
||||||
|
])
|
||||||
|
) as any;
|
||||||
|
};
|
|
@ -1,25 +0,0 @@
|
||||||
import { IterableEmitter } from './iterableEmitter';
|
|
||||||
|
|
||||||
export type Component<T, S, A> = {
|
|
||||||
render: (state: S, dispatch: (a: A) => () => void) => T;
|
|
||||||
handleActions: (action: A, state: S) => S;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function* runComponent<T, S, A>(
|
|
||||||
component: Component<T, S, A>,
|
|
||||||
initialState: S
|
|
||||||
): AsyncGenerator<T> {
|
|
||||||
const emitter = new IterableEmitter(initialState);
|
|
||||||
|
|
||||||
const dispatch = (state: S) => (action: A) => {
|
|
||||||
const newState = component.handleActions(action, state);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
emitter.next(newState);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
for await (const state of emitter) {
|
|
||||||
yield component.render(state, dispatch(state));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,18 +1,20 @@
|
||||||
import { Component, runComponent } from './component';
|
import { ComponentConfig, Component } from './Component';
|
||||||
|
|
||||||
export type EnvConfig<T, S, A> = {
|
export type EnvConfig<T, S, A> = {
|
||||||
render: (template: T, parent: HTMLElement) => void;
|
render: (template: T, parent: HTMLElement) => void;
|
||||||
parent: HTMLElement;
|
parent: HTMLElement;
|
||||||
component: Component<T, S, A>;
|
component: ComponentConfig<T, S, A, string, {}>;
|
||||||
initialState: S;
|
initialState: S;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const runUi = async <T, S, A>(
|
export const runUi = <T, S, A>(config: EnvConfig<T, S, A>) => {
|
||||||
config: EnvConfig<T, S, A>
|
const reRender = () => config.render(component.getTemplate(), config.parent);
|
||||||
): Promise<void> => {
|
|
||||||
const component = runComponent(config.component, config.initialState);
|
|
||||||
|
|
||||||
for await (const template of component) {
|
const component = new Component(config.initialState, config.component, _ => {
|
||||||
config.render(template, config.parent);
|
reRender();
|
||||||
}
|
});
|
||||||
|
|
||||||
|
reRender();
|
||||||
|
|
||||||
|
return component;
|
||||||
};
|
};
|
||||||
|
|
6
typescript/monadic/src/helpers.ts
Normal file
6
typescript/monadic/src/helpers.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
/**
|
||||||
|
* Typesafe version of Object.values
|
||||||
|
*/
|
||||||
|
export const values = Object.values as <T extends keyof object, U>(
|
||||||
|
object: Record<T, U>
|
||||||
|
) => U[];
|
|
@ -1 +1,7 @@
|
||||||
export * from './environment';
|
export { runUi, EnvConfig } from './environment';
|
||||||
|
export {
|
||||||
|
ComponentConfig,
|
||||||
|
Dispatcher,
|
||||||
|
makeComponent,
|
||||||
|
mkChild,
|
||||||
|
} from './Component';
|
||||||
|
|
|
@ -10,6 +10,10 @@ export class IterableEmitter<T> {
|
||||||
|
|
||||||
public constructor(private state: T) {}
|
public constructor(private state: T) {}
|
||||||
|
|
||||||
|
public alter(mapper: (f: T) => T) {
|
||||||
|
this.next(mapper(this.state));
|
||||||
|
}
|
||||||
|
|
||||||
async *[Symbol.asyncIterator](): AsyncGenerator<T> {
|
async *[Symbol.asyncIterator](): AsyncGenerator<T> {
|
||||||
const createPromise = () =>
|
const createPromise = () =>
|
||||||
new Promise<T>(resolve => {
|
new Promise<T>(resolve => {
|
||||||
|
|
|
@ -3709,6 +3709,11 @@ hash.js@^1.0.0, hash.js@^1.0.3:
|
||||||
inherits "^2.0.3"
|
inherits "^2.0.3"
|
||||||
minimalistic-assert "^1.0.1"
|
minimalistic-assert "^1.0.1"
|
||||||
|
|
||||||
|
helpers@^0.0.6:
|
||||||
|
version "0.0.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/helpers/-/helpers-0.0.6.tgz#b7cc9a46fffa877f8d58e447c8b8e74b830e08a9"
|
||||||
|
integrity sha1-t8yaRv/6h3+NWORHyLjnS4MOCKk=
|
||||||
|
|
||||||
hex-color-regex@^1.1.0:
|
hex-color-regex@^1.1.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e"
|
resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e"
|
||||||
|
|
Loading…
Reference in a new issue