Josh Hunt
Josh Hunt

Reputation: 14521

Sub-state with multiple parents / route for modal that opens over several states

I have a abstract profile state, which has multiple child states (for different tabs of the profile page), and then I want to have another child state of profile be a modal. I've implemented it something like this:

$stateProvider
    .state('profile', {
        url: '/profile/:profileID',
        templateUrl: 'profile.html',
        controller: 'ProfileCtrl',
        abstract:true,
        resolve: {
            user: ...
        }
    })
    .state('profile.circles', {
        url: '',
        templateUrl: 'profilecircles.html',
        controller: 'profilecirclesCtrl',
        resolve: { circle: ... }
    })
    .state('profile.squares', {
        url: '/collections',
        templateUrl: 'profilesquares.html',
        controller: 'profilesquaresCtrl',
        resolve: { squares: ... }
    })
    .state('profile.editprofile', {
        url: '/edit',
        onEnter: ['$window','$modal', function($window, $modal) {
            $modal.open({
                templateUrl: 'editprofile.html',
                controller: 'editProfileCtrl',
                resolve: {
                    user: ...
                }
            }).result.then(function() {
                $window.history.back();
            },function() {
                $window.history.back();
            });
        }]
    })

This works great, except for the fact that because editprofile is a sibling of squares and circles, when that state is active and the modal is in view, the squares or circle state is unloaded, and loaded back in again when the modal is closed.

Is there any way to have those states remain active when the profile.editprofile state is active? I'm after something like state..editprofile.

Upvotes: 8

Views: 3400

Answers (4)

theDmi
theDmi

Reputation: 18034

You can designate your tab states sticky: true by using ui-router-extras. Sticky states are preserved when navigating to a sibling and are especially useful for tabs.

To use it, you'll have to create one named view for each sticky state (you can hide inactive ones however, see below).

The API page describes the necessary steps:

  1. Mark a state with sticky: true
  2. Declare a named view on the state which will target a named ui-view in the parent state's template, e.g. views: { bar: { /* bar named-view definition */ } }
  3. Add the named ui-view to the parent state's template, e.g. <div ui-view="bar" />
  4. Optionally, hide the named ui-view when the state is not activated, e.g. <div ui-view="bar" ng-show="$state.includes("foo.bar") />

Upvotes: 0

m59
m59

Reputation: 43785

As there is currently no ideal solution, I have come up with a reasonably elegant solution. My code is generalized, easy to understand and commented so that it should be easy to adapt to any project.

See the comments in the code for the most significant points.

Live Demo (click).

Sample States

$stateProvider

// modal will open over this state from any substate
.state('foo', {
  abstract: true,
  url: '/:fooProp',
  templateUrl: 'foo.html',
  controller: 'FooController'
})
.state('foo.main', {
  url: '',
  templateUrl: 'foo-main.html',
  controller: 'FooMainController'
})
.state('foo.bar', {
  url: '/bars/:barId',
  templateUrl: 'foo-bar.html',
  controller: 'FooBarController'
})

// append /modal route to each `foo` substate
.state('foo.main.modal', getModalState())
.state('foo.bar.modal', getModalState())

;

function getModalState() {
  return {
    url: '/modal',
    onEnter: [
      '$modal',
      '$state',
      function($modal, $state) {
        $modal.open({
          templateUrl: 'foo-modal.html',
          controller: 'FooModalController'
        }).result.finally(function() {
          // go to parent state - the current `foo` substate
          $state.go('^');
        });
      }
    ]
  }
}

Controller

$scope.goToModalState = function() {
  // go to this state's `modal` state
  $state.go($state.current.name+'.modal');
};

View

<a ng-click="goToModalState()">Open Modal</a>

Upvotes: 3

Wawy
Wawy

Reputation: 6269

The point of ui-router is that the states nesting matches the url nesting. What that means is that in your case if you have:

profile.circles with a url: /profile/123
profiles.squares with a url: /profile/123/collections
profiles.edit with a url: /profile/123/edit

You can't expect to have profile.squares and profiles.edit active at the same time since they are two completely different urls. However It is possible to have:

profiles.circles.editprofile with a url: /profile/123/edit

That will keep profiles.circles active during your modal since it's a parent state. But in the case of profiles.squares, if you want to have a state representing editprofile you would need something like:

profiles.squares.editprofile with a url: /profile/123/collection/edit

Unfortunately there is no other way around it if you want to keep edit as a state.

Upvotes: 1

Ivar
Ivar

Reputation: 4911

Well unless you want ditch the modal implementation I would suggest then having 2 views in profile.html one for editprofile.html and other for profilecircles.html or profilesquares.html, something like following:

// profile.html
...
<div class="" ui-view="edit" autoscroll="false"></div>
<div class="" ui-view="content" autoscroll="false"></div>
...

// state config
$stateProvider
.state('profile', {
    url: '/profile/:profileID',
    templateUrl: 'profile.html',
    controller: 'ProfileCtrl',
    abstract:true,
    resolve: {
        user: ...
    }
})
.state('profile.circles', {
    url: '',
    views: {
        edit@: {
            templateUrl: 'editprofile.html',
            controller: 'editProfileCtrl'
        },
        content@: {
            templateUrl: 'profilecircles.html',
            controller: 'profilecirclesCtrl'
        }
    }
    resolve: { circle: ... }
})
.state('profile.squares', {
    url: '/collections',
    views: {
        edit@: {
            templateUrl: 'editprofile.html',
            controller: 'editProfileCtrl'
        },
        content@: {
            templateUrl: 'profilesquares.html',
            controller: 'profilesquaresCtrl'
        }
    }
    resolve: { squares: ... }
})

And then the hard part for actually showing/hiding the edit profile view. But that can be done with some toggle button or after action is validated in controller.

Upvotes: 2

Related Questions