3

I want to load a state as a modal so that I can overlay a state without effecting any other states in my application. So for example if I have a link like:

<a ui-sref="notes.add" modal>Add Note</a>

I want to then interrupt the state change using a directive:

.directive('modal', ['$rootScope', '$state', '$http', '$compile',
    function($rootScope, $state, $http, $compile){
        return {
            priority: 0,
            restrict: 'A',
            link: function(scope, el, attrs) {  
                $rootScope.$on('$stateChangeStart', function (event, toState, toParams) {
                    event.preventDefault();
                }); 
                el.click(function(e){
                    $http
                    .get('URL HERE')
                    .then(function(resp){
                        $('<div class="modal">' + resp.data + '</div>').appendTo('[ui-view=app]');
                        setTimeout(function(){
                            $('.wrapper').addClass('showModal');
                        },1);
                    });
                }); 
            }   
        }
    }
])

This successfully prevents the state change and loads the URL and appends it as a modal to the application. The problem is that it loads the entire application again...

How can I load just the state? e.g. the template files and the adjoining controller.

The state looks like:

.state('notes.add',
{
    parent: 'notes',
    url: '/add',
    views: {
        'content': {
            templateUrl: 'partials/notes/add.html',
            controller: 'NotesAddCtrl'
        }
    }
})

An example of how it should work using jQuery: http://dev.driz.co.uk/AngularModal

See how I can access StateA and StateB loading via AJAX that uses the History API to change the URL to reflect the current state change.

And regardless of whether I am on the index, StateA or StateB I can load StateA or StateB as a modal (even if I'm on that State already) and it doesn't change the url or the current content, it just overlays the state content.

This is what I want to be able to do in AngularJS.

Note. this example doesn't work with the browser back and forward buttons due to it being a quick example and not using the history api correctly.

Cameron
  • 27,963
  • 100
  • 281
  • 483
  • Looks like you're navigating to a different URL, so what state are you trying to load exactly? – Omri Aharon Apr 07 '15 at 08:54
  • Yes, the notes.add state. Which can be accessed in the browser via the URL or accessed via JS in a modal (without changing the URL (this all happens, but it loads the whole app again). – Cameron Apr 07 '15 at 09:12
  • Have you tried the solution suggested here ? http://stackoverflow.com/questions/21883559/opening-a-modal-in-a-route-in-angularjs-with-angular-ui-bootstrap – Omri Aharon Apr 07 '15 at 09:19
  • So what do I need to prevent it loading the whole app again? I'm not seeing the solution to prevent that mentioned in the answers. Thanks. – Cameron Apr 07 '15 at 09:31
  • Look at this fiddle: http://jsfiddle.net/7tzXh/27/ You might want to structure your code to match that, and it achieves what you're looking for – Omri Aharon Apr 07 '15 at 09:37
  • @OmriAharon The modal should still have a URL so if a user types in the url or opens link in new tab it loads it in the content page, but the JS directive can prevent default if called on click and then load the view in a modal on the page (WITHOUT changing the url this time). But it causes the app to be loaded all over again. – Cameron Apr 07 '15 at 10:15
  • I see.. not sure how (and if) it can be avoided.. – Omri Aharon Apr 07 '15 at 10:29
  • @Matho I think so yeah. The modal could do a variety of things, but it is usually Add or Edit. So for the example code above it was for adding a note, so would contain a simple form for adding a note. – Cameron Apr 08 '15 at 09:42
  • @Matho but what the modal does isn't the issue. The problem is being able to load in a state as an overlay without affecting the current state. So as though you have done an AJAX request for a page in a server app and appended the content to the DOM and not navigated elsewhere. – Cameron Apr 08 '15 at 13:23
  • I *think* we might have got our wires crossed, but if you can provide an example (plunkr, fiddle, etc.) then I can check it out and see if it's a possible solution. Thanks. – Cameron Apr 08 '15 at 14:57
  • State A would be Notes (a list of notes) and State A.B would the the Add Notes - Which can be accessed on its own or as an overlay of **ANY** state. So I could show State A.B as an overlay of State C even though they have no parent/sibling connection, *because it's an overlay!* – Cameron Apr 08 '15 at 15:04
  • hope this link helps: https://github.com/angular-ui/ui-router/wiki/Frequently-Asked-Questions#how-to-open-a-dialogmodal-at-a-certain-state – Khanh TO Apr 11 '15 at 10:21
  • it's overlay, but it's not a state. You can create a directive that opens modal, and you can pass a controller and templateUrl as parameter. – allenhwkim Apr 12 '15 at 04:08
  • @allenhwkim can you show an example? – Cameron Apr 12 '15 at 08:27

3 Answers3

2

I've seen your question a few days ago and it seemed interesting enough to try and set up something that would work.

I've taken the uiSref directive as a start, and modified the code to use angular-bootstrap's $modal to show the desired state.

angular.module('ui.router.modal', ['ui.router', 'ui.bootstrap'])
.directive('uiSrefModal', $StateRefModalDirective);

function parseStateRef(ref, current) {
  var preparsed = ref.match(/^\s*({[^}]*})\s*$/), parsed;
  if (preparsed) ref = current + '(' + preparsed[1] + ')';
  parsed = ref.replace(/\n/g, " ").match(/^([^(]+?)\s*(\((.*)\))?$/);
  if (!parsed || parsed.length !== 4) throw new Error("Invalid state ref '" + ref + "'");
  return { state: parsed[1], paramExpr: parsed[3] || null };
}

function stateContext(el) {
  var stateData = el.parent().inheritedData('$uiView');

  if (stateData && stateData.state && stateData.state.name) {
    return stateData.state;
  }
}

$StateRefModalDirective.$inject = ['$state', '$timeout', '$modal'];

function $StateRefModalDirective($state, $timeout, $modal) {
  var allowedOptions = ['location', 'inherit', 'reload'];

  return {
    restrict: 'A',
    link: function(scope, element, attrs) {
      var ref = parseStateRef(attrs.uiSrefModal, $state.current.name);
      var params = null, url = null, base = stateContext(element) || $state.$current;
      var newHref = null, isAnchor = element.prop("tagName") === "A";
      var isForm = element[0].nodeName === "FORM";
      var attr = isForm ? "action" : "href", nav = true;

      var options = { relative: base, inherit: true };
      var optionsOverride = scope.$eval(attrs.uiSrefModalOpts) || {};

      angular.forEach(allowedOptions, function(option) {
        if (option in optionsOverride) {
          options[option] = optionsOverride[option];
        }
      });

      var update = function(newVal) {
        if (newVal) params = angular.copy(newVal);
        if (!nav) return;

        newHref = $state.href(ref.state, params, options);

        if (newHref === null) {
          nav = false;
          return false;
        }
        attrs.$set(attr, newHref);
      };

      if (ref.paramExpr) {
        scope.$watch(ref.paramExpr, function(newVal, oldVal) {
          if (newVal !== params) update(newVal);
        }, true);
        params = angular.copy(scope.$eval(ref.paramExpr));
      }
      update();

      if (isForm) return;

      element.bind("click", function(e) {
        var button = e.which || e.button;
        if ( !(button > 1 || e.ctrlKey || e.metaKey || e.shiftKey || element.attr('target')) ) {
          e.preventDefault();

          var state = $state.get(ref.state);
          var modalInstance = $modal.open({
            template: '<div>\
              <div class="modal-header">\
                <h3 class="modal-title">' + ref.state + '</h3>\
              </div>\
              <div class="modal-body">\
                <ng-include src="\'' + state.templateUrl + '\'"></ng-include>\
              </div>\
            </div>',
            controller: state.controller,
            resolve: options.resolve
          });

          modalInstance.result.then(function (selectedItem) {
            $scope.selected = selectedItem;
          }, function () {
            console.log('Modal dismissed at: ' + new Date());
          });
        }
      });
    }
  };
}

You can use it like this <a ui-sref-modal="notes.add">Add Note</a>

Directive requires the angular-bootstrap to resolve the modal dialog. You will need to require the ui.router.modal module in your app.

Vinko Bradvica
  • 466
  • 3
  • 9
  • If you want to add the histoyAPI entry, you should do it after opening the modal. – Vinko Bradvica Apr 12 '15 at 21:11
  • This looks good. What about controllers though? As usually I link the state to a controller in the `$stateProvider`... – Cameron Apr 13 '15 at 11:49
  • you can pass a controller to the modal instance, I think it should be available on the `options` object `var modalInstance = $modal.open({ template: ..., controller: options.controller, resolve: options.resolve });` – Vinko Bradvica Apr 13 '15 at 11:54
  • sorry, just checked, controller is available on state object, i'll edit the code to show the change – Vinko Bradvica Apr 13 '15 at 11:59
  • How easy would it be to use this WITHOUT the angular-bootstrap modal? As it looks like it does more than just show an overlay, but actually handle all the controller and other stuff. Or is this not the case? – Cameron Apr 13 '15 at 13:33
  • The advantage of this approach is that the $modal service can handle the `resolve`, and `controller` aspects of your state. If you were to use the jQuery modal version, it would just show the modal with your template. It would not bind the controller unless you specify it with `ngController` directive. Also, it wouldn't run the `resolve` param unless you handle it manually. – Vinko Bradvica Apr 13 '15 at 13:36
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/75119/discussion-between-vinko-bradvica-and-cameron). – Vinko Bradvica Apr 13 '15 at 13:38
  • I've not had chance to test this yet so can't accept an answer until I know it's the solution. But I'm going to award you the bounty before the grace period ends based on your contributions. – Cameron Apr 14 '15 at 12:04
1

Since asked to provide an example for my comment,

Example Directive

  myapp.directive('openModal', function ($modal) {
    return function(scope, element, attrs) {
      element[0].addEventListener('click', function() {
        $modal.open({
          templateUrl : attrs.openModal,
          controller: attrs.controller,
          size: attrs.openModalSize,
          //scope: angular.element(element[0]).scope()
        });
      });
    };
  });

Example Html

<button 
  open-modal='views/poc/open-modal/small-modal.html'
  open-modal-size='sm' 
  controller="MyCtrl">modal small</button>

The above directive approach is not very different from using a state, which has templateUrl and controller except that url does not change.

.state('state1.list', {
  url: "/list",
  templateUrl: "partials/state1.list.html",
  controller: function($scope) {
    $scope.items = ["A", "List", "Of", "Items"];
  }
})
allenhwkim
  • 27,270
  • 18
  • 89
  • 122
-1

Apparently there is the issue Ui-sref not generating hash in URL (Angular 1.3.0-rc.3) refering to https://github.com/angular-ui/ui-router/issues/1397

It is seems to be fixed as per comments.

I personally dislike html5mode because it requires extra work on your server for no apparent advantage (I don't regard having "more beautiful url" as tangible advantage to justify the extra work).

There is another performance problem when using routers, that the view DOM is re-created upon each route change. I mentioned a very simple solution in this answer.


As a side remark, the example in http://dev.driz.co.uk/AngularModal/ does not behave quite well. It does not record history, so I can't go back. Further, if you click on links like Index or modals, and then reload, you don't get the same page.


UPDATE. It seems from the comments that a route change is not wanted when opening the modal. In that case the easiest solution is not to put ui-sref on the opening button and let the modal directive along handle it.

Community
  • 1
  • 1
Dmitri Zaitsev
  • 13,548
  • 11
  • 76
  • 110
  • I stated in the question that the example was just a quick and dirty example to explain the concept. And modals shouldn't appear on reload by design. – Cameron Apr 12 '15 at 16:17
  • I could not see any statement the example was "dirty" nor that feature about modals in your question. Does this solve your problem? – Dmitri Zaitsev Apr 12 '15 at 16:35
  • See the note at the end of my question about it not using the history api correctly. In any case... I'm not getting your answer? Can you show a code example for how to load a state into an overlay? – Cameron Apr 12 '15 at 16:38
  • Have you checked the links in my answer? Your code looks fine. The main question is - do you see the hash in your url? – Dmitri Zaitsev Apr 12 '15 at 16:51
  • Yes for the other links. But I shouldn't see the hash for the modals as we're not changing state. – Cameron Apr 12 '15 at 16:52
  • If you don't change state for modals, I don't understand the reason to use `ui-ref` inside the same HTML element. This was your remark which seems to say the opposite: "The modal should still have a URL..." – Dmitri Zaitsev Apr 12 '15 at 16:56