Skip to content

Instantly share code, notes, and snippets.

@linktohack
Last active May 15, 2019 20:57
Show Gist options
  • Save linktohack/bf99d9afb294ed3f6dc5eb63f8ea4b3d to your computer and use it in GitHub Desktop.
Save linktohack/bf99d9afb294ed3f6dc5eb63f8ea4b3d to your computer and use it in GitHub Desktop.
(Somewhat) Elm in TypeScript
// FRAMEWORK
type Html<Msg> =
| {
el: string;
attrs: { [key: string]: string };
events: { [key: string]: (event: Event) => Msg };
children: Html<Msg>[];
}
| { text: string };
class App<Model, Msg> {
private el!: Element;
constructor(
private model: Model,
private view: (model: Model) => Html<Msg>,
private update: (model: Model, msg: Msg) => Model
) {}
bootstrap(el: Element) {
this.el = el;
this.render();
}
private render() {
this.el.childNodes.forEach(this.el.removeChild.bind(this.el));
this.el.appendChild(this.toEl(this.view(this.model)));
}
private dispatch(msg: Msg) {
console.log("dispatched with msg", msg, "for current model", this.model);
this.model = this.update(this.model, msg); // <- time travelling etc...
this.render(); // <- virtual dom / diffing algo etc...
}
private toEl(html: Html<Msg>) {
function isTextNode<Msg>(html: Html<Msg>): html is { text: string } {
return (html as { text: string }).text !== undefined;
}
if (isTextNode(html)) {
return document.createTextNode(html.text);
}
const el = document.createElement(html.el);
Object.keys(html.attrs).forEach(attr => {
el.setAttribute(attr, html.attrs[attr]);
});
Object.keys(html.events).forEach(eventName => {
el.addEventListener(eventName, e =>
this.dispatch(html.events[eventName](e))
);
});
html.children.forEach(childHtml => {
el.appendChild(this.toEl(childHtml));
});
return el;
}
}
function h<Msg>(
el: string,
attrs: { [key: string]: string } = {},
events: { [key: string]: (event: Event) => Msg } = {},
children: Html<Msg>[] = []
): Html<Msg> {
return {
el,
attrs,
events,
children
};
}
function assertNever(x: never): never {
throw new Error("Unexpected object: " + x);
}
// APP
type Model = {
count: number;
};
type Msg =
| { type: "Change"; value: string }
| { type: "Increment" }
| { type: "Decrement" };
const model: Model = {
count: 1
};
const update: (model: Model, msg: Msg) => Model = (model, msg) => {
switch (msg.type) {
case "Change":
const numberOrNaN = parseInt(msg.value);
return {
...model,
count: isNaN(numberOrNaN) ? model.count : numberOrNaN
};
case "Increment":
return { ...model, count: model.count + 1 };
case "Decrement":
return { ...model, count: model.count - 1 };
default:
return assertNever(msg);
}
};
const view: (model: Model) => Html<Msg> = model =>
h<Msg>("div", {}, {}, [
h("button", {}, { click: () => ({ type: "Increment" }) }, [{ text: "-" }]),
h(
"input",
{ value: `${model.count}` },
{
change: e => ({
type: "Change",
value: (e.target as HTMLInputElement).value
})
},
[]
),
h("button", {}, { click: () => ({ type: "Decrement" }) }, [{ text: "-" }])
]);
new App(model, view, update).bootstrap(document.querySelector("#app")!);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment