AngularJSの$httpや$resourceでのリクエストにAuthorizationヘッダーでの認証を付加する

個別のリクエストではなくインターセプターを使ってまとめて追加。 インターセプターを作って$httpProvider.interceptors配列に追加するだけ。 下記コードは、サーバからのtokenを取得済みなら、Authorizationヘッダーに追加してリクエストするようになる例。

#auth_interceptor.coffee
"use strict"

angular.module("myApp")
.factory("AuthInterceptor", (AuthToken)->
  "request": (config) ->
    config.headers ?=  {}
    if AuthToken.isToken()
      #Authorizationへの記述形式は、サーバの認証方式による。例:Token token="abcdefg...."
      config.headers.Authorization = AuthToken.getTokenString()
    config
).config ($httpProvider)->
  $httpProvider.interceptors.push("AuthInterceptor")

One-time bindingと$resourceで気をつけること

前提

AngularJS1.3からOne-time bindingという最初の一回だけバインディングする仕組みが入った。 $resourceはpromiseではなく、先に空配列を返す。

問題

なので、下記のようにngRepeatをOne-time bindingにすると、空配列評価されて、何も表示されなくなったりする。 あ、下のindex()はquery()と読み替えて問題なし。

#sample.coffee
angular.module("myApp")
.controller "SampleCtrl", ($scope, Group) ->
  $scope.groups = Group.index()
<!--index.html-->
<div  ng-repeat="group in ::groups">
{{::group.name}}
</div>

解決

プロミスで処理しよう。 個人的には$resourceは出来るだけpromiseにしたほうがいいと思う。

#sample.coffee
angular.module("myApp")
.controller "SampleCtrl", ($scope, Group) ->
  Group.index().$promise.then((groups)->
    $scope.groups = groups
  )

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

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

そもそも論としてネストしたコントローラで、親のプロパティやメソッドが〜、というのは可読性やテスタビリティが下がるのでアレではある。

派生したスコープでのハマリもあるし。