user2247790
user2247790

Reputation: 71

AngularJS directive with ng-repeat not rendering

The problem is that I have to manage a list of gum balls that is retrieved from a service. The directive I've created seems to work when I hardcode the elements in the HTML, but when I attempt to dynamically allocate the gum balls using a ng-repeat.

HTML

<div ng-controller="GumballsCtrl">

<h1>Working</h1> 
    <ul>
        <li ng-repeat="gumball in Gumballs">
            <div class="gumballColor{{gumball.color}}">{{gumball.color}}</div>
        </li>
    </ul>

<h1>Problem - Expecting the same result at the work version</h1>

    <ul>
        <li ng-repeat="gumball in Gumballs">
            <mygumball id={{gumball.id}} color="{{gumball.color}}">{{gumball.color}}</mygumball>
        </li>
    </ul>
</div>

JavaScript

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

function GumballsCtrl($scope, Gumballs) {
    $scope.Gumballs = Gumballs;
}

myApp.factory('Gumballs', function () {
    return [{
        id: '1',
        color: 'R'
    }, {
        id: '2',
        color: 'G'
    }, {
        id: '3',
        color: 'B'
    }, {
        id: '4',
        color: 'Y'
    }, {
        id: '5',
        color: 'G'
    }];
});

myApp.directive('mygumball', function ($scope) {
    return {
        restrict: 'E',

        scope: {},

        link: function (scope, element, attrs) {
            if (attrs.color !== '' && attrs.color !== undefined) {
                scope.color = attrs.color;
            } else {
                scope.color = 'U';
            }
        },

        replace: true,

        template: "<div class='gumballColor{{color}}'>{{color}}</div>"
    };
});

CSS

.gumballColorR {
    font-size: 12px;
    text-align: center;
    padding: 2px;
    -moz-border-radius: 10px;
    -webkit-border-radius: 10px;
    border-radius: 10px;
    border: solid 1px #CC0000;
    background-color: #FF0000;
    width: 15px;
    height: 15px;
    margin-left: 5px;
    margin-top: 5px;
}
.gumballColorG {
    font-size: 12px;
    text-align: center;
    padding: 2px;
    -moz-border-radius: 10px;
    -webkit-border-radius: 10px;
    border-radius: 10px;
    border: solid 1px #00CC00;
    background-color: #00FF00;
    width: 15px;
    height: 15px;
    margin-left: 5px;
    margin-top: 5px;
}
.gumballColorB {
    font-size: 12px;
    text-align: center;
    padding: 2px;
    color: #FFFFFF;
    -moz-border-radius: 10px;
    -webkit-border-radius: 10px;
    border-radius: 10px;
    border: solid 1px #0000CC;
    background-color: #0000FF;
    width: 15px;
    height: 15px;
    margin-left: 5px;
    margin-top: 5px;
}
.gumballColorY {
    font-size: 12px;
    text-align: center;
    padding: 2px;
    -moz-border-radius: 10px;
    -webkit-border-radius: 10px;
    border-radius: 10px;
    border: solid 1px #CCCC00;
    background-color: #FFFF00;
    width: 15px;
    height: 15px;
    margin-left: 5px;
    margin-top: 5px;
}
.gumballColorU {
    font-size: 12px;
    text-align: center;
    padding: 2px;
    -moz-border-radius: 10px;
    -webkit-border-radius: 10px;
    border-radius: 10px;
    border: solid 1px #CCCCCC;
    background-color: #DDDDDD;
    width: 15px;
    height: 15px;
    margin-left: 5px;
    margin-top: 5px;
}

http://jsfiddle.net/i3sik/NGB9v/22/

The id and color attributes when passed into the directive end up being undefined when passed using the ng-repeat, but work when hardcoded in the HTML.

Upvotes: 7

Views: 5168

Answers (1)

Josh David Miller
Josh David Miller

Reputation: 120513

The problem here is your isolate scope. By using scope: {} you created a new, isolate scope to act on that element. Isolate scopes don't inherit from the parent scope. All attributes and content on directives with isolate scopes are evaluated within the context of the isolate scope. gumball doesn't exist in the isolate scope, so everything comes up as undefined.

You have two choices to fix this: (1) remove the isolate scope (e.g. scope: true to create a child scope); or (2) bind the values in your isolate scope.

To bind your attributes to scope variables, you simply need to specify the scope and the kind of binding you want:

scope: {
  id: '@',
  color: '@'
},

This says that the attributes id and color are to be interpolated in the context of the parent scope and then added to the scope. You can remove all that logic inside your link function - this will do it for you.

But this still leaves the problem of the content inside the directive. To interpolate that in the context of the parent scope, you need transclusion:

transclude: true,
template: "<div class='gumballColor{{color}}' ng-transclude></div>"

Transclusion takes the contents of the element and interpolates relative to a new child of the parent scope, e.g. where gumball would still be defined.

With these two changes, your directive will work as desired.

If you're confused about which scope to use, here's another SO question that might help: When writing a directive, how do I decide if a need no new scope, a new child scope, or a new isolate scope?


Side note: Even without the isolate scope, the logic in your link function to determine attribute values wouldn't work. Order of execution is the important part here, which is roughly: compiler -> controller -> link -> interpolation. Until the interpolation is done, there is no value for your attributes. So your checks won't work.

That said, you can set up an $observe on interpolated attributes; the $observe will always fire the first time, even if there was no value passed. You can use this to set your default. $observe is also very efficient.

attrs.$observe( 'attr1', function(val) {
  if ( !angular.isDefined( val ) ) {
    scope.attr1 = 'defaultValue';
  }
});

Upvotes: 10

Related Questions