20

For example, I have a partial in car-list.html, and I want to render it in several places with different collections of cars. Maybe something like this:

<h1>All New Cars</h1>
<div ng-include="car-list.html" ng-data-cars="allCars | onlyNew"></div>

<h1>All Toyotas</h1>
<div ng-include="car-list.html" ng-data-cars="allCars | make:toyota"></div>

The main difference from a normal include is that the partial doesn't need to know anything about which list of cars it's displaying. It's given an array of cars, and it displays them. Possibly like:

<!-- car-list.html -->
<div ng-repeat="car in cars" ng-controller="CarListControl">
    {{car.year}} {{car.make}} {{car.model}}
</div>
colllin
  • 9,442
  • 9
  • 49
  • 65
  • 3
    You should use a directive for that. It's been made to do exactly what you want to achieve. – maxdec Jul 25 '13 at 16:41
  • 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:35

3 Answers3

25

You can achieve that easily with a directive.

Something like that:

angular.module('myModule')
.directive('cars', function () {
  return {
    restrict: 'E',
    scope: { 'cars': '=data' },
    template: "<div ng-repeat='car in cars'>\n" +
    "  {{car.year}} {{car.make}} {{car.model}}\n" +
    "</div>"
  };
});

Then you can use it like that:

<h1>All New Cars</h1>
<cars data="allCars | onlyNew"></cars>

<h1>All Toyotas</h1>
<cars data="allCars | make:toyota"></cars>

You can find more info about directives here.

Dorian
  • 22,759
  • 8
  • 120
  • 116
maxdec
  • 5,707
  • 2
  • 30
  • 35
  • So, it's possible, but i have to create a directive every time I want to do it? Seems pretty verbose... Is there a way to dynamically specify the template and the variable name(s)? Maybe by specifying a data object? `ng-data="{cars: {{allCars | onlyNew}}, templateUrl: 'car-list.html'}"` – colllin Jul 25 '13 at 16:53
  • 2
    It depends. If you want to create a short piece of dynamic HTML that you want to embed here and there, directives are perfect. If you want to load a big template, like a menu, you can use `include` and give your template a controller. Then you get the data you need with that controller (through services, resources or whatever and assign them in the `$scope`). – maxdec Jul 25 '13 at 16:57
  • But one directive must achieve only one thing. You cannot give it a dynamic template. Moreover most of the time directives also have a `controller` or `link` function, that are specific. – maxdec Jul 25 '13 at 17:00
  • 2
    FYI it IS possible to have a more generic solution. See my answer – colllin Jul 29 '13 at 17:07
  • 1
    Wow, I finally understand what extending directives is good for. This is very powerful, thanks! – Amin Ariana Oct 30 '13 at 22:10
17

This directive provides 2-way data-binding between the parent scope and renamed "local" variables in the child scope. It can be combined with other directives like ng-include for awesome template reusability. Requires AngularJS 1.2.x

jsFiddle: AngularJS - Include a partial with local variables


The Markup

<div with-locals locals-cars="allCars | onlyNew"></div>

What's going on:

  • This is basically an extension of the ngInclude directive to allow you to pass renamed variables in from the parent scope. ngInclude is NOT required at all, but this directive has been designed to work well with it.
  • You can attach any number of locals-* attributes, which will all be parsed & watched for you as Angular expressions.
    • Those expressions become available to the included partial, attached as properties of a $scope.locals object.
    • In the example above, locals-cars="..." defines an expression that becomes available as $scope.locals.cars.
    • Similar to how a data-cars="..." attribute would be available via jQuery using .data().cars

The Directive

EDIT I've refactored to make use of (and be independent of) the native ngInclude directive, and move some of the calculations into the compile function for improved efficiency.

angular.module('withLocals', [])
.directive('withLocals', function($parse) {
    return {
        scope: true,
        compile: function(element, attributes, transclusion) {
            // for each attribute that matches locals-* (camelcased to locals[A-Z0-9]),
            // capture the "key" intended for the local variable so that we can later
            // map it into $scope.locals (in the linking function below)
            var mapLocalsToParentExp = {};
            for (attr in attributes) {
                if (attributes.hasOwnProperty(attr) && /^locals[A-Z0-9]/.test(attr)) {
                    var localKey = attr.slice(6);
                    localKey = localKey[0].toLowerCase() + localKey.slice(1);

                    mapLocalsToParentExp[localKey] = attributes[attr];
                }
            }

            var updateParentValueFunction = function($scope, localKey) {
                // Find the $parent scope that initialized this directive.
                // Important in cases where controllers have caused this $scope to be deeply nested inside the original parent
                var $parent = $scope.$parent;
                while (!$parent.hasOwnProperty(mapLocalsToParentExp[localKey])) {
                    $parent = $parent.$parent;
                }

                return function(newValue) {
                    $parse(mapLocalsToParentExp[localKey]).assign($parent, newValue);
                }
            };

            return {
                pre: function($scope, $element, $attributes) {

                    // setup `$scope.locals` hash so that we can map expressions
                    // from the parent scope into it.
                    $scope.locals = {};
                    for (localKey in mapLocalsToParentExp) {

                        // For each local key, $watch the provided expression and update
                        // the $scope.locals hash (i.e. attribute `locals-cars` has key
                        // `cars` and the $watch()ed value maps to `$scope.locals.cars`)
                        $scope.$watch(
                            mapLocalsToParentExp[localKey],
                            function(localKey) {
                                return function(newValue, oldValue) {
                                    $scope.locals[localKey] = newValue;
                                };
                            }(localKey),
                            true
                        );

                        // Also watch the local value and propagate any changes
                        // back up to the parent scope.
                        var parsedGetter = $parse(mapLocalsToParentExp[localKey]);
                        if (parsedGetter.assign) {
                            $scope.$watch('locals.'+localKey, updateParentValueFunction($scope, localKey));
                        }

                    }
                }
            };
        }
    };
});
colllin
  • 9,442
  • 9
  • 49
  • 65
  • Also useful without `ng-include`. For example, if you want to display a filtered list along with the count of filtered or unfiltered items. ` MATCHED {{locals.filteredList.length}} ITEM(S): {{item.name}}` – colllin Jul 29 '13 at 22:04
  • JSFiddle link is broken. – Sarah Vessels Nov 20 '13 at 16:55
  • 1
    I can't get this to work with AngularJS 1.1.5. The local I pass in appears to be undefined in the included template, even though it is defined and has properties in the scope when I use the `div` with `with-locals`. – Sarah Vessels Nov 20 '13 at 16:57
  • @SarahVessels - I found 2 bugs that I fixed today, please try again if you were having trouble before. It no longer assumes a `$parent` relationship between the two scopes -- sometimes other directives or controllers can cause this scope to be nested deeper than one level. Also there was a JS namespacing issue that caused local variables to overwrite each other. – colllin Jan 22 '14 at 18:07
  • $parent.hasOwnProperty will fail for me as $parent can be null. Any idea why this may be? – Dave Jul 13 '14 at 14:00
  • @Dave Can you post a jsfiddle? – colllin Jul 13 '14 at 20:18
  • @Dave That error appears if, for example, you specify `locals-cars="madMaxCollection"` but `madMaxCollection` isn't available anywhere in the `$scope` ancestry. Possibly (a) there was a typo, or (b) the variable hasn't been defined yet in the parent scope, or (c) there is an isolate scope somewhere. Maybe I should update to allow passing variables that haven't been defined in the parent scope? – colllin Aug 12 '14 at 21:58
3

I'd like to offer my solution, which is in a different design.

The ideal usage for you is:

<div ng-include-template="car-list.html" ng-include-variables="{ cars: (allCars | onlyNew) }"></div>

ng-include-variables's object is added to the local scope. Therefore, it doesn't litter your global (or parent) scope.

Here's your directive:

.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
    }
)

(It's in Coffeescript)

IMO, ng-include is a little bit strange. Having access to the global scope decreases its reusability.

Tanin
  • 1,853
  • 1
  • 15
  • 20