Angularでツリービューを作成することは可能ですか?


177

Webアプリでツリー構造のデータを表示したいと考えています。この作業にはAngularを使用したいと思っていました。

ng-repeatではノードのリストを反復処理できるように見えますが、特定のノードの深さが増加したときにネストを行うにはどうすればよいですか?

次のコードを試してみましたが、HTMLの自動エスケープによりこれが機能しなくなりました。さらに、ul終了タグが間違った場所にあります。

私はこの問題について完全に間違った方法で進んでいると確信しています。

何か案は?


:私は別の質問の上ではかなり一般的な方法でこれに答えstackoverflow.com/questions/14430655/...
tilgovi

回答:


231

このフィドルを見てください

オリジナル:http : //jsfiddle.net/brendanowen/uXbn6/8/

更新:http : //jsfiddle.net/animaxf/uXbn6/4779/

これは、tree like structure角度を使用して表示する方法の良いアイデアを与えるはずです。htmlで再帰を使用するようなものです!


94
なぜあなたの情報源を述べないのですか?あなたはそのスレッドで投稿を書いていて、今あなたは自分の名前を含むURLをここに投稿していますか?
Janus Troelsen 2012

5
これは同じバージョンです(私が思うに)。CSSセクションにインライン化されたTwitter Bootstrapがないため、(少なくとも私にとっては)非常に高速にロードされます。jsfiddle.net/brendanowen/uXbn6/8
KajMagnus

10
あなたはあなたのソースを述べる必要がありますね。
Ajax3.14 2013

46
私はこのURLに私の名前が含まれているとコメントする人が多いので本当にうんざりしていました(そのため、盗用です!)。残念ながら、これはjsfiddleの仕組みです。ログイン中に何かをフォークした場合、ユーザー名は保持されます。とはいえ、元のURLにリンクしました。間違っている場合は回答に反対票を投じる-このシナリオでは、たまたま私が持っていたバックアップURLに自分の名前が含まれているように見えるので、回答は正解です。
ganaraj 2013

5
私はあなたのバージョンに折りたたみと展開ボタンを追加しました:jsfiddle.net/uXbn6/639
jbaylina '28

77

ブートストラップCSSを使用している場合...

ブートストラップの「nav」リストに基づいて、AngularJS用の再利用可能なシンプルなツリーコントロール(ディレクティブ)を作成しました。字下げ、アイコン、アニメーションを追加しました。構成にはHTML属性が使用されます。

再帰は使用しません。

私はそれをangular-bootstrap-nav-treeと呼びました(キャッチーな名前、あなたは思いませんか?)

ここに例があり、ソースはここにあります


1
それは美しいですが、Angular 1.0.xブランチでは動作しないことに注意してください。
ダニタ2013

3
はい、新しいアニメーションのものを使用しています... Angular 1.1.5が必要です(そう思いますか?)
Nick Perkins

3
更新:Angular 1.1.5またはAngular 1.2.0で動作し、Bootsrap 2またはBootstrap 3でも動作します
Nick Perkins

1
参考までに、Bowerを使用している場合、Nickはこれを簡単にインストールできるようになりました。
arcseldon 2014年

2
@ニック・パーキンス-あなたのangular-bootstrap-nav-treeにブランチ/ノードを削除するためのAPIがない理由を説明してください。少なくとも、ソースの簡単な検査とテスト/例のチェックから、そのオプションはないようです。これは決定的な省略ですよね?
arcseldon 2014年

35

このようなものを作るときの最善の解決策は、再帰的なディレクティブです。ただし、このようなディレクティブを作成すると、AngularJSが無限ループに入っていることがわかります。

これを解決するには、コンパイルイベント中にディレクティブが要素を削除し、手動でコンパイルしてリンクイベントに追加します。

私はこのスレッドこれについて知りました、この機能をサービスに抽象化しました

module.factory('RecursionHelper', ['$compile', function($compile){
    return {
        /**
         * Manually compiles the element, fixing the recursion loop.
         * @param element
         * @param [link] A post-link function, or an object with function(s) registered via pre and post properties.
         * @returns An object containing the linking functions.
         */
        compile: function(element, link){
            // Normalize the link parameter
            if(angular.isFunction(link)){
                link = { post: link };
            }

            // Break the recursion loop by removing the contents
            var contents = element.contents().remove();
            var compiledContents;
            return {
                pre: (link && link.pre) ? link.pre : null,
                /**
                 * Compiles and re-adds the contents
                 */
                post: function(scope, element){
                    // Compile the contents
                    if(!compiledContents){
                        compiledContents = $compile(contents);
                    }
                    // Re-add the compiled contents to the element
                    compiledContents(scope, function(clone){
                        element.append(clone);
                    });

                    // Call the post-linking function, if any
                    if(link && link.post){
                        link.post.apply(null, arguments);
                    }
                }
            };
        }
    };
}]);

このサービスを使用すると、ツリーディレクティブ(または他の再帰的ディレクティブ)を簡単に作成できます。ツリーディレクティブの例を次に示します。

module.directive("tree", function(RecursionHelper) {
    return {
        restrict: "E",
        scope: {family: '='},
        template: 
            '<p>{{ family.name }}</p>'+
            '<ul>' + 
                '<li ng-repeat="child in family.children">' + 
                    '<tree family="child"></tree>' +
                '</li>' +
            '</ul>',
        compile: function(element) {
            return RecursionHelper.compile(element);
        }
    };
});

これを見て プランカーをデモについては、を。私はこのソリューションが一番好きです:

  1. HTMLをよりクリーンにする特別なディレクティブは必要ありません。
  2. 再帰ロジックはRecursionHelperサービスに抽象化されているため、ディレクティブをクリーンに保つことができます。

更新:カスタムリンク関数のサポートが追加されました。


1
これはとてもきちんとしていて強力なようですが、なぜこれがangularjsのデフォルトの動作ではないのでしょうか?
ポール

このような「コンパイル」を使用する場合、スコープに属性を追加するにはどうすればよいですか?「リンク」機能は、「コンパイル」があると使用できなくなるようです...
ブライアンケント

1
@ bkent314これのサポートを追加しました。コンパイルが返すことができるのと同じ方法でリンク関数を受け入れるようになりました。また、サービスのGithubプロジェクトも作成しました。
Mark Lagendijk 2014

@MarkLagendijk非常に滑らかです!ディレクティブから再帰を抽象化するには、多くの賛成票を投じる価値があります。私が見たすべてのディレクティブは、そのロジックが混在しているとどうしようもなく複雑に見えます。RecursionHelperをトランスクルージョンで動作させる方法はありますか?
acjay 2014年

このタイプのソリューションでいくつかのデータをスローすることをお勧めします-はい、ほとんどすべての人が再帰的なディレクティブでツリーを実装しているので、簡単です。しかし、ng-repeat $ digestのように非常に遅いです-何百ものノードに到達すると、これは実行されません。
Artemiy 14


15

次に、再帰ディレクティブを使用した例を示します。http//jsfiddle.net/n8dPm/ https://groups.google.com/forum/#!topic/angular/vswXTes_FtM から取得

module.directive("tree", function($compile) {
return {
    restrict: "E",
    scope: {family: '='},
    template: 
        '<p>{{ family.name }}</p>'+
        '<ul>' + 
            '<li ng-repeat="child in family.children">' + 
                '<tree family="child"></tree>' +
            '</li>' +
        '</ul>',
    compile: function(tElement, tAttr) {
        var contents = tElement.contents().remove();
        var compiledContents;
        return function(scope, iElement, iAttr) {
            if(!compiledContents) {
                compiledContents = $compile(contents);
            }
            compiledContents(scope, function(clone, scope) {
                     iElement.append(clone); 
            });
        };
    }
};
});

私はこれを実験していて、私もトランスクルージョンを使いたいのですが、それは可能だと思いますか?
L.Trabacchin、2015


5

別の例は、元のソースに基づいており、サンプルツリー構造がすでに配置されており(IMOの動作を簡単に確認できます)、フィルターを使用してツリーを検索しています。

JSFiddle


4

非常に多くの優れた解決策がありますが、私はそれらすべてが何らかの方法で少し複雑すぎると感じています。

@Mark Lagendijkのオーナの単純さを再現したものを作成したかったのですが、ディレクティブでテンプレートを定義せずに、「ユーザー」がHTMLでテンプレートを作成できるようにしたいのですが...

https://github.com/stackfull/angular-tree-repeatなどから得たアイデアを使用して、プロジェクトを作成しました。 https //github.com/dotJEM/angular-tree

これにより、次のようにツリーを構築できます。

<ul dx-start-with="rootNode">
  <li ng-repeat="node in $dxPrior.nodes">
    {{ node.name }}
    <ul dx-connect="node"/>
  </li>
</ul>

構造の異なるツリーに対して複数のディレクティブを作成する必要があるよりも、どちらがきれいなのでしょうか。本質的に、上記のツリーを呼び出すことは少し誤りです。@ ganarajの「再帰テンプレート」のオーナからはるかに多くを取り出しますが、ツリーが必要なテンプレートを定義します。

(スクリプトタグベースのテンプレートを使用してこれを行うこともできますが、実際のツリーノードのすぐ外側に配置する必要があり、それでも少し不自然に感じます...)

別の選択肢のためにここに残しました...


更新:1.5の時点で、Angularで再帰的ディレクティブがいくぶんネイティブにサポートされるようになりました。これにより、dotjem / angular-treeのユースケースが大幅に絞り込まれます。
イェンス、

3

あなたはAngular-Ui-Tree でAngular-Tree-DnDサンプルを試すことができますが、私は編集しました、テーブル、グリッド、リストとの互換性。

  • できるドラッグ&ドロップ
  • リストの拡張関数ディレクティブ(next、prev、getChildren、...)
  • データをフィルタリングします。
  • OrderBy(バージョン)

ありがとうございました。ドラッグアンドドロップが必要でしたが、これが唯一の解決策のようです。
Doug

2

@ganarajに基づいての答えは、と@ dnc253さんの回答、私はちょうど、追加、削除、および機能を編集すると、選択したツリー構造のためのシンプルな『指示』を作りました。

Jsfiddle:http ://jsfiddle.net/yoshiokatsuneo/9dzsms7y/

HTML:

<script type="text/ng-template" id="tree_item_renderer.html">
    <div class="node"  ng-class="{selected: data.selected}" ng-click="select(data)">
        <span ng-click="data.hide=!data.hide" style="display:inline-block; width:10px;">
            <span ng-show="data.hide && data.nodes.length > 0" class="fa fa-caret-right">+</span>
            <span ng-show="!data.hide && data.nodes.length > 0" class="fa fa-caret-down">-</span>
        </span>
        <span ng-show="!data.editting" ng-dblclick="edit($event)" >{{data.name}}</span>
        <span ng-show="data.editting"><input ng-model="data.name" ng-blur="unedit()" ng-focus="f()"></input></span>
        <button ng-click="add(data)">Add node</button>
        <button ng-click="delete(data)" ng-show="data.parent">Delete node</button>
    </div>
    <ul ng-show="!data.hide" style="list-style-type: none; padding-left: 15px">
        <li ng-repeat="data in data.nodes">
            <recursive><sub-tree data="data"></sub-tree></recursive>
        </li>
    </ul>
</script>
<ul ng-app="Application" style="list-style-type: none; padding-left: 0">
    <tree data='{name: "Node", nodes: [],show:true}'></tree>
</ul>

JavaScript:

angular.module("myApp",[]);

/* https://stackoverflow.com/a/14657310/1309218 */
angular.module("myApp").
directive("recursive", function($compile) {
    return {
        restrict: "EACM",
        require: '^tree',
        priority: 100000,

        compile: function(tElement, tAttr) {
            var contents = tElement.contents().remove();
            var compiledContents;
            return function(scope, iElement, iAttr) {
                if(!compiledContents) {
                    compiledContents = $compile(contents);
                }
                compiledContents(scope, 
                                     function(clone) {
                         iElement.append(clone);
                                         });
            };
        }
    };
});

angular.module("myApp").
directive("subTree", function($timeout) {
    return {
        restrict: 'EA',
        require: '^tree',
        templateUrl: 'tree_item_renderer.html',
        scope: {
            data: '=',
        },
        link: function(scope, element, attrs, treeCtrl) {
            scope.select = function(){
                treeCtrl.select(scope.data);
            };
            scope.delete = function() {
                scope.data.parent.nodes.splice(scope.data.parent.nodes.indexOf(scope.data), 1);
            };
            scope.add = function() {
                var post = scope.data.nodes.length + 1;
                var newName = scope.data.name + '-' + post;
                scope.data.nodes.push({name: newName,nodes: [],show:true, parent: scope.data});
            };
            scope.edit = function(event){
                scope.data.editting = true;
                $timeout(function(){event.target.parentNode.querySelector('input').focus();});
            };
            scope.unedit = function(){
                scope.data.editting = false;
            };

        }
    };
});


angular.module("myApp").
directive("tree", function(){
    return {
        restrict: 'EA',
        template: '<sub-tree data="data" root="data"></sub-tree>',
        controller: function($scope){
            this.select = function(data){
                if($scope.selected){
                    $scope.selected.selected = false;
                }
                data.selected = true;
                $scope.selected = data;
            };
        },
        scope: {
            data: '=',
        }
    }
});

0

はい、間違いなく可能です。ここでの質問はおそらくAngular 1.xを想定していますが、将来の参照のためにAngular 2の例を含めています:

概念的には、再帰的なテンプレートを作成するだけです。

<ul>
    <li *for="#dir of directories">

        <span><input type="checkbox" [checked]="dir.checked" (click)="dir.check()"    /></span> 
        <span (click)="dir.toggle()">{{ dir.name }}</span>

        <div *if="dir.expanded">
            <ul *for="#file of dir.files">
                {{file}}
            </ul>
            <tree-view [directories]="dir.directories"></tree-view>
        </div>
    </li>
</ul>

次に、ツリーオブジェクトをテンプレートにバインドし、Angularにその魔法を働かせます。この概念は明らかにAngular 1.xにも適用できます。

ここに完全な例があります:http : //www.syntaxsuccess.com/viewarticle/recursive-treeview-in-angular-2.0


0

そのためにangular-recursion-injectorを使用できます:https : //github.com/knyga/angular-recursion-injector

コンディショニングを使用して無制限のネストを行うことができます。必要な場合にのみ再コンパイルを行い、正しい要素のみをコンパイルします。コードに魔法はありません。

<div class="node">
  <span>{{name}}</span>

  <node--recursion recursion-if="subNode" ng-model="subNode"></node--recursion>
</div>

他のソリューションよりも速くて簡単に機能するための1つの方法は、「-再帰」サフィックスです。


0

ツリー構造が大きい場合、Angular(1.4.xまで)は再帰的なテンプレートのレンダリングが非常に遅くなります。これらの提案をいくつか試した後、私は単純なHTML文字列を作成し、ng-bind-htmlそれして表示することになりました。もちろん、これはAngular機能を使用する方法ではありません

最小限の再帰関数を次に示します(最小限のHTMLを使用)。

function menu_tree(menu, prefix) {
    var html = '<div>' + prefix + menu.menu_name + ' - ' + menu.menu_desc + '</div>\n';
    if (!menu.items) return html;
    prefix += menu.menu_name + '/';
    for (var i=0; i<menu.items.length; ++i) {
        var item = menu.items[i];
        html += menu_tree(item, prefix);
    }
    return html;
}
// Generate the tree view and tell Angular to trust this HTML
$scope.html_menu = $sce.trustAsHtml(menu_tree(menu, ''));

テンプレートでは、次の1行のみが必要です。

<div ng-bind-html="html_menu"></div>

これにより、Angularのすべてのデータバインディングがバイパスされ、再帰的なテンプレートメソッドのほんの一部の時間でHTMLが表示されます。

次のようなメニュー構造(Linuxファイルシステムの部分的なファイルツリー)の場合:

menu = {menu_name: '', menu_desc: 'root', items: [
            {menu_name: 'bin', menu_desc: 'Essential command binaries', items: [
                {menu_name: 'arch', menu_desc: 'print machine architecture'},
                {menu_name: 'bash', menu_desc: 'GNU Bourne-Again SHell'},
                {menu_name: 'cat', menu_desc: 'concatenate and print files'},
                {menu_name: 'date', menu_desc: 'display or set date and time'},
                {menu_name: '...', menu_desc: 'other files'}
            ]},
            {menu_name: 'boot', menu_desc: 'Static files of the boot loader'},
            {menu_name: 'dev', menu_desc: 'Device files'},
            {menu_name: 'etc', menu_desc: 'Host-specific system configuration'},
            {menu_name: 'lib', menu_desc: 'Essential shared libraries and kernel modules'},
            {menu_name: 'media', menu_desc: 'Mount point for removable media'},
            {menu_name: 'mnt', menu_desc: 'Mount point for mounting a filesystem temporarily'},
            {menu_name: 'opt', menu_desc: 'Add-on application software packages'},
            {menu_name: 'sbin', menu_desc: 'Essential system binaries'},
            {menu_name: 'srv', menu_desc: 'Data for services provided by this system'},
            {menu_name: 'tmp', menu_desc: 'Temporary files'},
            {menu_name: 'usr', menu_desc: 'Secondary hierarchy', items: [
                {menu_name: 'bin', menu_desc: 'user utilities and applications'},
                {menu_name: 'include', menu_desc: ''},
                {menu_name: 'local', menu_desc: '', items: [
                    {menu_name: 'bin', menu_desc: 'local user binaries'},
                    {menu_name: 'games', menu_desc: 'local user games'}
                ]},
                {menu_name: 'sbin', menu_desc: ''},
                {menu_name: 'share', menu_desc: ''},
                {menu_name: '...', menu_desc: 'other files'}
            ]},
            {menu_name: 'var', menu_desc: 'Variable data'}
        ]
       }

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

- root
/bin - Essential command binaries
/bin/arch - print machine architecture
/bin/bash - GNU Bourne-Again SHell
/bin/cat - concatenate and print files
/bin/date - display or set date and time
/bin/... - other files
/boot - Static files of the boot loader
/dev - Device files
/etc - Host-specific system configuration
/lib - Essential shared libraries and kernel modules
/media - Mount point for removable media
/mnt - Mount point for mounting a filesystem temporarily
/opt - Add-on application software packages
/sbin - Essential system binaries
/srv - Data for services provided by this system
/tmp - Temporary files
/usr - Secondary hierarchy
/usr/bin - user utilities and applications
/usr/include -
/usr/local -
/usr/local/bin - local user binaries
/usr/local/games - local user games
/usr/sbin -
/usr/share -
/usr/... - other files
/var - Variable data

-3

複雑ではありません。

<div ng-app="Application" ng-controller="TreeController">
    <table>
        <thead>
            <tr>
                <th>col 1</th>
                <th>col 2</th>
                <th>col 3</th>
            </tr>
        </thead>
        <tbody ng-repeat="item in tree">
            <tr>
                <td>{{item.id}}</td>
                <td>{{item.fname}}</td>
                <td>{{item.lname}}</td>
            </tr>
            <tr ng-repeat="children in item.child">
                <td style="padding-left:15px;">{{children.id}}</td>
                <td>{{children.fname}}</td>
            </tr>
        </tbody>
     </table>
</div>

コントローラーコード:

angular.module("myApp", []).
controller("TreeController", ['$scope', function ($scope) {
    $scope.tree = [{
        id: 1,
        fname: "tree",
        child: [{
            id: 1,
            fname: "example"
        }],
        lname: "grid"
    }];


}]);
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.