[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,
"name": "sveltekit",
"version": "0.0.1",
"scripts": {
"dev": "svelte-kit dev",
"build": "svelte-kit build",
@@ -9,7 +11,7 @@
"devDependencies": {
"@sveltejs/adapter-auto": "next",
"@sveltejs/kit": "next",
"svelte": "^3.44.0"
"svelte": "^3.46.0"
},
"type": "module",
"dependencies": {

View File

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

View File

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

View File

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

View File

@@ -22,7 +22,7 @@
<div class="counter-viewport">
<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>
</div>
</div>
@@ -94,4 +94,9 @@
width: 100%;
height: 100%;
}
.hidden {
top: -100%;
user-select: none;
}
</style>

View File

@@ -1,6 +1,8 @@
import { invalidate } from '$app/navigation';
// this action (https://svelte.dev/tutorial/actions) allows us to
// 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;
async function handle_submit(e) {
@@ -8,31 +10,35 @@ export function enhance(form, { pending, error, result }) {
e.preventDefault();
const body = new FormData(form);
const data = new FormData(form);
if (pending) pending(body, form);
if (pending) pending({ data, form });
try {
const res = await fetch(form.action, {
const response = await fetch(form.action, {
method: form.method,
headers: {
accept: 'application/json'
},
body
body: data
});
if (token !== current_token) return;
if (res.ok) {
result(res, form);
if (response.ok) {
if (result) result({ data, form, response });
const url = new URL(form.action);
url.search = url.hash = '';
invalidate(url.href);
} else if (error) {
error(res, null, form);
error({ data, form, error: null, response });
} else {
console.error(await res.text());
console.error(await response.text());
}
} catch (e) {
if (error) {
error(null, e, form);
error({ data, form, error: e, response: null });
} else {
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
endpoints to make calls to api.svelte.dev, which stores todos
This module is used by the /todos endpoint to
make calls to api.svelte.dev, which stores todos
for each user. The leading underscore indicates that this is
a private module, _not_ an endpoint — visiting /todos/_api
will net you a 404 response.
@@ -11,35 +11,12 @@
const base = 'https://api.svelte.dev';
export async function api(request, resource, data) {
// user must have a cookie set
if (!request.locals.userid) {
return { status: 401 };
}
const res = await fetch(`${base}/${resource}`, {
method: request.method,
export function api(method, resource, data) {
return fetch(`${base}/${resource}`, {
method,
headers: {
'content-type': 'application/json'
},
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>
import { enhance } from '$lib/form';
import { scale } from 'svelte/transition';
import { flip } from 'svelte/animate';
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>
<svelte:head>
@@ -46,13 +15,10 @@
<form
class="new"
action="/todos.json"
action="/todos"
method="post"
use:enhance={{
result: async (res, form) => {
const created = await res.json();
todos = [...todos, created];
result: async ({ form }) => {
form.reset();
}
}}
@@ -68,41 +34,33 @@
animate:flip={{ duration: 200 }}
>
<form
action="/todos/{todo.uid}.json?_method=PATCH"
action="/todos?_method=PATCH"
method="post"
use:enhance={{
pending: (data) => {
pending: ({ data }) => {
todo.done = !!data.get('done');
},
result: patch
}
}}
>
<input type="hidden" name="uid" value={todo.uid} />
<input type="hidden" name="done" value={todo.done ? '' : 'true'} />
<button class="toggle" aria-label="Mark todo as {todo.done ? 'not done' : 'done'}" />
</form>
<form
class="text"
action="/todos/{todo.uid}.json?_method=PATCH"
method="post"
use:enhance={{
result: patch
}}
>
<form class="text" action="/todos?_method=PATCH" method="post" use:enhance>
<input type="hidden" name="uid" value={todo.uid} />
<input aria-label="Edit todo" type="text" name="text" value={todo.text} />
<button class="save" aria-label="Save todo" />
</form>
<form
action="/todos/{todo.uid}.json?_method=DELETE"
action="/todos?_method=DELETE"
method="post"
use:enhance={{
pending: () => (todo.pending_delete = true),
result: () => {
todos = todos.filter((t) => t.uid !== todo.uid);
}
pending: () => (todo.pending_delete = true)
}}
>
<input type="hidden" name="uid" value={todo.uid} />
<button class="delete" aria-label="Delete todo" disabled={todo.pending_delete} />
</form>
</div>
@@ -158,7 +116,7 @@
.done {
transform: none;
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 {

View File

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