Skip to content

Instantly share code, notes, and snippets.

@IvanRainbolt
Forked from ivenmarquardt/todo-app.js
Created June 15, 2020 19:55
Show Gist options
  • Save IvanRainbolt/dcc034430e9b5400f88f28fd3eacaf1c to your computer and use it in GitHub Desktop.
Save IvanRainbolt/dcc034430e9b5400f88f28fd3eacaf1c to your computer and use it in GitHub Desktop.
Functional Reactive Programming (FRP) implemented with a couple rules, types and combinators
// Based on this blog post: https://medium.com/@iquardt/taming-the-dom-50c8f1a6e892
/* RULES
* Only the program may manipulate the output display, never the user
* User input is presented in the form of events
* GUI elements generate events only in response to user input, never in response to program output
*/
// data constructor
const Data = name => Dcons => {
const Data = k => {
const t = new Tcons();
t[`run${name}`] = k;
t.tag = name;
return t;
};
const Tcons =
Function(`return function ${name}() {}`) ();
return Dcons(Data);
};
// types
const Event = Data("Event")
(Event => k => Event(k));
const Behavior = Data("Behavior")
(Behavior => k => Behavior(k));
const Eff = Data("Eff")
(Eff => thunk => Eff(thunk));
const subscribe = o => {
o.target.addEventListener(
o.type,
o.observer,
o.options
);
return () => o.target.removeEventListener(
o.type,
o.observer,
o.options
);
};
// combinators
const markup = name => (...attr) => (...children) => {
const el = document.createElement(name);
attr.forEach(
a => el.setAttributeNode(a));
children.forEach(child =>
el.appendChild(child));
return el;
};
const attr = (k, v) => {
const a = document.createAttribute(k);
a.value = v;
return a;
};
const article = markup("article");
const header = markup("header");
const section = markup("section");
const footer = markup("footer");
const input = markup("input");
const h1 = markup("h1");
const ul = markup("ul");
const li = markup("li");
const li_ = li();
const style = markup("style");
const text = s =>
document.createTextNode(s);
const appendNode = parent => child =>
Eff(() => parent.append(child));
const insertAfter = predecessor => sibling =>
Eff(() => predecessor.insertBefore(sibling, predecessor.firstChild));
const insertBefore = successor => sibling =>
Eff(() => successor.insertBefore(sibling, successor.firstChild));
// TODO APP
// markup
const todoCompo = state =>
article(attr("style", "margin: 1em; width: 20em")) (
style() (cssRules),
header() (
h1(attr("style", "font-size: 2em"))
(text("todos")),
input(
attr("id", "todo-input"),
attr("placeholder", "What needs to be done?")) ()),
(state.todos.length
? section()
: section(attr("style", "display: none")))
(ul(attr("id", "todo-list"))
(...toTodoItemEl(state.todos))),
state.todos.length
? footer(attr("id", "todo-footer"))
(text(`${state.todos.length} items`))
: footer(attr("id", "todo-footer")) ()
);
const cssRules = text(`
ul {cursor: pointer}
ul > li:hover {text-decoration: underline}
ul > li:hover::after {content: ' -'}`);
const todoItem = s =>
li_(text(s));
const toTodoItemEl = todos =>
todos.map(s => todoItem(s));
// type instances
const todoInputB = initialState => {
let state = initialState;
const cancel = subscribe({
target: document.getElementById("todo-input"),
type: "input",
observer: e => state = e.target.value,
options: {capture: true}
});
const refresh = () =>
state = document.getElementById("todo-input").value;
return Object.assign(Behavior(
(k, e) => k(state)),
{cancel, refresh});
};
const addTodoE = Event((k, e) => subscribe({
target: document.getElementById("todo-input"),
type: "keydown",
observer: event => k(event),
options: {capture: true}
}));
const remTodoE = Event((k, e) => subscribe({
target: document.getElementById("todo-list"),
type: "click",
observer: event => k(event),
options: {capture: true}
}));
// state functions
const addTodo = state => todo =>
todo.length === 0
? UnchangedState
: (state.todos.push(todo), ChangedState);
const UnchangedState = Symbol("unchanged state");
const ChangedState = Symbol("changed state");
const remTodo = state => i =>
state.todos.splice(i, 1);
// patch functions
const patchAppendTodo = state => {
const el =
todoItem(state.todos.slice(-1));
document
.getElementById("todo-list")
.appendChild(el);
};
const patchRemoveTodo = el => state => {
document
.getElementById("todo-list")
.removeChild(el);
};
const patchResetTodoInput = state => {
document
.getElementById("todo-input")
.value = "";
};
const patchFooter = state => {
document
.getElementById("todo-footer")
.firstChild
.nodeValue = `${state.todos.length} items`;
};
// rendering
const render = state => (...fs) =>
fs.forEach(f => f(state));
// main program with fake DB request
const fetch = url =>
Promise.resolve({todos: ["foo", "bar"]});
fetch("foo.bar").then(state => {
insertBefore(document.body)
(todoCompo(state)).runEff();
state.todoInput = todoInputB("");
addTodoE.runEvent(e => {
if (e.key === "Enter") {
state.todoInput.runBehavior(todo => {
if (addTodo(state) (todo) === ChangedState) {
render(state) (patchAppendTodo, patchResetTodoInput, patchFooter);
state.todoInput.refresh(); // *
}
});
}
});
remTodoE.runEvent(e => {
const i =
state.todos.indexOf(e.target.firstChild.nodeValue);
if (i !== -1) {
remTodo(state) (i);
render(state) (patchRemoveTodo(e.target), patchFooter);
}
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment