web-dev-qa-db-fra.com

AngularJS Group By Directive sans dépendances externes

Je suis nouveau à Angular et je voudrais apprendre la meilleure façon de gérer un problème. Mon objectif est d'avoir un moyen réutilisable pour créer un groupe par en-têtes. J'ai créé une solution qui fonctionne, mais Je pense que cela devrait être une directive au lieu d'une fonction de portée dans mon contrôleur, mais je ne sais pas comment y parvenir, ou si une directive est même la bonne voie à suivre. Toutes les entrées seraient grandement appréciées.

Voir mon approche actuelle sur jsFiddle

Dans le HTML, c'est une simple liste utilisant ng-repeat où j'appelle ma fonction newGrouping () sur ng-show. La fonction transmet une référence à la liste complète, au champ que je souhaite regrouper et à l'index en cours.

<div ng-app>
<div ng-controller='TestGroupingCtlr'>
    <div ng-repeat='item in MyList'>
        <div ng-show="newGrouping($parent.MyList, 'GroupByFieldName', $index);">
            <h2>{{item.GroupByFieldName}}</h2>
        </div>
        {{item.whatever}}
    </div>
</div>
</div>

Dans mon contrôleur, j'ai ma fonction newGrouping () qui compare simplement le courant au précédent, sauf sur le premier élément, et renvoie vrai ou faux selon une correspondance.

function TestGroupingCtlr($scope) {

  $scope.MyList = [
    {GroupByFieldName:'Group 1', whatever:'abc'},
    {GroupByFieldName:'Group 1', whatever:'def'},
    {GroupByFieldName:'Group 2', whatever:'ghi'},
    {GroupByFieldName:'Group 2', whatever:'jkl'},
    {GroupByFieldName:'Group 2', whatever:'mno'}
  ];

  $scope.newGrouping = function(group_list, group_by, index) {
  if (index > 0) {
    prev = index - 1;
    if (group_list[prev][group_by] !== group_list[index][group_by]) {
      return true;
    } else {
      return false;
    }
  } else {
    return true;
  }
  };
}

La sortie ressemblera à ceci.

Groupe 1

  • abc
  • def

Groupe 2

  • ghi
  • jkl
  • mno

Il semble qu'il devrait y avoir un meilleur moyen. Je veux que ce soit une fonction d'utilité commune que je puisse réutiliser. Cela devrait-il être une directive? Existe-t-il un meilleur moyen de référencer l'élément précédent dans la liste que ma méthode de transmission de la liste complète et de l'index actuel? Comment pourrais-je aborder une directive pour cela?

Tout conseil est grandement appréciée.

MISE À JOUR: recherche d'une réponse qui ne nécessite pas de dépendances externes. Il existe de bonnes solutions en utilisant le soulignement/lodash ou le module de filtre angulaire.

Darryl

27
Darryl

Il s'agit d'une modification de la solution de Darryl ci-dessus, qui permet plusieurs groupes par paramètres. De plus, il utilise $ parse pour permettre l'utilisation de propriétés imbriquées en tant que paramètres de groupe par.

Exemple utilisant plusieurs paramètres imbriqués

http://jsfiddle.net/4Dpzj/6/

HTML

<h1>Multiple Grouping Parameters</h1>
<div ng-repeat="item in MyList  | orderBy:'groupfield' | groupBy:['groupfield', 'deep.category']">
    <h2 ng-show="item.group_by_CHANGED">{{item.groupfield}} {{item.deep.category}}</h2>
     <ul>
        <li>{{item.whatever}}</li>
     </ul>
</div>  

Filtre (Javascript)

app.filter('groupBy', ['$parse', function ($parse) {
    return function (list, group_by) {

        var filtered = [];
        var prev_item = null;
        var group_changed = false;
        // this is a new field which is added to each item where we append "_CHANGED"
        // to indicate a field change in the list
        //was var new_field = group_by + '_CHANGED'; - JB 12/17/2013
        var new_field = 'group_by_CHANGED';

        // loop through each item in the list
        angular.forEach(list, function (item) {

            group_changed = false;

            // if not the first item
            if (prev_item !== null) {

                // check if any of the group by field changed

                //force group_by into Array
                group_by = angular.isArray(group_by) ? group_by : [group_by];

                //check each group by parameter
                for (var i = 0, len = group_by.length; i < len; i++) {
                    if ($parse(group_by[i])(prev_item) !== $parse(group_by[i])(item)) {
                        group_changed = true;
                    }
                }


            }// otherwise we have the first item in the list which is new
            else {
                group_changed = true;
            }

            // if the group changed, then add a new field to the item
            // to indicate this
            if (group_changed) {
                item[new_field] = true;
            } else {
                item[new_field] = false;
            }

            filtered.Push(item);
            prev_item = item;

        });

        return filtered;
    };
}]);
34
JoshMB

Si vous utilisez déjà LoDash /Underscore, ou toute bibliothèque fonctionnelle, vous pouvez le faire en utilisant _. GroupBy () ( ou du même nom).


Dans le contrôleur :

var movies = [{"movieId":"1","movieName":"Edge of Tomorrow","lang":"English"},
              {"movieId":"2","movieName":"X-MEN","lang":"English"},
              {"movieId":"3","movieName":"Gabbar Singh 2","lang":"Telugu"},
              {"movieId":"4","movieName":"Resu Gurram","lang":"Telugu"}];
$scope.movies = _.groupBy(movies, 'lang');

Dans le modèle :

<ul ng-repeat="(lang, langMovs) in movies">{{lang}}
  <li ng-repeat="mov in langMovs">{{mov.movieName}}</li>
</ul>

Cela rend :

Anglais

  • Bord de demain
  • X MEN

Telugu

  • Gabbar Singh 2
  • Resu Gurram

Encore mieux, cela peut également être converti en un filtre très facilement, sans beaucoup de code passe-partout pour regrouper les éléments par une propriété.

Mise à jour: regrouper par plusieurs clés

Souvent, le regroupement à l'aide de plusieurs clés est très utile. Ex, en utilisant LoDash ( source ):

$scope.movies = _.groupBy(movies, function(m) {
    return m.lang+ "-" + m.movieName;
});

Mise à jour sur les raisons pour lesquelles je recommande cette approche: Utilisation de filtres sur ng-repeat/ng-options provoque de graves problèmes de performances à moins que ce filtre ne s'exécute rapidement. Google pour le problème de perf de filtres. Tu sauras!

23
manikanta

Voici ce que j'ai finalement décidé de gérer les regroupements dans ng-repeat. J'ai lu plus sur les directives et les filtres et bien que vous puissiez résoudre ce problème avec l'un ou l'autre, l'approche par filtre semblait être un meilleur choix. La raison en est que les filtres sont mieux adaptés aux situations où seules les données doivent être manipulées. Les directives sont meilleures lorsque des manipulations DOM sont nécessaires. Dans cet exemple, je n'avais vraiment besoin que de manipuler les données et de laisser le DOM tranquille. J'ai senti que cela donnait la plus grande flexibilité.

Voir mon approche finale des regroupements travaillant sur jsFiddle . J'ai également ajouté un petit formulaire pour montrer comment la liste fonctionnera lors de l'ajout dynamique de données.

Voici le HTML.

<div ng-app="myApp">
    <div ng-controller='TestGroupingCtlr'>
        <div ng-repeat="item in MyList  | orderBy:'groupfield' | groupBy:'groupfield'" >
            <h2 ng-show="item.groupfield_CHANGED">{{item.groupfield}}</h2>
            <ul>
                <li>{{item.whatever}}</li>
            </ul>
        </div>

        <form role="form" ng-submit="AddItem()">
            <input type="text" data-ng-model="item.groupfield" placeholder="Group">
            <input type="text" data-ng-model="item.whatever" placeholder="Item">
            <input class="btn" type="submit" value="Add Item">
        </form>
    </div>

</div>

Voici le Javascript.

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

app.controller('TestGroupingCtlr',function($scope) {

        $scope.MyList = [
            {groupfield: 'Group 1', whatever: 'abc'},
            {groupfield: 'Group 1', whatever: 'def'},
            {groupfield: 'Group 2', whatever: 'ghi'},
            {groupfield: 'Group 2', whatever: 'jkl'},
            {groupfield: 'Group 2', whatever: 'mno'}
        ];

        $scope.AddItem = function() {

            // add to our js object array
            $scope.MyList.Push({
            groupfield:$scope.item.groupfield,
                    whatever:$scope.item.whatever
            });
        };


    })


/*
 * groupBy
 *
 * Define when a group break occurs in a list of items
 *
 * @param {array}  the list of items
 * @param {String} then name of the field in the item from the list to group by
 * @returns {array} the list of items with an added field name named with "_new"
 *                  appended to the group by field name
 *
 * @example     <div ng-repeat="item in MyList  | groupBy:'groupfield'" >
 *              <h2 ng-if="item.groupfield_CHANGED">{{item.groupfield}}</h2>
 *
 *              Typically you'll want to include Angular's orderBy filter first
 */

app.filter('groupBy', function(){
    return function(list, group_by) {

    var filtered = [];
    var prev_item = null;
    var group_changed = false;
    // this is a new field which is added to each item where we append "_CHANGED"
    // to indicate a field change in the list
    var new_field = group_by + '_CHANGED';

    // loop through each item in the list
    angular.forEach(list, function(item) {

        group_changed = false;

        // if not the first item
        if (prev_item !== null) {

            // check if the group by field changed
            if (prev_item[group_by] !== item[group_by]) {
                group_changed = true;
            }

        // otherwise we have the first item in the list which is new
        } else {
            group_changed = true;
        }

        // if the group changed, then add a new field to the item
        // to indicate this
        if (group_changed) {
            item[new_field] = true;
        } else {
            item[new_field] = false;
        }

        filtered.Push(item);
        prev_item = item;

    });

    return filtered;
    };
})

Pour l'application dans laquelle j'utilise ceci, j'ai configuré le filtre comme un filtre réutilisable dans toute l'application.

Ce que je n'ai pas aimé dans l'approche directive, c'est que le code HTML était dans la directive, donc il ne semblait pas réutilisable.

J'ai aimé l'approche de filtrage précédente, mais elle ne semblait pas efficace car la liste devrait être parcourue deux fois au cours du cycle de digestion. Je traite de longues listes, donc cela pourrait être un problème. De plus, cela ne semblait pas aussi intuitif qu'une simple vérification par rapport à l'élément précédent pour voir s'il avait changé. De plus, je voulais pouvoir utiliser le filtre contre plusieurs champs facilement, ce que ce nouveau filtre gère simplement en redirigeant vers le filtre avec un autre nom de champ.

Un autre commentaire sur mon filtre groupBy - Je me rends compte que plusieurs regroupements entraîneraient la traversée du tableau plusieurs fois, donc je prévois de le réviser pour accepter un tableau de plusieurs groupes par champs afin qu'il ne traverse le tableau qu'une seule fois .

Merci beaucoup pour les entrées. Cela m'a vraiment aidé à en savoir plus sur les directives et les filtres dans Angular.

cheers, Darryl

4
Darryl

Le code de JoshMB ne fonctionnera pas correctement si vous avez plusieurs filtres sur le même ensemble de données dans la même vue. La deuxième fois que vous regroupez une version filtrée de l'ensemble de données, il modifie le même attribut dans l'objet d'origine, rompant ainsi les ruptures de groupe dans les versions précédemment filtrées.

J'ai résolu ce problème en ajoutant le nom de l'attribut "CHANGED" comme paramètre de filtre supplémentaire. Voici ma version mise à jour du code.

/*
 * groupBy
 *
 * Define when a group break occurs in a list of items
 *
 * @param {array}  the list of items
 * @param {String} then name of the field in the item from the list to group by
 * @param {String} then name boolean attribute that indicated the group changed for this filtered version of the set

 * @returns {array} the list of items with an added field name named with "_new"
 *                  appended to the group by field name
 *
 * @example     <div ng-repeat="item in MyList | filter:'a' | groupBy:'groupfield':'Agroup_CHANGED'" >
 *              <h2 ng-if="item.Agroupfield_CHANGED">{{item.groupfield}}</h2>
 *              <!-- now a differen filtered subset -->
 *              <div ng-repeat="item in MyList | filter:'b' | groupBy:'groupfield':'Bgroup_CHANGED'" >
 *              <h2 ng-if="item.Bgroupfield_CHANGED">{{item.groupfield}}</h2>
 *
 *              Typically you'll want to include Angular's orderBy filter first
 */

app.filter('groupBy', ['$parse', function ($parse) {
    return function (list, group_by, group_changed_attr) {

        var filtered = [];
        var prev_item = null;
        var group_changed = false;
        // this is a new field which is added to each item where we append "_CHANGED"
        // to indicate a field change in the list
        //var new_field = group_by + '_CHANGED'; //- JB 12/17/2013
        var new_field = 'group_by_CHANGED';
        if(group_changed_attr != undefined) new_field = group_changed_attr;  // we need this of we want to group different filtered versions of the same set of objects !

        // loop through each item in the list
        angular.forEach(list, function (item) {

            group_changed = false;

            // if not the first item
            if (prev_item !== null) {

                // check if any of the group by field changed

                //force group_by into Array
                group_by = angular.isArray(group_by) ? group_by : [group_by];

                //check each group by parameter
                for (var i = 0, len = group_by.length; i < len; i++) {
                    if ($parse(group_by[i])(prev_item) !== $parse(group_by[i])(item)) {
                        group_changed = true;
                    }
                }


            }// otherwise we have the first item in the list which is new
            else {
                group_changed = true;
            }

            // if the group changed, then add a new field to the item
            // to indicate this
            if (group_changed) {
                item[new_field] = true;
            } else {
                item[new_field] = false;
            }

            filtered.Push(item);
            prev_item = item;

        });

        return filtered;
    };
}]);
1
Matthijs

Vous trouverez ci-dessous une solution basée sur des directives, ainsi qu'un lien vers une démonstration JSFiddle. La directive permet à chaque instance de spécifier le nom de champ des éléments qu'elle doit regrouper, il existe donc un exemple utilisant deux champs différents. Il a un temps d'exécution linéaire dans le nombre d'éléments.

JSFiddle

<div ng-app='myApp'>
    <div ng-controller='TestGroupingCtlr'>
        <h1>Grouping by FirstFieldName</h1>
        <div group-with-headers to-group="MyList" group-by="FirstFieldName">
        </div>
        <h1>Grouping by SecondFieldName</h1>
        <div group-with-headers to-group="MyList" group-by="SecondFieldName">
        </div>
    </div>
</div>

angular.module('myApp', []).directive('groupWithHeaders', function() {
    return {
        template: "<div ng-repeat='(group, items) in groups'>" +
                    "<h2>{{group}}</h2>" +
                    "<div ng-repeat='item in items'>" +
                      "{{item.whatever}}" +   
                    "</div>" +
                  "</div>",
        scope: true,
        link: function(scope, element, attrs) {
            var to_group = scope.$eval(attrs.toGroup);
            scope.groups = {};
            for (var i = 0; i < to_group.length; i++) {
                var group = to_group[i][attrs.groupBy];
                if (group) {
                    if (scope.groups[group]) {
                        scope.groups[group].Push(to_group[i]);
                    } else {
                        scope.groups[group] = [to_group[i]];
                    }
                }    
            }
        }
      };
});

function TestGroupingCtlr($scope) {

  $scope.MyList = [
    {FirstFieldName:'Group 1', SecondFieldName:'Group a', whatever:'abc'},
    {FirstFieldName:'Group 1', SecondFieldName:'Group b', whatever:'def'},
    {FirstFieldName:'Group 2', SecondFieldName:'Group c', whatever:'ghi'},
    {FirstFieldName:'Group 2', SecondFieldName:'Group a', whatever:'jkl'},
    {FirstFieldName:'Group 2', SecondFieldName:'Group b', whatever:'mno'}
  ];
}
1
jelinson

AngularJS dispose de trois directives pour vous aider à afficher des groupes d'informations. Ces directives sont ngRepeat, ngRepeatStart et ngRepeatEnd. J'ai trouvé un article de blog qui montre comment afficher les groupes dans AngularJS . L'essentiel est quelque chose comme ceci:

<body ng-controller="OrdersCtrl">
  <div ng-repeat-start="customer in customers" class="header">{{customer.name}}</div>
  <div ng-repeat="order in customer.orders">{{order.total}} - {{order.description}}</div>
  <div ng-repeat-end><br /></div>
</body>

Des directives assez puissantes une fois que vous apprenez à les utiliser.

1
user3284007

EDIT: voici une approche de filtre personnalisé. Groups est créé par une fonction de filtre dans la portée pour générer un tableau de groupes à partir de la liste actuelle. L'ajout/la suppression d'éléments de liste liera la mise à jour du tableau de groupe car il est réinitialisé à chaque cycle de résumé.

HTML

<div ng-app="myApp">
    <div ng-controller='TestGroupingCtlr'>
        <div ng-repeat='group in getGroups()'>
             <h2>{{group}}</h2>
              <ul>
                <!-- could use another scope variable as predicate -->
                <li ng-repeat="item in MyList |  groupby:group">{{item.whatever}}</li>
            </ul>
        </div>
    </div>
</div>

JS

var app=angular.module('myApp',[]);
app.filter('groupby', function(){
    return function(items,group){       
       return items.filter(function(element, index, array) {
            return element.GroupByFieldName==group;
        });        
    }        
})        

app.controller('TestGroupingCtlr',function($scope) {

    $scope.MyList = [{  GroupByFieldName: 'Group 1', whatever: 'abc'},
                     {GroupByFieldName: 'Group 1',whatever: 'def'}, 
                     {GroupByFieldName: 'Group 2',whatever: 'ghi' },
                     {GroupByFieldName: 'Group 2',whatever: 'jkl'}, 
                     {GroupByFieldName: 'Group 2',whatever: 'mno'  }
                    ];
    $scope.getGroups = function () {
        var groupArray = [];
        angular.forEach($scope.MyList, function (item, idx) {
            if (groupArray.indexOf(item.GroupByFieldName) == -1)
              groupArray.Push(item.GroupByFieldName)
        });
        return groupArray.sort();
    }

})

DEMO

0
charlietfl

http://blog.csdn.net/Violet_day/article/details/17023219#t2

<!doctype html>  
<html ng-app>  
<head>  
    <script src="lib/angular/angular.min.js"></script>  
    <script>  
        function TestCtrl($scope) {  
            $scope.items = [  
                { id: 0, name: "Red"},  
                { id: 1, name: "Red"},  
                { id: 2, name: "Red"},  
                { id: 3, name: "Red"},  
                { id: 4, name: "Yellow"},  
                { id: 5, name: "Orange"}  
            ];  
        }  
    </script>  
</head>  
<body ng-controller="TestCtrl">  
<ul ng-repeat="a in items" ng-if="a.name!=items[$index-1].name">  
    {{ a.name }}  
    <li ng-repeat="b in items" ng-if="a.name==b.name">  
        {{ b.id }}  
    </li>  
</ul>  
</body>  
</html>  
0
Nemo