Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular ui-router instantiates the child controller multiple times

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

like image 207
Misha Moroshko Avatar asked Jul 23 '14 02:07

Misha Moroshko


1 Answers

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

like image 62
Radim Köhler Avatar answered Nov 15 '22 22:11

Radim Köhler