aboutsummaryrefslogtreecommitdiffstats
path: root/src/components/organisms/forms/comment-form/comment-form.tsx
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2023-11-04 19:26:16 +0100
committerArmand Philippot <git@armandphilippot.com>2023-11-11 18:15:27 +0100
commit2771de88f40a5f4ed7480bd8614532dda72deeda (patch)
treeed4d1b9ee3f6322817efa2d1b1113c247367a12a /src/components/organisms/forms/comment-form/comment-form.tsx
parentc4a561c333f6f82678efcffef5ce3ed0f8e322f4 (diff)
refactor(components): rewrite CommentForm component
* remove `Notice` prop to handle it directly in the form * replace `saveComment` prop with `onSubmit` * use `useForm` hook to handle the form
Diffstat (limited to 'src/components/organisms/forms/comment-form/comment-form.tsx')
-rw-r--r--src/components/organisms/forms/comment-form/comment-form.tsx350
1 files changed, 144 insertions, 206 deletions
diff --git a/src/components/organisms/forms/comment-form/comment-form.tsx b/src/components/organisms/forms/comment-form/comment-form.tsx
index 9059cbc..336e85a 100644
--- a/src/components/organisms/forms/comment-form/comment-form.tsx
+++ b/src/components/organisms/forms/comment-form/comment-form.tsx
@@ -1,26 +1,21 @@
-/* eslint-disable max-statements */
-import {
- type ChangeEvent,
- type FC,
- type FormEvent,
- type ReactNode,
- useCallback,
- useMemo,
- useState,
- useId,
-} from 'react';
+import { type FC, useCallback, useId } from 'react';
import { useIntl } from 'react-intl';
-import { useBoolean } from '../../../../utils/hooks';
+import type { Nullable } from '../../../../types';
+import {
+ type FormSubmitHandler,
+ useForm,
+ type FormSubmitStatus,
+ type FormSubmitMessages,
+} from '../../../../utils/hooks';
import {
Button,
Form,
type FormProps,
- Heading,
- type HeadingLevel,
Spinner,
Input,
TextArea,
Label,
+ Notice,
} from '../../../atoms';
import { LabelledField } from '../../../molecules';
import styles from './comment-form.module.scss';
@@ -33,93 +28,114 @@ export type CommentFormData = {
website?: string;
};
-export type CommentFormProps = Pick<FormProps, 'className'> & {
+export type CommentFormSubmit = FormSubmitHandler<CommentFormData>;
+
+export type CommentFormProps = Omit<FormProps, 'children' | 'onSubmit'> & {
/**
- * Pass a component to print a success/error message.
+ * A callback function to handle form submit.
*/
- Notice?: ReactNode;
+ onSubmit?: CommentFormSubmit;
/**
* The comment parent id.
*/
parentId?: number;
- /**
- * A callback function to save comment. It takes a function as parameter to
- * reset the form.
- */
- saveComment: (data: CommentFormData, reset: () => void) => Promise<void>;
- /**
- * The form title.
- */
- title?: string;
- /**
- * The title level. Default: 2.
- */
- titleLevel?: HeadingLevel;
};
export const CommentForm: FC<CommentFormProps> = ({
className = '',
- Notice,
+ onSubmit,
parentId,
- saveComment,
- title,
- titleLevel = 2,
...props
}) => {
+ const formId = useId();
const formClass = `${styles.form} ${className}`;
const intl = useIntl();
- const emptyForm: CommentFormData = useMemo(() => {
- return {
- author: '',
- comment: '',
- email: '',
- parentId,
- website: '',
- };
- }, [parentId]);
- const [data, setData] = useState(emptyForm);
- const {
- activate: activateNotice,
- deactivate: deactivateNotice,
- state: isSubmitting,
- } = useBoolean(false);
+ const { messages, submit, submitStatus, update, values } =
+ useForm<CommentFormData>({
+ initialValues:
+ /* The order matter: it will be reused to generate the fields in the right
+ * order. */
+ {
+ parentId,
+ author: '',
+ email: '',
+ website: '',
+ comment: '',
+ },
+ submitHandler: onSubmit,
+ });
- /**
- * Reset all the form fields.
- */
- const resetForm = useCallback(() => {
- setData(emptyForm);
- deactivateNotice();
- }, [deactivateNotice, emptyForm]);
+ const renderFields = useCallback(() => {
+ const entries = Object.entries(values) as [
+ keyof CommentFormData,
+ CommentFormData[keyof CommentFormData],
+ ][];
+ const labels: Record<Exclude<keyof CommentFormData, 'parentId'>, string> = {
+ author: intl.formatMessage({
+ defaultMessage: 'Name:',
+ description: 'CommentForm: name label',
+ id: 'ZIrTee',
+ }),
+ comment: intl.formatMessage({
+ defaultMessage: 'Comment:',
+ description: 'CommentForm: comment label',
+ id: 'A8hGaK',
+ }),
+ email: intl.formatMessage({
+ defaultMessage: 'Email:',
+ description: 'CommentForm: email label',
+ id: 'Bh7z5v',
+ }),
+ website: intl.formatMessage({
+ defaultMessage: 'Website:',
+ description: 'CommentForm: website label',
+ id: 'u41qSk',
+ }),
+ };
- const nameLabel = intl.formatMessage({
- defaultMessage: 'Name:',
- description: 'CommentForm: name label',
- id: 'ZIrTee',
- });
+ return entries.map(([field, value]) => {
+ const isRequired = field !== 'website';
+ const inputType = field === 'email' ? 'email' : 'text';
+ const fieldId = `${formId}-${field}`;
- const emailLabel = intl.formatMessage({
- defaultMessage: 'Email:',
- description: 'CommentForm: email label',
- id: 'Bh7z5v',
- });
+ return field === 'parentId' ? null : (
+ <LabelledField
+ className={styles.field}
+ field={
+ field === 'comment' ? (
+ <TextArea
+ id={fieldId}
+ isRequired
+ name={field}
+ onChange={update}
+ value={value}
+ />
+ ) : (
+ <Input
+ id={fieldId}
+ isRequired={isRequired}
+ name={field}
+ onChange={update}
+ type={inputType}
+ value={value}
+ />
+ )
+ }
+ key={field}
+ label={
+ <Label htmlFor={fieldId} isRequired={isRequired}>
+ {labels[field]}
+ </Label>
+ }
+ />
+ );
+ });
+ }, [values, formId, intl, update]);
- const websiteLabel = intl.formatMessage({
- defaultMessage: 'Website:',
- description: 'CommentForm: website label',
- id: 'u41qSk',
- });
-
- const commentLabel = intl.formatMessage({
- defaultMessage: 'Comment:',
- description: 'CommentForm: comment label',
- id: 'A8hGaK',
- });
-
- const formTitle = intl.formatMessage({
- defaultMessage: 'Comment form',
- description: 'CommentForm: aria label',
- id: 'dz2kDV',
+ const btnLabel = intl.formatMessage({
+ defaultMessage: 'Publish',
+ description: 'CommentForm: submit button',
+ id: 'OL0Yzx',
});
const loadingMsg = intl.formatMessage({
@@ -128,137 +144,59 @@ export const CommentForm: FC<CommentFormProps> = ({
id: 'IY5ew6',
});
- const formAriaLabel = title ? undefined : formTitle;
- const formId = useId();
- const formLabelledBy = title ? formId : undefined;
-
- const updateForm = useCallback(
- (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
- switch (e.target.name) {
- case 'author':
- setData((prevData) => {
- return { ...prevData, author: e.target.value };
- });
- break;
- case 'comment':
- setData((prevData) => {
- return { ...prevData, comment: e.target.value };
- });
- break;
- case 'email':
- setData((prevData) => {
- return { ...prevData, email: e.target.value };
- });
- break;
- case 'website':
- setData((prevData) => {
- return { ...prevData, website: e.target.value };
- });
- break;
+ const renderNotice = useCallback(
+ (
+ currentStatus: FormSubmitStatus,
+ msg: Nullable<Partial<FormSubmitMessages>>
+ ) => {
+ switch (currentStatus) {
+ case 'FAILED':
+ return msg?.error ? (
+ <Notice
+ // eslint-disable-next-line react/jsx-no-literals
+ kind="error"
+ >
+ {msg.error}
+ </Notice>
+ ) : null;
+ case 'PENDING':
+ return (
+ <Notice
+ // eslint-disable-next-line react/jsx-no-literals
+ kind="info"
+ >
+ <Spinner className={styles.spinner}>{loadingMsg}</Spinner>
+ </Notice>
+ );
+ case 'SUCCEEDED':
+ return msg?.success ? (
+ <Notice
+ // eslint-disable-next-line react/jsx-no-literals
+ kind="success"
+ >
+ {msg.success}
+ </Notice>
+ ) : null;
default:
- break;
+ return null;
}
},
- []
- );
-
- const sendForm = useCallback(
- (e: FormEvent) => {
- e.preventDefault();
- activateNotice();
- saveComment(data, resetForm).then(() => deactivateNotice());
- },
- [activateNotice, data, deactivateNotice, resetForm, saveComment]
+ [loadingMsg]
);
return (
- <Form
- {...props}
- aria-label={formAriaLabel}
- aria-labelledby={formLabelledBy}
- className={formClass}
- onSubmit={sendForm}
- >
- {title ? (
- <Heading className={styles.title} id={formId} level={titleLevel}>
- {title}
- </Heading>
- ) : null}
- <LabelledField
- className={styles.field}
- field={
- <Input
- id="commenter-name"
- isRequired
- name="author"
- onChange={updateForm}
- type="text"
- value={data.author}
- />
- }
- label={
- <Label htmlFor="commenter-name" isRequired>
- {nameLabel}
- </Label>
- }
- />
- <LabelledField
- className={styles.field}
- field={
- <Input
- id="commenter-email"
- isRequired
- name="email"
- onChange={updateForm}
- type="email"
- value={data.email}
- />
- }
- label={
- <Label htmlFor="commenter-email" isRequired>
- {emailLabel}
- </Label>
- }
- />
- <LabelledField
- className={styles.field}
- field={
- <Input
- id="commenter-website"
- name="website"
- onChange={updateForm}
- type="url"
- value={data.website}
- />
- }
- label={<Label htmlFor="commenter-website">{websiteLabel}</Label>}
- />
- <LabelledField
- className={styles.field}
- field={
- <TextArea
- id="commenter-comment"
- isRequired
- name="comment"
- onChange={updateForm}
- value={data.comment}
- />
- }
- label={
- <Label htmlFor="commenter-comment" isRequired>
- {commentLabel}
- </Label>
- }
- />
- <Button type="submit" kind="primary" className={styles.button}>
- {intl.formatMessage({
- defaultMessage: 'Publish',
- description: 'CommentForm: submit button',
- id: 'OL0Yzx',
- })}
+ <Form {...props} className={formClass} onSubmit={submit}>
+ {renderFields()}
+ <Button
+ className={styles.btn}
+ isLoading={submitStatus === 'PENDING'}
+ // eslint-disable-next-line react/jsx-no-literals
+ kind="primary"
+ type="submit"
+ >
+ {btnLabel}
</Button>
- {isSubmitting ? <Spinner>{loadingMsg}</Spinner> : null}
- {Notice}
+ {renderNotice(submitStatus, messages)}
</Form>
);
};