-
-
Notifications
You must be signed in to change notification settings - Fork 26
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
NgocNhi123
committed
Jun 30, 2024
1 parent
dab36ae
commit 2b24b71
Showing
5 changed files
with
412 additions
and
2 deletions.
There are no files selected for viewing
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,216 @@ | ||
import { Meta } from "@storybook/react/*"; | ||
import { ErrorMessage, Field, Form, Formik, FormikErrors } from "formik"; | ||
import { CSSProperties, useState } from "react"; | ||
import { Controller, useForm } from "react-hook-form"; | ||
import { Button, FormError, Input, TextArea } from "../../../core/src"; | ||
|
||
/** | ||
* Moai doesn't come with a built-in form solution. Instead, our input components | ||
* (like [Input][3] and [TextArea][4]) are designed to work with popular form | ||
* builders (such as [Formik][1] and [React Hook Form][2]) out of the box. | ||
* | ||
* [1]: https://formik.org/ | ||
* [2]: https://react-hook-form.com/ | ||
* [3]: /docs/components-input--primary | ||
* [4]: /docs/components-textarea--primary | ||
*/ | ||
const meta: Meta = { | ||
title: "Patterns/Form", | ||
}; | ||
|
||
export default meta; | ||
|
||
interface FormValues { | ||
title: string; | ||
message: string; | ||
} | ||
|
||
const ERRORS = { | ||
titleRequired: "Title is required", | ||
messageRequired: "Message is required", | ||
messageLength: "Message must be longer than 5 characters", | ||
}; | ||
|
||
const formStyles: CSSProperties = { | ||
display: "flex", | ||
flexDirection: "column", | ||
gap: 16, | ||
}; | ||
|
||
const SubmitButton = ({ busy }: { busy: boolean }): JSX.Element => ( | ||
<Button type="submit" highlight busy={busy} children="Submit" /> | ||
); | ||
|
||
const postToServer = async (values: FormValues): Promise<void> => { | ||
return new Promise((resolve) => { | ||
setTimeout(() => { | ||
alert(JSON.stringify(values, null, 2)); | ||
resolve(); | ||
}, 500); | ||
}); | ||
}; | ||
|
||
/** | ||
* To use Moai's input components with Formik, pass them to the "as" prop of | ||
* Formik's [Field][1] component: | ||
* | ||
* [1]: https://formik.org/docs/api/field | ||
* | ||
* ~~~tsx | ||
* import { Field } from "formik"; | ||
* import { Input } from "../../../core/src"; | ||
* | ||
* <label htmlFor="email">Email</label> | ||
* <Field id="email" type="email" name="email" as={Input} /> | ||
* ~~~ | ||
* | ||
* To show errors, pass FormError to the "component" prop of Formik's | ||
* [ErrorMessage][2] component: | ||
* | ||
* ~~~tsx | ||
* import { ErrorMessage } from "formik"; | ||
* import { FormError } from "../../../core/src"; | ||
* | ||
* <ErrorMessage name="email" component={FormError} /> | ||
* ~~~ | ||
* | ||
* [2]: https://formik.org/docs/api/errormessage | ||
* | ||
* Full example: | ||
*/ | ||
export const FormikExample = (): JSX.Element => { | ||
/* import { Input, Button, FormError } from "../../../core/src" */ | ||
|
||
const title = ( | ||
<div> | ||
<label htmlFor="fm-title">Title</label> | ||
<Field id="fm-title" type="text" name="title" as={Input} /> | ||
<ErrorMessage name="title" component={FormError} /> | ||
</div> | ||
); | ||
|
||
const message = ( | ||
<div> | ||
<label htmlFor="fm-message">Message</label> | ||
<Field id="fm-message" name="message" as={TextArea} /> | ||
<ErrorMessage name="message" component={FormError} /> | ||
</div> | ||
); | ||
|
||
const validate = (values: FormValues): FormikErrors<FormValues> => { | ||
const errors: FormikErrors<FormValues> = {}; | ||
if (!values.title) errors.title = ERRORS.titleRequired; | ||
if (!values.message) errors.message = ERRORS.messageRequired; | ||
if (values.message.length < 5) errors.message = ERRORS.messageLength; | ||
return errors; | ||
}; | ||
|
||
return ( | ||
<Formik<FormValues> | ||
initialValues={{ title: "", message: "" }} | ||
validate={validate} | ||
onSubmit={async (values, { setSubmitting }) => { | ||
await postToServer(values); | ||
setSubmitting(false); | ||
}} | ||
children={({ isSubmitting: busy }) => ( | ||
<Form style={formStyles}> | ||
{title} | ||
{message} | ||
<SubmitButton busy={busy} /> | ||
</Form> | ||
)} | ||
/> | ||
); | ||
}; | ||
|
||
/** | ||
* To use Moai's input components with React Hook Form, [render][2] them in the | ||
* "render" prop of RHF's [Controller][1] component: | ||
* | ||
* ~~~tsx | ||
* import { Controller } from "react-hook-form"; | ||
* import { Input } from "../../../core/src"; | ||
* | ||
* <label htmlFor="email">Email</label> | ||
* <Controller | ||
* name="email" | ||
* control={control} | ||
* render={({ field }) => ( | ||
* <Input {...field} id="email" type="email" /> | ||
* )} | ||
* rules={{ required: "Email is required" }} | ||
* /> | ||
* ~~~ | ||
* | ||
* [1]: https://react-hook-form.com/api#Controller | ||
* [2]: https://react-hook-form.com/get-started#IntegratingwithUIlibraries | ||
* | ||
* To show errors, pass RHF's [error messages][3] as children of Moai's FormError | ||
* component: | ||
* | ||
* ~~~tsx | ||
* import { FormError } from "../../../core/src"; | ||
* | ||
* <FormError children={errors.email?.message} /> | ||
* ~~~ | ||
* | ||
* [3]: https://react-hook-form.com/advanced-usage#ErrorMessages | ||
* | ||
* Full example: | ||
*/ | ||
export const ReactHookForm = (): JSX.Element => { | ||
/* import { Input, Button, FormError } from "../../../core/src" */ | ||
|
||
const { control, formState, handleSubmit } = useForm<FormValues>(); | ||
const { errors } = formState; | ||
const [busy, setBusy] = useState(false); | ||
|
||
const title = ( | ||
<div> | ||
<label htmlFor="rhf-title">Title</label> | ||
<Controller | ||
name="title" | ||
control={control} | ||
render={({ field }) => ( | ||
<Input {...field} id="rhf-title" type="text" /> | ||
)} | ||
rules={{ required: ERRORS.titleRequired }} | ||
defaultValue="" | ||
/> | ||
<FormError children={errors.title?.message} /> | ||
</div> | ||
); | ||
|
||
const message = ( | ||
<div> | ||
<label htmlFor="rhf-message">Message</label> | ||
<Controller | ||
name="message" | ||
control={control} | ||
render={({ field }) => <TextArea {...field} id="rhf-message" />} | ||
rules={{ | ||
required: { value: true, message: ERRORS.messageRequired }, | ||
minLength: { value: 5, message: ERRORS.messageLength }, | ||
}} | ||
defaultValue="" | ||
/> | ||
<FormError children={errors.message?.message} /> | ||
</div> | ||
); | ||
|
||
return ( | ||
<form | ||
onSubmit={handleSubmit(async (data) => { | ||
setBusy(true); | ||
await postToServer(data); | ||
setBusy(false); | ||
})} | ||
style={formStyles} | ||
> | ||
{title} | ||
{message} | ||
<SubmitButton busy={busy} /> | ||
</form> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
import { Meta } from "@storybook/react"; | ||
import { SVGAttributes } from "react"; | ||
import { RiSearchLine } from "react-icons/ri"; | ||
import { Button } from "../../../core/src"; | ||
|
||
/** | ||
* Moai doesn't have a built-in icon set. Instead, Moai's components work with any | ||
* SVG icons. This means you can use Moai with popular icon sets, like | ||
* [FontAwesome][1] and [Material Icons][2], or even with [your own icons][4]. | ||
* | ||
* This guide covers the usage of icons inside Moai's components (like in buttons | ||
* and inputs). To display an icon on its own, with the control of its size and | ||
* color, see the [Icon component][3]. | ||
* | ||
* [1]: https://fontawesome.com | ||
* [2]: https://fonts.google.com/icons | ||
* [3]: /docs/components-icon--primary | ||
* [4]: #advanced | ||
*/ | ||
const meta: Meta = { | ||
title: "Patterns/Icon", | ||
}; | ||
|
||
export default meta; | ||
|
||
/** | ||
* Moai components that support icons usually have an \`icon\` prop. The | ||
* recommended way to set this prop is using an icon from the [react-icons][1] | ||
* package. It provides icons from many popular sets that can be used directly in | ||
* Moai: | ||
* | ||
* ~~~ts | ||
* | ||
* import { RiSearchLine } from "react-icons/ri"; | ||
* | ||
* ~~~ | ||
* | ||
* [1]: https://react-icons.github.io/react-icons/ | ||
*/ | ||
export const Basic = (): JSX.Element => ( | ||
<Button icon={RiSearchLine} children="Search" /> | ||
); | ||
|
||
/** | ||
* In most cases, [screen readers][1] will [skip the icon][2] and only announce | ||
* the text content of a component (e.g. the label of a button). When a component | ||
* has no content to be announced (e.g. an icon-only button), you'll often be | ||
* asked to provide an explicit icon label: | ||
* | ||
* [1]: https://en.wikipedia.org/wiki/Screen_reader | ||
* [2]: https://www.sarasoueidan.com/blog/accessible-icon-buttons/#icon-sitting-next-to-text | ||
*/ | ||
export const IconLabel = (): JSX.Element => ( | ||
<Button icon={RiSearchLine} iconLabel="Search" /> | ||
); | ||
|
||
/** | ||
* When using with a component, the color and size of an icon are usually | ||
* controlled by the component itself. For example, in a large, highlight button, | ||
* the icon is white and enlarged: | ||
*/ | ||
export const ColorSize = (): JSX.Element => ( | ||
<Button | ||
highlight | ||
size={Button.sizes.large} | ||
icon={RiSearchLine} | ||
children="Search" | ||
/> | ||
); | ||
|
||
/** | ||
* Technically, these \`icon\` props simply expect a [function component][1] that | ||
* returns an SVG element. The type definition looks like this: | ||
* | ||
* ~~~ts | ||
* interface Props { | ||
* style?: CSSProperties; | ||
* className?: string; | ||
* } | ||
* | ||
* type Icon = (props: Props) => JSX.Element; | ||
* ~~~ | ||
* | ||
* This means you can use Moai with your own custom icons (e.g. logos, product | ||
* icons), by creating components that return them as SVG elements. For a full | ||
* icon set, consider tools like [React SVGR][2] to programmatically generate | ||
* these components from SVG files. | ||
* | ||
* [1]: https://reactjs.org/docs/components-and-props.html#function-and-class-components | ||
* [2]: https://react-svgr.com | ||
*/ | ||
export const Advanced = (): JSX.Element => { | ||
// In practice, this should be defined outside of your component, or even | ||
// better, automatically generated by a tool like react-svgr. | ||
const Icon = (props: SVGAttributes<SVGElement>) => ( | ||
<svg width="1em" height="1em" viewBox="0 0 48 1" {...props}> | ||
{/* This is just a horizontal line */} | ||
<path d="M0 0h48v1H0z" fill="currentColor" fillRule="evenodd" /> | ||
</svg> | ||
); | ||
return <Button icon={Icon} children="Search" />; | ||
}; |
Oops, something went wrong.