# React Promise Modal

`usePromiseModal()` is a React hook that allows you to define a modal
by providing a custom rendering function.

After defining your modal you can invoke it imperatively and await the returned promise
to resolve, to get the modal resolution result.

## Usage

1. Define your modal with `usePromiseModal()`
2. Invoke it from your event handler using `invoke()`
3. Wait for the modal to resolve with `await`

```js
// 1) Define your modal
const confirmation = usePromiseModal((props) => <MyModal {...props} />);

// 2) Call it in your event handler
async function handleClick() {
    // 3) Wait for the modal to resolve
    if (await confirmation.invoke()) {
        // TODO: Perform the operation.
    }
}
```

**Demo: https://codesandbox.io/p/sandbox/romantic-lovelace-4pmm3k**

## API

**The `usePromiseModal()` hook` returns the following values**:

```tsx
const { invoke, modal, isDisplayed } = usePromiseModal(/* ... */);
```

- `invoke` — imperatively invoke the modal, optionally passing additional call-time arguments.
  Returns a promise you can _await_ to get the modal resolution value, when it's available. 
  Or _undefined_ if the modal has been dismissed or cancelled.

- `modal` — the rendered modal markup (`ReactElement | null`). You should always render this
  value into your component subtree.

- `isDisplayed` — a boolean flag indicating if there is currently a pending modal
  for this definition.

**The modal render function receives these properties**:

```tsx
usePromiseModal(({ show, onDismiss, onSubmit }) => (
    <MyModal show={show} onDismiss={onDismiss} onSubmit={onSubmit} />
));
```


- `show` — boolean to tell if the window is visible or not. 
   Used for in/out transitions. 
   Primarily intended to be used as *react-bootstrap* Modal `show` property.

- `onDismiss` — should be invoked when the modal is dismissed.
  Always resolves the promise to `undefined`.

- `onSubmit` — should be invoked when the modal is submitted/confirmed. 
   Resolves to the value provided as an argument to it.
   The resolve value cannot be `undefined`, because it is already reserved for dismissal.

The function returns a *Promise* that is resolved with the submitted value, 
or `undefined` if it has been dismissed.

## Examples

### Confirmation

You can easily implement a confirmation modal using `usePromiseModal()`:

```jsx
import { usePromiseModal } from '@prezly/react-promise-modal';

function MyApp() {
    const confirmation = usePromiseModal(({ show, onSubmit, onDismiss }) => {
        // Use any modal implementation you want
        <MyConfirmationModal title="⚠️ Are you sure?" show={show} onConfirm={() => onSubmit(true)} onDismiss={onDismiss} />
    });
    
    async function handleDeleteAccount() {
        if (await confirmation.invoke()) {
            console.log('Conirmed');
        } else {
            console.log('Cancelled');
        }
    }
    
    return (
        <div>
           <button onClick={handleDeleteAccount}>Delete account</button>
           {confirmation.modal}
        </div>
    )
}
```

### Alert

Alert is basically the same as confirmation, except there is no difference whether
it is submitted or dismissed -- the modal has single action anyway. 
So we only need `onDismiss`:

```jsx
import { usePromiseModal } from '@prezly/react-promise-modal';

function MyApp() {
    const alert = usePromiseModal(({ show, onDismiss }) => {
        // Use any modal implementation you want
        <MyAlertModal title="✔ Account deleted!" show={show} onDismiss={onDismiss} />
    });

    async function handleDeleteAccount() {
        await api.deleteAccount();
        await alert.invoke();
    }

    return (
        <div>
            <button onClick={handleDeleteAccount}>Delete account</button>
            {alert.modal}
        </div>
    )
}

```

### Prompt User Input

For data prompts all you need is to resolve the promise by submitting the value to `onSubmit`:
either a scalar, or more complex shapes wrapped into an object:

```tsx
import { usePromiseModal } from '@prezly/react-promise-modal';

function MyApp() {
    const prompt = usePromiseModal<string, { title: string }>(
        (props) => <MyFilenamePromptModal {...props} />,
    );

    async function handleCreateFile() {
        const filename = await prompt.invoke({ title: 'Please enter filename:' });
        if (!filename) {
            console.error('Filename is required');
            return;
        }
        await api.createFile(filename);
    }

    return (
        <div>
            <button onClick={handleCreateFile}>Create new file</button>
            {prompt.modal}
        </div>
    )
}

interface Props {
    title: string;
    show: boolean;
    onSubmit: (filename: string) => void;
    onDismiss: () => void;
}

function MyFilenamePromptModal({ title, show, onSubmit, onDismiss }: Props) {
    const [filename, setFilename] = useState("Untitled.txt");

    return (
        // Use any modal implementation you want
        <Modal>
            <form onSubmit={() => onSubmit(filename)}>
                <p>{title}</p>
                <input autoFocus value={filename} onChange={(event) => setFilename(event.target.value)} />

                <button variant="secondary" onClick={onDismiss}>Cancel</button>
                <button variant="primary" type="submit">Confirm</button>
            </form>
        </Modal>
    );
}
```

## Additional Invoke-time Arguments

In addition to the three standard properties your modal render callback will always receive when rendered,
you can pass extra call-time properties. Declare them with the second generic type parameter of `usePromiseModal()`,
and then pass to the `invoke()` method:

```tsx
import { usePromiseModal } from "@prezly/react-promise-modal";

const failureFeedback = usePromiseModal<undefined, { status: Status, failures: OperationFailure[] }>(
    ({ status, failures, show, onSubmit, onDismiss }) => (
        <FailureModal status={status} failures={failures} show={show} onSubmit={onSubmit} onDismiss={onDismiss} />
    ),
);

// Invocation of the modal now requrires these additional properties:
async function handleFlakyOperation() {
    const { status, failures } = await api.flakyOperation();
    if (status !== 'success') {
        await failureFeedback.invoke({ status, failures }); // Note: here we pass additional parameters call-time
    }
}
```

------------------

# Credits

Brought to you with :metal: by [Prezly](https://www.prezly.com/?utm_source=github&utm_campaign=react-promise-modal).
