andzep andzep - 17 days ago 5
AngularJS Question

How to open an existing controller/view in an ui-modal in AngularJS 1.x with $routeProvider

I know this is possible with the ui-router and also that in angular 2 this isn't an issue anymore. But for our actual project, we're stuck for the moment with angular 1.x and $routeProvider.

So what I want is to open an existing component (eg. a login form), which already "lives" in it's own page, alternatively on a modal dialog ($uibModal) without having to duplicate or modify it's controller or template.

The easiest example would be a login form, having it's own route e.g. accessible through

myapp/#/login
and now I also want to show it on the top of the current "page" as a modal without having to modify the controller/template etc. When the modal is gone, I want the page underneath to be on the same state as I left it.

This solution should work for any view, not just the login form (as an example). So I'm looking for a generic solution.

Any ideas?

Answer

After some trying, I finally got what I was looking for:

First I created a service, which takes care of opening the generic modal. I called it genericModalSvc It's worth to mention, that I'm using a service named namedRouteSvc to get routes per name and other cool stuff. The route-name is defined as a custom parameter on the route like this:

$routeProvider
    .when('/something', {
        name: 'something', // this is a custom parameter to retrieve the route
        templateUrl: 'something.html',
        controller: 'SomethingCtrl',
        controllerAs: 'ctrl'
    })

The above is not necessary for the genericModalSvc to work, but it makes things easier to understand on the code below:

angular
    .module('my_app.common_stuff')
    .service('GenericModalSvc', GenericModalSvc);

GenericModalSvc.$inject = ['$uibModal', '$rootScope', 'namedRouteSvc'];
function GenericModalSvc($uibModal, $rootScope, namedRouteSvc) {
    this.$uibModal = $uibModal;
    this.$rootScope = $rootScope;
    this.namedRouteSvc = namedRouteSvc;

    this.modalInstance = null;

    // Broadcasts for close signal from modal's close button
    this.$rootScope.$on('GenericModalCloseClicked', this.close.bind(this));
}


GenericModalSvc.prototype.close = function() {
    this.modalInstance.close();
    // Emit a signal here, for other listeners to know the modal has been
    // closed.
    this.$rootScope.$emit('GENERIC_MODAL_CLOSED');
};

GenericModalSvc.prototype.open = function(params) {
    // The params argument here, has route information and parameters I
    // want to transfer to the model
    var route_name = params.route_name, // A string defined in each route
        args = params.args,       // The route parameters
        query = params.query,     // The route query arguments
        options = params.options; // Other options

    this.$rootScope.$emit('GENERIC_MODAL_OPENED');
    var route = this.namedRouteService.getRoute(route_name, args, query);
    route = route.hasOwnProperty('route') ? route.route : null;

    if (!route) {
        throw new Error('Could not find a route for the given parameters');
    }

    var modal_title = params.options.modal_title;

    // The following is the fun part. Now we can use the route to get the
    // controller and controllerAs attributes to define our modal here.
    this.modalInstance = this.$uibModal.open({
        // This template is the modal wrapper for our 'real' template.
        templateUrl: 'partials/generic-modal.html',
        controller: route.controller,
        controllerAs: route.controllerAs,
        backdrop: 'static',
        size: 'lg',
        resolve: {
            show_modal_close: function() {
                return Boolean(options && options.showCloseButton);
            },
            modal_title: function() {
                return modal_title;
            },
            template_to_render: function() {
                // We need to pass this information to our generic-modal.html
                // template to include it there.
                return route.templateUrl;
            },
            dummy: function($routeParams) {
                // This is a rather kind of q&d way of passing the route 
                // parameters and query arguments. On the controller, we
                // must look inside $routeParams for 'modalParameters' first 
                // (because we don't know there if we're inside a modal or not)
                // and if 'modalParameters' doesn't exist, then we retrieve the 
                // normal-ones inside $routeParams. I know, it's not perfect
                // but didn't come up with a better idea.
                $routeParams.modalParameters = {
                    routeParams: args,
                    queryParams: query
                };
            }
        }
    });
};

Then the generic-modal.html template is very simple:

<p id="generic-modal-header">
    <button type="button" class="close" data-dismiss="modal" aria-label="Close"
        ng-click="$emit('GenericModalCloseClicked')">
        <span aria-hidden="true">X</span></button></p>

<div id="generic-modal-container">
    <div ng-include="$resolve.template_to_render"></div>
</div>

And finally here is an example of how to use it:

angular
    .module('my_app.foo')
    .controller('FooCtrl', FooCtrl);
FooCtrl.$inject = ['...','genericModalSvc'];
function FooCtrl(..., genericModalSvc) {
    this.genericModalSvc = genericModalSvc;
}

FooCtrl.prototype.openBarDetailsOnModal = function() {
    var params = {
        route_name: 'bar-detail',
        args: {'id': this.some_id_for_bar},
        query: {},
        options: {}
    };
    this.genericModalSvc.open(params);
};

That's it!!!