2

I am relatively new to AngularJS. While venturing into directive creation, I can across this problem: How to dynamically add / remove attributes on the children of the directive's element when these children are dynamically added with 'ng-repeat'?

First, I thought of this solution:

template

...
a.list-group-item(ng-repeat='playlist in playlists', ng-click='addToPlaylist(playlist, track)', ng-href='playlist/{{ playlist._id }})
...

*directive

link: function(scope, elm, attrs) {   
  var listItems = angular.element(element[0].getElementsByClassName('list-group-item')
  angular.forEach(listItems, function(item, index) {
    'add' in attrs ? item.removeAttr('href') : item.removeAttr('ng-click');
    listItems[index] = item;
  }
... 

Result

It turns out, my code never enters this angular.forEach loop because listItems is empty. I suppose it's because the ng-repeat is waiting for the scope.playlists to populate with the data from a async call to a server via $resource.

temporary fix

in the directive definition, I added a boolean variable that checks for the presence of 'add' in the element's attributes: var adding = 'add' in attrs ? true : false;

And then in the template,

a.list-group-item(ng-if='adding', ng-repeat='playlist in playlists', ng-click='addToPlaylist(playlist, track)')
a.list-group-item(ng-if='!adding', ng-repeat='playlist in playlists', ng-href='playlist/{{playlist._id }}')

While it works fine, it is obviously not DRY at all. HELP!

1
  • Your final solution is not that bad at all actually, although it seems repetitive. Commented Feb 13, 2016 at 1:48

2 Answers 2

2

Instead of removing attributes, change your click handler.

Add $event to the list of arguments and conditionally use preventDefault().

<a ng-click='addToPlaylist($event,playlist)' ng-href='playlist'>CLICK ME</a>

In your controller:

$scope.addToPlaylist = function(event,playlist) {
     if (!$scope.adding) return;
     //otherwise
     event.preventDefault();
     //do add operation
};

When not adding, the function returns and the href is fetched. Otherwise the default is prevented and the click handler does the add operation.

From the Docs:

$event

Directives like ngClick and ngFocus expose a $event object within the scope of that expression. The object is an instance of a jQuery Event Object when jQuery is present or a similar jqLite object.

-- AngularJS Developer Guide -- $event

Sign up to request clarification or add additional context in comments.

2 Comments

Thanks a lot for this solution. It works like a charm and solve the problem in a DRY way. Thanks!
Not only is the solution DRY. It solves your problem without writing a custom directive. You can achieve what you want using the built-in directives. Thanks for the upvote.
2

The way that you are trying to do things may not be the most Angularish (Angularist? Angularyist?) way. When using angular.element() to select child elements as you are trying to do here, you can make sure the child elements are ready as follows:

link: function(scope, elm, attrs) {
  elm.ready(function() {
    var listItems = angular.element(element[0].getElementsByClassName('list-group-item')
    angular.forEach(listItems, function(item, index) {
      'add' in attrs ? item.removeAttr('href') : item.removeAttr('ng-click');
      listItems[index] = item;
    }
  });
}

However, this is unlikely to work in your situation, as @charlietfl points out below. If you want to avoid the solution you already have (which I think is better than your first attempt), you will have to reimplement your code altogether.

I would suggest defining an additional directive that communicates with its parent directive using the require property of the directive definition object. The new directive would have access to an add property of the parent (this.add in the parent directive's controller) and could be programmed to behave accordingly. The implementation of that solution is beyond the scope of this answer.

Update:

I decided to give the implementation something of a shot. The example is highly simplified, but it does what you are trying to do: alter the template of a directive based on the attributed passed to it. See the example here.

The example uses a new feature in Angular 1: components. You can read more about injectable templates and components here. Essentially, components allow you to define templates using a function with access to your element and its attributes, like so:

app.component('playlistComponent', {

    // We can define out template as a function that returns a string:
    template: function($element, $attrs) {
      var action = 'add' in $attrs
        ? 'ng-click="$ctrl.addToPlaylist(playlist, track)"'
        : 'ng-href="playlist/{{playlist._id}}"';

      return '<a class="list-group-item" ng-repeat="playlist in playlists" ' +
        action + '></a>';
    },

    // Components always use controllers rather than scopes
    controller: ['playlistService', function(playlists) {
      this.playlists = playlists;

      this.addToPlaylist = function(playlist, track) {
        // Some logic
      };
    }]
  });

7 Comments

explanation doesn't make sense. Angular doesn't bootstrap until dom is ready and directive can't fire unless element exists. Perception of how ready works is inaccurate. Repeating elements as children are a different situation
@charlietfl Would you be able to edit the answer to correct it or maybe provide a better one? I probably phrased things inaccurately, but I think the solution will work. I'll try to rephrase, but feel free to edit if I'm still not getting it right.
Whole question seems like "chicken before the egg". Doesn't make sense at all to loop over the repeating dom nodes in the first place
@charlietfl OK, I agree. That's why I suggested an alternative, although this would require a completely different implementation.
However I don't think removing ng-click attribute will work anyway after it has already been compiled ... wouldn't remove event listener on element
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.