AngularJSで$templateCacheを使ってまとめたテンプレートJSの中のCSSファイルや画像ファイルのリンクにhashをつける
grunt-filerevを使って、CSSファイルや画像ファイルにhashがbuild時につくようにしている場合に、grunt-angular-templatesを使う場合はどうするか
generator-angularで作ったAngularJSプロジェクトのbuildタスクではgrunt-useminの処理中、grunt-filerevの処理が入って、CSSファイルや画像ファイル、JSファイルにhashがつく。 要するにRailsのAssett Pipelineで処理したときによくみるあれと同様。
こんなやつ
index.html
<link rel="stylesheet" href="styles/main.b1c3621e.css"> <script src="scripts/scripts.a241652b.js">
grunt-angular-templatesを入れて何も考えずに素直にタスクに組み入れると、テンプレートキャッシュの中に画像などへの参照があった場合に、当然、このhashを付与する流れに入っていないので、hashが付与されないリンクのままでbuildされ、リンク切れという事になってしまう。
↓useminでのgrunt-filerevを考慮せずタスクに追加
#Gruntfile.coffee #...省略... # 追加 grunt.loadNpmTasks("grunt-angular-templates") #...省略... # 追加 ngtemplates: gambaApp: cwd: "<%= yeoman.app %>" src: "templates/*.html" dest: ".tmp/scripts/templates.js" options: collapseWhitespace: true conservativeCollapse: true collapseBooleanAttributes: true removeCommentsFromCDATA: true removeOptionalTags: true usemin: "scripts/scripts.js" #...省略... grunt.registerTask "build", [ "clean:dist" "wiredep" "useminPrepare" "ngtemplates" #<- 追加 "concurrent:dist" "autoprefixer" "concat" "ngAnnotate" "copy:dist" "cdnify" "cssmin" "uglify" "filerev" "usemin" "htmlmin" ]
index.html
<!-- build:js({.tmp,app}) scripts/scripts.js --> <script src="scripts/app.js"></script> <script src="scripts/templates.js"></script> <!- 追加↑ -->
ので、useminタスクに以下のように追加した。
#Gruntfile.coffee # Performs rewrites based on filerev and the useminPrepare configuration usemin: js: ["<%= yeoman.dist %>/scripts/scripts*.js"] #<- 追加 html: ["<%= yeoman.dist %>/{,*/}*.html"] css: ["<%= yeoman.dist %>/styles/{,*/}*.css"] options: assetsDirs: [ "<%= yeoman.dist %>" "<%= yeoman.dist %>/images" ] # patternsを追加して、画像のリンクにhashがつくようにする patterns: js: [ [/(hogefuga\.jpg)/g, "Replacing reference to hogefuga.jpg"] ]
自分で一からGruntfileでタスクを準備していった場合は、こういった部分も考慮にいれて使うパッケージを検討、タスクの順番や構成を組むだろうが、あとから追加していこうとすると、最初から色々用意したあったタスクやnpmのパッケージとの兼ね合いで若干面倒。
色々なnpmを使って、タスクをカスタマイズしまくるような場合はgenerator-angularなどを使用せず、面倒でも一からタスクを吟味して組んでいった方が幸せになれるかもしれない。
AngularJSで$templateCacheを使っている場合のテスト環境
ちなみにgenerator-angularで作成したプロジェクト。
developmentやbuildではgrunt-angular-templatesを使っているが、テストには、karma-ng-html2js-preprocessorを使った。
インストール
% npm install karma-ng-html2js-preprocessor --save-dev
karma.confに設定を追加
#karma.conf.coffee files: [ # ...省略... "app/templates/*.html" #<- テンプレートファイルを追加 "test/mock/**/*.coffee" "test/spec/**/*.coffee" ] plugins: [ "karma-phantomjs-launcher" "karma-jasmine" "karma-coffee-preprocessor" "karma-spec-reporter" "karma-coverage" "karma-ng-html2js-preprocessor" #<- 追加 ] preprocessors: "app/templates/*.html": ["ng-html2js"] # <- 追加 "app/scripts/*.coffee": "coffee" "app/scripts/*/**/*.coffee": "coverage" "test/**/*.coffee": "coffee" # 追加 ngHtml2JsPreprocessor: stripPrefix: "app/" moduleName: "test.templates" #<- 本番とは別に任意に設定した
あとはtemplateCacheのテンプレートを使っている部分のテストで beforeEach module "myApp", "test.templates"とか読みこめばOK.
参考
AngularJSのカステムディレクティブで別のディレクティブをrequireしている場合のテストの書き方
前の記事のでいえばchildItemディレクティブのテストの書き方
コード再掲
#parentChild.coffee "use strict" #parent #controller angular.module("myApp") .controller("ParentItemCtrl", ($scope, Child) -> #....... @removeChildItem = (childId, index)-> Child.destroy({childId: childId}, -> $scope.parent.cildren.splice(index, 1) ) ) #parent directive angular.module("myApp") .directive("parentItem", -> restrict: "EA" templateUrl: "templates/parent_item.html" controller: "ParentItemCtrl" scope: parent: "=item" ) #child directive angular.module("myApp") .directive("childItem", ($modal)-> restrict: "EA" templateUrl: "templates/child_item.html" require: "^parentItem" scope: child: "=item" link: (scope, element, attrs, parentItemCtrl) -> scope.open = ()-> modalInstance = $modal.open({ templateUrl: "/remplates/modal_confirm.html" }) modalInstance.result.then(-> parentItemCtrl.removeChildItem(scope.child.id, scope.$index) ) )
こんな感じでテストを書いた
#child_item_spec.coffee "use strict" describe "Directive: childItem", -> beforeEach module "myApp", "test.templates" scope = {} beforeEach inject ($controller, $rootScope) -> scope = $rootScope.$new() it "テンプレートに所定のデータが表示されること", inject ($compile) -> scope.child = { "id": 406 "content": "child4" "created_at": "2014-09-23T11:16:23.609+09:00" "user": "id": 5018 "profile": "name": "あい うえお" } element = angular.element '''<child-item item="child"></child-item>''' parentItemController = { removeChildItem: -> return } spyOn(parentItemController, "removeChildItem").andCallThrough() element.data("$parentItemController", parentItemController) element = $compile(element)(scope) childItemScope = element.find("child-item").scope() scope.$digest() expect(element.find(".child-user-name").text()).toEqual "あい うえお"
ChromeのDeveloper Toolsで確認するとわかるが、'$' + ディレクティブの名前 + 'Controller'という名前でcontrollerを取得しているので、それにあわせてmockを用意してあげればOK.
ちなみにディレクティブのhtmlは$templateCacheを使っていて、beforeEach module "myApp", "test.templates"の部分で、テスト用にテンプレートをJSにまとめたものを読み込んでいる。こちらについては別のエントリに書く。→書いた
参考
AngularJSを1.2系から1.3.0にあげたらrequireした親のコントローラを使っているdirectiveでエラーが出た
コードはこんな
"use strict" #parent #controller angular.module("myApp") .controller("ParentItemCtrl", ($scope,Child) -> #....... coffeeなので自動で波括弧をつけてくれるが明示的につけておいた ............ { removeChildItem: (childId, index)-> Child.destroy({childId: childId}, -> $scope.parent.children.splice(index, 1) ) } ) #directive angular.module("myApp") .directive("parentItem", -> restrict: "EA" templateUrl: "templates/parent_item.html" controller: "ParentItemCtrl" scope: parent: "=item" ) #child directive angular.module("myApp") .directive("childItem", ($modal)-> restrict: "EA" templateUrl: "templates/child_item.html" require: "^parentItem" scope: child: "=item" link: (scope, element, attrs, parentItemCtrl) -> scope.open = ()-> modalInstance = $modal.open({ templateUrl: "/remplates/modal_confirm.html" }) modalInstance.result.then(-> parentItemCtrl.removeChildItem(scope.child.id, scope.$index) # ↑ ここでremoveChildItemがないとエラー ) )
controllerの関数で公開したいものだけ(この場合はremoveChildItem)をモジュールパターンを使って返していて、1.2系では動いていたけど、1.3では動かなくなった。 chromeのDeveloper Toolsで確認すると、parentItemCtrlは、1.2系では期待通りオブジェクトがわたってきていたが、1.3では$get.Constructorとなっていた。
↓1.2
↓1.3
ので、コントローラを以下のように変更した。
# parent_item.coffee angular.module("myApp") .controller("ParentItemCtrl", ($scope, Child) -> @removeChildItem = (childId, index)-> Child.destroy({childId: childId}, -> $scope.parent.cildren.splice(index, 1) ) )
AngularJSでネストしたコントローラをテストする
前提
AngularJSでコントローラをネストして階層化すると、子コントローラ側から親コントローラのスコープを利用可能になる。
問題
子コントローラのユニットテストを書こうとした場合に、子コントローラ側で親コントローラのプロパティやメソッドを使用していると、そのままではテストが書けない。
解決
以下のように記述した。
ReportShowCtrlが親コントローラ、CommentNewCtrlが子コントローラ。
# comment_new_spec.coffee "use strict" describe "Controller: CommentNewCtrl", -> beforeEach module "myApp" CommentNewCtrl = {} scope = {} Comment = {} beforeEach inject ($controller, $rootScope) -> $controller("ReportShowCtrl", { $scope: $rootScope report: {id: 1} }) scope = $rootScope.$new() CommentNewCtrl = $controller "CommentNewCtrl", { $scope: scope } it "some test", -> # do something
そもそも論としてネストしたコントローラで、親のプロパティやメソッドが〜、というのは可読性やテスタビリティが下がるのでアレではある。
派生したスコープでのハマリもあるし。
AngularJSのフォームで投稿後に再利用する際手付かずの状態に戻したい
例えば記事へのコメント投稿後に、そのフォームを再利用する際に、データを初期化しただけだとエラー表示が出てしまう。
ので、$dirtyがfalse($pristineがtrue)の状態に戻したい。
いわゆるブログのコメント投稿とかのよくあるやつ。
form.FormControllerの$setPristine()を使う。
# comment_new.html <div ng-controller="CommentNewCtrl"> <form name="commentNewForm"> <div class="form-group" ng-class="{'has-error': commentNewForm.content.$dirty && commentNewForm.content.$invalid }"> <textarea class="form-control" ng-model="comment.content" placeholder="" tabindex="1" name="content" required></textarea> <span ng-show="commentNewForm.content.$dirty&&commentNewForm.content.$error.required"> 必須です </span> </div> <button ng-disabled="!commentNewForm.$valid" ng-click="save()">コメントを投稿</button> </form> </div>
# comment_new.coffee angular.module('myApp') .controller 'CommentNewCtrl', ($scope, Comment) -> # ......................... $scope.save = -> $scope.comment.$create((data)-> # ......................... $scope.comment = new Comment(report_id: reportId) #<- 初期化 $scope.commentNewForm.$setPristine() # <- コレ )
OK.