Search code examples
javascriptcssangularjsangularjs-directiveangularjs-scope

Angular Digest Loop on ng-style / Dynamic Photo Grid


I have a filter that changes filtered object. But when I'm using ng-style="item.gridSize" My Filter: (The Algorithm for size Grid was taken (changed for my needs) from Here

angular.module("custom.modules.photoGrid", []).filter('photoSearch', [function () {
    function getGrid(photos){
        var output = [];
        var HEIGHTS = [];
        var COLUMN_WIDTH = 227;
        var MARGIN = 6;
        var DELTA = 20;

        var size = window.innerWidth - 50;
        var n_columns = Math.floor(size / (2 * (COLUMN_WIDTH + MARGIN)));
        create_columns(n_columns);
        var small_images = [];

        function create_columns(n) {
            HEIGHTS = [];
            for (var i = 0; i < n; ++i) {
                HEIGHTS.push(0);
            }
        }

        function get_min_column() {
            var min_height = Infinity;
            var min_i = -1;
            for (var i = 0; i < HEIGHTS.length; ++i) {
                if (HEIGHTS[i] < min_height) {
                min_height = HEIGHTS[i];
                min_i = i;
                }
            }
            return min_i;
        }

        function gridSize(i, is_big) {
            var size = {
                'margin-left': (MARGIN + (COLUMN_WIDTH + MARGIN) * i)+'px',
                'margin-top': (HEIGHTS[Math.floor(i / 2)] * (COLUMN_WIDTH + MARGIN))+'px',
                'width': is_big ? (COLUMN_WIDTH * 2 + MARGIN)+'px' : COLUMN_WIDTH+'px',
                'height': is_big ? (COLUMN_WIDTH * 2 + MARGIN)+'px' : COLUMN_WIDTH+'px'
            };
            return size;
        }
        function createGrid(data){
            if (data.length >= 2) {
                for(var i = 0; i < data.length; i++){
                    var column = get_min_column();
                    if (Math.random() > 0.8) {
                        data[i]['gridSize'] = gridSize(column * 2, true);
                        HEIGHTS[column] += 2;
                    } else {
                        small_images.push(i);
                        if (small_images.length === 2) {
                            data[small_images[0]]['gridSize'] = gridSize(column * 2, false);
                            data[small_images[1]]['gridSize'] = gridSize(column * 2 + 1, false);
                            HEIGHTS[column] += 1;
                            small_images = [];
                        }
                    }
                }
                if (small_images.length) {
                    column = get_min_column();
                    data[(data.length-1)]['gridSize'] = gridSize(column * 2, false);
                }
            }

            return data;
        }
        var grid = createGrid(photos);
        return grid;
    }


    return function(photos, search) {
        var filtered = [];
        if(!!search){ /**@case1 if only search query is present**/
            search = search.toLowerCase();
            for(var i = 0; i < photos.length; i++){
                if(photos[i].photo_name.toLowerCase().indexOf(search) !== -1){
                    filtered.push(photos[i]);
                }
            }

        }else {
            /**@case2 no query is present**/
            filtered = photos;
        }
        filtered = getGrid(filtered);
        return filtered;
    }
}]);

Html:

<input type="text" ng-model="input.value"> <span>{{ results.length }}</span> Photo Found
<div ng-repeat='photo in photos | photoSearch:input.value as results track by photo.id' class="photo-item" ng-style="photo.gridSize">
                    <img ng-src="/photos/{{photo.url}}">
                </div>

A small explanation: Every time ng-model input.value changed filter is runed and creates different grid for filtered array of photos. all dimensions are written inside gridSize and this cause digest loop.

What I've tried until now: I've moved my ng-repeat in directive, but this way I can't access result.length and input.value.

I've also tried a bindonce directive but using it like bo-style="photo.gridSize" doesn't change the grid after user search(and is logically right because is bidden only once, but values changed.

So my question is how to make ng-repeat assign new grdiSize property without running in digest loop.

UPDATE: JSFiddle

Working Fiddle: JSFiddle


Solution

  • There were a couple of issues. It was not exactly a ng-style problem, but rather than in each digest cycle your photos were calculating different style objects, causing another digest cycle to run.

    Some issues I've found:

    • Error in logic was giving 0 colums, thus causing size to give NaN when calculating margin-top and failing. To fix this, I added a default value from 1 column.
    • your Math.random() > 0.8 was giving different results in each time your filter function was executing. In each digest cycle, since Math.random() gives different results, it was forcing another digest loop (you were updating gridSize object - since there's a $watch for each element in the ng-repeat it detects the changes and forces one digest cycle), and so on. That was the error log in console.

    I created this fiddle that works. The main changes are

    defined a fixed random value for each photo, after declaring your array

    $scope.photos.forEach(function(onePhoto){
        onePhoto.randomValue = Math.random();
      });
    

    then in the filter you would check against this value

    if (data[i].randomValue > 0.8) {
    }
    

    and set a minimum of 1 column when creating columns

    var n_columns = Math.max(1, Math.floor(size / (2 * (COLUMN_WIDTH + MARGIN))));
    

    Also (but I believe this occured only in your fiddle), there was no photo_name to filter for, so I used id instead.

    you might want to fix the NaN problem with other default value, but at least now you know the problems with the console error.

    If you wanted to update your randomValue everytime you executed the search, you could place a $watch over your input.value, move the code that iterates photos and creates random values into a function, and use that in the callback for that watch function. So everytime you update results in search, your grid uses different random values without causing interfering with digest cycle. Something like this

    var updateRandomValues = function() {
        $scope.photos.forEach(function(onePhoto){
            onePhoto.randomValue = Math.random();
          });
      };
      updateRandomValues();
      $scope.$watch('input.value', function(newVal, oldVal) {
        if (newVal !== oldVal) {
            updateRandomValues();
        }
      });
    

    However, if you want to get different css style only when you get different results (keep in mind that if you type and get same results as before, it will update your grid layout anyway), what you should $watch is the results variable instead. Like this

    $scope.$watch('results', function(newVal, oldVal) {
        if (newVal !== oldVal) {
            updateRandomValues();
        }
      });