Last active
June 22, 2019 19:05
-
-
Save spion/d73cf5db9c78c8b05afbd9422a987c3f to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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