GithubHelp home page GithubHelp logo

ebay / nice-modal-react Goto Github PK

View Code? Open in Web Editor NEW
1.9K 9.0 105.0 9.79 MB

A modal state manager for React.

Home Page: https://ebay.github.io/nice-modal-react

License: MIT License

JavaScript 42.60% TypeScript 57.40%
react modal

nice-modal-react's Introduction

Nice Modal

This is a small, zero dependency utility to manage modals in a natural way for React. It uses context to persist state of modals globally so that you can show/hide a modal easily either by the modal component or id.

You can also see the introduction at eBay tech blog.

Also check out our another nice utility! nice-form-react! 😜

NPM Downloads Build Status Coverage Status Demo MIT licensed

For example, you can use below code to show a modal anywhere:

import NiceModal from '@ebay/nice-modal-react';
import MyModal from './MyModal';

//...
NiceModal.show(MyModal, { someProp: 'hello' }).then(() => {
  // do something if the task in the modal finished.
});
//...

Or you can register the modal with an id so that you don't need to import the modal component to use it:

import NiceModal from '@ebay/nice-modal-react';
import MyModal from './MyModal';

NiceModal.register('my-modal', MyModal);

// you can use the string id to show/hide the modal anywhere
NiceModal.show('my-modal', { someProp: 'hello' }).then(() => {
  // do something if the task in the modal finished.
});
//...

NOTE: @ebay/nice-modal-react is not a React modal component but should be used with other modal/dialog implementions by UI libraries like Material UI, Ant.Design, Bootstrap React, etc.

Examples

You can see a list of examples at: https://ebay.github.io/nice-modal-react

Key Features

  • Zero dependency and small: ~2kb after gzip.
  • Uncontrolled. You can close modal itself in the modal component.
  • Decoupled. You don't have to import a modal component to use it. Modals can be managed by id.
  • The code of your modal component is not executed if it's invisible.
  • It doesn't break the transitions of showing/hiding a modal.
  • Promise based. Besides using props to interact with the modal from the parent component, you can do it easier by promise.
  • Easy to integrate with any UI library.

Motivation

Using modals in React is a bit frustrating. Think of that if you need to implement below UI:

The dialog is used to create a JIRA ticket. It could be shown from many places, from the header, to the context menu, to the list page. Traditionally, we had declared modal components with a JSX tag. But then the question became, “Where should we declare the tag?”

The most common option was to declare it wherever it was being used. But using modals in a declarative way is not only about a JSX tag, but also about maintaining the modal’s state like visibility, parameters in the container component. Declaring it everywehre means managing state everywhere. It's frustrating.

The other option put it in the Root component, for example:

const Root = () => {
  const [visible, setVisible] = useState(false);
  // other logic ...
  return (
    <>
      <Main />
      <NewTicketModal visible={visible} />
    </>
  );
}

However, when you declare the modal in the root component, there are some issues:

  1. Not scalable. It's unreasonable to maintain the modal's state in the root component. When you need more modals you need to maintain much state, especially you need to maintain arguments for the modal.
  2. It's hard to show or hide the modal from children components. When you maintain the state in a component then you need to pass setVisible down to the place where you need to show or hide the modal. It makes things too complicated.

Unfortunately, most examples of using modals just follow this practice, it causes such confusions when managing modals in React.

I believe you must once encountered with the scenario that originally you only needed to show a modal when click a button, then when requirements changed, you need to open the same modal from a different place. Then you have to refactor your code to re-consider where to declare the modal. The root cause of such annoying things is just because we have not understood the essential of a modal.

Rethink the Modal Usage Pattern in React

According to the wikipedia, a modal can be described as:

A window that prevents the user from interacting with your application until he closes the window.

From the definition we can get a conclusion: a modal is a global view that's not necessarily related with a specific context.

This is very similar with the page concept in a single page UI application. The visibility/ state of modals should be managed globally because, from the UI perspective, a modal could be showed above any page/component. The only difference between modal and page is: a modal allows you to not leave the current page to do some separate tasks.

For pages management, we already have router framework like React Router, it helps to navigate to a page by URL. Actually, you can think URL a global id for a page. So, similarly, what if you assign a uniq id to a modal then show/hide it by the id? This is just how we designed NiceModal.

However, besides using id, NiceModal allows to use the modal component directly to manage it.

Usage

Installation

# with yarn
yarn add @ebay/nice-modal-react

# or with npm
npm install @ebay/nice-modal-react

Create Your Modal Component

With NiceModal you can create a separate modal component easily. It's just the same as you create a normal component but wrap it with high order compponent by NiceModal.create. For example, below code shows how to create a dialog with Ant.Design:

import { Modal } from 'antd';
import NiceModal, { useModal } from '@ebay/nice-modal-react';

export default NiceModal.create(({ name }: { name: string }) => {
  // Use a hook to manage the modal state
  const modal = useModal();
  return (
    <Modal
      title="Hello Antd"
      onOk={() => modal.hide()}
      visible={modal.visible}
      onCancel={() => modal.hide()}
      afterClose={() => modal.remove()}
    >
      Hello {name}!
    </Modal>
  );
});

From the code, we can see:

  • The modal is uncontrolled. You can hide your modal inside the component regardless where it is showed.
  • The high order component created by NiceModal.create ensures your component is not executed before it becomes visible.
  • You can call modal.remove to remove your modal component from the React component tree to reserve transitions.

Next, let's see how to use the modal.

Using Your Modal Component

There are very flexible APIs for you to manage modals. See below for the introduction.

Embed your application with NiceModal.Provider:

Since we will manage status of modals globally, the first thing is embedding your app with NiceModal provider, for example:

import NiceModal from '@ebay/nice-modal-react';
ReactDOM.render(
  <React.StrictMode>
    <NiceModal.Provider>
      <App />
    </NiceModal.Provider>
  </React.StrictMode>,
  document.getElementById('root'),
);

The provider will use React context to maintain all modals' state.

Using the modal by component

You can control a nice modal by the component itself.

import NiceModal from '@ebay/nice-modal-react';
import MyAntdModal from './my-antd-modal'; // created by above code

function App() {
  const showAntdModal = () => {
    // Show a modal with arguments passed to the component as props
    NiceModal.show(MyAntdModal, { name: 'Nate' })
  };
  return (
    <div className="app">
      <h1>Nice Modal Examples</h1>
      <div className="demo-buttons">
        <button onClick={showAntdModal}>Antd Modal</button>
      </div>
    </div>
  );
}

Use the modal by id

You can also control a nice modal by id:

import NiceModal from '@ebay/nice-modal-react';
import MyAntdModal from './my-antd-modal'; // created by above code

// If use by id, need to register the modal component.
// Normally you create a modals.js file in your project
// and register all modals there.
NiceModal.register('my-antd-modal', MyAntdModal);

function App() {
  const showAntdModal = () => {
    // Show a modal with arguments passed to the component as props
    NiceModal.show('my-antd-modal', { name: 'Nate' })
  };
  return (
    <div className="app">
      <h1>Nice Modal Examples</h1>
      <div className="demo-buttons">
        <button onClick={showAntdModal}>Antd Modal</button>
      </div>
    </div>
  );
}

Use modal with the hook

The useModal hook can not only be used inside a modal component but also any component by passing it a modal id/component:

import NiceModal, { useModal } from '@ebay/nice-modal-react';
import MyAntdModal from './my-antd-modal'; // created by above code

NiceModal.register('my-antd-modal', MyAntdModal);
//...
// if use with id, need to register it first
const modal = useModal('my-antd-modal');
// or if with component, no need to register
const modal = useModal(MyAntdModal);

//...
modal.show({ name: 'Nate' }); // show the modal
modal.hide(); // hide the modal
//...

Declare your modal instead of register

The nice modal component you created can be also used as a normal component by JSX, then you don't need to register it. For example:

import NiceModal, { useModal } from '@ebay/nice-modal-react';
import MyAntdModal from './my-antd-modal'; // created by above code

function App() {
  const showAntdModal = () => {
    // Show a modal with arguments passed to the component as props
    NiceModal.show('my-antd-modal')
  };
  return (
    <div className="app">
      <h1>Nice Modal Examples</h1>
      <div className="demo-buttons">
        <button onClick={showAntdModal}>Antd Modal</button>
      </div>
      <MyAntdModal id="my-antd-modal" name="Nate" />
    </div>
  );
}

With this approach, you can get the benifits:

  • Inherit React context in the modal component under some component node.
  • Pass arguments to the modal component via props.

NOTE: if you show the component by id but the modal is not declared or registered. Nothing will happen but only a warning message in the dev console.

Using promise API

Besides using props to interact with the modal from the parent component, you can do it easier by promise. For example, we have a user list page with a add user button to show a dialog to add user. After user is added the list should refresh itself to reflect the change, then we can use below code:

NiceModal.show(AddUserModal)
  .then(() => {
    // When call modal.resolve(payload) in the modal component
    // it will resolve the promise returned by `show` method.
    // fetchUsers will call the rest API and update the list
    fetchUsers()
  })
  .catch(err=> {
    // if modal.reject(new Error('something went wrong')), it will reject the promise
  }); 

You can see the live example on codesandbox.

Integrating with Redux

Though not necessary, you can integrate Redux to manage state of nice modals. Then you can use Redux dev tools to track/debug state change of modals. Here is how to do it:

// First combine the reducer
import { createStore, applyMiddleware, compose, combineReducers } from 'redux';
import { Provider, useSelector, useDispatch } from 'react-redux';
import NiceModal from '@ebay/nice-modal-react';
import { Button } from 'antd';
import { MyAntdModal } from './MyAntdModal';
import logger from 'redux-logger';

const composeEnhancers = (typeof window !== 'undefined' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) || compose;
const enhancer = composeEnhancers(applyMiddleware(logger));

const store = createStore(
  combineReducers({
    modals: NiceModal.reducer,
    // other reducers...
  }),
  enhancer,
);

// Passing Redux state to the nice modal provider
const ModalsProvider = ({ children }) => {
  const modals = useSelector((s) => s.modals);
  const dispatch = useDispatch();
  return (
    <NiceModal.Provider modals={modals} dispatch={dispatch}>
      {children}
    </NiceModal.Provider>
  );
};

export default function ReduxProvider({ children }) {
  return (
    <Provider store={store}>
      <ModalsProvider>{children}</ModalsProvider>
    </Provider>
  );
}

Using with any UI library

NiceModal provides lifecyle methods to manage the state of modals. You can use modal handler returned by useModal hook to bind any modal like component to the state. Below are typical state and methods you will use:

  • modal.visible: the visibility of a modal.
  • modal.hide: will hide the modal, that is, change modal.visible to false.
  • modal.remove: remove the modal component from the tree so that you modal's code is not executed when it's invisible. Usually you call this method after the modal's transition.
  • modal.keepMounted if you don't want to remove the modal from the tree for some instances, you can decide if call modal.remove based on value of keepMounted.

Based on these properties/methods, you can easily use NiceModal with any modal-like component provided by any UI libraries.

Using help methods

As you already saw, we use code similar with below to manage the modal state:

//...
const modal = useModal();
return (
  <Modal
    visible={modal.visible}
    title="Hello Antd"
    onOk={() => modal.hide()}
    onCancel={() => modal.hide()}
    afterClose={() => modal.remove()}
  >
    Hello NiceModal!
  </Modal>
);
//...

It binds visible property to the modal handler, and use modal.hide to hide the modal when close button is clicked. And after the close transition it calls modal.remove to remove the modal from dom node.

For every modal implementation we always need to these binding manually. So, to make it easier to use we provides helper methods for 3 popular UI libraries Material UI, Ant.Design and Bootstrap React.

import NiceModal, {
  muiDialog,
  muiDialogV5,
  antdModal,
  antdModalV5,
  antdDrawer,
  antdDrawerV5,
  bootstrapDialog
} from '@ebay/nice-modal-react';

//...
const modal = useModal();
// For MUI
<Dialog {...muiDialog(modal)}>

// For MUI V5
<Dialog {...muiDialogV5(modal)}>

// For ant.design
<Modal {...antdModal(modal)}>

// For ant.design v4.23.0 or later
<Modal {...antdModalV5(modal)}>

// For antd drawer
<Drawer {...antdDrawer(modal)}>

// For antd drawer v4.23.0 or later
<Drawer {...antdDrawerV5(modal)}>

// For bootstrap dialog
<Dialog {...bootstrapDialog(modal)}>

These helpers will bind modal's common actions to correct properties of the component. However you can always override the property after the helpers property. For example:

const handleSubmit = () => {
  doSubmit().then(() => {
    modal.hide();
  });
}
<Modal {...antdModal(modal)} onOk={handleSubmit}>

In the example, the onOk property will override the result from antdModal helper.

API Reference

https://ebay.github.io/nice-modal-react/api/

Testing

You can test your nice modals with tools like @testing-library/react.

import NiceModal from '@ebay/nice-modal-react';
import { render, act, screen } from '@testing-library/react';
import { MyNiceModal } from '../MyNiceModal';

test('My nice modal works!', () => {
  render(<NiceModal.Provider />
  
  act(() => {
    NiceModal.show(MyNiceModal);
  });
  
  expect(screen.getByRole('dialog')).toBeVisible();
});

Contribution Guide

# 1. Clone repo
git clone https://github.com/eBay/nice-modal-react.git

# 2. Install deps
cd nice-modal-react
yarn

# 3. Make local repo as linked
yarn link

# 4. Start dev server
yarn dev

# 5. Install examples deps
cd example
yarn

# 6. Use local linked lib
yarn link @ebay/nice-modal-react

# 7. Start examples dev server
yarn start

Then you can access http://localhost:3000 to see the examples.

FAQ

Can I get context in the component tree in a modal?

Yes. To get the data from context in the component tree you need to use the declarative way. For example:

export default function AntdSample() {
  return (
    <>
      <Button type="primary" onClick={() => NiceModal.show('my-antd-modal', { name: 'Nate' })}>
        Show Modal
      </Button>
      <MyAntdModal id="my-antd-modal" {...otherProps} />
    </>
  );
}

See more here.

License

MIT

nice-modal-react's People

Contributors

alexandredev3 avatar chooin avatar daolou avatar dependabot[bot] avatar facuparedes-litebox avatar fandridis avatar fengxie2021 avatar fpmanuel avatar geowarin-sg avatar gnsharma avatar janczizikow avatar kabirsky avatar kirtanprht avatar ledenis avatar mokeyjay avatar romaincscn avatar sevenoutman avatar shoota avatar supnate avatar xeinebiu avatar xxleyi avatar yedidyar avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

nice-modal-react's Issues

Generic type error on `NiceModal.create`

Hi, thanks for this great tool! I'm having little trouble with NiceModal.create method when it always require my component props to have an extra [key: string]: unknown property signature.

Here's my modal component with normal prop declaration but fail because of incompatibility with Record<string, unknown>.

image

I always have to add this extra [key: string]: unknown property to dismiss the ts error.

image

Is there a changelog?

Hi folks,
I'm looking for some sort of changelog, but I'm unable to find one. If there is none, would it be an option to create GitHub releases for future releases? Thanks in advance!

Disable body scroll lock and react portal

Hi, this package works fantastically for modals however I can't figure out how to disable the body scroll lock and react portal usage so that I can use this for dialogs? i.e. be able to anchor the modal to another component while not having the modal blocking the user from interacting with the rest of the page while it's open. Think something like a notification dialog, or a tool tip.

Is this possible? I've looked at the docs and searched for past issues and it doesn't seem like this has been addressed anywhere.

[Question] How to make TS warn me if I forget to pass Modal's props when calling NiceModal.show()

@supnate so I am basically using the Use modal with the hook pattern to show the modal as follows -

import NiceModal, { useModal } from '@ebay/nice-modal-react';
import ModalIWantToShow from './ModalIWantToShow.tsx'; // created by above code

const modalIWantToShow = useModal(ModalIWantToShow);

//...
modalIWantToShow.show({ name: 'Nate' }); // show the modal
//...

Now the problem with the above is that I may forget to pass the props while calling 'show()'. Is there a way to make sure that TS warns me if I have forgot to pass the props [because the props are not optional most of the times]??

Edit - @supnate There is another big problem with this. Let's suppose we have a modal, which initially have no props to it. Now later the requirements change and we have to add props to that modal. Now in this case someone might think, that where ever I need props TS will warn and I will add the props [especially if its a big project and the modal is being used at a lot of places]. After compiling I find there are no errors and thus I move forward. But actually whereever I used the modal with hook, I am in trouble because right now it's not warning if we don't pass props. So this is not just about 'try and remember to pass the props if using show()'. This behaviour defeats the purpose of TS altogether

removing args as a return value of useModal hook

Hey I realized that issue described in #53 is still present when calling the show handler with arguments 😞

The following works as desired after the fix:

const [counter, setCounter] = React.useState(0);
const userModal = useModal(UserInfoModal);
const { show } = userModal; 

React.useEffect(() => {
  if (counter >= 2) {
    show();
  }
}, [counter, show]) 

The effect doesn't cause re-rendering loop as it was before. But if we call show with a non-primitive type it would again cause an infinite re-rendering loop 😢

const [counter, setCounter] = React.useState(0);
const userModal = useModal(UserInfoModal);
const { show } = userModal; 

React.useEffect(() => {
  if (counter >= 2) {
    show({ title: 'test' }); // <- passing some args to `show`
  }
}, [counter, show]) // <- show identity will change every time we call show with args

It's the same reason for it as described in #54. This time though the problem is modalInfo.args - it's an object and it will change on every render. I think there are 2 possible ways of solving this:

Option 1: Don't memoize args returned from useModal instead memoize just callbacks:

export function useModal(modal?: any, args?: any): any {
// ...
-  return React.useMemo(
-   () => ({
-     id: mid,
-     args: modalInfo?.args,
-    visible: !!modalInfo?.visible,
-    keepMounted: !!modalInfo?.keepMounted,
-     show: (args?: Record<string, unknown>) => show(mid, args),
-     hide: () => hide(mid),
-     remove: () => remove(mid),
-     resolve: (args?: unknown) => {
-      modalCallbacks[mid]?.resolve(args);
-       delete modalCallbacks[mid];
-    },
-    reject: (args?: unknown) => {
-      modalCallbacks[mid]?.reject(args);
-      delete modalCallbacks[mid];
-    },
-    resolveHide: (args?: unknown) => {
-      hideModalCallbacks[mid]?.resolve(args);
-      delete hideModalCallbacks[mid];
-    },
-    }),
-    [mid, modalInfo?.args, modalInfo?.keepMounted, modalInfo?.visible]
- );
+  const showCallback = React.useCallback(
+    (args?: Record<string, unknown>) => show(mid, args),
+    [mid]
+  );
+  const hideCallback = React.useCallback(() => hide(mid), [mid]);
+  const removeCallback = React.useCallback(() => remove(mid), [mid]);
+  const resolveCallback = React.useCallback(
+    (args?: unknown) => {
+      modalCallbacks[mid]?.resolve(args);
+      delete modalCallbacks[mid];
+    },
+    [mid]
+  );
+  const rejectCallback = React.useCallback(
+    (args?: unknown) => {
+      modalCallbacks[mid]?.reject(args);
+      delete modalCallbacks[mid];
+    },
+    [mid]
+  );
+  const resolveHide = React.useCallback(
+    (args?: unknown) => {
+      hideModalCallbacks[mid]?.resolve(args);
+      delete hideModalCallbacks[mid];
+    },
+    [mid]
+  );

+ return {
+    id: mid,
+    args: modalInfo?.args,
+    visible: !!modalInfo?.visible,
+    keepMounted: !!modalInfo?.keepMounted,
+    show: showCallback,
+    hide: hideModalCallbacks,
+    remove: removeCallback,
+    resolve: resolveCallback,
+    reject: rejectCallback,
+    resolveHide,
+  };
}

Now we can rely on show, because they will only change if id of modal changes, which I don't think it's a common case. All is well and if we desctructure show from the return value it works in our example:

const [counter, setCounter] = React.useState(0);
const userModal = useModal(UserInfoModal);
const { show } = userModal; // we have to separate show 

React.useEffect(() => {
  if (counter >= 2) {
    show({ title: 'test' }); // <- passing some args to `show`
  }
}, [counter, show]) // <- show identity only changes when id of modal changes, so this is 👌 

// this is also fine:
React.useEffect(() => {
  if (counter >= 2) {
    userModal.show({ title: 'test' }); // <- passing some args to `show`
  }
}, [counter, userModal.show]) // <- show identity only changes when id of modal changes, so this is 👌 

But if someone passes the whole object returned returned from the hook it will again trigger an re-rendering loop

const [counter, setCounter] = React.useState(0);
const userModal = useModal(UserInfoModal);


React.useEffect(() => {
  if (counter >= 2) {
    userModal.show({ title: 'test' }); // <- passing some args to `show`
  }
}, [counter, userModal]) // <- we pass the whole state of userModal which is gonna change `modalInfo.args` in the reducer indefinitely :(

Option 2: Don't return args in useModal

  return React.useMemo(
    () => ({
      id: mid,
-    args: modalInfo?.args,
      visible: !!modalInfo?.visible,
      keepMounted: !!modalInfo?.keepMounted,
      show: (args?: Record<string, unknown>) => show(mid, args),
      hide: () => hide(mid),
      remove: () => remove(mid),
      resolve: (args?: unknown) => {
        modalCallbacks[mid]?.resolve(args);
        delete modalCallbacks[mid];
      },
      reject: (args?: unknown) => {
        modalCallbacks[mid]?.reject(args);
        delete modalCallbacks[mid];
      },
      resolveHide: (args?: unknown) => {
        hideModalCallbacks[mid]?.resolve(args);
        delete hideModalCallbacks[mid];
      },
    }),
-   [mid, modalInfo?.args, modalInfo?.keepMounted, modalInfo?.visible]
+   [mid, modalInfo?.keepMounted, modalInfo?.visible]
  );

This eliminates the problem of comparing the modalInfo.args with === and we can pass return values from useModal hook however we like in useEffect or other hooks. I think it's probably a better approach in my opinion, but it also means a change in the API.

I would be curious to hear your opinion and discuss whether removing args from the return value would be something you would consider? Would be happy to open PR for it as well if we decide which approach we would like to take 🙂

Will the function of limiting the number of modals be added

hello, In some business scenarios, modals may be opened in cycles.For example, modal A open modal B, modal B open modal C, and modal C can open modal A. Could you add a stack to control the number of opened modals? If the number exceeds the limit, the opened modal at the bottom of the stack will be removed.

MUI Drawer help method

I noticed that antd has a helper for the drawer component, but MUI don't. Is there a reason for this?

Nice Modal integration with Material UI

When we use Nice Modal with the Material UI library the theming of MUI is not passed down to the Dialog.

tsx
// use case
NiceModal.show(CustomDialog, {usedBeforeActivity: true});

// CustomDialog
const CustomDialog = () => {
  return <Dialog {/* config... */}>/* … */</Dialog>
}

Add type safety to modal.show()

First of all thanks for a great library!

I think it will be great if you can add type safety to the modal arguments when calling the show method. Could be using generics:

modal.show<Props>(props: Props)

Of course it will be amazing if somehow it will infer it from the modal component itself but I'm not sure it's possible and the generics solution at least gives basic type safety.

Thanks!

returned functions of useModal hook have "unstable" identity

First of thanks a lot for a cool library 😁

I found that function identity of "handlers" returned from useModal hook isn't stable. It changes on some renders making it a bit hard to reliably use with other hooks. For example:

const [counter, setCounter] = React.useState(0);
const userModal = useModal(UserInfoModal);
const { show } = userModal;

React.useEffect(() => {
  if (counter >= 2) {
    show();
  }
}, [counter, show]) // <- once `counter` reaches 2 the component will continue to infinitely re-render

I think it would be a nice improvement to memoize the handlers in a way that they wouldn't change depending on the state - right now calling show() will change the identity of show 🤔
A small demo showcasing the issue: https://codesandbox.io/s/nice-modal-lqj0kg?file=/src/UserList.tsx

Hot module reloading / fast refresh support?

I'm working on an older project that uses HMR. I find that if I have a modal open and I make an update to the code, everything else HMRs correctly, but the modal is stuck in time until I close it an re-open it. I tried using the declarative syntax as well to no avail. Is this just an issue with HMR? Would upgrading to fast refresh fix this? Does anyone else have this issue?

muiDialog Function is Outdated

Hello!

All of the modals in our app are based on MUI Dialogs, but we are unable to use the muiDialog function because it's passing the deprecated onExited prop. This results in a console warning that we are passing deprecated props and should be using TransitionProps instead.

See the note here in the MUI v4 documentation, also the corresponding MUI v5 documentation here.

I think the function should be using TransitionProps like so:

export const muiDialog = (modal: NiceModalHandler): { open: boolean; onClose: () => void; TransitionProps: { onExited: () => void } } => {
  return {
    open: modal.visible,
    onClose: () => modal.hide(),
    TransitionProps: {
      onExited: () => {
        modal.resolveHide();
        !modal.keepMounted && modal.remove();
      },
    },
  };
};

Overall great package though guys thanks for your work!

Need to know modal status

Before all thank you for this awesome lib ! ❤️

In our project we use this lib for manage all modals and it's just perfect.

However, we have some error handlers to manage error when we call apis.

In these handlers, we can open a modal (with Nicemodal.show we can) but we want to know which modals are open. For that, I would like to pass the state to the handler.

Would it be possible to export the state or the context ?

Thank you !

useModal and removeAll()

Hi,

Absolutely love this lib. It helped me to resolve many issues i was having with modals, and its much cleaner now as well.

However, as iam still a beginner in react and javascript, could you help me with implementing removeAll() method?.

For example i have a modal for editing user, in there is an option to delete said user which will prompt confirm modal. Once confirmed ,confirm modal will close using remove() however the edit user modal will still be open.

Could you please help.

Thank you

MUI v5 onExited not supported

I am using the muiDialog helper in MUI v5

e.g.
<Dialog {...muiDialog(modal)}>...</Dialog>

This issue with this is that the onExited property is not supported which means that modal.remove() is never called.

This is my workaround but it would be great if this handled in the library instead

<Dialog {...muiDialog(modal)} TransitionProps={{ onExited: () => { modal.resolveHide() modal.remove() } }} >

How to avoid multiple triggers when modal.keepMounted is true

Thanks a lot for such a great library that makes the code better split!

I wanted to keep the user's temporary input and found that modal.keepMounted = true would do it

but this will cause the callback function to be triggered multiple times on the last submit, and I want it to be called only once at the end.

code Link

returned object from useModal hook have "unstable" identity

Hi, so nice library!

and this fix for
#53

just in time (I've just updated to 1.2.4)
Thank you!

But could we do the same for whole object returned from useModal?

const userModal = useModal(UserInfoModal);

to make userModal referential equality all the time?
so if I test it here like this:

const userModal = useModal(UserInfoModal);

React.useEffect(() => {
    console.log('endless show...');
    userModal.show();
}, [userModal]) // <- referential identity will change every time and we have endless loop here

Overlapping the same modal

I was wondering if there was a way to overlap the same modal over each other? I don't see a way to give the same modal a unique id?

Like a group stack of modals that you can close one by one or all at once.

ExampleModal > ExampleModal > ExampleModal

Thanks for this awesome lib!

Check Promise status

Hello once again,

Looking at the documentation I could not find the answer.
I'm trying to implement a loading inside my modal, after the user submits (resolves, or rejects the promise)

For that, I thought about know if the Promise is Pending or Resolves/Rejected
if Resolves/rejected the loading should appear

I suggest add a isPending property for example

Example code after the implementation

{!isPending && <span>Loading ...</span>}

Radix support

Do you plan to support Radix ?
or a way to achieve this with a clear example like bootstrap, antd ... ?

[Request] Add a modal.removeResolve(false) helper function

In all my modal components, I either return a response, or if the user closes the modal I do something like this:

<Modal
    onClose={() => {
       modal.resolve(false);
       modal.remove();
    }}
>

However, this is not ideal since I'm passing a new onClose function instance on each render. It would be great if there was a utility function on the modal instance that did both of these steps in one. 🙌

Unnecessary re-render when showing multiple modals..?

I have this structure:

-- Parent
---- Modal
-------- Child Modal

When I call NiceModal.show() in the Parent, it opens the Modal, without re-rendering the Parent (which is desired!).
Then, in the Modal, I have a function to open a Child Modal. However this causes a re-render to the Modal (which is not desired!).

I have realised that useModal hook seems to be the culprit here, by watching the state change of useModal:
image

Here is the sequence of events, shown with console.log:

Opening Modal:
image

Then opening Child Modal:
image

As you can see, opening the Child Modal causes the first Modal to update (from visible: false back to visible: true).

How can I get around this?

Keep track of modals based on url?

I know this is out of scope but does anybody have ideas on opening nice modals based on the URL?

Example: /posts/create it opens the create post modal but if the user refreshes the page it opens the modal based on the URL.

This lib is awesome!

TS1005: ',' expected.

I keep getting TS1005: ',' expected. at @ebay/nice-modal-react/lib/esm/index.d.ts(41,59)

This looks like a nice way to tackle modals thanks!

Globally check if any modal is visible

Is there nicer a way to check if a modal is open on the page globally?
I'm currently using below method

const modals = useContext(NiceModal.NiceModalContext);
const isModalOpen = Object.keys(modals).length > 0

But this requires a new component inside the NiceModal.Provider.

Doesn't work well with NextJS

I've tried to incorporate nice-modal-react on a NextJS project, but the provider doesn't work when wrapping the app at the root of the tree

function MyApp({ Component, pageProps: { session, ...pageProps }}) {
  return (
    <NiceModal.Provider>
      <Component {...pageProps} />
    </NiceModal.Provider>
  )
}

export default MyApp

It works when wrapping a single page with the provider, so the problem comes from the provider itself.
Unfortunately, it's not a clean solution since I have to wrap every page with a Provider.

Sequential use of the same Modal

Thanks for the package, looks great 💪

I have a question, I'm trying to implement the same Modal to appear twice, one after the other.
When the user submits the first Modal, the second will appear. Sadly, isn't working perfectly

PS: I'm using bootstrap

const inputModal = useModal(ModalInput)

inputModal.show({ title: 'Step 1', defaultValue: 'Hello' }).then((r) => {
  inputModal.show({ title: 'Step 2', defaultValue: 'World' }).then((r) => alert(r.value))
})

That alert at the end, is reporting the value from the first Modal, also, the second defaultValue never appears, I guess the second Modal never shows.
I tried added a hide() before the show() but couldn't make it work (maybe hide() doesn't return a promise so I can wait for it to hide)

How should I make this work? Is this a limitation or can I have a workaround to make it work?

Can the .show() method be smarter about its return type?

export function show<T extends string>(modal: T, args?: Record<string, unknown>): Promise<unknown>;

Right now, the .show() method always returns Promise<unknown>. If I'm not mistaken, whatever I end up passing into .resolve() is what gets returned to .show().

I'm only still learning TypeScript, but can't you use generics to accept a type? Right now, I'm having to do a manual type-check method in order to assert my return type. It feels like I shouldn't have to do that.

For example, if when constructing the modal, you do

modal.resolve(true)

In the app when you do

modal.show().then((res) => whatever...

TypeScript thinks res is unknown. Surely there's a way I can assert that it's a boolean?

No modal id found in NiceModal.useModal

Hi, I'm attempting to use this for the first time, in a Nextjs project, and am running into the error (server-side):

Error: No modal id found in NiceModal.useModal

When following the docs:

import { Modal } from 'antd';
import NiceModal, { useModal } from '@ebay/nice-modal-react';

export default NiceModal.create(({ name }) => {
  // Use a hook to manage the modal state
  const modal = useModal();
  return (
    <Modal
      title="Hello Antd"
      onOk={() => modal.hide()}
      visible={modal.visible}
      onCancel={() => modal.hide()}
      afterClose={() => modal.remove()}
    >
      Hello {name}!
    </Modal>
  );
});

The main difference being that instead of using antd, I'm using a modal called Dialog from Radix UI

I'll set up an example for you but in the meantime, am I doing something wrong that should be obvious?

Thanks

Add Changelog please : )

I love this repo, and i use it in my work heavily.

it's better if we can know change while upgrading the package

Support optional component props generic in `NiceModalArgs`?

Hello, thanks for this great tool, it's a joy to work with!

When passing optional props to the registered modal component (and the component type passes a condition in type NiceModalArgs<T>), the component props are typed as Partial<Omit<React.ComponentProps<T>, 'id'>>.

Using Partial<> means that otherwise required component props are incorrectly loosened up. The related comment specifically mentions using Partial<>, so I guess there was a good reason to do so. But to achieve type safety, now a wrapper function must be used that enforces the required component props:

function registerModalWithProps<C extends React.FC<any>>(id: string, component: C, props: React.ComponentProps<C>) {
  register(id, component, props)
}

Could this situation be avoided if an optional component props generic were to be introduced?

type ReactJSXOrConstructor<T> = T extends keyof JSX.IntrinsicElements | React.JSXElementConstructor<any> ? T : never

type PartialComponentProps<T> = Partial<React.ComponentProps<ReactJSXOrConstructor<T>>>

declare type NiceModalArgs<T, P = PartialComponentProps<T>> = T extends ReactJSXOrConstructor<T> ? Omit<P, 'id'> : Record<string, unknown>;

I understand the optional generic would creep into the signatures of useModal(), register(), and possibly even show(), so an on-demand wrapper function might be simpler overall.

Better way to provide type of modal props in TypeScript

At the moment if I have to provide type of modal props (while calling NiceModal.show()), then I do something like the below -

image

It would be nice if I wouldn't have to declare a separate variable - just so that TS helps me to provide correct props to the modal (when this can be done without declaring the variable). Implementing this suggestions would help in readability and save a LOC and variable. One suggestion from me is something like this -

image

Right now, trying to do the above, provides the 'DeleteModalProps' type to the returned promise's value (not to the props of modal).

[Question] Typesafe args

I'm not an expert ts developer so there might be something I missed. When I change the signature of the show method to below, args parameter becomes type safe, instead of any. Is there a reason why this isn't the case?

export declare function show<T extends any, Arg extends any>(modal: React.FC<Arg>, args?: NiceModalArgs<React.FC<Arg>>): Promise<T>;

Problem with nested NiceModal Provider

Steps to reproduce

  1. Wrap the whole app with a NiceModal.Provider
  2. Then use react-router or simple state to create 2 page
  3. On page 1 use NiceModal.Provider to wrap all children
  4. Add a button and simple Modal for page 1
  5. On page 2 don't wrap it with a NiceModal.Provider
  6. Add a button and simple Modal for page 2
  7. Add buttons to switch between pages
  8. Start the app

Case 1:

  • Go to page 1 directly then click the button to open the dialog -> it works
  • Go to page 2 by clicking the button we add in step 7 above then click the button to open the dialog -> it doesn't work

Case 2:

  • Go to page 2 directly then click the button to open the dialog -> it works
  • Go to page 1 by clicking the button we add in step 7 then click the button to open the dialog -> it works
  • Go to page 2 by clicking the button we add in step 7 then click the button to open the dialog -> it doesn't work

Github repository

https://github.com/quangphuchuynh95/test-nice-modal

Codesandbox

https://codesandbox.io/s/wizardly-goldberg-rp3vtz?file=/src/index.js

NiceModalProps should let objects with id

NiceModalHocProps has a field named id. This is too common and prevents us to pass a type with id in it.

This is not possible:

NiceModal.create(
  (object: { id: string; foo: string }) => { ... }

I actually opened a PR for this #96. But closed it since it wasn't backward compatible.

To reproduce:

export const FoodModal = NiceModal.create(
  (arg: { id: string }) => {

    return (
      <div></div>
    );
  }
);


NiceModal.show(FoodModal, {id: "foo"}) // No overload matches this call
export const FoodModal = NiceModal.create(
  (arg: { fooId: string }) => {

    return (
      <div></div>
    );
  }
);


NiceModal.show(FoodModal, {fooId: "foo"}) // It's okay now

Question: How to update props based on fetch status

Hello, firstly, I wanted to thank you for the library!

Also, I have a question concerning updating props based on some fetch call status.

Is it somehow possible for isLoading to be updated based on what happens inside of the handleDelete function?

Would like to avoid declaring the modal like this: <Modal id="my-antd-modal" isLoading={isLoading} handleDelete={handleDelete} />

const [isLoading, setIsLoading] = useState(false)
const modal = useModal(DeleteModal);

const handleDelete = () => {
  setIsLoading(true)
  api.delete("url").then(() => {}).finally(() => setIsLoading(false))
};

modal.show({
  handleDelete,
  isLoading
});

export default NiceModal.create(({ isLoading, handleDelete }) => {
  const modal = useModal();
  return (
    <Modal visible={modal.visible}>
        Are you sure you want to delete?
      <Button onClick={handleDelete} disabled={isLoading}>Confirm</Button>
    </Modal>
  );
});

Help with sequential async modal using Radix Primitive's Dialog component

Hi, I'm really close to getting this library to work with Radix Primitives Dialog component, but am not sure what I'm doing wrong.

I'm able to get a normal modal working by connecting useModal's methods to Dialog's props. However, when I attempt to get a sequential modal going while following your async example, it doesn't seem to work after the first modal.

Here's the sandbox that I currently have working.

Are you able to spot what I'm doing wrong?

Source map references file not in npm package

The just-released version 1.2.9 added source mapping, but the source map references a file that isn't included in the published package. This results in an error like the following:

WARNING in ./node_modules/@ebay/nice-modal-react/lib/esm/index.js
Module Warning (fom ./node_modules/source-map-loader/dist/cjs.js):
Failed to parse source map from '<path>\node_modules\@ebay\nice-modal-react\src\index.tsx' file: Error: ENOENT: no such file or directory, open '<path>\node_modules\@ebay\nice-modal-react\src\index.tsx'

(I had to retype this message by hand for unrelated reasons so apologies if I typoed anything)

This doesn't appear to actually break anything, so it's probably a low-priority error, but the source map likely doesn't work.

FeatureI Idea: Allow developer to update props

Thanks for the great library! This is similar to how I have implemented "confirm" modals many times. But this is much more flexible and can be applied to multiple modals, unlike my implementations.

I have found it useful to have some update method available. This allows the developer to update the props of the modal before closing. For example, we may have some disabled and text state and we want to keep the modal open with the button disabled and the text updated before closing. The below example is similar to this: https://github.com/eBay/nice-modal-react#use-modal-with-the-hook

export default NiceModal.create(({ text, disabled, handleOk }) => {
  const modal = useModal();
  return (
    <Modal visible={modal.visible}>
      {text} 
      <Button onClick={handleOk} disabled={disabled}>Confirm</Button>
    </Modal>
  );
});
const modal = useModal(MyConfirmModal);

const handleOk = () => {
  modal.update({ text: 'Deleting...', disabled: true });
  await doTheAsyncDelete();
  modal.close();
};

modal.show({
  text: 'Are you sure you want to delete?',
  disabled: false,
  handleOk
});

I am currently solving this by calling show again and merging the args property.

const modal = useModal(MyConfirmModal);

const handleOk = () => {
  // Note we call show again instead of update
  modal.show({ ...modal.args,  text: 'Deleting...', disabled: true });
  await doTheAsyncDelete();
  modal.close();
};

modal.show({
  text: 'Are you sure you want to delete?',
  disabled: false,
  handleOk
});

Thanks again for the library. I am testing it out in a large app that uses lots of Modals now. Please let me know if calling show({ ...modal.args, disabled: true }) is the right way to handle this. It seems to work when passing the modal in, but I am unsure how it will work when using ids.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.