Last active
December 8, 2021 20:33
-
-
Save ryanflorence/89b16f9e1b115f08f6ff4d1e337a5ca2 to your computer and use it in GitHub Desktop.
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
import type { Action, Loader } from "@remix-run/loader"; | |
import { json, parseFormBody, redirect } from "@remix-run/loader"; | |
import { readTodos, createTodo, deleteTodo } from "../data/todo"; | |
let action: Action = async ({ request, context: { session } }) => { | |
let [method, body] = await methodOverride(request); | |
await new Promise((res) => setTimeout(res, 1000)); | |
switch (method) { | |
case "post": { | |
let [_, error] = await createTodo(body!.name); | |
if (error) { | |
session.set("error", error); | |
} | |
return redirect("/todos"); | |
} | |
case "delete": { | |
await deleteTodo(body!.id); | |
return redirect("/todos"); | |
} | |
default: { | |
throw new Error(`Unknown method! ${method}`); | |
} | |
} | |
}; | |
let loader: Loader = async ({ context: { session } }) => { | |
let todos = await readTodos(); | |
let error = session.consume("error"); | |
return json({ todos, error: error || null }); | |
}; | |
async function methodOverride( | |
request: Request | |
): Promise<["get", null] | [string, { [key: string]: any }]> { | |
let method = request.method.toLowerCase(); | |
if (method === "get") { | |
return ["get", null]; | |
} | |
let body = await parseFormBody(request); | |
if (method === "post" && body._method) { | |
method = body._method.toLowerCase(); | |
} | |
return [method, body]; | |
} | |
export { loader, action }; |
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
import React from "react"; | |
import { useRouteData, Form, usePendingFormSubmit } from "@remix-run/react"; | |
import type { Todo } from "../../loaders/data/todo"; | |
export function headers() { | |
return { | |
"cache-control": "no-cache", | |
}; | |
} | |
export function meta() { | |
return { | |
title: "Todos Yo", | |
description: "You have to have a todo list, right?", | |
}; | |
} | |
export default function Todos() { | |
let { todos, error } = useRouteData(); | |
let ref = React.useRef<null | HTMLInputElement>(null); | |
let pendingForm = usePendingFormSubmit(); | |
let state = | |
pendingForm?.method === "post" | |
? "creating" | |
: pendingForm?.method === "delete" | |
? "deleting" | |
: "idle"; | |
let showErrorTodo = state === "idle" && error; | |
let pendingTodo = pendingForm | |
? Object.fromEntries(pendingForm.data) | |
: undefined; | |
return ( | |
<div> | |
<h2>Todos</h2> | |
<Form | |
method="post" | |
onSubmit={(event) => { | |
if (pendingForm) { | |
event.preventDefault(); | |
} else { | |
requestAnimationFrame(() => { | |
ref.current!.value = ""; | |
}); | |
} | |
}} | |
> | |
<input ref={ref} name="name" /> | |
</Form> | |
<ul style={{ lineHeight: "2" }}> | |
{showErrorTodo && ( | |
<li> | |
<span style={{ opacity: 0.5 }}>{error.name}</span>{" "} | |
<span style={{ color: "red" }}>{error.message}</span> | |
</li> | |
)} | |
{state === "creating" && ( | |
<Delayed ms={200}> | |
<li style={{ opacity: 0.5 }}>{pendingTodo!.name}</li> | |
</Delayed> | |
)} | |
{todos.map((todo: Todo) => ( | |
<li | |
key={todo.id} | |
style={{ | |
opacity: | |
state === "deleting" && pendingTodo!.id === todo.id ? 0.25 : 1, | |
}} | |
> | |
{todo.name}{" "} | |
<DeleteButton | |
id={todo.id} | |
disabled={state === "deleting" || state === "creating"} | |
/> | |
</li> | |
))} | |
</ul> | |
</div> | |
); | |
} | |
function DeleteButton({ | |
id, | |
disabled, | |
...props | |
}: { | |
id: string; | |
disabled?: boolean; | |
}) { | |
return ( | |
<Form replace method="delete" style={{ display: "inline" }}> | |
<input type="hidden" name="_method" value="delete" /> | |
<input type="hidden" name="id" value={id} />{" "} | |
<button disabled={disabled} {...props}> | |
<TrashIcon /> | |
</button> | |
</Form> | |
); | |
} | |
function TrashIcon() { | |
// https://heroicons.com/ trash | |
return ( | |
<svg | |
style={{ width: "0.75rem", height: "0.75rem" }} | |
xmlns="http://www.w3.org/2000/svg" | |
viewBox="0 0 20 20" | |
fill="currentColor" | |
> | |
<path | |
fillRule="evenodd" | |
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" | |
clipRule="evenodd" | |
/> | |
</svg> | |
); | |
} | |
function Delayed({ | |
ms, | |
children, | |
}: { | |
ms: number; | |
children: React.ReactElement; | |
}) { | |
let [show, setShow] = React.useState(false); | |
React.useEffect(() => { | |
let id = setTimeout(() => { | |
setShow(true); | |
}, ms); | |
return () => clearTimeout(id); | |
}, []); | |
return show ? children : null; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment