Skip to content

Commit

Permalink
refactor(core/presentation): Change how ReactModal works (#4874)
Browse files Browse the repository at this point in the history
- Render the Modal component automatically
- Pass a Component class, instead of a bunch of JSX.Element
- Provide closeModel/dismissModal callbacks to the contents component
  • Loading branch information
christopherthielen authored and Justin Reynolds committed Feb 20, 2018
1 parent eb1b2cc commit 895fb3f
Show file tree
Hide file tree
Showing 2 changed files with 73 additions and 25 deletions.
26 changes: 13 additions & 13 deletions app/scripts/modules/core/src/entityTag/EntityTagEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ export interface IEntityTagEditorProps {
entityRef: IEntityRef;
isNew: boolean;
show?: boolean;
onHide?(event: any): void;
closeModal?(result?: any): void; // provided by ReactModal
dismissModal?(rejection?: any): void; // provided by ReactModal
onUpdate?(): void;
}

Expand All @@ -59,7 +60,8 @@ export interface IEntityTagEditorState {
@BindAll()
export class EntityTagEditor extends React.Component<IEntityTagEditorProps, IEntityTagEditorState> {
public static defaultProps: Partial<IEntityTagEditorProps> = {
onHide: noop,
closeModal: noop,
dismissModal: noop,
onUpdate: noop,
};

Expand All @@ -70,7 +72,7 @@ export class EntityTagEditor extends React.Component<IEntityTagEditorProps, IEnt

/** Shows the Entity Tag Editor modal */
public static show(props: IEntityTagEditorProps): Promise<void> {
return ReactModal.show(React.createElement(EntityTagEditor, props));
return ReactModal.show(EntityTagEditor, props);
}

constructor(props: IEntityTagEditorProps) {
Expand All @@ -94,8 +96,8 @@ export class EntityTagEditor extends React.Component<IEntityTagEditorProps, IEnt
const promise = deferred.promise;
this.$uibModalInstanceEmulation = {
result: promise,
close: () => this.setState({ show: false }),
dismiss: () => this.setState({ show: false }),
close: (result: any) => this.props.closeModal(result),
dismiss: (error: any) => this.props.dismissModal(error),
} as IModalServiceInstance;
Object.assign(this.$uibModalInstanceEmulation, { deferred });
}
Expand All @@ -116,9 +118,9 @@ export class EntityTagEditor extends React.Component<IEntityTagEditorProps, IEnt
this.setState({ isValid: false });
}

private onHide(): void {
private close(): void {
this.setState({ show: false });
this.props.onHide.apply(null, arguments);
this.props.dismissModal.apply(null, arguments);
this.$uibModalInstanceEmulation.deferred.resolve();
}

Expand Down Expand Up @@ -163,20 +165,19 @@ export class EntityTagEditor extends React.Component<IEntityTagEditorProps, IEnt

const closeButton = (
<div className="modal-close close-button pull-right">
<a className="btn btn-link" onClick={this.onHide}>
<a className="btn btn-link" onClick={this.close}>
<span className="glyphicon glyphicon-remove" />
</a>
</div>
);

const submitLabel = `${isNew ? ' Create' : ' Update'} ${tag.value.type}`;
const cancelButton = <button type="button" className="btn btn-default" onClick={this.onHide}>Cancel</button>;
const cancelButton = <button type="button" className="btn btn-default" onClick={this.close}>Cancel</button>;

const { TaskMonitorWrapper } = NgReact;

return (
<Modal show={this.state.show} onHide={this.onHide} dialogClassName="entity-tag-editor-modal">

<div>
<TaskMonitorWrapper monitor={this.state.taskMonitor} />

<Formsy.Form
Expand Down Expand Up @@ -220,8 +221,7 @@ export class EntityTagEditor extends React.Component<IEntityTagEditorProps, IEnt

</Modal.Footer>
</Formsy.Form>

</Modal>
</div>
);
}
}
Expand Down
72 changes: 60 additions & 12 deletions app/scripts/modules/core/src/presentation/ModalService.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,52 @@
import { ReactElement } from 'react';
import * as React from 'react';
import { Modal, ModalProps } from 'react-bootstrap';
import * as ReactDOM from 'react-dom';

import { pick, omit } from 'lodash';

/** The Modal content Component will be passed these two props */
export interface IModalComponentProps {
// Close modal with a result value (i.e., OK button)
closeModal?(result: any): void;

// Dismiss/reject modal (i.e., Cancel button)
dismissModal?(rejectReason: any): void;
}

/** An imperative service for showing a react component as a modal */
export class ReactModal {
public static show(modal: ReactElement<any>): Promise<any> {
return new Promise((resolve) => {
/**
* example:
* const MyComponent = ({ closeModal, dismissModal }) => {
* <h1>Modal Contents!</h1>
* <button onClick={() => closeModal('A')}>Choice A</button>
* <button onClick={() => closeModal('B')}>Choice B</button>
* <button onClick={() => dismissModal('cancelled')}>Cancel</button>
* }
*
* ...
*
* ModalService.show<string>(MyComponent).then(result => {
* this.setState({ result });
* });
*
* @param ModalComponent the component to be rendered inside a modal
* @param props to pass to the Modal and ModalComponent
* @returns {Promise<T>}
*/
public static show<P extends IModalComponentProps, T = any>(ModalComponent: React.ComponentType<P>, props?: P): Promise<T> {
const modalPropKeys: [keyof ModalProps] = [
'onHide', 'animation', 'autoFocus', 'backdrop', 'backdropClassName', 'backdropStyle',
'backdropTransitionTimeout', 'bsSize', 'container', 'containerClassName', 'dialogClassName',
'dialogComponent', 'dialogTransitionTimeout', 'enforceFocus', 'keyboard', 'onBackdropClick',
'onEnter', 'onEntered', 'onEntering', 'onEscapeKeyUp', 'onExit', 'onExited', 'onExiting',
'onShow', 'transition',
];

const modalProps: ModalProps = pick(props, modalPropKeys);
const componentProps = omit(props, modalPropKeys);

const modalPromise = new Promise<T>((resolve, reject) => {
let mountNode = document.createElement('div');
let show = true;

Expand All @@ -20,22 +61,29 @@ export class ReactModal {
mountNode = null;
}

function onHide(action: any) {
const destroy = (resultHandler: (result: any) => void) => (result: any) => {
resultHandler(result);
// Use react-bootstrap modal lifecycle, i.e. show=false, which triggers onExited
show = false;
resolve(action);
render();
}
};

const handleClose = destroy(resolve);
const handleDismiss = destroy(reject);

function render() {
ReactDOM.render(
React.cloneElement(modal, {
show,
onExited,
onHide,
}),
ReactDOM.render((
<Modal show={show} {...modalProps} onExited={onExited}>
<ModalComponent {...componentProps} dismissModal={handleDismiss} closeModal={handleClose}/>
</Modal>
),
mountNode
);
}
});

modalPromise.catch(() => {});

return modalPromise;
}
}

0 comments on commit 895fb3f

Please sign in to comment.