Forms
Backend
Frontend
some-liquid-file.liquid
brush/src/components/contact-form.ts
The
Section titled āThe POST routeā
api/routes/contact-form/POST.ts
Implementing forms with Brush ; with server side validation.
Architecture
Section titled āArchitectureāThe frontend needs:
- A form in a Shopify Liquid fileā¦
- ā¦having each fields to be validated identified by a
validator-field="FIELD_NAME_FOR_SERVER_VALIDATION"attribute. - This form is powered by an AlpineJS componentā¦
- ⦠which instanciates a
Brush.Form.Validatorand handles form submission logic.
The backend needs:
- A
POSTroute⦠- ⦠which declares a Zod schema where each entry matches a field from the frontend that needs validation.
- Then the request is validated against this schema using Brushās
Form.validateutility.
Frontend implementation
Section titled āFrontend implementationāForm in a Shopify Liquid file
Section titled āForm in a Shopify Liquid fileā<div x-data="ContactForm"> <form> <div> <input type="text" id="lastname" x-model="formData.lastname" validator-field="lastname" placeholder="Last name"> <input type="text" id="firstname" x-model="formData.firstname" validator-field="firstname" placeholder="First name"> </div> <div> <select name="topic" id="topic" x-model="formData.topic" validator-field="topic"> <option value="">Choose a topic</option> <template x-for="(label, value) in topics"> <option :value="value" x-text="label"></option> </template> </select> </div> <div> <label> <input type="checkbox" id="newsletter" x-model="formData.newsletter"> Subscribe to newsletter </label> </div> <div> <button x-on:click.prevent="submit" :disabled="submitting">Submit</button> </div> </form></div>AlpineJS component
Section titled āAlpineJS componentāā¹ļø Please see AlpineJS components to learn how to build and declare those with Brush.
const initialFormData = { lastname: "", firstname: "", topic: "", newsletter: false, };
export default function () { Alpine.data("ContactForm", () => ({ submitting: false, validator: undefined as InstanceType<typeof Brush.Form.Validator> | undefined, topics: { // See "Translations" reference for more infomation on how to use $t() order: $t("form.topics.order"), product: $t("form.topics.product"), other: $t("form.topics.other"), }, formData: { ...initialFormData },
async submit() { this.submitting = true; this.validator = this.validator || new Brush.Form.Validator(this.$root.querySelector("form")!); this.validator.clearErrors();
const res = await ggtFetch("contact-form", { method: "POST", body: JSON.stringify(this.formData), }); if (res.valid) { this.formData = { ...initialFormData }; } else { this.validator.showErrors(res.errors); }
this.submitting = false; }, })); }Backend implementation
Section titled āBackend implementationāThe POST route
Section titled āThe POST routeāimport { RouteHandler } from "gadget-server";import { z } from "zod";import { Form } from "../../../../_brush";import { BrushContext } from "../../../../_brush/types";
const handler: RouteHandler = async (context: BrushContext) => { // Define Zod schema // Note: Messages will be translated on the frontend automatically by Brush using $t() under the hood. const schema = z.object({ firstname: z.string().min(1, { message: "form_errors.required" }), lastname: z.string().min(1, { message: "form_errors.required" }), topic: z.enum(["order", "product", "other"], { message: "form_errors.required" }), });
// Validate request against Zod schema const result = Form.validate(context, schema);
// Business logic if (result.valid) { context.logger.info({ body: context.body }, "[brush] Form is valid"); } else { context.logger.error( { error: new Error("[brush] Form is invalid"), body: context.body, errors: result.errors }, "[brush] Form is invalid" ); }
// Send response return context.reply.send(result);};
export default handler;