AngularJSでアコーディオンパネルを作成

目的

いわゆるアコーディオンパネルが欲しかったのですが、MEANスタックなんて言葉を聞いてちょっと興味があったので、AngularJSで作ってみました。これだっというサンプルが意外に見つからなかったので一応メモに残しておこうと思った次第です。要件はおおむね次の通りです。

  • 複数の要素が存在
  • 各要素の表題?部分をクリックすると開閉する
  • ある要素を開いた時は、他の要素はすべて閉じる(常に一つの要素しか開かない)

やっていないこと

  • 開閉アニメーション(CSSを少し書き換えるだけ、のはず)
  • 自動スクロール(要調査)

サンプルコード

codepen.io

ポイントは以下の3つです。

  1. 双方向データバインディングによるパネルの自動生成
  2. ng-classを使った、条件指定によるclassの付け外し
  3. $indexによる選択位置の特定

一つ目。バージョン1.2から使える機能のようですが、ng-repeat-start, ng-repeat-end*1の組み合わせで複数要素(ここではddとdt)のバインディングができます。後はjs側で$scope.items配列にデータを放り込んでいけば順次dd, dtが追加されていきます。

二つ目。変化も含め見た目関連は極力CSSに…ということで、展開・折りたたみはクラスの付け外しによって行います。jQueryであればremoveClass, addClassとするところですが、ng-classを使って$scope.isSelected()関数の結果がfalseのときだけnot-selectedクラスをつけるようにします。ちなみに、あちこちで見かけたサンプルではクラス名をそのまま書いてあったのですが、手元ではシングルクォーテーションで囲わないとなぜか動きませんでした。かなりハマったのですがどういうことなんでしょう…。

三つ目。二つ目に書いた$scope.isSelected()関数でもしれっと使ってますが、html側では$indexで自身がitemsの何番目の要素から作られたかを取得することができます。これを利用して、$scope.toggleSelected()関数では$scope.selectedIdに選択されたインデックスを格納します(前回と同じ場合は-1を入れて未選択を表しています)。$scope.isSelected()ではこの格納されたインデックスと自身のインデックスが同じかを判定しています。

参考にしたサイト

感想

AngularJSはデータバインディングなど中々いいものでした。ただ、ブラックボックス感がものすごく強いので、どこにフレームワークの壁(それ以上進むには深い理解が必要な箇所)が転がっているのかという恐怖心が常につきまといますね。jQueryを触っていた時は、もう少し苦労する代わりにそういった恐怖心は余り感じませんでしたが。

余談

上記は素のhtmlで書いていますが、元は(CL-Markup)https://github.com/arielnetworks/cl-markupを使った以下のCommon Lispコードから生成しました。なお、生成後のhtmlはサンプル用に少し書き直したので1対1対応にはなっていません。

(defun test-angular-html ()
  (with-markup-to-string
    (html5 :ng-app "testApp"
           (:head
            (:title "Test AngularJS")
            (:script :src "https://ajax.googleapis.com/ajax/libs/angularjs/1.2.27/angular.min.js" nil)
            (:script :src "js/test-angular.js" nil)
            (:link :rel "stylesheet" :type "text/css" :href "css/main.css"))
           (:body
            (:div (:a :href "/" "Top"))
            (:div :ng-controller "testController"
                  "{{test}}" (:br)
                  "selected-id = {{selectedId}}" (:br)
                  (:button :ng-click "getTestData()" "Get test data") (:br)
                  (:dl
                   (:dt :ng-click "toggleSelected($index)" :ng-repeat-start "item in items" "{{item.name}}")
                   (:dd :ng-class "{'not-selected': !isSelected($index)}" :ng-repeat-end nil "{{item.mail}}")))))))

eshamster/caveman-sample · GitHub

*1:念のため、ng-repeat自体はそれ以前からある機能