Create post form

cyco130

Fatih Aygün

Posted on August 10, 2022

Create post form

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>
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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) {
    // ...
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
+       },
+   };
Enter fullscreen mode Exit fullscreen mode

And then we'll use it to initialize our textarea element's default value:

<textarea name="content" rows={4} defaultValue={actionData?.content} />
Enter fullscreen mode Exit fullscreen mode

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");
}
Enter fullscreen mode Exit fullscreen mode

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")
    );
}
Enter fullscreen mode Exit fullscreen mode

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.

💖 💪 🙅 🚩
cyco130
Fatih Aygün

Posted on August 10, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related