Last active
May 15, 2019 20:57
-
-
Save linktohack/bf99d9afb294ed3f6dc5eb63f8ea4b3d to your computer and use it in GitHub Desktop.
(Somewhat) Elm in TypeScript
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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