Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refine dialog wrapper and use it for all existing dialogs #3407

Merged
merged 8 commits into from
Feb 8, 2019
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
206 changes: 206 additions & 0 deletions client/app/components/DialogWrapper.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
import { isFunction } from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';

/**
Wrapper for dialogs based on Ant's <Modal> component.


Using wrapped dialogs
=====================

Wrapped component is an object with two fields:

{
showModal: (dialogProps) => object({
result: Promise,
close: (result) => void,
dismiss: (reason) => void,
}),
Component: React.Component, // wrapped dialog component
}

To open dialog, use `showModal` method; optionally you can pass additional properties that
will be expanded on wrapped component:

const dialog = SomeWrappedDialog.showModal()

const dialog = SomeWrappedDialog.showModal({ greeting: 'Hello' })

To get result of modal, use `result` property:

dialog.result
.then(...) // pressed OK button or used `close` method; resolved value is a result of dialog
.catch(...) // pressed Cancel button or used `dismiss` method; optional argument is a rejection reason.

Also, dialog has `close` and `dismiss` methods that allows to close dialog by caller. Passed arguments
will be used to resolve/reject `dialog.result` promise. `update` methods allows to pass new properties
to dialog.


Creating a dialog
================

1. Add imports:

import { wrap as wrapDialog, DialogPropType } from 'path/to/DialogWrapper';

2. define a `dialog` property on your component:

propTypes = {
dialog: DialogPropType.isRequired,
};

`dialog` property is an object:

{
props: object, // properties for <Modal> component;
close: (result) => void, // method to confirm dialog; `result` will be returned to caller
dismiss: (reason) => void, // method to reject dialog; `reason` will be returned to caller
}

3. expand additional properties on <Modal> component:

render() {
const { dialog } = this.props;
return (
<Modal {...dialog.props}>
);
}

4. wrap your component and export it:

export default wrapDialog(YourComponent).

Your component is ready to use. Wrapper will manage <Modal>'s visibility and events.
If you want to override behavior of `onOk`/`onCancel` - don't forget to close dialog:

customOkHandler() {
this.saveData().then(() => {
this.props.dialog.close({ success: true }); // or dismiss();
});
}

render() {
const { dialog } = this.props;
return (
<Modal {...dialog.props} onOk={() => this.customOkHandler()}>
);
}


Settings
========

You can setup this wrapper to use custom `Promise` library (for example, Bluebird):
kravets-levko marked this conversation as resolved.
Show resolved Hide resolved

import DialogWrapper from 'path/to/DialogWrapper';
import Promise from 'bluebird';

DialogWrapper.Promise = Promise;

*/

export const DialogPropType = PropTypes.shape({
props: PropTypes.shape({
visible: PropTypes.bool,
onOk: PropTypes.func,
onCancel: PropTypes.func,
afterClose: PropTypes.func,
}).isRequired,
close: PropTypes.func.isRequired,
dismiss: PropTypes.func.isRequired,
});

// default export of module
const DialogWrapper = {
Promise,
DialogPropType,
wrap() {},
};

function openDialog(DialogComponent, props) {
const dialog = {
props: {
visible: true,
onOk: () => {},
onCancel: () => {},
afterClose: () => {},
},
close: () => {},
dismiss: () => {},
};

const dialogResult = {
resolve: () => {},
reject: () => {},
};

const container = document.createElement('div');
document.body.appendChild(container);

function render() {
ReactDOM.render(<DialogComponent {...props} dialog={dialog} />, container);
}

function destroyDialog() {
// Allow calling chain to roll up, and then destroy component
setTimeout(() => {
ReactDOM.unmountComponentAtNode(container);
document.body.removeChild(container);
}, 10);
}

function closeDialog(result) {
dialogResult.resolve(result);
dialog.props.visible = false;
render();
}

function dismissDialog(reason) {
dialogResult.reject(reason);
dialog.props.visible = false;
render();
}

dialog.props.onOk = closeDialog;
dialog.props.onCancel = dismissDialog;
dialog.props.afterClose = destroyDialog;
dialog.close = closeDialog;
dialog.dismiss = dismissDialog;

const result = {
close: closeDialog,
dismiss: dismissDialog,
update: (newProps) => {
props = { ...props, ...newProps };
render();
},
result: new DialogWrapper.Promise((resolve, reject) => {
dialogResult.resolve = resolve;
dialogResult.reject = reject;
}),
kravets-levko marked this conversation as resolved.
Show resolved Hide resolved
};

render(); // show it only when all structures initialized to avoid unnecessary re-rendering

// Some known libraries support
// Bluebird: http://bluebirdjs.com/docs/api/suppressunhandledrejections.html
if (isFunction(result.result.suppressUnhandledRejections)) {
result.result.suppressUnhandledRejections();
}

return result;
}

export function wrap(DialogComponent) {
return {
Component: DialogComponent,
showModal: props => openDialog(DialogComponent, props),
};
}

DialogWrapper.wrap = wrap;

export default DialogWrapper;
Loading