[examples] Update todo functionality in the "sveltekit" example (#7506)

This commit is contained in:
Sam Ko
2022-03-01 15:50:04 -08:00
committed by GitHub
parent 6ccb4354f9
commit 9ff86a896c
13 changed files with 505 additions and 439 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,7 @@
{ {
"private": true, "private": true,
"name": "sveltekit",
"version": "0.0.1",
"scripts": { "scripts": {
"dev": "svelte-kit dev", "dev": "svelte-kit dev",
"build": "svelte-kit build", "build": "svelte-kit build",
@@ -9,7 +11,7 @@
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-auto": "next", "@sveltejs/adapter-auto": "next",
"@sveltejs/kit": "next", "@sveltejs/kit": "next",
"svelte": "^3.44.0" "svelte": "^3.46.0"
}, },
"type": "module", "type": "module",
"dependencies": { "dependencies": {

View File

@@ -8,6 +8,6 @@
%svelte.head% %svelte.head%
</head> </head>
<body> <body>
<div id="svelte">%svelte.body%</div> <div>%svelte.body%</div>
</body> </body>
</html> </html>

View File

@@ -1 +0,0 @@
/// <reference types="@sveltejs/kit" />

View File

@@ -1,19 +1,22 @@
import cookie from 'cookie'; import cookie from 'cookie';
import { v4 as uuid } from '@lukeed/uuid'; import { v4 as uuid } from '@lukeed/uuid';
export const handle = async ({ request, resolve }) => { export const handle = async ({ event, resolve }) => {
const cookies = cookie.parse(request.headers.cookie || ''); const cookies = cookie.parse(event.request.headers.get('cookie') || '');
request.locals.userid = cookies.userid || uuid(); event.locals.userid = cookies.userid || uuid();
const response = await resolve(request); const response = await resolve(event);
if (!cookies.userid) { if (!cookies.userid) {
// if this is the first time the user has visited this app, // if this is the first time the user has visited this app,
// set a cookie so that we recognise them when they return // set a cookie so that we recognise them when they return
response.headers['set-cookie'] = cookie.serialize('userid', request.locals.userid, { response.headers.set(
path: '/', 'set-cookie',
httpOnly: true cookie.serialize('userid', event.locals.userid, {
}); path: '/',
httpOnly: true
})
);
} }
return response; return response;

View File

@@ -22,7 +22,7 @@
<div class="counter-viewport"> <div class="counter-viewport">
<div class="counter-digits" style="transform: translate(0, {100 * offset}%)"> <div class="counter-digits" style="transform: translate(0, {100 * offset}%)">
<strong style="top: -100%" aria-hidden="true">{Math.floor($displayed_count + 1)}</strong> <strong class="hidden" aria-hidden="true">{Math.floor($displayed_count + 1)}</strong>
<strong>{Math.floor($displayed_count)}</strong> <strong>{Math.floor($displayed_count)}</strong>
</div> </div>
</div> </div>
@@ -94,4 +94,9 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
.hidden {
top: -100%;
user-select: none;
}
</style> </style>

View File

@@ -1,6 +1,8 @@
import { invalidate } from '$app/navigation';
// this action (https://svelte.dev/tutorial/actions) allows us to // this action (https://svelte.dev/tutorial/actions) allows us to
// progressively enhance a <form> that already works without JS // progressively enhance a <form> that already works without JS
export function enhance(form, { pending, error, result }) { export function enhance(form, { pending, error, result } = {}) {
let current_token; let current_token;
async function handle_submit(e) { async function handle_submit(e) {
@@ -8,31 +10,35 @@ export function enhance(form, { pending, error, result }) {
e.preventDefault(); e.preventDefault();
const body = new FormData(form); const data = new FormData(form);
if (pending) pending(body, form); if (pending) pending({ data, form });
try { try {
const res = await fetch(form.action, { const response = await fetch(form.action, {
method: form.method, method: form.method,
headers: { headers: {
accept: 'application/json' accept: 'application/json'
}, },
body body: data
}); });
if (token !== current_token) return; if (token !== current_token) return;
if (res.ok) { if (response.ok) {
result(res, form); if (result) result({ data, form, response });
const url = new URL(form.action);
url.search = url.hash = '';
invalidate(url.href);
} else if (error) { } else if (error) {
error(res, null, form); error({ data, form, error: null, response });
} else { } else {
console.error(await res.text()); console.error(await response.text());
} }
} catch (e) { } catch (e) {
if (error) { if (error) {
error(null, e, form); error({ data, form, error: e, response: null });
} else { } else {
throw e; throw e;
} }

View File

@@ -1,14 +0,0 @@
import { api } from './_api';
// PATCH /todos/:uid.json
export const patch = async (request) => {
return api(request, `todos/${request.locals.userid}/${request.params.uid}`, {
text: request.body.get('text'),
done: request.body.has('done') ? !!request.body.get('done') : undefined
});
};
// DELETE /todos/:uid.json
export const del = async (request) => {
return api(request, `todos/${request.locals.userid}/${request.params.uid}`);
};

View File

@@ -1,6 +1,6 @@
/* /*
This module is used by the /todos.json and /todos/[uid].json This module is used by the /todos endpoint to
endpoints to make calls to api.svelte.dev, which stores todos make calls to api.svelte.dev, which stores todos
for each user. The leading underscore indicates that this is for each user. The leading underscore indicates that this is
a private module, _not_ an endpoint — visiting /todos/_api a private module, _not_ an endpoint — visiting /todos/_api
will net you a 404 response. will net you a 404 response.
@@ -11,35 +11,12 @@
const base = 'https://api.svelte.dev'; const base = 'https://api.svelte.dev';
export async function api(request, resource, data) { export function api(method, resource, data) {
// user must have a cookie set return fetch(`${base}/${resource}`, {
if (!request.locals.userid) { method,
return { status: 401 };
}
const res = await fetch(`${base}/${resource}`, {
method: request.method,
headers: { headers: {
'content-type': 'application/json' 'content-type': 'application/json'
}, },
body: data && JSON.stringify(data) body: data && JSON.stringify(data)
}); });
// if the request came from a <form> submission, the browser's default
// behaviour is to show the URL corresponding to the form's "action"
// attribute. in those cases, we want to redirect them back to the
// /todos page, rather than showing the response
if (res.ok && request.method !== 'GET' && request.headers.accept !== 'application/json') {
return {
status: 303,
headers: {
location: '/todos'
}
};
}
return {
status: res.status,
body: await res.json()
};
} }

View File

@@ -0,0 +1,66 @@
import { api } from './_api';
export const get = async ({ locals }) => {
// locals.userid comes from src/hooks.js
const response = await api('get', `todos/${locals.userid}`);
if (response.status === 404) {
// user hasn't created a todo list.
// start with an empty array
return {
body: {
todos: []
}
};
}
if (response.status === 200) {
return {
body: {
todos: await response.json()
}
};
}
return {
status: response.status
};
};
export const post = async ({ request, locals }) => {
const form = await request.formData();
await api('post', `todos/${locals.userid}`, {
text: form.get('text')
});
return {};
};
// If the user has JavaScript disabled, the URL will change to
// include the method override unless we redirect back to /todos
const redirect = {
status: 303,
headers: {
location: '/todos'
}
};
export const patch = async ({ request, locals }) => {
const form = await request.formData();
await api('patch', `todos/${locals.userid}/${form.get('uid')}`, {
text: form.has('text') ? form.get('text') : undefined,
done: form.has('done') ? !!form.get('done') : undefined
});
return redirect;
};
export const del = async ({ request, locals }) => {
const form = await request.formData();
await api('delete', `todos/${locals.userid}/${form.get('uid')}`);
return redirect;
};

View File

@@ -1,28 +0,0 @@
import { api } from './_api';
// GET /todos.json
export const get = async (request) => {
// request.locals.userid comes from src/hooks.js
const response = await api(request, `todos/${request.locals.userid}`);
if (response.status === 404) {
// user hasn't created a todo list.
// start with an empty array
return { body: [] };
}
return response;
};
// POST /todos.json
export const post = async (request) => {
const response = await api(request, `todos/${request.locals.userid}`, {
// because index.svelte posts a FormData object,
// request.body is _also_ a (readonly) FormData
// object, which allows us to get form data
// with the `body.get(key)` method
text: request.body.get('text')
});
return response;
};

View File

@@ -1,40 +1,9 @@
<script context="module">
import { enhance } from '$lib/form';
// see https://kit.svelte.dev/docs#loading
export const load = async ({ fetch }) => {
const res = await fetch('/todos.json');
if (res.ok) {
const todos = await res.json();
return {
props: { todos }
};
}
const { message } = await res.json();
return {
error: new Error(message)
};
};
</script>
<script> <script>
import { enhance } from '$lib/form';
import { scale } from 'svelte/transition'; import { scale } from 'svelte/transition';
import { flip } from 'svelte/animate'; import { flip } from 'svelte/animate';
export let todos; export let todos;
async function patch(res) {
const todo = await res.json();
todos = todos.map((t) => {
if (t.uid === todo.uid) return todo;
return t;
});
}
</script> </script>
<svelte:head> <svelte:head>
@@ -46,13 +15,10 @@
<form <form
class="new" class="new"
action="/todos.json" action="/todos"
method="post" method="post"
use:enhance={{ use:enhance={{
result: async (res, form) => { result: async ({ form }) => {
const created = await res.json();
todos = [...todos, created];
form.reset(); form.reset();
} }
}} }}
@@ -68,41 +34,33 @@
animate:flip={{ duration: 200 }} animate:flip={{ duration: 200 }}
> >
<form <form
action="/todos/{todo.uid}.json?_method=PATCH" action="/todos?_method=PATCH"
method="post" method="post"
use:enhance={{ use:enhance={{
pending: (data) => { pending: ({ data }) => {
todo.done = !!data.get('done'); todo.done = !!data.get('done');
}, }
result: patch
}} }}
> >
<input type="hidden" name="uid" value={todo.uid} />
<input type="hidden" name="done" value={todo.done ? '' : 'true'} /> <input type="hidden" name="done" value={todo.done ? '' : 'true'} />
<button class="toggle" aria-label="Mark todo as {todo.done ? 'not done' : 'done'}" /> <button class="toggle" aria-label="Mark todo as {todo.done ? 'not done' : 'done'}" />
</form> </form>
<form <form class="text" action="/todos?_method=PATCH" method="post" use:enhance>
class="text" <input type="hidden" name="uid" value={todo.uid} />
action="/todos/{todo.uid}.json?_method=PATCH"
method="post"
use:enhance={{
result: patch
}}
>
<input aria-label="Edit todo" type="text" name="text" value={todo.text} /> <input aria-label="Edit todo" type="text" name="text" value={todo.text} />
<button class="save" aria-label="Save todo" /> <button class="save" aria-label="Save todo" />
</form> </form>
<form <form
action="/todos/{todo.uid}.json?_method=DELETE" action="/todos?_method=DELETE"
method="post" method="post"
use:enhance={{ use:enhance={{
pending: () => (todo.pending_delete = true), pending: () => (todo.pending_delete = true)
result: () => {
todos = todos.filter((t) => t.uid !== todo.uid);
}
}} }}
> >
<input type="hidden" name="uid" value={todo.uid} />
<button class="delete" aria-label="Delete todo" disabled={todo.pending_delete} /> <button class="delete" aria-label="Delete todo" disabled={todo.pending_delete} />
</form> </form>
</div> </div>
@@ -158,7 +116,7 @@
.done { .done {
transform: none; transform: none;
opacity: 0.4; opacity: 0.4;
filter: drop-shadow(0px 0px 1px rgba(0, 0, 0, 0.1)); filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.1));
} }
form.text { form.text {

View File

@@ -5,8 +5,10 @@ const config = {
kit: { kit: {
adapter: adapter(), adapter: adapter(),
// hydrate the <div id="svelte"> element in src/app.html // Override http methods in the Todo forms
target: '#svelte' methodOverride: {
allowed: ['PATCH', 'DELETE']
}
} }
}; };