Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Realworld update #290

Merged
merged 30 commits into from
Mar 25, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
54dcbd1
store user info in JWT. yeah, yeah, i know
Rich-Harris Dec 20, 2020
f99169b
use prefetching promise if available, reload on query change
Rich-Harris Dec 21, 2020
c7efcb7
inject credentials in server-side fetch
Rich-Harris Dec 21, 2020
e003e75
start updating realworld example
Rich-Harris Dec 21, 2020
6db72d6
always supply a body when posting
Rich-Harris Dec 21, 2020
cc58597
swap tabs
Rich-Harris Dec 21, 2020
3f0fe81
simplify setup
Rich-Harris Dec 21, 2020
6363b2f
simplify settings page
Rich-Harris Dec 21, 2020
6b59ee8
replace/pushState on goto
Rich-Harris Dec 21, 2020
8f49289
handle redirects
Rich-Harris Dec 21, 2020
8d1569f
update lockfile
Rich-Harris Dec 21, 2020
1a088b2
add preloading indicator
Rich-Harris Dec 21, 2020
d1d9f26
fix prettier config
Rich-Harris Dec 21, 2020
40010ee
remove double layout
Rich-Harris Dec 21, 2020
1e7e67a
update manifest/logo
Rich-Harris Dec 21, 2020
23ad4b5
remove unwanted segment
Rich-Harris Dec 21, 2020
08180a4
fix settings page
Rich-Harris Dec 21, 2020
5744e5b
fix following logic
Rich-Harris Dec 21, 2020
298518b
various
Rich-Harris Dec 23, 2020
fd37ff3
only insist on a body for GET requests
Rich-Harris Dec 23, 2020
096e19b
only attempt 304s for get requests
Rich-Harris Dec 23, 2020
36bea7b
allow empty responses for non-GET requests
Rich-Harris Dec 23, 2020
48d7a38
allow forms to override method with ?_method=delete etc
Rich-Harris Dec 23, 2020
bcbb033
fix comment submission/deleting, including progressive enhancement
Rich-Harris Dec 23, 2020
39a6aa0
some changes i apparently made before christmas
Rich-Harris Jan 4, 2021
e4f39e8
merge master -> realworld-update
Rich-Harris Mar 23, 2021
d4d9b43
update imports
Rich-Harris Mar 24, 2021
20c59c6
minor fixes
Rich-Harris Mar 24, 2021
fb58287
fix imports
Rich-Harris Mar 25, 2021
a8f277b
Merge branch 'master' into realworld-update
Rich-Harris Mar 25, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
fix comment submission/deleting, including progressive enhancement
  • Loading branch information
Rich-Harris committed Dec 23, 2020
commit bcbb033c59b3d2cc6213640b1e92c2d780e0eb29
28 changes: 28 additions & 0 deletions examples/realworld.svelte.dev/src/common/actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const noop = () => {};

export function ajax(node, { onsubmit = noop, onresponse = noop } = {}) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I somewhat wonder if this would be a nice helper to have in SvelteKit. (I might call it formHandler instead of ajax)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think something like this should be easy to reuse, though I don't know if this is exactly the right thing — it does some slightly opinionated stuff (like setting the accept header, so that a no-JS form submission can get a 303 response while an AJAX submission gets the posted data), and we'd need to be more careful about the design if it wasn't just a little helper inside one of the example apps.

But yeah, would be nice to have some version of this in the toolkit, though maybe in Svelte itself rather than SvelteKit-specific (sveltejs/rfcs#24)

const handler = async (event) => {
event.preventDefault();
const body = node.method === 'post' || node.method === 'put' ? new FormData(node) : null;

onsubmit(body);

const response = await fetch(node.action, {
method: node.method,
body,
headers: {
accept: 'application/json'
}
});

onresponse(response);
};

node.addEventListener('submit', handler);

return {
destroy() {
node.removeEventListener('submit', handler);
}
};
}
1 change: 1 addition & 0 deletions examples/realworld.svelte.dev/src/common/constants.js
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export const page_size = 10;
export const placeholder = 'https://static.productionready.io/images/smiley-cyrus.jpg';
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<script>
import { createEventDispatcher } from 'svelte';
import { ajax } from '$common/actions.js';

export let comment;
export let slug;
export let user;

const dispatch = createEventDispatcher();
const onresponse = (res) => {
if (res.ok) {
// check the comment was deleted (e.g. we didn't
// double-click and submit twice)
dispatch('deleted');
}
};
</script>

<div class="card">
<div class="card-block">
<p class="card-text">{comment.body}</p>
</div>

<div class="card-footer">
<a href="/profile/@{comment.author.username}" class="comment-author">
<img src={comment.author.image} class="comment-author-img" alt={comment.author.username} />
</a>

<a
href="/profile/@{comment.author.username}"
class="comment-author"
>{comment.author.username}</a>

<span class="date-posted"> {new Date(comment.createdAt).toDateString()} </span>

{#if user && comment.author.username === user.username}
<form
action="/article/{slug}/comments/{comment.id}.json?_method=delete"
method="post"
class="mod-options"
use:ajax={{ onresponse }}
>
<button class="ion-trash-a" />
</form>
{/if}
</div>
</div>

<style>
button {
background: none;
border: none;
padding: 0;
margin: 0;
font-size: inherit;
margin-left: 5px;
opacity: 0.6;
cursor: pointer;
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<script>
import CommentInput from './_CommentInput.svelte';
import Comment from './_Comment.svelte';

export let comments;
export let slug;
export let user;
</script>

<div class="col-xs-12 col-md-8 offset-md-2">
{#if user}
<div>
<CommentInput
{slug}
{user}
on:commented={({ detail }) => (comments = [detail.comment, ...comments])}
/>
</div>
{:else}
<p>
<a href="/login">Sign in</a>
or
<a href="/register">sign up</a>
to add comments on this article.
</p>
{/if}

{#each comments as comment, i (comment.id)}
<Comment
{comment}
{slug}
{user}
on:deleted={() => (comments = [...comments.slice(0, i), ...comments.slice(i + 1)])}
/>
{/each}
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<script>
import { createEventDispatcher } from 'svelte';
import { placeholder } from '$common/constants.js';
import { ajax } from '$common/actions.js';

export let slug;
export let user;

const dispatch = createEventDispatcher();

let body = '';
let submitting = false;

const onsubmit = () => {
submitting = true;
};

const onresponse = async (res) => {
if (res.ok) {
const comment = await res.json();
dispatch('commented', { comment });
body = '';
} else {
// TODO error handling
}

submitting = false;
};
</script>

<form
action="/article/{slug}/comments.json"
method="post"
class="card comment-form"
use:ajax={{ onsubmit, onresponse }}
>
<div class="card-block">
<textarea
disabled={submitting}
class="form-control"
name="comment"
placeholder="Write a comment..."
bind:value={body}
rows="3"
/>
</div>

<div class="card-footer">
<img src={user.image || placeholder} class="comment-author-img" alt={user.username} />
<button disabled={submitting} class="btn btn-sm btn-primary" type="submit">Post Comment</button>
</div>
</form>
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import * as api from '$common/api.js';

export async function del(request, context) {
if (!context.user) {
return { status: 401 };
}

const { slug, id } = request.params;
const { status, error } = await api.del(`articles/${slug}/comments/${id}`, context.user.token);

if (error) {
return { status, body: { error } };
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import * as api from '$common/api.js';

export async function get(request, context) {
const { slug } = request.params;
const { comments } = await api.get(
`articles/${slug}/comments`,
context.user && context.user.token
);

return {
body: comments
};
}

export async function post(request, context) {
if (!context.user) {
return { status: 401 };
}

const { slug } = request.params;
const body = request.body.get('comment');

const { comment } = await api.post(
`articles/${slug}/comments`,
{ comment: { body } },
context.user.token
);

// for AJAX requests, return the newly created comment
if (request.headers.accept === 'application/json') {
return {
status: 201, // created
body: comment
};
}

// for traditional (no-JS) form submissions, redirect
// back to the article page
console.log(`redirecting to /article/${slug}`);
return {
status: 303, // see other
headers: {
location: `/article/${slug}`
}
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import * as api from '$common/api.js';

export async function get(request, context) {
const { slug } = request.params;
const { article } = await api.get(`articles/${slug}`, context.user && context.user.token);

return {
body: article
};
}
Original file line number Diff line number Diff line change
@@ -1,35 +1,29 @@
<script context="module">
import * as api from '$common/api.js';

export async function load({ page }) {
export async function load({ page, fetch }) {
const { slug } = page.params;
const { article } = await api.get(`articles/${slug}`, null);
const [article, comments] = await Promise.all([
fetch(`/article/${slug}.json`).then((r) => r.json()),
fetch(`/article/${slug}/comments.json`).then((r) => r.json())
]);

return {
props: { article, slug }
props: { article, comments, slug }
};
}
</script>

<script>
import { onMount } from 'svelte';
import { session } from '$app/stores';
import marked from 'marked';

import ArticleMeta from './_ArticleMeta.svelte';
import CommentContainer from './_CommentContainer.svelte';

export let article;
export let comments;
export let slug;

let commentErrors, comments = []; // we'll lazy-load these in onMount
$: markup = marked(article.body);

onMount(() => {
api.get(`articles/${slug}/comments`).then((res) => {
comments = res.comments;
});
});
</script>

<svelte:head>
Expand All @@ -40,31 +34,31 @@
<div class="banner">
<div class="container">
<h1>{article.title}</h1>
<ArticleMeta {article} user={$session.user}/>
<ArticleMeta {article} user={$session.user} />
</div>
</div>

<div class="container page">
<div class="row article-content">
<div class="col-xs-12">
<div>{@html markup}</div>
<div>
{@html markup}
</div>

<ul class="tag-list">
{#each article.tagList as tag}
<li class="tag-default tag-pill tag-outline">
{tag}
</li>
<li class="tag-default tag-pill tag-outline">{tag}</li>
{/each}
</ul>
</div>
</div>

<hr />

<div class="article-actions"></div>
<div class="article-actions" />

<div class="row">
<CommentContainer {slug} {comments} user={$session.user} errors={commentErrors}/>
<CommentContainer {slug} {comments} user={$session.user} errors={[]} />
</div>
</div>
</div>
</div>
39 changes: 0 additions & 39 deletions examples/realworld.svelte.dev/src/routes/article/_Comment.svelte

This file was deleted.

Loading