DanHeidel
DanHeidel

Reputation: 671

How do I do dynamic template manipulation in an AngularJS directive?

I'm just getting started on Angular and am trying to wrap my head around proper directive use. I'm writing a custom directive that takes an object array and parses it into a variable number of vertical divs. It's basically a grid system where the elements are arranged into stacked vertical columns rather than in rows. The number of divs dynamically varies with the width of the screen, requiring dynamic changes in the div class as well as reconstructing the ordering of the array elements in the div columns as the page resizes.

When I use the contents of the template as plain, static HTML, everything loads just fine. The filters dynamically change the dataset when you use the input fields, etc.

When I use my directive, the initial page-load looks fine. However, dynamic filtering is broken - it is no longer bound to the input fields. More importantly, on a page resize, the HTML fails to compile at all, leaving a blank screen and uncompiled directive tags in the DOM.

I don't know Angular well enough to troubleshoot this. If I had to guess, it sounds like something is not being bound properly on the page $compile due to a problem with scope.

Note: I know doing string concat for the template is poor practice but I just want to get things working before I start messing around with nesting directives.

Edit: here's a link to the Github repo for my front-end code: https://github.com/danheidel/education-video.net/tree/master/site

HTML

<body ng-controller="channelListController">
  Creator: <input ng-model="query.creators">
  Tags: <input ng-model="query.tags">
  query: {{query}}
  <div id="channel-view">
    <channel-drawers channels="channels"></channel-drawers>
  </div>
</body>

JS

.controller('channelListController', function ($scope, $http){
  $http.get('api/v1/channels').success(function(data){
    $scope.channels = data;
  });
})
.directive('channelDrawers', function($window, $compile){
  return{
    restrict: 'E',
    replace: true,
    scope: {
      channels: '='
    },
    controller: 'channelListController',
    //templateUrl: 'drawer.html',
    link: function(scope, element, attr){
      scope.breakpoints = [
        {width: 0, columns: 1},
        {width: 510, columns: 2},
        {width: 850, columns: 3},
        {width: 1190, columns: 4},
        {width: 1530, columns: 5}
      ];

      angular.element($window).bind('resize', setWindowSize);
      setWindowSize();  //call on init

      function setWindowSize(){
        scope.windowSize = $window.innerWidth;
        console.log(scope.windowSize);
        _.forEach(scope.breakpoints, function(point){
          if(point.width <= scope.windowSize){
            scope.columns = point.columns;
          }
        });
        var tempHtml = '';
        for(var rep=0;rep<scope.columns;rep++){
          tempHtml +=
          '<div class="cabinet' + scope.columns + '">' +
            '<div class="drawer" ng-class="{' + ((rep%2 === 0)?'even: $even, odd: $odd':'even: $odd, odd:$even') + '}" ng-repeat="channel in channels | looseCreatorComparator : query.creators | looseTagComparator : query.tags | modulusFilter : ' + scope.columns + ' : ' + rep + '">' +
              '<ng-include src="\'drawer.html\'"></ng-include>' +
            '</div>' +
          '</div>';
        }
        console.log(tempHtml);
        element.html(tempHtml);
        $compile(element.contents())(scope);
      }
    }
  };
})

Upvotes: 1

Views: 3481

Answers (3)

DanHeidel
DanHeidel

Reputation: 671

First, thanks to pixelbits and pfooti for their input. It put me on the right track. However, I wanted the answer to be a clean slate since our discussions got into technical matters that ended up being tangential to the actual answer.

Basically, this question is poorly framed. After doing more reading, it became clear that I was using the Angular elements in ways they aren't really intended for.

In this case, I'm doing a bunch of model manipulation in my directive and it really should occur in the controller instead. I ended up doing that and also moving the window resize handler code to another component.

Now, I don't even need a directive to properly format my data. A couple of nested ng-repeats with a dusting of ng-class and ng-style do the job just fine in 2 lines of HTML.

<div ng-repeat="modColumn in splitChannels"
  ng-class="{'col-even' : !$even, 'col-odd' : !$odd}"
  ng-style="{ width: 99 / windowAttr.columns + '%' }"
  class="cabinet">
  <div ng-repeat="channel in modColumn"
    ng-class="{'row-even' : !$even, 'row-odd' : !$odd}"
    ng-cloak
    class="panel roundnborder">
    <ng-include src="'panel.html'"></ng-include>
  </div>
</div>

If I could give a bit of advice from one Angular beginner to another, it would be this: if your code is getting complex or you're digging into the internals of things, step back and rethink how you're using the Angular components. You're probably doing an action that should be done in another component class. Proper Angular code tends to be very terse, modular and simple.

Upvotes: 1

pfooti
pfooti

Reputation: 2704

Is there a reason you're doing it with templates?

Can't you bind the number of columns to a variable on the scope? I've done similar stuff with just fiddling around with either ng-if directives to hide stuff that's not important now, or to have general layout stuff attached to the current scope (I generally stuff it all into properties on $scope.view)

There's also plenty of this kind of stuff that already works in css3's media selectors as well, without needing mess with the DOM at all. Without a clearer picture of what you're trying to accomplish I'm not sure if this is super-necessary. More than one ways to skin a cat, etc etc.

Otherwise, @pixelbits is right - if you are fiddling with the DOM tree directly, that needs to happen in compile - values going into the DOM goes into link.

Upvotes: 0

Michael Kang
Michael Kang

Reputation: 52837

The $compile function should be implemented when you want to manipulate your template. The link function should be implemented when you want to bind your template to your scope and/or setup any watchers. If you have dynamic HTML that you're inserting into your DOM, then ask your self these questions:

  1. Are you modifying the template? If so, then create an element template (angular.element(...)) and append it to your element parameter.

  2. Have you modified the template (step 1) and your template contains binding expressions, interpolation expressions, and/or attributes that should bind from other templates? If so, you need to compile and link your element you created from step 1.

Here is an example:

      .directive('myDirective',function($compile) {
            restrict: 'E',
            scope: '=',
            compile: function(element, attr) {
                  // manipulating template?
                  var e = angular.element('<div ng-model="person">{{person.name}}</div>');
                  element.append(e);

                  // the following is your linking function
                  return function(scope, element, attr) {
                       // template contains binding expressions? Yes
                       $compile(e)(scope);
                  };
            }
      });

To fix your code, try moving the template manipulation to your compile function, and in your linking function, call $compile(e)(scope).

Upvotes: 1

Related Questions