In the two previous articles, we set up and deployed a project that can retrieve data from Cloudflare Workers KV store. Now we're gonna create a form for creating new posts.
Rakkas has built-in support for form handling. We'll start with creating the form itself by adding the following lines to src/routes/index.page.tsx
, right after the closing </ul>
tag of the post list and before the closing </main>
tag:
<form method="POST">
<p>
<textarea name="content" rows={4} />
</p>
<button type="submit">Submit</button>
</form>
Fairly conventional so far. The cool part is the action handler. If you export a function named action
from a page file, Rakkas will call it when a form is submitted to that address. The code in the action function will always run on the server-side, similar to the code in the useServerSideQuery
callback. Let's add it to the bottom of the file:
// ActionHandler type is defined in the `rakkasjs` package.
// Add it to your imports.
export const action: ActionHandler = async (ctx) => {
// Retrieve the form data
const data = await ctx.requestContext.request.formData();
const content = data.get("content");
// Do some validation
if (!content) {
return { data: { error: "Content is required" } };
} else if (typeof content !== "string") {
// It could be a file upload!
return { data: { error: "Content must be a string" } };
} else if (content.length > 280) {
return { data: { error: "Content must be less than 280 characters" } };
}
await ctx.requestContext.locals.postStore.put(generateKey(), content, {
metadata: {
// We don't have login/signup yet,
// so we'll just make up a user name
author: "Arden Eberhardt",
postedAt: new Date().toISOString(),
},
});
return { data: { error: null } };
};
function generateKey() {
// This generates a random string as the post key
// but we'll talk more about this later.
return Math.random().toString(36).slice(2);
}
If you spin up the dev server, you will see that you can add new posts now!
Improving the user experience
Cool, but we have several UX problems here. First of all, we're not showing validation errors to the user.
If the action handler returns an object with the data
key, that data will be available to the page component in the actionData
prop. It will be undefined if there were no form submissions. So we'll change the signature of the HomePage
component like this:
// PageProps type is defined in the `rakkasjs` package.
// Add it to your imports.
export default function HomePage({ actionData }: PageProps) {
// ...
Now we'll add an error message right above the submit button:
<form method="POST">
<p>
<textarea name="content" rows={4} />
</p>
{actionData?.error && <p>{actionData.error}</p>}
<button type="submit">Submit</button>
</form>
Now you'll be able to see an error message if you try to submit an empty post or if the content's too long. But it's still not very user-friendly that the form is cleared when there is an error. One solution is to echo back the form data in the return value of the action handler and then use it to populate the form. So we'll change the part that returns the "too long" error like this:
- return { data: { error: "Content must be less than 280 characters" } };
+ return {
+ data: {
+ error: "Content must be less than 280 characters",
+ content, // Echo back the form data
+ },
+ };
And then we'll use it to initialize our textarea element's default value:
<textarea name="content" rows={4} defaultValue={actionData?.content} />
If you try again and submit a post that is too long, you will see that the form will not be cleared and you will be able to edit the content down to 280 characters to re-submit.
Sorting the posts
You may have noticed that newly created posts are inserted at a random position in the list. It would be better if we saw them in the newest-first order. The KV store doesn't have a method for sorting by content or metadata. But it always returns the items in the alphabetical order of the keys. Instead of random keys, we could use the creation time but it would be the exact opposite of what we want since 2022-08-01T00:00:00.000Z
comes after 2020-08-01T00:00:00.000Z
when sorted alphabetically.
So we'll have to get creative here. The JavaScript Date
instances have a getTime()
method that returns a timestamp which is the number of milliseconds since January 1, 1970. You can also create a Date from a timestamp with, e.g. new Date(0)
. What's the date for the timestamp 9,999,999,999,999? new Date(9_999_999_999_999)
returns November 20, 2286. I'm fairly certain ublog will not be around for that long. So my idea is to use 9_999_999_999_999 - new Date().getTime()
as our key.
To make sure that the keys are small we'll use the base-36 encoding and to ensure alphabetical sorting, we'll left-pad the result with zeroes. The base-36 encoding of 9,999,999,999,999 is 3jlxpt2pr
which is 9 characters long. So we will left-pad until the key is at least 9 characters:
function generateKey() {
return (9_999_999_999_999 - new Date().getTime())
.toString(36)
.padStart(9, "0");
}
The keys should be unique but what if two users create posts at the same time? We can reduce the possibility of key collisions to "practically zero" by appending a random string at the end:
function generateKey() {
return (
(9_999_999_999_999 - new Date().getTime()).toString(36).padStart(9, "0") +
Math.random().toString(36).slice(2).padStart(6, "0")
);
}
In a real application, you'd probably want to use a more sophisticated key generation routine like UUID v4 but this is fine for our purposes.
Now if you spin up the dev server, you will see that the posts are sorted by creation time except for the mock ones. You can fix those by changing their made-up keys from 1
-3
to z1
-z3
so that they always stay at the bottom.
That's it! We can now add new posts to the list and view them in the newest-first order.
Testing with Miniflare
Since anyone can create posts now, it's best if we don't deploy this to Cloudflare Workers yet. But we can test our workers bundle with Miniflare by building with npm run build
and launching with npm run local
. Miniflare has built-in KV store support so everything should work as expected.
What's next?
In the next article, we'll implement authentication (sign-in/sign-up) using the GitHub OAuth API.
You can find the progress up to this point on GitHub.
Top comments (0)