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

Feat(forms): Add sample form implementation. #23

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 3 additions & 1 deletion app/[[...slug]]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { graphqlClient } from "#/lib/graphqlClient";
import { draftMode } from "next/headers";
import { ComponentHeroBannerFieldsFragment } from "#/components/hero-banner-ctf/hero-banner-ctf";
import { ComponentDuplexFieldsFragment } from "#/components/duplex-ctf/duplex-ctf";
import { ComponentFormFieldsFragment } from "#/components/form-ctf/form-ctf";

const getPage = async (slug: string, locale: string, preview = false) => {
const pageQuery = graphql(
Expand All @@ -22,13 +23,14 @@ const getPage = async (slug: string, locale: string, preview = false) => {
items {
...ComponentHeroBannerFields
...ComponentDuplexFields
...ComponentFormFields
}
}
}
}
}
`,
[ComponentHeroBannerFieldsFragment, ComponentDuplexFieldsFragment]
[ComponentHeroBannerFieldsFragment, ComponentDuplexFieldsFragment, ComponentFormFieldsFragment]
);
return (
await graphqlClient(preview).request(pageQuery, {
Expand Down
3 changes: 3 additions & 0 deletions components/component-renderer/mappings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,7 @@ export const componentMap = {
ComponentDuplex: dynamic(() =>
import("#/components/duplex-ctf").then((mod) => mod.DuplexCtf)
),
ComponentForm: dynamic(() =>
import("#/components/form-ctf").then((mod) => mod.FormCtf)
),
};
22 changes: 22 additions & 0 deletions components/form-ctf/form-ctf.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { FragmentOf, readFragment, graphql } from "gql.tada";
import { FormCtfClient } from './from-ctf-client';

export const ComponentFormFieldsFragment = graphql(/* GraphQL */ `
fragment ComponentFormFields on ComponentForm {
__typename
sys {
id
}
headline
form
}
`);

export type FormProps = {
data: FragmentOf<typeof ComponentFormFieldsFragment>;
};

export const FormCtf: React.FC<FormProps> = (props) => {
const data = readFragment(ComponentFormFieldsFragment, props.data);
return <FormCtfClient data={data}/>;
};
24 changes: 24 additions & 0 deletions components/form-ctf/from-ctf-client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
'use client';

import { formsMap } from '../forms/forms-mapping';
import { useComponentPreview } from '../hooks/use-component-preview';
import { FormContainer } from '../ui/form-container';
import {ResultOf} from "gql.tada";
import {ComponentFormFieldsFragment} from "#/components/form-ctf/form-ctf";

export const FormCtfClient: React.FC<{
data: ResultOf<typeof ComponentFormFieldsFragment>;
}> = (props) => {
const { data: originalData } = props;
const { data, addAttributes } =
useComponentPreview<typeof originalData>(originalData);
if(data.form) {
// @TODO: Fix typings for formsMap.
// @ts-ignore
const Form = formsMap[data.form];

return <FormContainer form={<Form />} headline={data.headline} addAttributes={addAttributes}/> ;
}

return null;
};
1 change: 1 addition & 0 deletions components/form-ctf/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { FormCtf } from "./form-ctf";
61 changes: 61 additions & 0 deletions components/forms/SubscriptionForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
'use client';

import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { subscriptionSchema } from './schema/subscription.schema';
import { Button } from '../ui/button';
import { Input } from '../ui/input';
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '../ui/form';
import { useFormState } from 'react-dom';
import { useRef } from 'react';
import onSubscribeFormAction from './server-actions/subscription.action';

const initialState = {
message: '',
};

export const SubscriptionForm = () => {
const [state, formAction] = useFormState(onSubscribeFormAction, initialState)
const form = useForm<z.infer<typeof subscriptionSchema>>({
resolver: zodResolver(subscriptionSchema),
defaultValues: {
email: "",
}
});

const formRef = useRef<HTMLFormElement>(null);

return (
<>
{state?.message && <p>{state.message}</p>}
<Form {...form}>
<form
ref={formRef}
action={formAction}
onSubmit={form.handleSubmit(() => formRef?.current?.submit())}
>
<FormField control={form.control} name="email" render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder='' {...field} />
</FormControl>
<FormDescription />
<FormMessage />
</FormItem>
)} />
<Button type='submit'>Subscribe</Button>
</form>
</Form>
</>
);
};
7 changes: 7 additions & 0 deletions components/forms/forms-mapping.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import dynamic from "next/dynamic";

export const formsMap = {
'1. Subscription Form (subscriptionForm)': dynamic(() =>
import("#/components/forms/SubscriptionForm").then((mod) => mod.SubscriptionForm)
),
};
7 changes: 7 additions & 0 deletions components/forms/schema/subscription.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { z } from 'zod';

export const subscriptionSchema = z.object({
email: z.string().trim().email({
message: "Invalid email address.",
}),
});
23 changes: 23 additions & 0 deletions components/forms/server-actions/subscription.action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
'use server';

import { z } from "zod";
import { subscriptionSchema } from "../schema/subscription.schema";

const onSubscribeFormAction = async (prevState: {
message: string;
user?: z.infer<typeof subscriptionSchema>;
issues?: string[];
}, formData: FormData) => {
const data = Object.fromEntries(formData);
const parsed = subscriptionSchema.safeParse(data);
if (parsed.success) {
return { message: "Subscribed successfully!", user: parsed.data }
} else {
return {
message: "Invalid data", error: parsed.error,
status: 400,
}
}
};

export default onSubscribeFormAction;
32 changes: 32 additions & 0 deletions components/ui/form-container/form-container.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { Meta, StoryObj } from "@storybook/react";
import { SampleForm } from "../form/form.stories";
import { FormContainer } from "./form-container";

// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
const meta = {
title: "Components/FormContainer",
component: FormContainer,
parameters: {
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/react/configure/story-layout
layout: "fullscreen",
},
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/react/writing-docs/autodocs
tags: ["autodocs"],
// More on argTypes: https://storybook.js.org/docs/react/api/argtypes
argTypes: {
headline: { control: "text" },
},
} satisfies Meta<typeof FormContainer>;

export default meta;
type Story = StoryObj<typeof meta>;
const defaultArgs = {
headline: "The from headline",
form: <SampleForm />
};
// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args
export const Default: Story = {
args: {
...defaultArgs,
},
};
33 changes: 33 additions & 0 deletions components/ui/form-container/form-container.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { ReactNode } from "react";

interface FormContainerProps {
headline?: string | null;
form?: ReactNode;
addAttributes?: (name: string) => object | null;
}

export function FormContainer(props: FormContainerProps) {
const {
headline,
form,
addAttributes = () => ({}), // Default to no-op.
} = props;

return (
<div className="container py-12">
<div className="max-w-screen-sm mx-auto">
{headline && (
<h2
{...addAttributes("headline")}
className={
"text-4xl font-bold mb-4"
}
>
{headline}
</h2>
)}
{form}
</div>
</div>
);
}
1 change: 1 addition & 0 deletions components/ui/form-container/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./form-container";
10 changes: 10 additions & 0 deletions components/ui/form/form.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Canvas, Meta } from '@storybook/blocks';

import * as FormStories from './form.stories';

<Meta of={FormStories} />

# Form

We are using a Form component from [Shadcn/UI/Form](https://ui.shadcn.com/docs/components/form). Please, follow the link above about the usage details.
<Canvas of={FormStories.SampleForm} />
43 changes: 43 additions & 0 deletions components/ui/form/form.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { Meta } from "@storybook/react";
import { useForm } from "react-hook-form";
import { Button } from "../button";
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "../form/form";
import { Input } from "../input";

export const SampleForm = () => {
const form = useForm();

return (
<Form {...form}>
<form action="">
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="shadcn" {...field} />
</FormControl>
<FormDescription>This is your public display name.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
</form>
</Form>
);
};

// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
const meta = {
title: "UI/Form",
component: SampleForm,
parameters: {
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/react/configure/story-layout
layout: "centered",
},
} satisfies Meta<typeof SampleForm>;

export default meta;
Loading