ui router - nested views with shared controller
Asked Answered
B

4

32

I have an abstract parent view that is meant to share a controller with its nested views.

.state('edit', {
    abstract: true,
    url: '/home/edit/:id',
    templateUrl: 'app/templates/editView.html',
    controller: 'editController'
})
.state('edit.details', {
    url: '/details',
    templateUrl: 'app/templates/editDetailsView.html'
})
.state('edit.info', {
    url: '/info',
    templateUrl: 'app/templates/editInfoView.html'
})

The routing works as expected.

The problem is that when I update a $scope variable from one of the nested views, the change is not reflected in the view. When I do the same from the parent view, it works fine. This is not situation that requires an $apply.

My guess is that a new instance of editController is being created for each view, but I'm not sure why or how to fix it.

Boneyard answered 4/1, 2015 at 17:4 Comment(0)
A
41

The issue here would be related to this Q & A: How do I share $scope data between states in angularjs ui-router?.

The way how to solve it is hidden in the:

Understanding Scopes

In AngularJS, a child scope normally prototypically inherits from its parent scope.
...

Having a '.' in your models will ensure that prototypal inheritance is in play.

// So, use
<input type="text" ng-model="someObj.prop1"> 
// rather than
<input type="text" ng-model="prop1">.

And also this

Scope Inheritance by View Hierarchy Only

Keep in mind that scope properties only inherit down the state chain if the views of your states are nested. Inheritance of scope properties has nothing to do with the nesting of your states and everything to do with the nesting of your views (templates).

It is entirely possible that you have nested states whose templates populate ui-views at various non-nested locations within your site. In this scenario you cannot expect to access the scope variables of parent state views within the views of children states.

Having that we should do this in edit Controller

controller('editController', function ($scope) {
  $scope.Model = $scope.Model || {SomeProperty : "xxx"};
})

And we can even reuse that controller: 'editController' (we can do not have to, because the $scope.Model will be there - thanks to inheritance)

.state('edit', {
    abstract: true,
    url: '/home/edit/:id',
    templateUrl: 'app/templates/editView.html',
    controller: 'editController'
})
.state('edit.details', {
    url: '/details',
    templateUrl: 'app/templates/editDetailsView.html',
    controller: 'editController'
})
.state('edit.info', {
    url: '/info',
    templateUrl: 'app/templates/editInfoView.html',
    controller: 'editController'
})

Now, the same controller will be instantiated many times (parent all the children) but the $scope.Model will be initiated only once (inside of parent) and available everywhere

Check this similar working example here

Aerography answered 4/1, 2015 at 17:10 Comment(3)
How does one go about this issue in a Rails app? I have a nested controller with home, and organizations. When I apply a style to the organizations view on its own everything works fine, but a different style applied to the nested organization view is over ridden by the un-nested view.Orthorhombic
Is it possible to do this when using the controllerAs pattern giving the child state it's own controller?Whoredom
@Whoredom - hope this will give you the answerTad
A
14

Based on a comment by PilotBob

Is it possible to do this when using the controllerAs pattern giving the child state it's own controller?

I decided to append another solution, using controllerAs while keeping the above/original concept

There is a working plunker

The states would be now having different controllers and parent state will name it "parentCtrl" (to be NOT overwritten in a child scope with child controller)

 .state("main", {
      controller:'mainController',
      controllerAs: "parentCtrl",
      ...
  .state("main.1", {
      parent: 'main',
      controller:'child1Controller',
      controllerAs: "ctrl",
      ...
  .state("main.2", {
      parent: 'main',
      controller:'child2Controller',
      controllerAs: "ctrl", 
      ... 

And these are controllers:

.controller('mainController', function ($scope) {
    this.Model =  {Name : "yyy"};
})
.controller('child1Controller', function ($scope) {
    $scope.Model = $scope.parentCtrl.Model;
})
.controller('child2Controller', function ($scope) {
    $scope.Model = $scope.parentCtrl.Model; 
})

Check it in action here

Aerography answered 23/6, 2015 at 5:45 Comment(2)
thanks, that's pretty much what I did, injecting scope into the child controller to access the parent model. I thought it would be simpler like using vm.$parent.model in the view, but that didn't work.Whoredom
Great answer! I do think it's a bit confusing to still have the '$scope` parameter in the controller function, as the idea of using controller-as syntax is to use this instead of $scope. Of course as far as I understand you WILL still need to inject $scope when you want use watchers in your controller (e.g. $scope.$watch('propName', function...).Primal
F
1

Another alternative using resolve

.state('edit', {
    abstract: true,
    url: '/home/edit/:id',
    templateUrl: 'app/templates/editView.html',
    controller: 'editController',
    resolve: {
        baseData: function() {
            return {};
        }
    }
})
.state('edit.details', {
    url: '/details',
    templateUrl: 'app/templates/editDetailsView.html',
    controller: 'editController'
})
.state('edit.info', {
    url: '/info',
    templateUrl: 'app/templates/editInfoView.html',
    controller: 'editController'
})


.controller('editController', function (baseData) {
    baseData.foo = baseData.foo || 'bar';
});
Fechter answered 22/9, 2015 at 7:36 Comment(0)
S
0

In the child controller you can do:

angular.extend($scope, $scope.$parent)

In case the controller is used with alias e.g. 'vm' you can do:

let vm = angular.extend(this, $scope.$parent.vm);
Selfdrive answered 4/4, 2018 at 16:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.