Skip to content

Commit

Permalink
Remove Client Side Form Validation (#253)
Browse files Browse the repository at this point in the history
* Add Field Errors to Server Actions

* Remove Client Side Form Validation

- Only use simple client side validation

* Add Reset

* Uodate runAction Hook States

* Fix Schema

* Improve next/navigation Functions Handling
  • Loading branch information
timoclsn authored Oct 28, 2023
1 parent c9b53f5 commit 1e950ac
Show file tree
Hide file tree
Showing 17 changed files with 405 additions and 445 deletions.
165 changes: 77 additions & 88 deletions apps/resources/app/resources/Suggestion/SuggestionForm.tsx
Original file line number Diff line number Diff line change
@@ -1,82 +1,52 @@
'use client';

import { useUser } from '@clerk/nextjs';
import { Button, InfoBox } from 'design-system';
import { track } from 'lib/tracking';
import { AlertTriangle, CheckCircle2, Loader2, Mail } from 'lucide-react';
import { useEffect } from 'react';
import { SubmitHandler } from 'react-hook-form';
import { useRef } from 'react';
import {
errorStyles,
inputStyles,
} from '../../../components/ForrestSection/ForrestSection';
import { useZodForm } from '../../../hooks/useZodForm';
import { useAction } from '../../../lib/serverActions/client';
import { submit } from './actions';
import { SuggestionFormSchema, suggestionFormSchema } from './schemas';

export const SuggestionForm = () => {
const { user } = useUser();
const { error, isRunning, isSuccess, runAction } = useAction(submit, {
onSuccess: () => {
reset();
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
track('Resource Suggestion', {});
},
onError: () => {
setFocus('link', { shouldSelect: true });
},
});
const {
register,
handleSubmit,
formState: { errors },
reset,
setFocus,
setValue,
formState: { dirtyFields },
} = useZodForm({
schema: suggestionFormSchema,
});

let name = '';
const userName = user?.fullName;
const userEmail = user?.emailAddresses.at(0)?.emailAddress;

if (userName && userEmail) {
name = `${userName} (${userEmail})`;
} else {
name = userName || userEmail || '';
}

useEffect(() => {
if (name && !dirtyFields.name) {
setValue('name', name);
}
if (!name) {
setValue('name', '');
}
name;
}, [dirtyFields.name, name, setValue]);

const onSubmit: SubmitHandler<SuggestionFormSchema> = ({
link,
message,
name,
}) => {
runAction({
link,
message,
name: userEmail || name,
const formRef = useRef<HTMLFormElement>(null);
const linkInputRef = useRef<HTMLInputElement>(null);
const { error, validationErrors, isRunning, isSuccess, runAction } =
useAction(submit, {
onSuccess: () => {
formRef.current?.reset();
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
track('Resource Suggestion', {});
},
onError: () => {
if (!linkInputRef.current) return;
linkInputRef.current.focus();
linkInputRef.current.type = 'text';
linkInputRef.current.setSelectionRange(
0,
linkInputRef.current.value.length,
);
linkInputRef.current.type = 'url';
},
});
};

return (
<form
id="suggestion-form"
ref={formRef}
className="mx-auto flex w-full max-w-prose flex-col items-start gap-10"
onSubmit={handleSubmit(onSubmit)}
action={(formData) => {
runAction({
link: String(formData.get('link')),
name: String(formData.get('name')),
message: String(formData.get('name')),
});
}}
>
{/* Link input */}
<div className="relative w-full">
Expand All @@ -85,12 +55,19 @@ export const SuggestionForm = () => {
</label>
<input
id="link"
ref={linkInputRef}
name="link"
type="url"
required
aria-describedby="link-error"
placeholder="Link to resource"
className={inputStyles({ error: !!errors.link })}
{...register('link')}
className={inputStyles({ error: !!validationErrors?.link })}
/>
{errors.link && <p className={errorStyles}>{errors.link.message}</p>}
{validationErrors?.link && (
<div id="link-error" aria-live="polite">
<p className={errorStyles}>{validationErrors.link[0]}</p>
</div>
)}
</div>

{/* Name input */}
Expand All @@ -100,12 +77,17 @@ export const SuggestionForm = () => {
</label>
<input
id="name"
name="name"
type="text"
aria-describedby="name-error"
placeholder="Your name for attribution (optional)"
className={inputStyles({ error: !!errors.name })}
{...register('name')}
className={inputStyles({ error: !!validationErrors?.name })}
/>
{errors.name && <p className={errorStyles}>{errors.name.message}</p>}
{validationErrors?.name && (
<div id="name-error" aria-live="polite">
<p className={errorStyles}>{validationErrors.name[0]}</p>
</div>
)}
</div>

{/* Message input */}
Expand All @@ -115,41 +97,48 @@ export const SuggestionForm = () => {
</label>
<textarea
id="message"
name="message"
aria-describedby="message-error"
placeholder="Message about the resource (optional)"
rows={6}
className={inputStyles({ error: !!errors.message })}
{...register('message')}
className={inputStyles({ error: !!validationErrors?.message })}
/>
{errors.message && (
<p className={errorStyles}>{errors.message.message}</p>
{validationErrors?.message && (
<div id="message-error" aria-live="polite">
<p className={errorStyles}>{validationErrors.message[0]}</p>
</div>
)}
</div>

{/* Submit button */}
<Button type="submit" size="large">
<Button type="submit" size="large" disabled={isRunning}>
{isRunning ? <Loader2 className="animate-spin" /> : <Mail />}
Submit
</Button>

{/* Server messages */}
{isSuccess && (
<InfoBox
variant="success"
icon={<CheckCircle2 />}
className="animate-in zoom-in-0 fade-in duration-150 ease-in-out"
>
Thanks for your contribution! We&apos;ll get the resource on the site
as soon as possible.
</InfoBox>
<div aria-live="polite" role="status">
<InfoBox
variant="success"
icon={<CheckCircle2 />}
className="animate-in zoom-in-0 fade-in duration-150 ease-in-out"
>
Thanks for your contribution! We&apos;ll get the resource on the
site as soon as possible.
</InfoBox>
</div>
)}
{error && (
<InfoBox
variant="error"
icon={<AlertTriangle />}
className="animate-in zoom-in-50 fade-in duration-150 ease-in-out"
>
{error}
</InfoBox>
{error && !validationErrors && (
<div aria-live="polite" role="status">
<InfoBox
variant="error"
icon={<AlertTriangle />}
className="animate-in zoom-in-50 fade-in duration-150 ease-in-out"
>
{error}
</InfoBox>
</div>
)}
</form>
);
Expand Down
14 changes: 12 additions & 2 deletions apps/resources/app/resources/Suggestion/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,22 @@

import { createAction } from 'lib/serverActions/create';
import nodemailer from 'nodemailer';
import { envSchema, suggestionFormSchema } from './schemas';
import { z } from 'zod';

const envSchema = z.object({
SUGGESTION_MAIL_PASSWORD: z.string(),
});

const { SUGGESTION_MAIL_PASSWORD } = envSchema.parse(process.env);

export const submit = createAction({
input: suggestionFormSchema,
input: z.object({
link: z.string().min(1, { message: 'Link is required' }).url({
message: 'Must be a valid URL',
}),
message: z.string().optional(),
name: z.string().optional(),
}),
action: async ({ input }) => {
const { link, message, name } = input;

Expand Down
15 changes: 0 additions & 15 deletions apps/resources/app/resources/Suggestion/schemas.ts

This file was deleted.

Loading

1 comment on commit 1e950ac

@vercel
Copy link

@vercel vercel bot commented on 1e950ac Oct 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.