Migrating AngularJS UI Bootstrap Modals to Angular 7

Posted on April 21, 2019

I’ve recently been involved in upgrading a AngularJS + UI-Bootstrap + UI Router project to Angular 7 + ngx-bootstrap + UI Router.

The reason for the upgrade was that the support for a lot of the other components was fading, and the availability of new components was far better for Angular 7 than AngularJS. Having said that, AngularJS is still going strong for better or worse.

The approach was to use the Angular Upgrade module which allowed a component-by-component upgrade, and basically seamless interop between the two versions. This was helped a lot by the use of UI Router which navigates to both AngularJS and Angular 7 container components.

The main issue we hit was that when nested modals were used, they both needed to come from the same modal library if they were to display correctly. If you tried to display a UI Bootstrap modal on top of a nxg-bootstrap modal it would disappear behind.

The return semantics is different between the libraries, with UI Bootstrap returning a promise, and ngx-bootstrap returning component reference.

The most straight forward approach we could see was to simply adapt the UI Boostrap modals into ngx-bootstrap modals, providing translations for the call semantics also.

Start by setting up a wrapper method that provides the environment for UI Bootstrap

/**
 * This is the return value from an ui-bootstrap modal
 */
export interface ModalReturnValue<T> {
    $value: T;
}

export function wrapModal<T>(modalService: BsModalService, component: any, _class: string, resolveData: any): Promise<T> {
    return new Promise<any>((resolve, reject) => {
        let modalInstance: { modal: BsModalRef } = { modal: null };
        const close = (result: T) => {
            modalInstance.modal.hide();
            resolve(result);
        };
        const dismiss = (error: any) => {
            modalInstance.modal.hide();
            reject(error);
        };
        try {
            modalInstance.modal = modalService.show(component,
                { ...MODAL_OPTIONS, class: _class, initialState: { resolve: resolveData, close, dismiss } });
        } catch (e) {
            console.error(e);
            dismiss(e);
        }

    });
}

The interface ModalReturnValue models the return type of close(...) and dismiss(...).

The method wrapModal takes an upgraded UI Bootstrap modal, and provides it with the environment it requires, including a resolve object, and close and dismiss functions. The function itself returns a Promise so it can be called be used without modification from the original call sites.

The modal controller itself needs to be upgraded

@Directive({
    selector: 'example-modal-directive',
})
export class ExampleModalDirective extends UpgradeComponent {
    @Input() resolve: {
        dataForModal: any
    };
    @Output() close: EventEmitter<ModalReturnValue<any>>;
    @Output() dismiss: EventEmitter<void>;

    constructor(elementRef: ElementRef, injector: Injector) {
        super('exampleModal', elementRef, injector);
    }
}

And then wrapped in an Angular 7 component.

@Component({
    selector: 'example-modal',
    template: `<example-modal-directive [resolve]="resolve" (close)="close($event.$value)" (dismiss)="dismiss()"></example-modal-directive>`
})
export class ExampleModal {
    @Input() resolve: {
        dataForModal: any
    };
    @Output() close: EventEmitter<any>;
    @Output() dismiss: EventEmitter<void>;

}

Ideally these two steps should be just one, but I’ve not managed to make everything work as expected.

An example call site would now look like:

wrapDialogue(this.modalService, ExampleModal, 'modal-lg',
    { dataForModal })
    .then(result => this.handleSuccess(result), error => this.handleCancel(error))

where this.modalService is BsModalService. All of our modals are made available by a service, so the above code would live in that service, meaning that the original call site code would actually not changes.

If there is any interest I can put together an example project, but I think the above will help.