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

Rewrite ModalManager and state to Typescript #3007

Merged
merged 11 commits into from
Oct 30, 2021
4 changes: 2 additions & 2 deletions js/src/common/Component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,13 +138,13 @@ export default abstract class Component<Attrs extends ComponentAttrs = Component
if ('children' in attrs) {
throw new Error(
`[${
(this.constructor as any).name
(this.constructor as typeof Component).name
}] The "children" attribute of attrs should never be used. Either pass children in as the vnode children or rename the attribute`
);
}

if ('tag' in attrs) {
throw new Error(`[${(this.constructor as any).name}] You cannot use the "tag" attribute name with Mithril 2.`);
throw new Error(`[${(this.constructor as typeof Component).name}] You cannot use the "tag" attribute name with Mithril 2.`);
}
}

Expand Down
149 changes: 0 additions & 149 deletions js/src/common/components/Modal.js

This file was deleted.

171 changes: 171 additions & 0 deletions js/src/common/components/Modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import Component from '../Component';
import Alert, { AlertAttrs } from './Alert';
import Button from './Button';

import type Mithril from 'mithril';
import type ModalManagerState from '../states/ModalManagerState';
import type RequestError from '../utils/RequestError';
import type ModalManager from './ModalManager';
import fireDebugWarning from '../helpers/fireDebugWarning';

interface IInternalModalAttrs {
state: ModalManagerState;
animateShow: ModalManager['animateShow'];
animateHide: ModalManager['animateHide'];
}

/**
* The `Modal` component displays a modal dialog, wrapped in a form. Subclasses
* should implement the `className`, `title`, and `content` methods.
*/
export default abstract class Modal<ModalAttrs = {}> extends Component<ModalAttrs & IInternalModalAttrs> {
davwheat marked this conversation as resolved.
Show resolved Hide resolved
/**
* Determine whether or not the modal should be dismissible via an 'x' button.
*/
static readonly isDismissible = true;

protected loading: boolean = false;

/**
* Attributes for an alert component to show below the header.
*/
alertAttrs!: AlertAttrs;

oninit(vnode: Mithril.VnodeDOM<ModalAttrs & IInternalModalAttrs, this>) {
super.oninit(vnode);

// TODO: [Flarum 2.0] Remove the code below.
// This code prevents extensions which do not implement all abstract methods of this class from breaking
// the forum frontend. Without it, function calls would would error rather than returning `undefined.`

const missingMethods: string[] = [];

['className', 'title', 'content', 'onsubmit'].forEach((method) => {
if (!(this as any)[method]) {
(this as any)[method] = function (): void {};
missingMethods.push(method);
}
});

if (missingMethods.length > 0) {
fireDebugWarning(
`Modal \`${this.constructor.name}\` does not implement all abstract methods of the Modal super class. Missing methods: ${missingMethods.join(
', '
)}.`
);
}
}

oncreate(vnode: Mithril.VnodeDOM<ModalAttrs & IInternalModalAttrs, this>) {
super.oncreate(vnode);

this.attrs.animateShow(() => this.onready());
}

onbeforeremove(vnode: Mithril.VnodeDOM<ModalAttrs & IInternalModalAttrs, this>): Promise<void> | void {
super.onbeforeremove(vnode);

// If the global modal state currently contains a modal,
// we've just opened up a new one, and accordingly,
// we don't need to show a hide animation.
if (!this.attrs.state.modal) {
this.attrs.animateHide();
// Here, we ensure that the animation has time to complete.
// See https://mithril.js.org/lifecycle-methods.html#onbeforeremove
// Bootstrap's Modal.TRANSITION_DURATION is 300 ms.
return new Promise((resolve) => setTimeout(resolve, 300));
}
}

view() {
if (this.alertAttrs) {
this.alertAttrs.dismissible = false;
}

return (
<div className={'Modal modal-dialog ' + this.className()}>
<div className="Modal-content">
{(this.constructor as typeof Modal).isDismissible && (
<div className="Modal-close App-backControl">
{Button.component({
icon: 'fas fa-times',
onclick: this.hide.bind(this),
className: 'Button Button--icon Button--link',
})}
</div>
)}

<form onsubmit={this.onsubmit.bind(this)}>
<div className="Modal-header">
<h3 className="App-titleControl App-titleControl--text">{this.title()}</h3>
</div>

{this.alertAttrs ? <div className="Modal-alert">{Alert.component(this.alertAttrs)}</div> : ''}

{this.content()}
</form>
</div>
</div>
);
}

/**
* Get the class name to apply to the modal.
*/
abstract className(): string;
davwheat marked this conversation as resolved.
Show resolved Hide resolved

/**
* Get the title of the modal dialog.
*/
abstract title(): string;

/**
* Get the content of the modal.
*/
abstract content(): Mithril.Children;

/**
* Handle the modal form's submit event.
*/
abstract onsubmit(e: Event): void;

/**
* Callback executed when the modal is shown and ready to be interacted with.
*
* @remark Focuses the first input in the modal.
*/
onready(): void {
this.$().find('input, select, textarea').first().trigger('focus').trigger('select');
}

/**
* Hides the modal.
*/
hide() {
this.attrs.state.close();
}

/**
* Sets `loading` to false and triggers a redraw.
*/
loaded() {
this.loading = false;
m.redraw();
}

/**
* Shows an alert describing an error returned from the API, and gives focus to
* the first relevant field involved in the error.
*/
onerror(error: RequestError) {
this.alertAttrs = error.alert;

m.redraw();

if (error.status === 422 && error.response?.errors) {
this.$('form [name=' + (error.response.errors as any[])[0].source.pointer.replace('/data/attributes/', '') + ']').trigger('select');
} else {
this.onready();
}
}
}
Loading