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.2.png

↓1.3 1.3.png

ので、コントローラを以下のように変更した。

# parent_item.coffee

angular.module("myApp")
.controller("ParentItemCtrl", ($scope, Child) ->

  @removeChildItem = (childId, index)->
    Child.destroy({childId: childId}, ->
      $scope.parent.cildren.splice(index, 1)
    )
)

RailsでPunditとshoulda-matchersを併用して、Rspecのcustom matchersが被った

Punditとshoulda-matchersにはpermitという同名のカスタムマッチャーがあって、衝突してしまう。

ので、punditのpermitは、spec_helper.rbでのrequire "pundit/rspec"をやめて、punditのカスタムマッチャーのpermitを使いたい個別のspecファイル、例えばuser_policy_spec.rbで、require "pundit/rspec"して解決した。

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.