Условная разметка angular.js в ng-repeat

Я использую angular.js и (ради аргумента) bootstrap. Теперь мне нужно перебрать «вещи» и отобразить их в «строках»:

<div class="row">
  <div class="span4">...</div>
  <div class="span4">...</div>
  <div class="span4">...</div>
</div>
<div class="row">
  etc...

Теперь, как я могу закрыть свой .row div для каждой третьей вещи с помощью angular? Я пробовал ui-if из angular-ui, но даже это не помогло.

Если бы я использовал рендеринг на стороне сервера, я бы сделал что-то вроде этого (здесь синтаксис JSP, но это не имеет значения):

<div class="row>
  <c:forEach items="${things}" var="thing" varStatus="i">
    <div class="span4">
        ..
    </div>
  <%-- Here is the trick:--%>
  <c:if test="${i.index % 3 == 2}">
          </div><div class="row">
  </c:if>
  </c:forEach>
</div>

Обратите внимание, что здесь мне нужно фактически изменить DOM, а не только элементы, скрывающие css. Я пробовал с повтором в div .row и .span4, но безрезультатно.


person ebottard    schedule 02.01.2013    source источник
comment
напишите фильтр, который принимает число n в качестве аргумента, который разбивает массив на массив массивов из n элементов, а затем просто повторяет основной массив и повторяет подмассивы.   -  person mpm    schedule 02.01.2013


Ответы (3)


Редактировать 12 ноября 2013 г.

Кажется, что angular не только немного изменился в 1.2, но и есть еще лучший метод. Я создал два фильтра. Я попытался объединить их в один, но получил ошибки дайджеста. Вот два фильтра:

.filter("mySecondFilter", function(){
    return function(input, row, numColumns){
        var returnArray = [];
        for(var x = row * numColumns; x < row * numColumns + numColumns; x++){
            if(x < input.length){
                returnArray.push(input[x]);                    
            }
            else{
                returnArray.push(""); //this is used for the empty cells
            }
        }
        return returnArray;   
    }
})
.filter("myFilter", function(){
    return function(input, numColumns){
        var filtered = [];
        for(var x = 0; x < input.length; x++){
            if(x % numColumns === 0){
                filtered.push(filtered.length);
            }
        }
        return filtered;
    }
});

И теперь html будет выглядеть так:

<table border="1">
     <tr data-ng-repeat="rows in (objects | myFilter:numColumns)">
          <td data-ng-repeat="column in (objects | mySecondFilter:rows:numColumns)">{{ column.entry }}</td>
     </tr>  
</table>

jsFiddle: http://jsfiddle.net/W39Q2/


Изменить от 20 сентября 2013 г.

Работая с большим количеством данных, которым нужны динамические столбцы, я придумал лучший метод.

HTML:

<table border="1">
    <tr data-ng-repeat="object in (objects | myFilter:numColumns.length)">
        <td data-ng-repeat="column in numColumns">{{ objects[$parent.$index * numColumns.length + $index].entry }}</td>
    </tr>  
</table>

Javascript:

$scope.objects = [ ];
for(var x = 65; x < 91; x++){
    $scope.objects.push({
        entry: String.fromCharCode(x)
    });
}

$scope.numColumns = [];
$scope.numColumns.length = 3;

Новый фильтр:

.filter("myFilter", function(){
    return function(input, columns){
        var filtered = [];
        for(var x = 0; x < input.length; x+= columns){
             filtered.push(input[x]);   
        }
        return filtered;
    }
});

Это позволяет ему быть динамичным. Чтобы изменить столбцы, просто измените numColumns.length. В скрипте js вы можете видеть, что я подключил его к раскрывающемуся списку.

jsFiddle: http://jsfiddle.net/j4MPK/


Ваша html-разметка будет выглядеть так:

<div data-ng-repeat="row in rows">
    <div data-ng-repeat="col in row.col">{{col}}</div>
</div>

И тогда вы можете создать переменную в своем контроллере следующим образом:

$scope.rows = [
    {col: [ 1,2,3,4 ]},
    {col: [ 5,6,7 ]},
    {col: [ 9,10,11,12 ]}
]; 

Таким образом, вы можете иметь любое количество столбцов.

jsfiddle http://jsfiddle.net/rtCP3/39/


Изменить Я изменил скрипт, чтобы теперь он поддерживал плоский массив объектов:

jsfiddle: http://jsfiddle.net/rtCP3/41/

Теперь html выглядит так:

<div class="row" data-ng-repeat="row in rows">
    <div class="col" data-ng-repeat="col in cols">
        {{objects[$parent.$index * numColumns + $index].entry}}
    </div>
</div>  

И затем в контроллере у меня есть:

$scope.objects = [
    {entry: 'a'},
    {entry: 'b'},
    {entry: 'c'},
    {entry: 'd'},
    {entry: 'e'},
    {entry: 'f'},
    {entry: 'g'},
    {entry: 'h'}    
];

$scope.numColumns = 3;
$scope.rows = [];
$scope.rows.length = Math.ceil($scope.objects.length / $scope.numColumns);
$scope.cols = [];
$scope.cols.length = $scope.numColumns;

Переменная $scope.numColumns используется для указания количества столбцов в каждой строке.


Чтобы обрабатывать изменения размера динамического массива, следите за длиной массива (а не всего массива, это было бы избыточно)

$scope.numColumns = 3;  
$scope.rows = [];    
$scope.cols = [];    
$scope.$watch("objects.length", function(){
    $scope.rows.length = Math.ceil($scope.objects.length / $scope.numColumns);
    $scope.cols.length = $scope.numColumns;        
});

jsfiddle: http://jsfiddle.net/rtCP3/45/

person Mathew Berg    schedule 02.01.2013
comment
Я думаю, что наличие двухуровневого массива - единственное решение, благодаря вам и комментарию Камю. - person ebottard; 02.01.2013
comment
Привет, подождите, метод фильтра может не работать таким образом, скоро я приду с примером. - person mpm; 02.01.2013
comment
Кроме того, хотя это кажется хорошей идеей, я предполагаю, что это вызовет проблемы, когда я захочу, например, отсортировать свой список (который больше не будет плоским). - person ebottard; 02.01.2013
comment
Можете ли вы предоставить мне структуру списка, который вы ищете? Измените jsfiddle, который я опубликовал, если это необходимо. - person Mathew Berg; 02.01.2013
comment
Мой список — это просто массив объектов (и мне нужно будет отсортировать некоторые свойства этих объектов). Но тогда, я думаю, мне придется добавить логику сортировки|фильтрации внутри фильтра, который преобразует плоский список во вложенный список. - person ebottard; 02.01.2013
comment
Смотрите мое последнее редактирование. Я надеюсь, это то, что вы хотите. Если это не так, добавьте еще один комментарий :) - person Mathew Berg; 02.01.2013
comment
@ Мэтью, хорошее решение (+1). Я заметил, что если массив динамически растет (скажем, на 2 записи - я добавил для этого функцию в вашу скрипту и запустил ее с помощью <a ng-click="add2Items()">...), он не отображает последний элемент... потому что нечего делать. инициировать обновление свойств $scope. Я предполагаю, что вычисления свойства $scope можно было бы обернуть в функцию, и эту функцию нужно будет вызывать всякий раз, когда изменяется размер массива. - person Mark Rajcok; 03.01.2013
comment
Да, вам нужно будет обновить эти свойства, когда вы добавите больше. Впрочем, это не сложно, просто сделайте $watch на $scope.objects.length. Я добавил еще одну скрипту для отслеживания изменений размера динамического массива. - person Mathew Berg; 03.01.2013
comment
Хорошее креативное решение для небольших статичных коллекций. Однако сортировка/фильтрация этого списка не будет работать (естественно, поскольку ng-repeat модерирует строки и столбцы по сравнению с самими элементами массива). - person Sean O; 10.04.2013
comment
Вы правы, но вы можете наблюдать за всем объектом, а затем выполнять фильтрацию/сортировку, а затем сразу после этого выполнять разделение строк/столбцов. Честно говоря, это звучит все больше и больше похоже на директиву! - person Mathew Berg; 11.04.2013
comment
К сожалению, angular.js не может решить базовую проблему компоновки без утечки информации о пользовательском интерфейсе в контроллер. Я думал, что избежал действительно простых вещей, таких как вложенные циклы, которые не работали, когда я отказался от JSF, но похоже, что в angular.js вы не можете преобразовать плоский массив в куски длины ‹ = n и перебрать кусок в двух- вложенный цикл уровня. Angular.js просто выплюнет: Error: 10 $digest() iterations reached. Aborting! - person Distortum; 29.04.2013
comment
О, это был просто пример, чтобы показать, как это сделать. Я бы на самом деле подключил директиву (где вы должны манипулировать домом) и заставил бы ее разобраться там. - person Mathew Berg; 29.04.2013
comment
Это решение больше не работает с Angular›=1.2. Оно выдает ошибку дублирования. Вам нужно будет заполнить массивы строк и столбцов фиктивными значениями. - person Tinou; 13.11.2013
comment
@MathewBerg, да, это работает! Почему вы не использовали свое первое выражение rows.length повторно вместо использования по модулю? Что-то вроде этого: jsfiddle.net/M785Y/1 - person Tinou; 13.11.2013
comment
@Tinou, потому что я немного возился с этим. Много способов содрать шкуру с кота :) - person Mathew Berg; 13.11.2013

Почему бы не использовать что-то простое вроде этого? http://jsfiddle.net/everdimension/ECCL7/3/

<div ng-controller="MyCtrl as ctr">
    <div class="row" ng-repeat="project in ctr.projects" ng-if="$index % 3 == 0">
         <h4 class="col-sm-4" ng-repeat="project in ctr.projects.slice($index, $index+3)">
            {{project}}
        </h4>
    </div>
</div>
person timetowonder    schedule 25.05.2014

Я рекомендую директиву по нескольким причинам:

  • его можно повторно использовать и параметризовать в HTML (т. е. «каждая третья вещь» может быть атрибутом директивы)
  • он не требует каких-либо свойств кода/$scope контроллера и, следовательно, не требует пересчета свойств контроллера $scope, если размер массива «вещи» изменяется.

Вот рекомендуемая директива элемента:

<row-generator row-data=objects col-count=3></row-generator>

В реализации я использовал код, аналогичный вашему серверному примеру:

myApp.directive('rowGenerator', function() {
    var rowTemplate = '<div class="row">',
        colTemplate = '<div class="span4">';
    return {
        restrict: 'E',
        // use '=' for colCount instead of '@' so that we don't
        // have to use attr.$observe() in the link function
        scope: { rowData: '=', colCount: '='},
        link: function(scope, element) {
            // To save CPU time, we'll just watch the overall
            // length of the rowData array for changes.  You
            // may need to watch more.
            scope.$watch('rowData.length', function(value) {
                var html = rowTemplate;
                for(var i=0; i < scope.rowData.length; i++) {
                    html += colTemplate + scope.rowData[i].key + '</div>';
                    if (i % scope.colCount == scope.colCount - 1) {
                        html += '</div>' + rowTemplate;
                    }
                }
                html += '</div>';
                // don't use replaceWith() as the $watch 
                // above will not work -- use html() instead
                element.html(html);
            })
        }
    }
});

Данные:

$scope.things = [
    {key: 'one'},
    {key: 'two'},
    {key: 3},
    {key: 4},
    {key: 'five'},
    {key: 'six'},
    {key: 77},
    {key: 8}    
];

Скрипка

Чтобы быть эффективным, показанная директива ищет только изменения длины массива rowData (т. е. вещей). Если вы хотите, чтобы директива обновляла представление при изменении значения одного из элементов массива, вам понадобится более дорогой $watch:

scope.$watch('rowData', function(value){ ... }, true)

true в конце выполняет «поверхностную» грязную проверку («сравнивает объект на равенство, а не на ссылку») — см. документы.

Есть одна вещь, которая мне не нравится в этой директиве — она должна знать, что записи rowData имеют свойство с именем key:

html += colTemplate + scope.rowData[i].key + '</div>';
person Mark Rajcok    schedule 03.01.2013
comment
Любое предложение по получению значения из вложенного HTML в row-generator вместо свойства key в каждом объекте в списке? - person Sarah Vessels; 20.11.2013