41

I have a controller that I wrote that I use in multiple places in my app with ng-include and ng-repeat, like this:

<div
  ng-repeat="item in items"
  ng-include="'item.html'"
  ng-controller="ItemController"
></div>

In the controller/template, I expect the item value to exist, and the whole thing is built around this idea. Now, though, I need to use the controller in a slightly different way, without the ng-repeat, but still need to be able to pass in an item. I saw ng-init and thought it could do what I needed, like this:

<div
  ng-init="item = leftItem"
  ng-include="'item.html'"
  ng-controller="ItemController"
></div>
<div
  ng-init="item = rightItem"
  ng-include="'item.html'"
  ng-controller="ItemController"
></div>

But that does not seem to be working. Anyone have any ideas how I can pass in a variable for scope in a singular instance like this?

Edit: The controller above this is loading in the leftItem and rightItem values, something like this:

.controller('MainController', function($scope, ItemModel) {
    ItemModel.loadItems()
        .then(function(items) {
            $scope.$apply(function() {
                $scope.leftItem = items.left;
                $scope.rightItem = items.right;
            });
        });
});
kbjr
  • 1,225
  • 1
  • 10
  • 21
  • A solution is create a new directive, as i said in this [answer](http://stackoverflow.com/a/36916276/2516399) – smartmouse Apr 28 '16 at 13:33
  • kbjr, please bear in mind that ng-repeat is a directive that creates an new child scope and ng-include too.... Besides that, ng-init can be used in conjuntion with ng-include... i'm afraid you will have to show the controller (The code of the ItemController) file an also the template file (item.html) in order to fully understand the scenario and why is not working. – Victor Jul 25 '16 at 19:38

8 Answers8

37

Late to the party, but there is a little angular 'hack' to achieve this without implementing a dumb directive.

Adding a built-in directive that will extend your controller's scope (like ng-if) everywhere you use the ng-include will actually let you isolate the variable name for all the included scopes.

So:

<div ng-include="'item.html'"
  ng-if="true"
  onload="item = rightItem">
</div>
<div ng-include="'item.html'"
  ng-if="true"
  onload="item = leftItem">
</div>

You can then bind your template item.html to the item variable several times with different items.

Here is a plunker to achieve what you want

The problem was the item keeps changing in the controller scope that only holds one reference to item variable which is erased at each onload instruction.

Introducing a directive that extends the current scope, lets you have a isolated scope for all the ng-include. As a consequence the item reference is preserved and unique in all extended scope.

Piou
  • 1,058
  • 1
  • 7
  • 10
  • 6
    Thank you, this works perfectly. Tip: I found that when adding a value that is populated dynamically after loading (asynchronously) that it wouldn't work because it wasn't available when the include statement was evaluated, but changing the if statement to to ng-if="myVariable" so that it would only load when myVariable was populated resolved that issue. – Iain Collins Sep 17 '15 at 11:11
  • Using onload is not clean because it sets a variable in the global scope. Please see my answer. – Tanin Oct 25 '15 at 18:14
  • 1
    @Tanin that's why I inject a ng-if directive, in order to make a unique isolated child scope for each ng-include and to avoid variable names colision. – Piou Oct 27 '15 at 12:12
33

You can use the onload attribute that ngInclude provides to do this:

<div ng-include="'item.html'"
     ng-controller="ItemController"
     onload="item = rightItem">
</div>

Link to the documentation.

EDIT

Try doing something like this in the parent scope:

$scope.dataHolder = {};

Then when the asynchronous data is received, store the data on dataHolder:

$scope.dataHolder.leftItem = items.leftItem;
$scope.dataHolder.rightItem = items.rightItem;

Now when ng-include loads the template, it will create a child scope that inherits the properties of the parent. So $scope.dataHolder will be defined in this child scope (initially as an empty object). But when your asynchronous data is received, the reference to the empty object should then contain the newly received data.

Sunil D.
  • 17,539
  • 6
  • 48
  • 60
  • That didn't seem to work either. I think it may have to do with the fact the items are being loaded in async, and onload only runs once maybe? – kbjr Sep 05 '14 at 05:04
  • @kbjr Hmm, I just tested this out in my application and it worked. I think your conclusion about the items being loaded asynchronously may be the explanation. It works, for example, if I pass in a string value. – Sunil D. Sep 05 '14 at 05:17
  • Here is a [plunkr](http://plnkr.co/edit/vniCIVvqhqi1ftM7aNxL?p=preview) I created based off the example in the documentation. In "template1" I reference a variable called `bar` that is initialized with from a scope variable `foo`. – Sunil D. Sep 05 '14 at 05:20
  • @kbjr in fact it's not even necessary to use `onload` in my trivial example, because the scope created by `ng-include` inherits properties of the parent scope. So you might be able to solve this by initializing an object in the parent scope, and then when the asynchronous data is loaded, changing a property of the object you initialized. I'll edit my answer... – Sunil D. Sep 05 '14 at 05:28
  • I would still need to reference this new value in the template, though, right? So instead of using `item` in the template, I use `dataHolder.item`. That would break the other use case when I need to use `ng-repeat`. – kbjr Sep 05 '14 at 05:34
  • Yeah, it doesn't quite fit in your example... but I think you can at least be assured you've diagnosed the problem correctly. The child scope created by ng-include doesn't see the updates to $scope.leftItem/$scope.rightItem. My suggestion is one way to ensure that both scopes have a reference to the same object. Another idea would be to use a service to store these values. But neither of things fit your original paradigm :) – Sunil D. Sep 05 '14 at 05:38
  • I think I'm going to have to rebuild it into a directive so I can bind the scope. I just really didn't want to >_> – kbjr Sep 05 '14 at 05:41
  • just add $scope.dataHolder = {} in your controller and then your ng-include will use it – mariomol Mar 29 '16 at 20:36
17

I ended up rewriting it into a directive and binding the needed value in the scope with

scope: {
    item: '='
}
kbjr
  • 1,225
  • 1
  • 10
  • 21
  • 2
    Voted up because the ng-init="" and onload="" just dont seem to work, although they should ... Its a pity that we need to create a directive each time for this simple task. – Davy Aug 21 '15 at 07:14
  • Writing a specific directive every time is not ideal. Please see my solution. I hope it helps! – Tanin Oct 25 '15 at 18:13
13

LOVE @Tanin's answer. solves so many problems at once and in a very elegant way. For those of you like me who don't know Coffeescript, here's the javascript...

NOTE: For reasons I'm too new to understand, this code requires you to quote your template name once, rather than ng-include's requirement to twice-quote your template names, ie. <div ng-include-template="template-name.html" ... > instead of <div ng-include-template="'template-name.html'" ... >

.directive('ngIncludeTemplate', function() {  
  return {  
    templateUrl: function(elem, attrs) { return attrs.ngIncludeTemplate; },  
    restrict: 'A',  
    scope: {  
      'ngIncludeVariables': '&'  
    },  
    link: function(scope, elem, attrs) {  
      var vars = scope.ngIncludeVariables();  
      Object.keys(vars).forEach(function(key) {  
        scope[key] = vars[key];  
      });  
    }  
  }  
})
Mike
  • 688
  • 10
  • 12
11

Using onload is not a clean solution because it litters the global scope. If you have something more complex, it'll start to fail.

ng-include is not that reusable because it has access to the global scope. It's a little weird.

The above is not true. ng-if with onload doesn't litter the global scope

We also don't want to write a specific directive for every situation.

Making a generic directive instead of ng-include is a cleaner solution.

The ideal usage looks like:

<div ng-include-template="'item.html'" ng-include-variables="{ item: 'whatever' }"></div>
<div ng-include-template="'item.html'" ng-include-variables="{ item: variableWorksToo }"></div>

The directive is:

.directive(
  'ngIncludeTemplate'
  () ->
    {
      templateUrl: (elem, attrs) -> attrs.ngIncludeTemplate
      restrict: 'A'
      scope: {
        'ngIncludeVariables': '&'
      }
      link: (scope, elem, attrs) ->
        vars = scope.ngIncludeVariables()
        for key, value of vars
          scope[key] = value
    }
)

You can see that the directive doesn't use the global scope. Instead, it reads the object from ng-include-variables and add those members to its own local scope.

I hope this is what you would like; it's clean and generic.

Bhargav Rao
  • 41,091
  • 27
  • 112
  • 129
Tanin
  • 1,741
  • 1
  • 13
  • 18
4

ng-init is better for this I think.

<div ng-include='myFile.html' ng-init="myObject = myCtrl.myObject; myOtherObject=myCtrl.myOtherObject"/>
Will Sadler
  • 144
  • 1
  • 3
  • 2
    Please consider editing your post to add more explanation about what your code does and why it will solve the problem. An answer that mostly just contains code (even if it's working) usually wont help the OP to understand their problem. – SuperBiasedMan Nov 13 '15 at 16:41
  • 1
    This doesn't work for binding the same template several times with different variables – smartmouse Apr 28 '16 at 12:10
4

Above will not work for second level attributes, like <div ng-include-template=... ng-include-variables="{ id: var.id }">. Notice the var.id.

Updated directive(ugly, but works):

.directive('ngIncludeTemplate', function() {
  return {
    templateUrl: function(elem, attrs) { return attrs.ngIncludeTemplate; },
    restrict: 'A',
    scope: {
      'ngIncludeVariables': '&'
    },
    link: function(scope, elem, attrs) {
      var cache = scope.ngIncludeVariables();
      Object.keys(cache).forEach(function(key) {
        scope[key] = cache[key];
      });

      scope.$watch(
        function() {
          var val = scope.ngIncludeVariables();
          if (angular.equals(val, cache)) {
            return cache;
          }
          cache = val;
          return val;
        },
        function(newValue, oldValue) {
          if (!angular.equals(newValue, oldValue)) {
            Object.keys(newValue).forEach(function(key) {
              scope[key] = newValue[key];
            });
          }
        }
      );

    }
  };
});
Viktor Aseev
  • 698
  • 4
  • 9
  • 1
    This should really be part of ngInclude itself! `ng-include` is the basic unit of modularity in Angular, and it should have a clean way to parameterize. – David Bau Mar 29 '17 at 02:46
  • This solution takes care of the model changes that the first solution doesn't...great work! – Billy McCafferty Jul 29 '20 at 03:53
1

Maybe obvious update for Mike and Tanin answers - if you using inline templates as:

<script type="text/ng-template" id="partial">{{variable}}</script>

<div ng-include-template="'partial'" ng-include-variables="{variable: variable}"></div>

Then, in directive ngIncludeTemplate, replace

templateUrl: function(elem, attrs) { return attrs.ngIncludeTemplate; },  

With

template: function(elem, attrs) { return document.getElementById(attrs.ngIncludeTemplate.split("'")[1]).innerHTML },