2

I created a "tessellate" directive that lets you wrap a number of divs.

<tessellate columns="4">
  <div class="thumbnail" ng-repeat="item in items track by item.id">
      {{item.Name}}<br />
      {{item.Summary}}
  </div>
</tessellate>

It takes one div at a time and appends it to the shortest of the specified number of columns to create a tessellation/mosaic effect.

See the plunkr here: http://plnkr.co/edit/ur0bVCFRSz1UbRHeLjz8?p=preview

The problem is that when the model changes, the ng-repeat uses the order that the divs appear in the DOM instead of the order in the model to redraw the elements. You can see that the items are sorted correctly at first and after clicking Add it is sorting the items from the first column horizontally, then the ones from the next column, etc.

How can I keep ng-repeat from using the DOM order to redraw elements? I already tried adding orderBy item.id, but it didn't help.

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

app.controller('itemController', ['$scope', function ($scope) {
    $scope.items = [
             { id:"1", Name:"Item1", Summary:"This is the summary of Item1" },
             { id:"2", Name:"Item2", Summary:"This is the summary of Item2. Some extra text on item two to test different heights." },
             { id:"3", Name:"Item3", Summary:"This is the summary of Item3" },
             { id:"4", Name:"Item4", Summary:"This is the summary of Item4. Some extra text on item four to test different heights." },
             { id:"5", Name:"Item5", Summary:"This is the summary of Item5. Some extra text on item five to test different heights. Some extra text on item to test different heights." },
             { id:"6", Name:"Item6", Summary:"This is the summary of Item6" },
             { id:"7", Name:"Item7", Summary:"This is the summary of Item7. Some extra text on item seven to test different heights." },
             { id:"8", Name:"Item8", Summary:"This is the summary of Item8" },
             { id:"9", Name:"Item9", Summary:"This is the summary of Item9. Some extra text on item nine to test different heights." },
             { id:"10", Name:"Item10", Summary:"This is the summary of Item10. Some extra text on item ten to test different heights." },
             { id:"11", Name:"Item11", Summary:"This is the summary of Item11" },
             { id:"12", Name:"Item12", Summary:"This is the summary of Item12. Some extra text on item to test different heights." },
             { id:"13", Name:"Item13", Summary:"This is the summary of Item13" },
             { id:"14", Name:"Item14", Summary:"This is the summary of Item14. Some extra text on item to test different heights." },
             { id:"15", Name:"Item15", Summary:"This is the summary of Item15. Some extra text on item to test different heights. Some extra text on item to test different heights." },
             { id:"16", Name:"Item16", Summary:"This is the summary of Item16" },
             { id:"17", Name:"Item17", Summary:"This is the summary of Item17. Some extra text on item to test different heights." },
             { id:"18", Name:"Item18", Summary:"This is the summary of Item18" }
             ];
    $scope.inc = $scope.items.length;
    $scope.add = function() {
        $scope.inc = $scope.inc + 1;
        $scope.items.push({ id: $scope.inc, Name: "New Item" + $scope.inc, Summary:"New Summary" });
    };
}]);

app.directive('tessellate', [function () {
    return {
        restrict: 'E',
        replace: true,
        transclude: true,
        scope: {
            columns: '='
        },
        controller: ['$scope', '$element', '$attrs', function ($scope, $element, $attrs) {
            $scope.numberToArray = function (num) {
                return new Array(num);
            };
        }],
        link: function (scope, elem, attrs, ctrl) {

            scope.$watch(function () {
                return elem.children().first().height();
            }, function (height) {
                if (height > 0) {
                    var containers = elem.children();
                    var transcludedDivsContainer = containers.first();
                    var targetColumns = containers.eq(1).children();

                    // Add the transcluded divs one at a time into the shortest column.
                    angular.forEach(transcludedDivsContainer.children(), function (div) {
                        var shortCol = null;
                        angular.forEach(targetColumns, function (col) {
                            col = angular.element(col);
                            if (shortCol === null || col.height() < shortCol.height()) {
                                shortCol = col;
                            }
                        });
                        shortCol.append(div);
                    });
                }
            }
            );
        },
        templateUrl: "tessellateTemplate.html"
    };
}]);
adam0101
  • 29,096
  • 21
  • 96
  • 174
  • I do not believe the problem lies in ng-repeat. The source of the problem is in your directive. The directive takes the items by their DOM order and sets them accordingly while ng-repeat puts them in the "correct" order. Just add `{{$index}}` beside the name (e.g. `{{item.Name}}--||--{{$index}}`) and see for yourself that the indexing is good. I did not delve too deep into your directive but the conflict arises from there, as I can see you are looking at the DOM structure to sort the elements. – yccteam Apr 09 '14 at 21:03
  • @yccteam, but if I step through the code, I can see the order is already messed up before my $watch even gets hit. I believe ng-repeat crawls the DOM to get the existing elements so it doesn't have to recreate them, but assumes the elements are in the same order as when it first ran. – adam0101 Apr 09 '14 at 21:20
  • You are correct. It's scenerio I've never tried before, but Ben explains about it in this [blog post](http://www.bennadel.com/blog/2443-Rendering-DOM-Elements-With-ngRepeat-In-AngularJS.htm) – yccteam Apr 09 '14 at 22:02

1 Answers1

1

I forked your plunkr and messed around with it. I think it works the way you want now.

http://plnkr.co/edit/1y8jE0SLuJK6XTNRBKF3?p=preview

Mainly what fixed it was sorting the list of dom elements by their index, and to do that I added the $index to a data-index attr on the element.

aet
  • 7,192
  • 3
  • 27
  • 25
  • it looks good so far from my phone, I'll check on my desktop when I get back to my desk. How did you know to set the index attribute like that? – adam0101 Apr 10 '14 at 12:26
  • oh, never mind. I thought you were overriding some ng-repeat behavior, but I see that you're using the index yourself to sort the elements. This works great, but I'm surprised that ng-repeat removes and replaces all the elements even if only one of them changed. Do you know if this is normal or if it's something this code is forcing ng-repeat to do? I'm wondering if it could be tweaked to only affect the new or updated elements for performance. – adam0101 Apr 10 '14 at 15:43
  • Looking at the ng-repeat code it seems like it just operates on the changes. Elements that are still in the list should not be removed/re-added to the dom. To make the tessellation faster you might have to use some caching technique like what ng-repeat is doing. Right now it re-tessellates the entire list each time something is added. Would need to keep some kind of representation of the tessellated state, restore it, then add the new element without recalculating all of it each time. – aet Apr 10 '14 at 16:04
  • Well, this is good enough for me. Although, and I know this is a separate question, if you delete an item from the array, the order still gets messed up. – adam0101 Apr 10 '14 at 17:42