Skip to content

Instantly share code, notes, and snippets.

@spion spion/promise-dialog.tsx
Last active Jun 22, 2019

Embed
What would you like to do?
/**
* Implementation of promise dialogs
*/
import { observable, action } from 'mobx';
import * as React from 'react';
import { AppSingleton } from '@h4bff/core';
import { AppContext } from '../router';
import { observer } from 'mobx-react';
interface Resolver<T> {
resolve(t: T): void;
reject(e: Error): void;
}
interface PromiseDialogData<TProps, TResult> {
component: typeof PromiseDialog;
input: TProps;
resolver: Resolver<TResult>;
promise: Promise<TResult>;
}
function createResolvable<T>() {
let resolver: Resolver<T> = {
resolve: null!,
reject: null!,
};
let promise = new Promise<T>((resolve, reject) => {
resolver = { resolve, reject };
});
return { promise, resolver };
}
type PromiseDialogC<Props, Result> = {
new (t: PropsWithResolver<Props, Result>): PromiseDialog<Props, Result>;
};
type PropsWithResolver<Props, Result> = Props & { _resolver: Resolver<Result> };
/**
* Override this class to implement a new dialog. The dialog can take params which will be passed
* as props to the react component. The dialog also has a return value, which is what the promise
* resolves to.
*
* @example
* ```
* class ConfirmDialog extends PromiseDialog<{title?: string, message: string, yesButton?:string, noButton?:string}, void> {
* render() {
* let reject = () => { this.reject(new Error('Confirmation cancelled')); }
* return <Dialog2 isOpen={true} title={this.props.title || 'Confirmation request'} onClose={reject} footer={<>
* <Button intent="primary" text={this.yesButton || 'Yes'} onClick={this.resolve} />
* <Button intent="primary" text={this.noButton || 'No'} onClick={reject} />
* </>}>
* {this.message}
* </Dialog2>
* }
* }
*
* app.getSingleton(DialogManager).show(ConfirmDialog, {
* message: `Are you sure you want to delete "${file.name}"?`
* }).then(() => sendDeleteRequest(file));
* ```
*/
export class PromiseDialog<Props, Result> extends React.Component<
PropsWithResolver<Props, Result>
> {
/**
* Use this method to resolve the promise with a success result (e.g. new data input by the
* user, etc)
*/
protected resolve(u: Result): void {
return this.props._resolver.resolve(u);
}
/**
* Use this method to resolve the dialog promise with a failure result (e.g. dialog closed,
* cancel button clicked etc)
*/
protected reject(e: Error): void {
return this.props._resolver.reject(e);
}
}
/**
* A dialog manager contains the currently active dialogs and can be used to show new dialogs.
*
* @remarks To use it, put it at the bottom of the main app
* component using `<DialogManagerComponent />`. Then you can show new dialogs
* using the show method: `app.getSingleton(DialogManager).show(DialogClass, dialogParams)`
*/
export class DialogManager extends AppSingleton {
@observable private dialogs: PromiseDialogData<any, any>[] = [];
/**
* Shows a new dialog
* @param component - The dialog component to show. Must inherit from {@link PromiseDialog}
* @param input - these params will get passed to the dialog constructor
* @return the result of this dialog, which is resolved or rejected when the dialog closes. Its up to the dialog to
* decide whether it was closed with an error or closed successfully with a result, and what that result is.
*
* @example
* ```typescript
* class UserEditor extends PromiseDialog<{user:User}, User> {
* @observable editedUser: User
* constructor(props) {
* this.editedUser.id = props.user.id;
* this.editedUser.firstName = props.user.firstName;
* this.editedUser.lastName = props.user.lastName;
* }
* render() {
* <Dialog onClose={() => this.reject(new Error('dialog closed, editing cancelled'))}>
* <input type="text" onChange={e => this.editedUser.firstName = e.target.value} />
* <input type="text" onChange={e => this.editedUser.lastName = e.target.value} />
* <button onClick={() => this.resolve(this.editedUser)} />
* </Dialog>
* }
* }
*
* app.getSingleton(DialogManager).show(UserEditor, this.userToEdit)
* .then(user => app.getSingleton(MyApi).updateUser(user))
* .then(() => app.getSingleton(Tooltip).show({title: 'Success', message: 'UserUpdated'}))
* })
* ```
*/
@action show<Props, Result>(
component: PromiseDialogC<Props, Result>,
input: Props,
): Promise<Result> {
let { promise, resolver } = createResolvable<Result>();
let dialogData = observable({
component,
input,
promise,
resolver,
});
this.dialogs.push(dialogData);
let remove = action(() => {
let found = this.dialogs.indexOf(dialogData);
if (found >= 0) this.dialogs.splice(found, 1);
});
return dialogData.promise.then(
res => {
remove();
return res;
},
e => {
remove();
throw e;
},
);
}
/**
* @internal
*/
render() {
return (
<>
{this.dialogs.map(renderItem => (
<renderItem.component
key={renderItem.resolver}
{...renderItem.input}
_resolver={renderItem.resolver}
/>
))}
</>
);
}
}
/**
* Renders the dialog manager, which will either show the currently active dialog or nothing.
* Used typically at the bottom of the main app page.
*
* @example
* ```tsx
* <DialogManagerComponent />
* ```
*/
export let DialogManagerComponent = observer(() => (
<AppContext.Consumer>{ctx => ctx.app.getSingleton(DialogManager).render()}</AppContext.Consumer>
));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.