Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

angularjs inheriting scope in nested directives

Tags:

angularjs

example in: http://jsfiddle.net/avowkind/PS8UT/

I want a nested child directive to get its data from its wrapping parent directive if present, otherwise from the outer controller.

<div ng-controller="MyCtrl">
    <parent index="1">
        <child></child>
    </parent>
    <parent index="2">
        <child></child>
    </parent>
     <h1>No Parent</h1>
    <child></child>
</div>
<hr>

Desired output

Parent 1
  Child of parent 1
Parent 2
  Child of parent 2
No Parent
  Child of parent 0

Currently my child object only sees the outer controller value:

Actual output

Parent 1
  Child of parent 0
Parent 2
  Child of parent 0
No Parent
  Child of parent 0

This is the simple version; in reality the outer directives get data from a server that is formatted by the nested child so what is communicated is a complex object not a simple string. Furthermore the child is a visualisation that will work on different data sets so the outer parent directive is not always the same type.

More generally the pattern I am trying to get here is to have separate directives for populating the model and viewing it. so a more realistic usage would be

<temperature-for city="Auckland">
   <plot/>
   <analysis/>
</temperature-for>

<humidity-for city="Hamilton">
   <plot/>
   <analysis/>
</temperature-for>


<test-data>
   <plot/>
</test-data>
like image 801
avowkind Avatar asked Apr 01 '14 19:04

avowkind


2 Answers

A different approach which I personally have had great success using is to define the plot and analysis directives as isolate scopes, and then two-way bind the required input.

This way the directive are completely standalone components, with a explicit, defined interface. I personally made a plotting directive like this:

<plot data="countries['Auckland'].plot.data" options="countries['Auckland'].plot.options" legend="external" zoom="xy"></plot>

Scope would look like:
scope: {
    data: '=',
    options: '=',
    zoom: '@?',  // '?' indicates optional
    legend: '@?',
}

This way there's no confusion what data is required for this component to work, and you can write documentation inside the directive for the desired input attributes.

All in all, this is a pattern which works very well for a large portion of use cases in AngularJS, i.e. whenever there is a case for reusability.

Edit: Just wanted to add to that: Looking at your HTML, there's absolutely no indication what those directives use, they could depend on anything (e.g. do they get all the data from a service? or do they depend on a parent scope? If so, what scope?)

like image 120
SveinT Avatar answered Oct 13 '22 01:10

SveinT


There are a couple different ways to do this but assuming that you truly want to use the parent scopes here is a solution to go along with your fiddle.

var myApp = angular.module('myApp', []);

function MyCtrl($scope) {
  $scope.index = 0;
}

myApp.directive('parent', function () {
  return {
    transclude: true,
    scope: {
      index: '='
    },
    restrict: 'EA',
    template: '<h2>Parent {{ index }}</h2>',
    compile: function(tE, tA, transcludeFn) {
      return function (scope, elem, attrs) {
        elem.append(transcludeFn(scope)[1]);
      };
    }
  }
});

myApp.directive('child', function () {
  return {
    restrict: 'EA',
    scope: false,
    template: '<p>Child of parent {{ index }}</p>'
  }
});

You can see a fork of your fiddle here.

The idea is that by getting rid of the ngTranscludeDirective and manually creating the transclusion, you can link the transclusion with the scope of your choosing. Then you can append the result where ever you like in the element resulting from your directive's compilation.

The other main point is to make sure the child directive doesn't create a scope (at all, whether an isolate scope, transcluded scope, or new scope).

I think this will give you the results you're asking for.

NOTE: Study your scopes well, because tweaking these behaviors can have unexpected results.

For example, if you add a linking function to the child directive and it sets index to 5:

link: function(scope) {
  scope.index = 5;
}

this will not affect scope.items for children nested in the parent. However it WILL affect the an external parent scope (in this case MyCtrl's scope). Any directive not inside a parent directive will just keep altering the MyCtrl index.

However if you add a new property to scope in the child link function:

link: function(scope) {
  scope.somethingElse = foo;
}

scope.somethingElse will be available on the parent scope whether nested in a parent directive or not.

like image 22
Stephen J Barker Avatar answered Oct 13 '22 01:10

Stephen J Barker