diff --git a/README.md b/README.md index e48d7a1..ba95f42 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,38 @@ # react-confirm -Small library which makes your Dialog component callable. +react-confirm is a lightweight library that simplifies the implementation of confirmation dialogs in React applications by offering a Promise-based API that works seamlessly with async/await syntax, similar to `window.confirm`. -This library does not provide any view component. Just adds a functionality to be callable like `window.confirm`. +One key feature of react-confirm is that it doesn't provide a specific view or component for the confirmation dialog, allowing you to easily customize the appearance of the dialog to match your application's design. -In the [example](https://github.com/haradakunihiko/react-confirm/tree/master/example), [react-bootstrap](https://react-bootstrap.github.io/components.html#modals) and [material-ui](http://www.material-ui.com/#/components/dialog) are used with. +In the [example](https://github.com/haradakunihiko/react-confirm/tree/master/example), [react-bootstrap](https://react-bootstrap-v3.netlify.app/components/modal/) and [material-ui](http://www.material-ui.com/#/components/dialog) are used with. [![npm version](https://badge.fury.io/js/react-confirm.svg)](https://badge.fury.io/js/react-confirm) ## Motivation - React is great. And I respect the concept to render the view reactively only by it's state. However, it easily becomes really complex to manage all states which are only needed just temporarily like confirmation dialog. The question is... Is it worth to manage them inside your app? I guess the answer is not always yes. +React is a powerful library that allows for reactive rendering based on component state. However, managing temporary states like confirmation dialogs can quickly become complex. The question is: is it worth implementing these states within your app? The answer is not always a clear yes. ## What you can do - With this library, - - You can open a dialog component by calling function and it will be rendered outside your application. The function returns promise so that you can define callbacks to handle the confirmation result. - - You can pass arguments to the function and use them inside the dialog component. - - You can get values from the component in the promise. - - There is no limitation in the dialog. You can use input forms, multiple buttons, whatever you want (see demo site). +react-confirm library offers several benefits: + +- You can open a dialog component by calling a function without appending it into your React tree. The function returns a promise, allowing you to handle confirmation results with callbacks. +- You can pass arguments to the function and use them inside the dialog component. +- You can retrieve values from the component in the promise. +- The library provides flexibility in designing the dialog. There is no limitation in the type of components you can use, whether it be input forms or multiple buttons. You can even check out the demo site to see examples of how to customize the dialog. ## Demo https://codesandbox.io/s/react-confirm-with-react-bootstrap-kjju1 ## Versions -- React 18+ users should use `react-confirm` version 0.2.x +- React 18+ users should use `react-confirm` version 0.2.x or 0.3.x - React <=17 users should stick to `react-confirm` version 0.1.x ## Usage -1. create your dialog component. -2. apply `confirmable` to your component (optional, but usually recommended). -3. create function with `createConfirmation` by passing your confirmable component. -4. call it! +1. Create your dialog component. +2. Apply `confirmable` HOC to your component (Optional. See `confirmable` implementation). +3. Create a function using `createConfirmation` by passing your `confirmable` component. +4. Call it! -### create confirmable component +### Create your dialog component and Apply `confirmable` HOC to your component. ```js import React from 'react'; @@ -56,16 +57,9 @@ YourDialog.propTypes = { // confirmable HOC pass props `show`, `dismiss`, `cancel` and `proceed` to your component. export default confirmable(YourDialog); - -// or, use `confirmable` as decorator -@confirmable -class YourDialog extends React.Component { -} - - ``` -### create confirm function +### Create a function using `createConfirmation` ```js import { createConfirmation } from 'react-confirm'; import YourDialog from './YourDialog'; @@ -77,10 +71,9 @@ export const confirm = createConfirmation(YourDialog); export function confirmWrapper(confirmation, options = {}) { return confirm({ confirmation, options }); } - ``` -### use it! +### Call it! Now, you can show dialog just like window.confirm with async-await. The most common example is onclick handler for submit buttons. ```js @@ -108,6 +101,42 @@ const handleOnClick2 = async () => { You can check more complex example in [codesandbox](https://codesandbox.io/s/react-confirm-with-react-bootstrap-kjju1) +## Using with Context +By default, this library renders the confirmation dialog without appending the component to your app's React component tree. While this can be useful, it may cause issues if you need to consume context in your component. To overcome this problem, you can use the `MountPoint` component to include your confirmation dialog within your app's tree, enabling it to access context and other data from the app. + +Create your own `createConfirmation` using `createConfirmationCreater` and `createReactTreeMounter`. + +```js +import { createConfirmationCreater, createReactTreeMounter, createMountPoint } from 'react-confirm'; + +const mounter = createReactTreeMounter(); + +export const createConfirmation = createConfirmationCreater(mounter); +export const MountPoint = createMountPoint(mounter); +``` + +Put `MountPoint` into your React tree. +```js +const YourRootComponent = () => { + return ( + + + + + ) +} +``` + +use your `createConfirmation` as usual. +```js +export const confirm = createConfirmation(YourDialog); +``` + +To render the confirmation dialog within the React component tree but in a different part of the DOM, you can pass a DOM element to the `createReactTreeMounter` function. This will use the `createPortal` method to render the confirmation dialog in the specified DOM element while keeping it within the React component tree. + +```js +const mounter = createReactTreeMounter(document.body); +``` ## typescript diff --git a/src/createConfirmation.js b/src/createConfirmation.js index 0a47bf7..24ba72f 100644 --- a/src/createConfirmation.js +++ b/src/createConfirmation.js @@ -1,21 +1,11 @@ -import React from 'react'; -import { createRoot } from 'react-dom/client'; +import { createDomTreeMounter } from './mounter/domTree'; -const createConfirmation = (Component, unmountDelay = 1000, mountingNode) => { +export const createConfirmationCreater = (mounter) => (Component, unmountDelay = 1000, mountingNode) => { return (props) => { - const wrapper = (mountingNode || document.body).appendChild(document.createElement('div')); - const root = createRoot(wrapper); - + let mountId; const promise = new Promise((resolve, reject) => { try { - root.render( - - ); + mountId = mounter.mount(Component, { reject, resolve, dispose, ...props}, mountingNode) } catch (e) { console.error(e); throw e; @@ -24,12 +14,7 @@ const createConfirmation = (Component, unmountDelay = 1000, mountingNode) => { function dispose() { setTimeout(() => { - root.unmount(); - setTimeout(() => { - if (wrapper && wrapper.parentNode) { - wrapper.parentNode.removeChild(wrapper); - } - }); + mounter.unmount(mountId); }, unmountDelay); } @@ -43,4 +28,4 @@ const createConfirmation = (Component, unmountDelay = 1000, mountingNode) => { } } -export default createConfirmation; +export default createConfirmationCreater(createDomTreeMounter()); diff --git a/src/index.js b/src/index.js index ed37f23..2a4ed4b 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,6 @@ import confirmable from './confirmable'; -import createConfirmation from './createConfirmation'; +import createConfirmation, { createConfirmationCreater } from './createConfirmation'; +import { createDomTreeMounter } from './mounter/domTree'; +import { createReactTreeMounter, createMountPoint } from './mounter/reactTree'; -export { confirmable, createConfirmation }; +export { confirmable, createConfirmation, createConfirmationCreater, createDomTreeMounter, createReactTreeMounter, createMountPoint }; diff --git a/src/mounter/domTree.js b/src/mounter/domTree.js new file mode 100644 index 0000000..75e0dd1 --- /dev/null +++ b/src/mounter/domTree.js @@ -0,0 +1,35 @@ +import React from 'react'; +import { createRoot } from 'react-dom/client'; + +export function createDomTreeMounter(defaultMountNode) { + const confirms = {}; + const callbacks = {}; + + function mount(Component, props, mountNode){ + const key = Math.floor(Math.random() * (1 << 30)).toString(16); + const wrapper = (mountNode || defaultMountNode || document.body).appendChild(document.createElement('div')); + confirms[key] = wrapper; + + const root = createRoot(wrapper); + + root.render( + + ); + callbacks.mounted && callbacks.mounted(); + return key; + } + + function unmount(key) { + const wrapper = confirms[key]; + delete confirms[key]; + + if (wrapper && wrapper.parentNode) { + wrapper.parentNode.removeChild(wrapper); + } + } + return { + mount, unmount, options: {} + } +} diff --git a/src/mounter/reactTree.js b/src/mounter/reactTree.js new file mode 100644 index 0000000..83dd7c8 --- /dev/null +++ b/src/mounter/reactTree.js @@ -0,0 +1,55 @@ +import React, { useState, useEffect } from 'react' +import { createPortal } from 'react-dom'; + +export function createReactTreeMounter(mountNode) { + const confirms = {}; + const callbacks = {}; + + function mount(Component, props){ + const key = Math.floor(Math.random() * (1 << 30)).toString(16); + confirms[key] = { Component, props}; + callbacks.mounted && callbacks.mounted(confirms); + return key; + } + function unmount(key) { + delete confirms[key]; + callbacks.mounted && callbacks.mounted(confirms); + } + + function setMountedCallback(func) { + callbacks.mounted = func; + } + + return { + mount, unmount, + options: { + setMountedCallback, mountNode + } + } +} + +export function createMountPoint(reactTreeMounter) { + return () => { + const [confirmComponents, setConfirmComponents] = useState({}); + + useEffect(() => { + return reactTreeMounter.options.setMountedCallback((components) => { + setConfirmComponents({...components}); + }); + }, []); + + let element = ( + <> + {Object.keys(confirmComponents).map((key) => { + const { Component, props } = confirmComponents[key]; + return + })} + + ) + if (reactTreeMounter.options.mountNode) { + element = createPortal(element, reactTreeMounter.options.mountNode); + } + + return element; + } +}