Created
October 29, 2018 21:29
-
-
Save dead-claudia/02c7fdf98e42b3d56c48392babac121e to your computer and use it in GitHub Desktop.
React if they went with a new language rather than an embedded DSL
This file contains 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
// This is a rough port of their TodoMVC app, using their Hooks API | |
import React, { useRef, useState, useEffect, useMemo } from "react" | |
import { render } from "react-dom" | |
function uuid() { | |
let uuid = "" | |
for (let i = 0; i < 32; i++) { | |
if (i === 8 || i === 12 || i === 16 || i === 20) uuid += "-" | |
if (i === 12) uuid += "4" | |
else if (i === 16) uuid += (Math.random() * 3 | 8).toString(16) | |
else uuid += (Math.random() * 16 | 0).toString(16) | |
} | |
return uuid | |
} | |
const TodoItem = React.memo(props => { | |
const editField = useRef() | |
const [editText, setEditText] = useState(props.todo.title) | |
useEffect(() => { | |
if (props.editing) { | |
const node = editField.current | |
node.focus() | |
node.setSelectionRange(node.value.length, node.value.length) | |
} | |
}, [props.editing]) | |
function onConfirm() { | |
const newEditText = editText.trim() | |
if (newEditText) { | |
props.onSave(newEditText) | |
setEditText(newEditText) | |
} else { | |
props.onDestroy() | |
} | |
} | |
return ( | |
<li className={( | |
(props.todo.completed ? "completed " : "") + | |
(props.editing ? "editing " : "") | |
)}> | |
<div className="view"> | |
<input | |
className="toggle" | |
type="checkbox" | |
checked={props.todo.completed} | |
onChange={props.onToggle} | |
/> | |
<label onDoubleClick={() => { | |
setEditText(props.todo.title) | |
props.onEdit() | |
}}> | |
{props.todo.title} | |
</label> | |
<button className="destroy" onClick={props.onDestroy} /> | |
</div> | |
<input | |
ref={editField} | |
className="edit" | |
value={editText} | |
onBlur={onConfirm} | |
onChange={ev => { | |
if (props.editing) setEditText(ev.target.value) | |
}} | |
onKeyDown={ev => { | |
if (ev.which === 27) { | |
onCancel() | |
editText <= todo.title | |
} else if (ev.which !== 13) { | |
return | |
} | |
onConfirm() | |
}} | |
/> | |
</li> | |
) | |
}) | |
function TodoFooter({ | |
count, nowShowing, completedCount, onClearCompleted | |
}) { | |
const filter = (route, value, label) => <li> | |
<a href={route} className={ | |
nowShowing === value ? "selected" : undefined | |
}> | |
{label} | |
</a> | |
</li> | |
return ( | |
<footer className="footer"> | |
<span className="todo-count"> | |
<strong>{count}</strong> item{count === 1 ? "" : "s"} left | |
</span> | |
<ul className="filters"> | |
{filter("#/", "all", "All")}{" "} | |
{filter("#/active", "active", "Active")}{" "} | |
{filter("#/completed", "completed", "Completed")} | |
</ul> | |
{completedCount ? ( | |
<button | |
className="clear-completed" | |
onClick={onClearCompleted}> | |
Clear completed | |
</button> | |
) : null} | |
</footer> | |
) | |
} | |
const initialTodos = JSON.parse(localStorage["react-todos"] || "[]") | |
function TodoApp() { | |
const [nowShowing, setNowShowing] = useState("all") | |
const [editing, setEditing] = useState(null) | |
const [newTodo, setNewTodo] = useState("") | |
const [todos, setTodos] = useState(initialTodos) | |
const [initialized, setInitialized] = useState(false) | |
useEffect( | |
() => localStorage["react-todos"] = JSON.stringify(todos), | |
[todos] | |
) | |
const remove = todo => setTodos(todos.filter(current => current !== todo)) | |
const update = (todo, props) => setTodos(todos.map(current => | |
current === todo ? {...current, ...props} : current | |
)) | |
// Do this synchronously. | |
if (!initialized) { | |
setInitialized(true) | |
new Router({ | |
"/": () => setNowShowing("all"), | |
"/active": () => setNowShowing("active"), | |
"/completed": () => setNowShowing("completed"), | |
}).init("/") | |
} | |
const shownTodos = useMemo(() => { | |
switch (nowShowing) { | |
case "all": return todos | |
case "active": return todos.filter(todo => !todo.completed) | |
case "completed": return todos.filter(todo => todo.completed) | |
} | |
}, [todos, nowShowing]) | |
const activeCount = useMemo( | |
() => todos.reduce((acc, todo) => acc + !todo.completed, 0), | |
[todos] | |
) | |
return ( | |
<div> | |
<header className="header"> | |
<h1>todos</h1> | |
<input | |
className="new-todo" | |
placeholder="What needs to be done?" | |
value={newTodo} | |
onKeyDown={ev => { | |
if (ev.keyCode !== 13) break | |
ev.preventDefault() | |
const title = newTodo.trim() | |
if (title) { | |
setTodos([...todos, {id: uuid(), title, completed: false}]) | |
setNewTodo("") | |
} | |
}} | |
onChange={ev => setNewTodo(ev.target.value)} | |
autoFocus={true} | |
/> | |
</header> | |
{todos.length ? <> | |
<section className="main"> | |
<input | |
id="toggle-all" | |
className="toggle-all" | |
type="checkbox" | |
onChange={ev => { | |
const completed = ev.target.checked | |
setTodos(todos.map(todo => {...todo, completed})) | |
}} | |
checked={activeCount === 0} | |
/> | |
<label htmlFor="toggle-all" /> | |
<ul className="todo-list"> | |
{shownTodos.map(todo => <TodoItem | |
key={todo.id} | |
todo={todo} | |
editing={editing === todo.id} | |
onToggle={() => { | |
update(todo, {completed: !todo.completed}) | |
}} | |
onDestroy={() => remove(todo)} | |
onEdit={() => setEditing(todo.id)} | |
onSave={title => { | |
update(todo, {title}) | |
setEditing(null) | |
}} | |
onCancel={() => setEditing(null)} | |
/>)} | |
</ul> | |
</section> | |
<TodoFooter | |
count={activeCount} | |
completedCount={todos.length - activeCount} | |
nowShowing={nowShowing} | |
onClearCompleted={() => { | |
setTodos(todos.filter(todo => !todo.completed)) | |
}} | |
/> | |
</> : null} | |
</div> | |
) | |
} | |
React.render(<TodoApp />, document.getElementsByClassName("todoapp")[0]) |
This file contains 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
// This is a rough port of their TodoMVC app, using a hypothetical DSL | |
function uuid() { | |
let uuid = "" | |
for (let i = 0; i < 32; i++) { | |
if (i === 8 || i === 12 || i === 16 || i === 20) uuid += "-" | |
if (i === 12) uuid += "4" | |
else if (i === 16) uuid += (Math.random() * 3 | 8).toString(16) | |
else uuid += (Math.random() * 16 | 0).toString(16) | |
} | |
return uuid | |
} | |
component <TodoItem todo editing onEdit onCancel onSave onDestroy onToggle /> { | |
static const editField = {current: undefined} | |
cell editText = todo.title | |
effect { | |
if (editing) { | |
const node = editField.current | |
node.focus() | |
node.setSelectionRange(node.value.length, node.value.length) | |
} | |
} | |
function onConfirm() { | |
const newEditText = editText.trim() | |
if (newEditText) { | |
onSave(newEditText) | |
editText <= newEditText | |
} else { | |
onDestroy() | |
} | |
} | |
return ( | |
<li className={( | |
(todo.completed ? "completed " : "") + | |
(editing ? "editing " : "") | |
)}> | |
<div className="view"> | |
<input | |
className="toggle" | |
type="checkbox" | |
checked={todo.completed} | |
onChange={onToggle} | |
/> | |
<label onDoubleClick={() => { | |
editText <= todo.title | |
onEdit() | |
}}>{todo.title}</label> | |
<button className="destroy" onClick={onDestroy} /> | |
</div> | |
<input | |
ref={editField} | |
className="edit" | |
value={editText} | |
onBlur={onConfirm} | |
onChange={ev => { if (editing) editText <= ev.target.value }} | |
onKeyDown={ev => { | |
if (ev.which === 27) { | |
onCancel() | |
editText <= todo.title | |
} else if (ev.which !== 13) { | |
return | |
} | |
onConfirm() | |
}} | |
/> | |
</li> | |
) | |
} | |
component <TodoFooter count nowShowing completedCount onClearCompleted /> { | |
component <Filter route value label /> { | |
return <li> | |
<a href={route} className={ | |
nowShowing === value ? "selected" : undefined | |
}> | |
{label} | |
</a> | |
</li> | |
} | |
return ( | |
<footer className="footer"> | |
<span className="todo-count"> | |
<strong>{count}</strong> item{count === 1 ? "" : "s"} left | |
</span> | |
<ul className="filters"> | |
<Filter route="#/" value="all" label="All" />{" "} | |
<Filter route="#/active" value="active" label="Active" />{" "} | |
<Filter route="#/completed" value="completed" label="Completed" /> | |
</ul> | |
{completedCount ? ( | |
<button | |
className="clear-completed" | |
onClick={onClearCompleted}> | |
Clear completed | |
</button> | |
) : null} | |
</footer> | |
) | |
} | |
component <TodoApp /> { | |
cell nowShowing = "all" | |
cell editing = null | |
cell newTodo = "" | |
cell todos = JSON.parse(localStorage["react-todos"] || "[]") | |
cell initialized = false | |
effect { | |
localStorage["react-todos"] = JSON.stringify(todos) | |
} | |
const update = (todo, props) => todos <= todos.map(current => | |
current === todo ? {...current, ...props} : current | |
) | |
// Do this synchronously. | |
if (!initialized) { | |
initialized <= true | |
new Router({ | |
"/": () => nowShowing <= "all", | |
"/active": () => nowShowing <= "active", | |
"/completed": () => nowShowing <= "completed", | |
}).init("/") | |
} | |
const shownTodos = use memo([todos, nowShowing], () => { | |
switch (nowShowing) { | |
case "all": return todos | |
case "active": return todos.filter(todo => !todo.completed) | |
case "completed": return todos.filter(todo => todo.completed) | |
} | |
}) | |
const activeCount = use memo([todos], () => | |
todos.reduce((acc, todo) => acc + !todo.completed, 0) | |
) | |
return ( | |
<div> | |
<header className="header"> | |
<h1>todos</h1> | |
<input | |
className="new-todo" | |
placeholder="What needs to be done?" | |
value={newTodo} | |
onKeyDown={ev => { | |
if (ev.keyCode !== 13) break | |
ev.preventDefault() | |
const title = newTodo.trim() | |
if (title) { | |
todos <= [...todos, {id: uuid(), title, completed: false}] | |
newTodo <= "" | |
} | |
}} | |
onChange={ev => newTodo <= ev.target.value} | |
autoFocus={true} | |
/> | |
</header> | |
{todos.length ? <> | |
<section className="main"> | |
<input | |
id="toggle-all" | |
className="toggle-all" | |
type="checkbox" | |
onChange={ev => { | |
const completed = ev.target.checked | |
todos <= todos.map(todo => {...todo, completed}) | |
}} | |
checked={activeCount === 0} | |
/> | |
<label htmlFor="toggle-all" /> | |
<ul className="todo-list"> | |
{shownTodos.map(todo => <TodoItem | |
key={todo.id} | |
todo={todo} | |
editing={editing === todo.id} | |
onToggle={() => { | |
update(todo, {completed: !todo.completed}) | |
}} | |
onDestroy={() => { | |
todos <= todos.filter(t => t !== todo) | |
}} | |
onEdit={() => editing <= todo.id} | |
onSave={title => { | |
update(todo, {title}) | |
editing <= null | |
}} | |
onCancel={() => editing <= null} | |
/>)} | |
</ul> | |
</section> | |
<TodoFooter | |
count={activeCount} | |
completedCount={todos.length - activeCount} | |
nowShowing={nowShowing} | |
onClearCompleted={() => { | |
todos <= todos.filter(todo => !todo.completed) | |
}} | |
/> | |
</> : null} | |
</div> | |
) | |
} | |
render(document.querySelector(".todoapp")) { return <TodoApp /> } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment