9

SWEET DEMO

I have a list of profiles, where each profile can be either in a summary or in a detailed view. Only one profile can have a detailed view at any time.

profiles.html

<div ng-repeat="profile in profiles">
  <div ui-view="profile-summary"></div>
  <div ui-view="profile-details"></div>
</div>

profile-summary.html

<div ng-if="$state.params.profileId !== profile.id">
  {{ profile.name }}
  <button ng-click="showProfileDetails(profile.id)">More</button>
</div>

profile-summary-controller.js

$scope.showProfileDetails = function(profileId) {
  $state.go('profiles.profile', { profileId: profileId });
};

profile-details .html

<div ng-if="$state.params.profileId === profile.id">
  Detailed profile of {{ profile.name }}
  <button ng-click="hideProfileDetails()">Less</button>
</div>

profile-details-controller.js

$scope.hideProfileDetails = function() {
  $state.go('profiles.profile', { profileId: null });
};

ui-router configuration

$stateProvider
  .state('profiles', {
    url: '/profiles?keywords',
    views: {
      'profiles': {
        templateUrl: 'profiles.html',
        controller: 'ProfilesCtrl'
      },
      'profile-summary@profiles': {
        templateUrl: 'profile-summary.html',
        controller: 'ProfileSummaryCtrl'
      }
    }
  })
  .state('profiles.profile', {
    url: '/:profileId',
    views: {
      'profile-details': {
        templateUrl: 'profile-details.html',
        controller: 'ProfileDetailsCtrl'
      }
    }
  });

Questions I have:

  • When the More button is clicked, ProfileDetailsCtrl is instantiated 3 times. How could I instantiate it only for profile that is extended?
  • Am I utilizing the ui-router flexibility properly, or there is a better way to implement this? (Note: When profile is expanded, it should be reflected in the URL (to make it bookmarkable))

PLAYGROUND HERE

Misha Moroshko
  • 166,356
  • 226
  • 505
  • 746

1 Answers1

6

Extend with detail Replace

After discussion in comments below, there is a different solution for the basic (required) concept:

Inside a list - How to replace a row (on-click), with more details - using ui-router?

I.e. let's start with this:

  • More 1 basic info1
  • More 2 basic info2 // click more here
  • More 3 basic info2

getting this when More 2 is clicked (ang getting back if Less 2 is clicked next)

  • More 1 basic info1
  • Less 2 very detailed information loaded with ui-router state machine...
  • More 3 basic info2

This would be the solution:

1) The list view template, would have ng-if, checking if there is some detail info, or not:

<div ng-repeat="profile in profiles">

  <div ng-if="!profile.detail">
    <button ui-sref=".profile({profileId:profile.id})">More</button>
    {{ profile.name }}
  </div>

  <div ng-if="profile.detail">        
    <button ui-sref=".">Less</button>
    {{ profile.detail | json }}
  </div>

</div>

A few fancy parts to mention: instead of some ng-click we just do use the built in ui-sref with a relative path def ui-sref=".profile({profileId:profile.id})" - That will call the child state profile.

Once child is loaded, we can just get back by re-calling the parent ui-sref="." (wow...)

2) Our detail state will be doing two things

  1. load the detail by ID // GetById called on a Server
  2. cleanup on leave // Restoring the list as it was

    // find a profile from parent collection var profile = _.find($scope.profiles, {id : $stateParams.profileId});

    $http .get("detail.json") // getById .then(function(response){

      // would contain just a detail for id... here we filter
      var detail = _.find(response.data, {id : $stateParams.profileId});
    
      // assign the detail
      profile.detail = detail;
    
    });
    

    // cleanup - remove the details var cleanup = function(){ delete profile.detail; } $scope.$on("$destroy", cleanup);

A few things to mention: we hook on the $scope event "destroy". This will be fired, once we go back to the parent. And that's the place where we clean all the foot prints made during the ui-router detail state round-trip...

3) the detail view

There is NONE. None, becuase we do not need a template. Well in fact we still need the view anchor, where is the detail state placed... and the DetailController is called!

  .state('profiles.profile', {
    url: '/:profileId',
    views: {
      'profile-details': {   // NO template
        controller: 'ProfileDetailsCtrl'
      }
    }
  });

so there must be the view anchor somewhere in the parent:

<div ui-view="profile-details"></div>

Working code example:

Take a look a that solution here... it should be clear

(below is the original part of the answer why multiple times fired controller)

Original part of the answer

The controller is instantiated as many times, as many times is its view injected into the page. And you do inject the view 3 times.

Here is the source of the profiles

$scope.profiles = [
    { id: '100', name: 'Misko Hevery' },
    { id: '101', name: 'Igor Minar' },
    { id: '102', name: 'Vojta Jina' },
];

Here we do create anchors/targets with the same ui-view name:

<div ng-repeat="profile in profiles">   // for each profile in our array
  <div ui-view="profile-summary"></div>
  <div ui-view="profile-details"></div> // we inject this view named profile-details
</div>

And finally, we ask to inject our view into these (3) parent/child view-targets:

.state('profiles.profile', {
    url: '/:profileId',
    views: {
      'profile-details': {                 // the parent view-target is there 3 times
        templateUrl: 'profile-details.html',
        controller: 'ProfileDetailsCtrl'
      }
    }
  });

Solution: this should not happen. We should not use one ui-view="viewName" moret than once. It is working. but it is not what we can correctly manage... simply move the targets from repeater...

EXTEND here I updated the plunker, I made the profiles.html like this

// NO repeater here
<div ui-view="profile-summary"></div>
<div ui-view="profile-details"></div>

And I do iterate inside fo the summary:

<div ng-repeat="profile in profiles">
  {{ profile.name }}
  <button ng-click="showProfileDetails(profile.id)">More</button>
</div>

So now, each ui-view is there only once... see that in action here

Radim Köhler
  • 122,561
  • 47
  • 239
  • 335
  • I can't see how can I move the summary and the details from the repeater. Could you update my example to demonstrate that? Thanks a lot for your support!! – Misha Moroshko Jul 23 '14 at 05:17
  • 1
    Here is a small update http://plnkr.co/edit/FInnnwoh5oHuN6wYGKhK?p=preview, which is similar to *may way*, my thinking *(and that does mean what it means ;)* ... so I respect if you do not like it... ;) – Radim Köhler Jul 23 '14 at 05:26
  • When I click "More" in your updated example, nothing happens (I don't see the detailed view). – Misha Moroshko Jul 23 '14 at 11:52
  • Sorry, I missed that part.. updated version should work as expected... *(really mis it sorry;)* http://plnkr.co/edit/FInnnwoh5oHuN6wYGKhK?p=preview – Radim Köhler Jul 23 '14 at 12:02
  • You changed the behaviour! The required behaviour is that the detailed view **will replace** the summary view! – Misha Moroshko Jul 23 '14 at 12:52
  • Well, either you have targeted your states as defined in `app.js` or not. You are **NOT replacing** stuff. NO.. you are using **`ng-if`** to hide one or other view!!!. Which is not correct. Either you want to replace the content of one view or not. If yes.. then you have to change your `app.js` the way that 'profile-details' will be replaced with 'profile-summary' see http://plnkr.co/edit/vMJdYnO250beiaKWcetG?p=preview. That's how the replacing with ui-router works. But in fact.. I still can add **`ng-if`** to have the same hacking as you had... – Radim Köhler Jul 23 '14 at 13:01
  • I hate to leave any job unfinished... cannot help my self. I am sure that this update is what you wanted... and will love ;) 100%... That's it for now. Enjoy `ui-router` – Radim Köhler Jul 26 '14 at 04:30
  • Radim: First of all, thanks a lot for all your efforts! I'm, impressed by the amount of time you dedicated to solve this problem! StackOverflow is great because of people like YOU!! Regarding your solution: I feel like you are cheating a bit ;) The requirement is that profile details view will be taken from `profile-details.html`, which you just ignore as fas as I can see... – Misha Moroshko Jul 27 '14 at 12:56
  • My thinking went this way: 1) We do have a state represented by list view *(profiles)* 2) we would like to use power of `ui-router` infrastructure - **a state machine** 3) Let's profit from url change and use the state change to go for detailed data 4) because the child scope inherits from parent... we can directly manipulate with the profiles collection 5) effectively we do not need that view... that was my thinking... and the result is approach, which I'd like to use more and more... good if that helped anyhow ... Other words, I would say: I used the power of ui-router without cheeting ;) ;) – Radim Köhler Jul 27 '14 at 13:00