Qwik City routeAction$ with Form component

Actions in Qwik City allow you to handle form submissions, allowing to perform side effects such as writing to a database or sending an email.

Since actions are not executed during rendering, they can have side effects such as writing to a database, or sending an email. An action only runs when called explicitly.

Actions can be declared using the routeAction$() or globalAction$() exported from @builder.io/qwik-city. The best way to call an action is using the <Form/> component exported in @builder.io/qwik-city.

import { component$ } from '@builder.io/qwik';
import { routeAction$, Form } from '@builder.io/qwik-city';
 
export const useAddUser = routeAction$(async (data, requestEvent) => {
  // This will only run on the server when the user submits the form (or when the action is called programmatically)
  const userID = await db.users.add({
    firstName: data.firstName,
    lastName: data.lastName,
  });
  return {
    success: true,
    userID,
  };
});
 
export default component$(() => {
  const action = useAddUser();
 
  return (
    <>
      <Form action={action}>
        <input name="firstName" />
        <input name="lastName" />
        <button type="submit">Add user</button>
      </Form>
      {action.value?.success && (
        // When the action is done successfully, the `action.value` property will contain the return value of the action
        <p>User {action.value.userID} added successfully</p>
      )}
    </>
  );
});

When the action is done successfully, the `action.value` property will contain the return value of the action. So whatever you return in the routeAction$ can be accessed through 'action.value?.property'.

       {action.value?.success && (
          <div class="text-center mt-4">
            <p class="text-white bg-green-500 px-4 py-2 rounded">
              Email sent successfully!
            </p>
          </div>
        )}

In order to return non-success values, the action must use the fail() method.

import { routeAction$, zod$, z } from '@builder.io/qwik-city';
 
export const useAddUser = routeAction$(
  async (user, { fail }) => {
    // `user` is typed { name: string }
    const userID = await db.users.add(user);
    if (!userID) {
      return fail(500, {
        message: 'User could not be added',
      });
    }
    return {
      userID,
    };
  },
  zod$({
    name: z.string(),
  })
);

Failures are stored in the action.value property, just like the success value. However, the action.value.failed property is set to true when the action fails. Futhermore, failure messages can be found in the fieldErrors object according to properties defined in your Zod schema.

import { component$ } from '@builder.io/qwik';
import { Form } from '@builder.io/qwik-city';
 
export default component$(() => {
  const action = useAddUser();
  return (
    <Form action={action}>
      <input name="name" />
      <button type="submit">Add user</button>
      {action.value?.failed && <p>{action.value.fieldErrors.name}</p>}
      {action.value?.userID && <p>User added successfully</p>}
    </Form>
  );
});

We can use the onSubmitCompleted$ event handler after an action is executed successfully and returns some data. It is particularly useful when you need to perform additional tasks, such as resetting UI elements or updating the application state, once an action has been completed successfully.

       <Form
            action={action}
            onSubmitCompleted$={(result) => {
              if (result.detail.value.success) {
                (result.target as HTMLFormElement).reset();
              }
            }}
          >

To determine if the action is currently running, check if action.formData is not null and action.value is null. If both conditions are true, display the loading message.

export default component$(() => {
  const action = useSendEmail();

  const isLoading = action.formData && !action.value;

  return (
    <>
        {isLoading && (
          <div class="text-center mt-4">
            <p class="text-white bg-yellow-500 px-4 py-2 rounded">
              Sending email...
            </p>
          </div>
        )}
    </>
  );
});

Creating a contact with Qwik's routeAction$ and Form component

In today's digital age, having a contact form on your website is essential. It provides an easy way for visitors to get in touch with you, whether for inquiries, feedback, or other purposes. In this article, I'll walk you through how I created a contact form for my website using Qwik's routeAction$ and Form component.

Setting Up the Serverless Function

Before diving into the frontend, let's set up a serverless function using Netlify to handle the email sending process. This function will be triggered when the form is submitted.

Here's the code for the serverless function named sendEmail.ts:

// sendEmail.ts
import type { Handler, HandlerEvent, HandlerContext } from "@netlify/functions";
const sgMail = require("@sendgrid/mail");

const handler: Handler = async (event: HandlerEvent, context: HandlerContext) => {
  const { name, email, message } = JSON.parse(event.body as string);
  sgMail.setApiKey(process.env.SENDGRID_API_KEY);
  const msg = {
    to: "receiving email",
    from: "sender email",
    subject: `${name} has contacted you!`,
    text: `Name: ${name}\nEmail: ${email}\nMessage: ${message}`,
  };

  try {
    await sgMail.send(msg);
    return {
      statusCode: 200,
      body: JSON.stringify({ success: true }),
    };
  } catch (error) {
    return {
      statusCode: 500,
      body: JSON.stringify({ message: "Email could not be sent" }),
    };
  }
};

export { handler };

This function uses the SendGrid API to send emails. When the form is submitted, the function retrieves the form data, constructs an email message, and sends it using SendGrid.

Building the Contact Form with Qwik

Now, let's move on to the frontend. Using Qwik's routeAction$ and Form component, we can easily create a contact form that integrates with our serverless function.

First, we define a validation schema using Zod to ensure that the form data is valid:

const emailFormSchema = z.object({
  name: z.string().min(1, "Name is required"),
  email: z.string().email("Invalid email address"),
  message: z.string().min(10, "Message should be at least 10 characters long"),
});

Next, we create a routeAction$ to handle the form submission:

export const useSendEmail = routeAction$(async (formData, { fail }) => {
  try {
    // Define the URL for the Netlify function.
    // Replace 'yourFunctionName' with the name of your Netlify function.
    const netlifyFunctionURL =
      "URL";

    // Send a POST request to the Netlify function with the form data.
    const response = await fetch(netlifyFunctionURL, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify(formData),
    });

    // Check if the response is successful.
    if (!response.ok) {
      throw new Error("Failed to send email");
    }

    // You can return the response or any other data if needed.
    return await response.json();
  } catch (error) {
    // Handle any errors that occur during the fetch.
    console.error("Error sending email:", error);
    return fail(500, {
      message: "Failed to send email",
    });
  }
}, zod$(emailFormSchema));

This action sends a POST request to our serverless function with the form data. If the email is sent successfully, it returns a success response. Otherwise, it handles any errors that might occur.

Finally, we build the contact form using Qwik's Form component:

export default component$(() => {
  const action = useSendEmail();

  // Determine if the action is currently running
  //Check if action.formData is not null and action.value is null.
  //If both conditions are true, display the loading message.
  const isLoading = action.formData && !action.value;

  return (
    <>
      <div class="text-center p-5">
        <h3>Get in contact with Andrew!</h3>
        <p>
          Fill out the form below and I'll get back to you as soon as I can!
        </p>
      </div>
      <div class="text-black flex flex-col items-center justify-center">
        <div>
          <Form
            action={action}
            onSubmitCompleted$={(result) => {
              if (result.detail.value.success) {
                (result.target as HTMLFormElement).reset();
              }
            }}
            class="bg-black p-8 rounded-lg space-y-4 text-white w-full max-w-md"
          >
            <label class="block mb-2 font-bold">
              Name:
              <input
                class="input mt-2 bg-white text-black p-2 border border-gray-300 rounded w-full"
                type="text"
                name="name"
                required
              />
            </label>

            <label class="block mb-2 font-bold">
              Email:
              <input
                class="input mt-2 bg-white text-black p-2 border border-gray-300 rounded w-full"
                type="email"
                name="email"
                required
              />
            </label>

            <label class="block mb-2 font-bold">
              Message:
              <textarea
                class="textarea mt-2 bg-white text-black p-2 border border-gray-300 rounded w-full"
                name="message"
                required
              ></textarea>
            </label>

            {action.value?.failed && (
              <p class="text-red-500">{action.value.fieldErrors.name}</p>
            )}
            {action.value?.failed && (
              <p class="text-red-500">{action.value.fieldErrors.email}</p>
            )}
            {action.value?.failed && (
              <p class="text-red-500">{action.value.fieldErrors.message}</p>
            )}

            <button
              class="btn btn-primary mt-4 bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded"
              type="submit"
              disabled={isLoading}
            >
              Send
            </button>
          </Form>
        </div>

        {isLoading && (
          <div class="text-center mt-4">
            <p class="text-white bg-yellow-500 px-4 py-2 rounded">
              Sending email...
            </p>
          </div>
        )}

        {action.value?.success && (
          <div class="text-center mt-4">
            <p class="text-white bg-green-500 px-4 py-2 rounded">
              Email sent successfully!
            </p>
          </div>
        )}
      </div>
    </>
  );
});

The form includes input fields for the user's name, email, and message. It also provides feedback to the user, such as validation errors or success messages.

Conclusion

Building a contact form with Qwik's routeAction$ and Form component is straightforward and efficient. With the combination of Qwik's powerful features and serverless functions, we can create a seamless user experience while ensuring that the form data is handled securely and efficiently.

If you're looking to add a contact form to your website, I highly recommend giving Qwik a try. Its intuitive API and robust features make it a great choice for modern web development.