AngularJS - How to render a partial with variables?
Asked Answered
W

3

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>
Waits answered 25/7, 2013 at 16:31 Comment(2)
You should use a directive for that. It's been made to do exactly what you want to achieve.Thea
A solution is create a new directive, as i said in this answer: https://mcmap.net/q/88560/-renaming-a-variable-in-ng-include-duplicateNagano
W
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));
                        }

                    }
                }
            };
        }
    };
});
Waits answered 25/7, 2013 at 16:31 Comment(7)
Also useful without ng-include. For example, if you want to display a filtered list along with the count of filtered or unfiltered items. <span with-locals locals-filtered-list="myList | filter:'a query'"> MATCHED {{locals.filteredList.length}} ITEM(S): <span ng-repeat="item in locals.filteredList">{{item.name}}</span></span>Waits
JSFiddle link is broken.Whisenhunt
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.Whisenhunt
@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.Waits
$parent.hasOwnProperty will fail for me as $parent can be null. Any idea why this may be?Gertrudis
@Gertrudis Can you post a jsfiddle?Waits
@Gertrudis 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?Waits
T
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.

Thea answered 25/7, 2013 at 16:50 Comment(5)
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'}"Waits
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).Thea
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.Thea
FYI it IS possible to have a more generic solution. See my answerWaits
Wow, I finally understand what extending directives is good for. This is very powerful, thanks!Gabby
W
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));
                        }

                    }
                }
            };
        }
    };
});
Waits answered 25/7, 2013 at 16:31 Comment(7)
Also useful without ng-include. For example, if you want to display a filtered list along with the count of filtered or unfiltered items. <span with-locals locals-filtered-list="myList | filter:'a query'"> MATCHED {{locals.filteredList.length}} ITEM(S): <span ng-repeat="item in locals.filteredList">{{item.name}}</span></span>Waits
JSFiddle link is broken.Whisenhunt
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.Whisenhunt
@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.Waits
$parent.hasOwnProperty will fail for me as $parent can be null. Any idea why this may be?Gertrudis
@Gertrudis Can you post a jsfiddle?Waits
@Gertrudis 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?Waits
A
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.

Agan answered 25/10, 2015 at 17:49 Comment(3)
Does this bind the data in even one direction?Waits
I think so, unless value in scope[key] = value is an object. We should have done a deep copy there.Agan
Wouldn't you need to watch the ngIncludeVariables?Waits

© 2022 - 2024 — McMap. All rights reserved.