Skip to content

Commit

Permalink
migrate patterns
Browse files Browse the repository at this point in the history
  • Loading branch information
NgocNhi123 committed Jun 30, 2024
1 parent dab36ae commit 2b24b71
Show file tree
Hide file tree
Showing 5 changed files with 412 additions and 2 deletions.
Binary file modified .yarn/install-state.gz
Binary file not shown.
18 changes: 17 additions & 1 deletion new-docs/.storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@
// does not use the src of core like the docs, but instead use the dist build
import "@moai/core/dist/bundle.css";

import { DocsContainer } from "@storybook/blocks";
import {
Controls,
Description,
DocsContainer,
Stories,
Subtitle,
Title,
} from "@storybook/blocks";
import type { Preview } from "@storybook/react";
import React, { ReactElement } from "react";
import "../../core/font/remote.css";
Expand Down Expand Up @@ -39,6 +46,15 @@ const preview: Preview = {
</DocsContainer>
);
},
page: () => (
<>
<Title />
<Subtitle />
<Description />
<Stories />
<Controls />
</>
),
},
},
tags: ["autodocs"],
Expand Down
216 changes: 216 additions & 0 deletions new-docs/src/patterns/form.stories.tsx
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>
);
};
102 changes: 102 additions & 0 deletions new-docs/src/patterns/icon.stories.tsx
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" />;
};
Loading

0 comments on commit 2b24b71

Please sign in to comment.