项目作者: andrewrosss

项目描述 :
Simple declarative API for rendering Material UI Dialogs
高级语言: TypeScript
项目地址: git://github.com/andrewrosss/react-mui-dialog.git
创建时间: 2021-03-09T22:03:08Z
项目社区:https://github.com/andrewrosss/react-mui-dialog

开源协议:MIT License

下载


react-mui-dialog

Simple declarative API for rendering Material UI Dialogs written in TypeScript

Motivation

react-mui-dialog was written while building a learning portal website. With around ~15 unique Dialogs, it became obvious that many of the dialogs shared much of the same structure and that we could configure and reuse the same components through a single extensible API.

Installation

  1. npm install react-mui-dialog --save

Note: react-mui-dialog (as the name suggests) assumes that you’re using react, specifically version >= 16.8, and material-ui.

Getting Started

Checkout the codesandbox for working examples.

In a nutshell there are 3 objects to know about.

  • DialogProvider - to be included near the root of the tree.
  • useDialog - the hook associated with the provider.
  • openDialog - The function which will configure, open, and handle the dialog.

To start, wrap your app (or some part of the subtree) in the provider, something like:

  1. // file: App.tsx
  2. import * as React from "react";
  3. import { DialogProvider } from "react-mui-dialog";
  4. import { ThemeProvider } from "@material-ui/core";
  5. // ...
  6. export const App = () => {
  7. // ...
  8. return (
  9. <ThemeProvider>
  10. <DialogProvider>
  11. {/* ---------- other components ---------- */}
  12. </DialogProvider>
  13. </ThemeProvider>
  14. );
  15. };

Note: If you’re using Material UI’s ThemeProvider you probably want the DialogProvider to be a child of that component.

Then in a nested component, hook into the dialog context. For example, let’s create a button that will show the user a notification:

  1. // file: NotificationButton.tsx
  2. import * as React from "react";
  3. import { useDialog } from "react-mui-dialog";
  4. export const NotificationButton = () => {
  5. const { openDialog } = useDialog();
  6. // ...
  7. };

Now inside of the nested component we can configure and open up the dialog in response to a user action (in this case after clicking a button) by using the openDialog function provided through the hook.

In the simplest case we show the user a dialog with a title, some text, and a button to dismiss the dialog (so, no cancel button, just a “submit” button letting us know that the user moved on past the dialog). An example might be delivering some kind of notification to a user, say when a user first logs into a site after some time and a dialog pops up to tell them about new features.

Continuing the snippet above:

  1. // file: NotificationButton.tsx
  2. import * as React from "react";
  3. import { Button } from "@material-ui/core";
  4. import { useDialog } from "react-mui-dialog";
  5. export const NotificationButton = () => {
  6. const { openDialog } = useDialog();
  7. const handleClick = () =>
  8. openDialog({
  9. // set the dialog's title
  10. title: "There's change in the Air!",
  11. // include some text to show the user, NOTE: this could be any arbitrary
  12. // component, not just a string.
  13. contentText: "Here's what's new in version 2.0 ...",
  14. // don't render the cancel button, because in this case the only thing a
  15. // user can do is "dismiss" the notification.
  16. cancelButton: false,
  17. // configure the submit button. MUI defaults to text buttons, let's
  18. // use a contained button styled with the theme's primary color
  19. submitButton: {
  20. children: "Dismiss", // <-- the button text
  21. props: {
  22. variant: "contained",
  23. color: "primary",
  24. },
  25. },
  26. // onSubmit is the action we take when the user "accepts" whatever
  27. // the dialog is prompting them about. the function must return a
  28. // promise, and since no fields have been defined (in this particular
  29. // case), all we need to know is _that_ the user clicked the submit
  30. // (dismiss) button.
  31. onSubmit: async () => alert("The user dismissed this notification."),
  32. });
  33. return (
  34. <Button variant="contained" color="primary" onClick={handleClick}>
  35. Show Notification
  36. </Button>
  37. );
  38. };

This dialog config would yield something like:

And there we go, we can render this button somewhere in our app and can show the user a basic dialog.

Dialog Structure

Before getting into the examples it’s worth touching on the general structure of a dialog that this package assumes. Basically, a dialog is viewed as just a fancy form which under the right conditions is configured and subsequently shown to a user as an overlay over the current page content.

Just taking a quick looks at the components which “make-up” a dialog as defined by the material-ui package we see a general structure:

  • Title
  • Content
  • Actions

This package imposes additional assumed structure on the contents of Dialog, the most important aspect being: every dialog is a form.

What about a dialog with a title, some short text, and a button to close the dialog (something like the notification example above)? This can be viewed as a “trivial form”, basically just a submit button.

The question might then be, why not just use a button instead of a “trivial form”, and the answer is that dialogs are purposefully interruptive requiring the user to take additional action. For example, a dialog which is presented to the user before deleting a document. The fact that the user simply submitted our “trivial form” is enough for us to know that we should carry on with the associated action.

With this in mind. This package assumes the following sturcture in addition to material-ui’s Title, Content, Actions sections. Each Dialog (form), has:

  • Title
  • Content, where the content can have:
    • Text describing the reason for the interruption in flow.
    • Any number of optional fields (possibly 0 - by default text fields) to collect additional information from the user, if required.
  • Actions, which by default are:
    • A cancel button which always closes the dialog and “aborts” submitting the form.
    • A submit button which the user clicks to submit the form, effectively accepting or continuing with the associated action.

Examples

The section outlines some (most) of the available configuration that can be passed to calls to openDialog.

A working codesandbox is available here. The code in the sandbox can also be viewed on github here.

Dialog with custom buttons

We can customize the cancel and submit buttons which are shown to the user:

  1. // elsewhere ...
  2. import { Typography } from "@material-ui/core";
  3. // ...
  4. const { openDialog, closeDialog } = useDialog();
  5. // ^^^^^^^^^^^
  6. // ...
  7. openDialog({
  8. title: "Delete this document?",
  9. // a component this time
  10. contentText: (
  11. <Typography color="textSecondary">
  12. You are about to delete the document <b>{docName}</b>. This cannot be
  13. undone.
  14. </Typography>
  15. ),
  16. // In this case we'll pass our own button components.
  17. // NOTE: Because we're passing our own component we have to
  18. // handle closing the dialog when we click cancel
  19. cancelButton: {
  20. component: <CustomButton onClick={closeDialog}>Cancel</CustomButton>,
  21. },
  22. // NOTE: make sure to set type='submit' for the submit button
  23. submitButton: {
  24. component: (
  25. <HighEmphasisCustomButton type="submit" variant="contained">
  26. Yes I'm sure, delete this document
  27. </HighEmphasisCustomButton>
  28. ),
  29. },
  30. onSubmit: async () =>
  31. alert(`Deleting document name [${docName}] with ID [${docId}]`),
  32. });

Dialog with fields

By default you can easily add-in (text) fields. If you require other types of fields consult the example after this one. Importantly, this package uses formik under the hood, if you’re familiar with formik then some of the nomenclature should sound familiar.

  1. // elsewhere ...
  2. import * as Yup from "yup";
  3. // ...
  4. const { openDialog } = useDialog();
  5. // ...
  6. openDialog({
  7. title: "Subscribe",
  8. contentText:
  9. "To subscribe to this website, please enter your email address here. We will send updates occasionally.",
  10. // Render formik fields in the dialog by specifying fields (below), each
  11. // key is used as the name of a field in the formik form. There is
  12. // a 1:1 mapping between the keys below and fields in the form.
  13. fields: {
  14. emailAddress: {
  15. // behind the scenes this packages gathers all the initialValue
  16. // values found in this "fields" object, constructs an
  17. // 'initialValues' object and passes that to the <Formik ></Formik> component
  18. initialValue: "",
  19. // for convenience we could omit 'label' and react-mui-dialog would use this
  20. // field's name for the label
  21. label: "Email Address",
  22. // These props are passed directly to the underlying
  23. // formik <Field ></Field> component.
  24. fieldProps: { variant: "filled" },
  25. },
  26. },
  27. // optional validationSchema, defined just as you would with
  28. // formik, used to validate the fields.
  29. validationSchema: Yup.object({
  30. emailAddress: Yup.string()
  31. .required("This field is required")
  32. .email("Must be a valid email"),
  33. }),
  34. cancelButton: { children: "No Thanks" },
  35. submitButton: { children: "Subscribe" },
  36. // the keys of the fields object (above) are how you reference
  37. // values received by the form (as is typical with formik)
  38. onSubmit: async ({ emailAddress }) =>
  39. alert(`Added email [${emailAddress}] to the mailing list!`),
  40. });

This config would result in something like:

Dialog with Custom Fields

For more control over the fields which are rendered, or if we simply want something other than text fields, we can pass formik <Field ></Field> components directory to openDialog.

Note: this is a heavily truncated example, check out the codesandbox for a working example.

  1. // elsewhere ...
  2. import * as Yup from "yup";
  3. import { CheckboxWithLabel, Select } from "formik-material-ui";
  4. import { FormControl, InputLabel, MenuItem } from "@material-ui/core";
  5. import { Field } from "formik";
  6. // ...
  7. const user = getUserProfile();
  8. // ...
  9. openDialog({
  10. title: "Profile Settings",
  11. contentText: null,
  12. fields: {
  13. username: {
  14. initialValue: user.username,
  15. // NOTE: we omit passing a label
  16. },
  17. // here we render something other than a text field by modifying
  18. // the props that are passed to the formik <Field ></Field> component.
  19. onMailingList: {
  20. initialValue: user.onMailingList,
  21. fieldProps: {
  22. component: CheckboxWithLabel,
  23. type: "checkbox",
  24. Label: { label: "Receive newsletter" },
  25. },
  26. },
  27. // Here we pass our own component, if [fieldName].component is
  28. // specified then this component will be rendered and
  29. // [fieldName].fieldProps will be ignored.
  30. notificationRetention: {
  31. initialValue: user.notificationRetention,
  32. component: (
  33. <FormControl>
  34. <InputLabel htmlFor="notificationRetention">
  35. Keep notifications for
  36. </InputLabel>
  37. <Field
  38. component={Select}
  39. name="notificationRetention"
  40. inputProps={{
  41. id: "notificationRetention",
  42. }}
  43. >
  44. <MenuItem value={"1_week"}>1 Week</MenuItem>
  45. <MenuItem value={"2_weeks"}>2 Weeks</MenuItem>
  46. <MenuItem value={"1_month"}>1 Month</MenuItem>
  47. </Field>
  48. </FormControl>
  49. ),
  50. },
  51. },
  52. validationSchema: Yup.object({
  53. username: Yup.string().required("username cannot be empty"),
  54. onMailingList: Yup.boolean(),
  55. notificationRetention: Yup.string(),
  56. }),
  57. cancelButton: { children: "Close" },
  58. submitButton: {
  59. children: "Save",
  60. props: { variant: "contained", color: "secondary" },
  61. },
  62. onSubmit: async ({ username, onMailingList, notificationRetention }) =>
  63. alert(
  64. `Saving settings Username [${username}], Receive newsletter [${onMailingList}], Keep notifications for [${notificationRetention}]`
  65. ),
  66. });

This config would yeild something like:

Custom Everything

Finally if you just want something completely custom you can override the entire contents of the dialog for something that suits your needs:

Let’s make ourselves a custom form. Note this component has nothing to do with react-mui-dialog:

  1. const CustomForm: React.FC<{ onCancel: () => void) }> = ({ onCancel }) => {
  2. const [state, setState] = React.useState({
  3. email: "email@domain.com",
  4. terms: false,
  5. mailing: true,
  6. });
  7. const handleTextChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  8. setState({ ...state, [e.target.name]: e.target.value });
  9. };
  10. const handleCheckChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  11. setState({ ...state, [e.target.name]: e.target.checked });
  12. };
  13. const handleSubmit = (e: React.FormEvent) => {
  14. e.preventDefault();
  15. alert(
  16. `Email [${state.email}] with answers to terms [${state.terms}] and mailing [${state.mailing}]`
  17. );
  18. onCancel();
  19. };
  20. return (
  21. <form
  22. onSubmit={handleSubmit}
  23. style={{
  24. display: "flex",
  25. flexDirection: "column",
  26. alignItems: "start",
  27. gap: "16px",
  28. padding: "16px",
  29. }}
  30. >
  31. <Typography variant="h6">Terms & Privacy</Typography>
  32. <Typography color="textSecondary">We've updated our terms ...</Typography>
  33. <TextField
  34. type="email"
  35. name="email"
  36. label={"Updated Email"}
  37. variant="filled"
  38. fullWidth
  39. required
  40. value={state.email}
  41. onChange={handleTextChange}
  42. ></TextField>
  43. <div style={{ display: "flex", alignItems: "center" }}>
  44. <FormControlLabel
  45. control={
  46. <Checkbox
  47. name="terms"
  48. checked={state.terms}
  49. required
  50. onChange={handleCheckChange}
  51. ></FormControlLabel>
  52. }
  53. label="Accept Terms"
  54. />
  55. <FormControlLabel
  56. control={
  57. <Checkbox
  58. name="mailing"
  59. checked={state.mailing}
  60. onChange={handleCheckChange}
  61. ></FormControlLabel>
  62. }
  63. label="Receive newsletter"
  64. />
  65. </div>
  66. <div style={{ alignSelf: "end", display: "flex", gap: "16px" }}>
  67. <Button onClick={onCancel}>Cancel</Button>
  68. <Button variant="contained" type="submit">
  69. Udpate
  70. </Button>
  71. </div>
  72. </form>
  73. );
  74. };

And then let’s pass this component to react-mui-dialog to handle displaying it to the user:

  1. // elsewhere ...
  2. const { openDialog, closeDialog } = useDialog();
  3. // ...
  4. openDialog({
  5. customContent: <CustomForm onCancel={closeDialog} ></CustomForm>,
  6. });

Which would yield something like: