Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Modal window with custom URL in AngularJS

Tags:

angularjs

I need to display a modal window (basically just a hidden div that will be loaded with a compiled template) in my Angular app. The problem is, I need the URL to change when the modal opens so users can copy the link and go directly to the modal window and also use the back button to close the modal window and return to the previous page. This is similar to the way Pinterest handles modal windows when you click on a pin.

So far I've created a directive that loads the template, compiles it using $compile, injects the $scope and then displays the compiled template. This works fine.

The problem is as soon as I use $location to change the path, the route controller fires and loads the template into ng-view.

I thought of 2 ways of overcoming this, but have not been able to implement either:

  1. Somehow prevent the route controller from firing when I change the url using $location. I've added a listener to $routeChangeStart to prevent the default from happening, but that does not seem to work.

  2. Somehow add another view handler to the page (basically have 2 named ng-view directives on the page) and have each able to handle different routes. Can't see that Angular supports this at the moment though.

The URL needs to be of the format /item/item_id and not /item?item_id=12345.

Any help would be appreciated.

like image 308
2 revs Avatar asked Dec 11 '12 02:12

2 revs


4 Answers

You can now do this if you use the ui.router module as described here: https://github.com/angular-ui/ui-router/wiki/Frequently-Asked-Questions#how-to-open-a-dialogmodal-at-a-certain-state.

Their example:

$stateProvider.state("items.add", {
    url: "/add",
    onEnter: function($stateParams, $state, $modal, $resource) {
        $modal.open({
            templateUrl: "items/add",
            resolve: {
              item: function() { new Item(123).get(); }
            },
            controller: ['$scope', 'item', function($scope, item) {
                $scope.dismiss = function() {
                    $scope.$dismiss();
                };

                $scope.save = function() {
                    item.update().then(function() {
                        $scope.$close(true);
                    });
                };
            }]
        }).result.then(function(result) {
            if (result) {
                return $state.transitionTo("items");
            }
        });
    }
});

I also pass an 'errorCallback' function to the then() function to handle modal dismissal by pressing Escape or clicking on the background.

like image 73
dwong Avatar answered Nov 17 '22 06:11

dwong


I had similar requirements. I needed:

  • Modal window to trigger a change to the route, so it has it's own hash url.
  • When you visit the modal url, the main screen loads then triggers the modal to popop.
  • The back button closes the modal (assuming you started on the home screen).
  • When you click OK or Cancel the modal is closed and the route changes back to the home screen.
  • When you click the background to close the modal, the route changes back to the home screen.

I couldn't find a good example of all of this working together, but I was able to find examples of bits and pieces, so I put them all together into a working example as follows:

<!DOCTYPE html>
<html lang="en" ng-app="app">

<head>
  <meta charset="UTF-8">
  <script src="//cdnjs.cloudflare.com/ajax/libs/angular.js/1.3.0/angular.js"></script>
  <script src="//cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.2.11/angular-ui-router.js"></script>
  <script src="//cdnjs.cloudflare.com/ajax/libs/angular-ui-bootstrap/0.11.2/ui-bootstrap-tpls.js"></script>
  <link href="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.1/css/bootstrap.css" rel="stylesheet">
  <script>

    // init the app with ui.router and ui.bootstrap modules
    var app = angular.module('app', ['ui.router', 'ui.bootstrap']);

    // make back button handle closing the modal
    app.run(['$rootScope', '$modalStack',
      function($rootScope, $modalStack) {
        $rootScope.$on('$stateChangeStart', function() {
          var top = $modalStack.getTop();
          if (top) {
            $modalStack.dismiss(top.key);
          }
        });
      }
    ]);

    // configure the stateProvider
    app.config(['$stateProvider', '$urlRouterProvider',
      function($stateProvider, $urlRouterProvider) {

        // default page to be "/" (home)
        $urlRouterProvider.otherwise('/');

        // configure the route states
        $stateProvider

          // define home route "/"
          .state('home', {
            url: '/'
          })

          // define modal route "/modal"
          .state('modal', {
            url: '/modal',

            // trigger the modal to open when this route is active
            onEnter: ['$stateParams', '$state', '$modal',
              function($stateParams, $state, $modal) {
                $modal

                  // handle modal open
                  .open({
                    template: '<div class="modal-header"><h3 class="modal-title">Modal</h3></div><div class="modal-body">The modal body...</div><div class="modal-footer"><button class="btn btn-primary" ng-click="ok()">OK</button><button class="btn btn-warning" ng-click="cancel()">Cancel</button></div>',
                    controller: ['$scope',
                      function($scope) {
                        // handle after clicking Cancel button
                        $scope.cancel = function() {
                          $scope.$dismiss();
                        };
                        // close modal after clicking OK button
                        $scope.ok = function() {
                          $scope.$close(true);
                        };
                      }
                    ]
                  })

                  // change route after modal result
                  .result.then(function() {
                    // change route after clicking OK button
                    $state.transitionTo('home');
                  }, function() {
                    // change route after clicking Cancel button or clicking background
                    $state.transitionTo('home');
                  });

              }
            ]

          });
      }
    ]);
  </script>
</head>

<body>

  <a href="#/modal" class="btn btn-default">POPUP!</a>

  <div ui-view></div>

</body>

</html>

You can view it in action at plunker: http://plnkr.co/edit/tLyfRP6sx9C9Vee7pOfC

Click the "Open the embedded view" link in plunker to see the hash tag working.

like image 25
cornernote Avatar answered Nov 17 '22 06:11

cornernote


hermanschutte's solution worked (see comment on Tiago Roldão's post). I'm posting step by step to help clarify.

First, in the config:

.when('/posts', {
  templateUrl: 'views/main.html',
  controller: 'MainCtrl',
  reloadOnSearch: false     // this allows us to use the back button to get out of modals.
})

Then, on opening modal (you'll need the location module):

$location.search('modal');

And in modal controller:

$scope.$on('$locationChangeSuccess', function() {
  $scope.close(); // call your close function
});
like image 5
Dane Macaulay Avatar answered Nov 17 '22 06:11

Dane Macaulay


The named view solution you mentioned has had great discussion on the angularJS mailing list, but as of now (and the near to mid-future) the $route system doesn't handle multiple views.

As I have said in a few places, and here in SO a few times as well, the $route system is one of the situations where angular's very opinionated stance can become a hinderance: it's meant to be simple, but it isn´t very configurable. I personally don't use it directly, but rather bypass it via a solution I found here.

It complicates things a little, but it gives you greater flexibility - the idea being that the $route only serves to trigger a (manual) render function, where you can assign the values and trigger $broadcasts as you wish (normally, from the main controller).

One thing I haven't tried is a "hybrid" solution - that is, using the $route system normally, and configuring your /item/item_id route to NOT have a view parameter (so as to not change the main view, initiated by the other routes) and do something via the $routeChangeStart event (remember you can assign any value to the routes, as you can see by the page I referenced)

Again, personally I prefer fully using the alternative method.

like image 2
Tiago Roldão Avatar answered Nov 17 '22 06:11

Tiago Roldão