web-dev-qa-db-ja.com

外部依存関係のないAngularJS Group Byディレクティブ

Angularが初めてで、問題を処理する最良の方法を学びたいと思っています。私の目標は、ヘッダーごとにグループを作成するための再利用可能な手段を持つことです。これはコントローラー内のスコープ関数ではなくディレクティブであるべきだと思いますが、これをどのように実現するか、ディレクティブが正しい方法であるかどうかはわかりません。

jsFiddle に取り組んでいる現在のアプローチをご覧ください

HTMLでは、ng-showでnewGrouping()関数を呼び出すng-repeatを使用した単純なリストです。この関数は、完全なリスト、グループ化するフィールド、および現在のインデックスへの参照を渡します。

<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>

私のコントローラーには、最初の項目を除き、現在の値と前の値を単純に比較し、一致に応じてtrueまたはfalseを返すnewGrouping()関数があります。

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;
  }
  };
}

出力は次のようになります。

グループ1

  • abc
  • def

グループ2

  • jkl
  • mno

もっと良い方法があるはずだと感じています。これを再利用できる一般的なユーティリティ関数にしたいです。これはディレクティブである必要がありますか?完全なリストと現在のインデックスを渡す方法よりも、リスト内の前のアイテムを参照するより良い方法はありますか?これの指示にどのようにアプローチしますか?

どんなアドバイスも大歓迎です。

更新:外部依存関係を必要としない回答を探しています。アンダースコア/ロダッシュまたは角度フィルターモジュールを使用する適切なソリューションがあります。

ダリル

27
Darryl

これは、上記のダリルのソリューションを修正したもので、複数のグループごとのパラメーターを使用できます。さらに、$ parseを使用して、ネストされたプロパティをパラメーターごとのグループとして使用できるようにします。

複数のネストされたパラメーターを使用する例

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>  

フィルター(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

すでに LoDash /Underscore、または機能ライブラリを使用している場合は、_。groupBy()(または同様の名前の)関数を使用してこれを行うことができます。


コントローラ内

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');

テンプレート内

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

これによりレンダリングされます

英語

  • 明日の端
  • エックスメン

テルグ語

  • ギャバーシン2
  • レス・グララム

さらに良いことに、これは非常に簡単にフィルターに変換することができ、プロパティごとに要素をグループ化するボイラープレートコードはあまり必要ありません。

更新:複数のキーでグループ化

多くの場合、複数のキーを使用したグループ化は非常に便利です。例、LoDashを使用( source ):

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

このアプローチを推奨する理由の更新:ng-repeat/ng-optionsでフィルターを使用すると、そのフィルターが迅速に実行されない限り、重大なパフォーマンスの問題が発生します。フィルターのパフォーマンスの問題についてはGoogle。わかるよ!

23
manikanta

Ng-repeat内でグループ化を処理することに最終的に決めたのは次のとおりです。ディレクティブとフィルターの詳細を読みましたが、どちらでもこの問題を解決できますが、フィルターのアプローチがより良い選択のように思えました。その理由は、データのみを操作する必要がある状況により適しているためです。ディレクティブは、DOM操作が必要な場合に優れています。この例では、データを操作してDOMをそのままにしておくだけで十分です。これが最大の柔軟性を与えると感じました。

jsFiddle で作業するグループ化への私の最終アプローチを参照してください。また、データを動的に追加するときにリストがどのように機能するかを示す小さなフォームを追加しました。

これが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>

これが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;
    };
})

これを使用しているアプリケーションでは、フィルターをアプリ全体で再利用可能なフィルターとして設定します。

ディレクティブアプローチについて気に入らなかったのは、HTMLがディレクティブに含まれていたため、再利用できないと感じたということです。

以前のフィルターアプローチは好きでしたが、ダイジェストサイクルごとにリストを2回走査する必要があるため、効率的とは思えませんでした。私は長いリストを扱っているので、問題になるかもしれません。さらに、前のアイテムが変更されたかどうかを確認するための単純なチェックほど直感的ではなかったようです。さらに、複数のフィールドに対してフィルターを簡単に使用できるようにしたかったのですが、この新しいフィルターは、別のフィールド名でフィルターに再度パイプするだけで処理できます。

GroupByフィルターに関するもう1つのコメント-複数のグループ化により配列が複数回走査されることがわかっているので、複数のgroup byフィールドの配列を受け入れるように修正し、配列を1回走査するだけで済むようにする予定です。 。

入力をどうもありがとう。これは、Angularのディレクティブとフィルターについてさらに学ぶのに本当に役立ちました。

乾杯、ダリル

4
Darryl

同じビューの同じデータセットに複数のフィルターがある場合、JoshMBによるコードは正しく機能しません。データセットのフィルターバージョンを2回目にグループ化すると、元のオブジェクトの同じ属性が変更されるため、以前のフィルターバージョンのグループブレークが解除されます。

これを解決するには、「CHANGED」属性の名前を追加のフィルターパラメーターとして追加しました。以下は私のコードの更新バージョンです。

/*
 * 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

以下は、ディレクティブベースのソリューションと、それをデモするJSFiddleへのリンクです。このディレクティブにより、各インスタンスはグループ化するアイテムのフィールド名を指定できるため、2つの異なるフィールドを使用する例があります。アイテムの数に線形ランタイムがあります。

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には、情報のグループを表示するのに役立つ3つのディレクティブがあります。これらのディレクティブは、ngRepeat、ngRepeatStart、およびngRepeatEndです。 AngularJSのグループを表示 の方法を示すブログ投稿を見つけました。その要点は次のようなものです。

<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>

それらを使用する方法を学ぶとかなり強力なディレクティブ。

1
user3284007

編集:ここにカスタムフィルターアプローチがあります。 Groupsは、スコープ内のフィルター関数によって作成され、現在のリストからグループの配列を生成します。リストアイテムを追加/削除すると、ダイジェストサイクルごとにリセットされるため、グループ配列の更新がバインドされます。

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