コントローラーについて

コントローラーの理解

Angularでは、コントローラーはAngularのスコープを引数として使用するJavaScriptのコンストラクタ関数です。

ng-controllerディレクティブを介して、コントローラーがDOM要素に割り当てられると、 Angularは指定されたコントローラーのコンストラクタ関数を使用して、新しいコントローラーオブジェクトをインスタンス化します。 新しい子スコープは、$scopeとしてコントローラーのコンストラクタ関数へ注入されることで、引数として利用可能になります。

コントローラーは下記のようなことをするのに使用されます。

  • $scopeオブジェクトの初期状態をセットアップします。
  • $scopeオブジェクトの振る舞いを追加します。

$scopeオブジェクトの初期状態のセットアップ

通常、アプリケーションを作成していると、Angularの$scopeの初期状態のセットアップが必要になります。 開発者は$scopeオブジェクトへプロパティを割り当てることによって、初期状態のセットアップします。 プロパティはビューモデルを含みます。(このモデルは、ビューによって提供されます) 全ての$scopeプロパティは、コントローラーが登録されたDOMの箇所のテンプレートで利用可能になります。

下記は、'Hola!'の文字列を含むgreetingプロパティが$scopeに割り当てられる、 非常に単純なコントローラーのコンストラクタ関数の例です。

function GreetingCtrl($scope) {
    $scope.greeting = 'Hola!';
}

DOMにコントローラーにが割り当てられると、greetingプロパティはテンプレートにデータバインディングされます。

<div ng-controller="GreetingCtrl">
  {{ greeting }}
</div>

注意: Angularはグローバル空間にコントローラー関数を作成することを許可しているものの、これは非推奨とされています。 実際のアプリケーションでは、Angularモジュールの.controllerメソッドを下記のように使用してください。

var myApp = angular.module('myApp',[]);

myApp.controller('GreetingCtrl', ['$scope', function($scope) {
    $scope.greeting = 'Hola!';
}]);

上記はインラインアノテーション(inline injection annotation)によってコントローラが$scopeに依存していることを明確にすることで、 Angularからコントローラーに$scopeサービスが提供されます。 詳細は、ガイドの依存性注入(DI)を参照して下さい。

スコープオブジェクトへの振る舞いの追加

ビュー内でイベントを反応させる、または計算の実行をするには、scopeに振るまいを提供しなければいけません。 $scopeオブジェクトへの割り当てメソッドによって、振る舞いをスコープに追加します。 これらのメソッドは、テンプレート/ビューから呼び出されることによって利用可能になります。

下記の例は、コントローラーを使ってスコープに数値を2倍にするdobuleメソッドを追加しています。

var myApp = angular.module('myApp',[]);

myApp.controller('DoubleCtrl', ['$scope', function($scope) {
    $scope.double = function(value) { return value * 2; };
}]);

DOMへコントローラーが割り当てられると、doubleメソッドはテンプレートのAngular式内で実行可能になります。

<div ng-controller="DoubleCtrl">
  Two times <input ng-model="num"> equals {{ double(num) }}
</div>

コンセプト概要で取り上げていますが、 スコープにオブジェクト(または、プリミティブ)が割り当てられ、それがモデルプロパティになります。 スコープへ割り当てられたメソッドは、テンプレート/ビュー内で利用可能になり、 Angular式とngイベントハンドラーのディレクティブを介して実行可能となります。(例: ngClick)

正しいコントローラーの使用方法

通常、コントローラーに多くのことを行わせるべきではありません。 単一のビューに必要なビジネスロジックのみを含めるべきです。

コントローラーをスリムに保つための最良の方法はカプセル化であり、 サービスに対してコントローラーが依存しないようにし、 これらのサービスは依存性注入を介してコントローラー内で使用するようにします。 これについては、このガイドの依存性注入サービスで取り上げています。

Do not use Controllers for:

DOM操作に関すること
コントローラーはビジネスロジックのみを含めるべきです。 DOM操作(アプリケーションのプレゼンテーションロジック)は、テストしづらい事で知られています。 プレゼンテーションロジックをコントローラー内に置くことは、ビジネスロジックでのテストに対して重大な影響を与えることに繋がります。 Angularは、自動的的に行われるDOM操作のためにデータバインディング機能を提供します。 もし、自身の手動でのDOM操作の実行が必要な場合は、ディレクティブ内でプレゼンテーションロジックをカプセル化してください。
入力フォーマット
代わりにAngularのフォームコントローラーを使用してください。
フィルタ出力
代わりにAngularのフィルターを使用してください。
コントローラーをまたいだステートレス、ステートフルのコードの共有
代わりにAngularサービスを使用してください。
他のコンポーネントのライフサイクルの管理
(例えば、サービスインスタンスの作成など)

Angularのスコープオブジェクトとコントローラーの関連性について

ngControllerディレクティブまたは$routeサービスを介して、 暗黙的にスコープオブジェクトをコントローラーに関連付けることが可能です。

単純なコントローラーの例

Angularでコントローラーのコンポーネントがどのように動作するかを実例で分かりやすく理解するために、 下記の要件を満たす小さなアプリケーションを作成してみましょう。

  • 2つのボタンと単一のメッセージを持つテンプレート
  • spiceと命名された文字列のモデル
  • spiceの値を設定する2つの関数を持つコントローラー

テンプレート内のメッセージには、spiceモデルへの紐付けが含まれ、デフォルトで"very"の文字列が設定されます。 ボタンがクリックされると、spiceモデルにchili(唐辛子)またはjalapeño(メキシコの極辛の唐辛子)が設定されるように紐付けられ、 メッセージはデータバインディングによって自動的に更新されます。

<!doctype html>
<html ng-app="spicyApp1">
  <head>
    <script src="http://code.angularjs.org/1.2.0/angular.min.js"></script>
    <script src="script.js"></script>
  </head>
  <body>
    <div ng-app="spicyApp1" ng-controller="SpicyCtrl">
     <button ng-click="chiliSpicy()">Chili</button>
     <button ng-click="jalapenoSpicy()">Jalapeño</button>
     <p>The food is {{spice}} spicy!</p>
    </div>
  </body>
</html>
var myApp = angular.module('spicyApp1', []);

myApp.controller('SpicyCtrl', ['$scope', function($scope){
    $scope.spice = 'very';

    $scope.chiliSpicy = function() {
        $scope.spice = 'chili';
    };

    $scope.jalapenoSpicy = function() {
        $scope.spice = 'jalapeño';
    };
}]);
<!doctype html>
<html ng-app="spicyApp1">
  <head>
    <script src="http://code.angularjs.org/1.2.0/angular.min.js"></script>
<script>var myApp = angular.module('spicyApp1', []);

myApp.controller('SpicyCtrl', ['$scope', function($scope){
    $scope.spice = 'very';

    $scope.chiliSpicy = function() {
        $scope.spice = 'chili';
    };

    $scope.jalapenoSpicy = function() {
        $scope.spice = 'jalapeño';
    };
}]);
</script>
  </head>
  <body>
    <div ng-app="spicyApp1" ng-controller="SpicyCtrl">
     <button ng-click="chiliSpicy()">Chili</button>
     <button ng-click="jalapenoSpicy()">Jalapeño</button>
     <p>The food is {{spice}} spicy!</p>
    </div>
  </body>
</html>

この例から、下記のことを確認してください。

  • ng-controllerディレクティブは、テンプレートに(暗黙的に)スコープを作成するのに使用され、 スコープはSpicyCtrlコントローラーによって補助(?)(管理)されます。
  • SpicyCtrlは、単なるプレーンなJavaScript関数です。 (任意で)慣習に基づいた命名によって、関数名の1文字目を大文字にし、最後に"Ctrl"または"Controller"を付けています。
  • $scopeへプロパティを割り当てることで、モデルが作成または更新されます。
  • コントローラーのメソッドは、直接スコープを割り当てることで作成することが出来ます。(chiliSpicyメソッド参照)
  • コントローラーのメソッドとプロパティは、テンプレート内で利用可能です。 (<div>要素とその子要素で)

Spicyで引数を取得する例

また、コントローラーのメソッドは引数を取得することが可能です。 前述の例を少し変更した、下記のデモを確認してください。

<!doctype html>
<html ng-app="spicyApp2">
  <head>
    <script src="http://code.angularjs.org/1.2.0/angular.min.js"></script>
    <script src="script.js"></script>
  </head>
  <body>
    <div ng-app="spicyApp2" ng-controller="SpicyCtrl">
     <input ng-model="customSpice">
     <button ng-click="spicy('chili')">Chili</button>
     <button ng-click="spicy(customSpice)">Custom spice</button>
     <p>The food is {{spice}} spicy!</p>
    </div>
  </body>
</html>
var myApp = angular.module('spicyApp2', []);

myApp.controller('SpicyCtrl', ['$scope', function($scope){
    $scope.customSpice = "wasabi";
    $scope.spice = 'very';

    $scope.spicy = function(spice){
        $scope.spice = spice;
    };
}]);
<!doctype html>
<html ng-app="spicyApp2">
  <head>
    <script src="http://code.angularjs.org/1.2.0/angular.min.js"></script>
<script>var myApp = angular.module('spicyApp2', []);

myApp.controller('SpicyCtrl', ['$scope', function($scope){
    $scope.customSpice = "wasabi";
    $scope.spice = 'very';

    $scope.spicy = function(spice){
        $scope.spice = spice;
    };
}]);
</script>
  </head>
  <body>
    <div ng-app="spicyApp2" ng-controller="SpicyCtrl">
     <input ng-model="customSpice">
     <button ng-click="spicy('chili')">Chili</button>
     <button ng-click="spicy(customSpice)">Custom spice</button>
     <p>The food is {{spice}} spicy!</p>
    </div>
  </body>
</html>

ここでSpicyCtrlコントローラーは、spiceという1つの引数を受け取るspicyと呼ばれる1つのメソッドを定義していることに注目してください。 このテンプレートは、このコントローラーのメソッドを参照し、 最初のボタンの紐付けによって'chili'という文字列を渡し、 2つ目のボタンではspiceのモデルプロパティ(input入力欄に紐付けられている)の値を渡します。

スコープ継承の例

DOM階層の異なる層にコントローラーが割り当てられることは、よくあることです。 ng-controllerディレクティブは、新しい子スコープを作成するため、 それぞれから継承したスコープの階層を取得します。 各コントローラーが受け取る$scopeは、上の階層のコントローラーによって定義されたプロパティとメソッドにアクセスすることが出来ます。 スコープの継承について、より詳しく知りたければ、 Understanding Scopesを参照してください。

<!doctype html>
<html ng-app="scopeInheritance">
  <head>
    <link rel="stylesheet" type="text/css" href="style.css">
    <script src="http://code.angularjs.org/1.2.0/angular.min.js"></script>
    <script src="script.js"></script>
  </head>
  <body>
    <div ng-app="scopeInheritance" class="spicy">
      <div ng-controller="MainCtrl">
        <p>Good {{timeOfDay}}, {{name}}!</p>

        <div ng-controller="ChildCtrl">
          <p>Good {{timeOfDay}}, {{name}}!</p>

          <div ng-controller="GrandChildCtrl">
            <p>Good {{timeOfDay}}, {{name}}!</p>
          </div>
        </div>
      </div>
    </div>
  </body>
</html>
div.spicy div {
  padding: 10px;
  border: solid 2px blue;
}
var myApp = angular.module('scopeInheritance', []);
myApp.controller('MainCtrl', ['$scope', function($scope){
  $scope.timeOfDay = 'morning';
  $scope.name = 'Nikki';
}]);
myApp.controller('ChildCtrl', ['$scope', function($scope){
  $scope.name = 'Mattie';
}]);
myApp.controller('GrandChildCtrl', ['$scope', function($scope){
  $scope.timeOfDay = 'evening';
  $scope.name = 'Gingerbreak Baby';
}]);
<!doctype html>
<html ng-app="scopeInheritance">
  <head>
<style type="text/css">div.spicy div {
  padding: 10px;
  border: solid 2px blue;
}
</style>
    <script src="http://code.angularjs.org/1.2.0/angular.min.js"></script>
<script>var myApp = angular.module('scopeInheritance', []);
myApp.controller('MainCtrl', ['$scope', function($scope){
  $scope.timeOfDay = 'morning';
  $scope.name = 'Nikki';
}]);
myApp.controller('ChildCtrl', ['$scope', function($scope){
  $scope.name = 'Mattie';
}]);
myApp.controller('GrandChildCtrl', ['$scope', function($scope){
  $scope.timeOfDay = 'evening';
  $scope.name = 'Gingerbreak Baby';
}]);
</script>
  </head>
  <body>
    <div ng-app="scopeInheritance" class="spicy">
      <div ng-controller="MainCtrl">
        <p>Good {{timeOfDay}}, {{name}}!</p>

        <div ng-controller="ChildCtrl">
          <p>Good {{timeOfDay}}, {{name}}!</p>

          <div ng-controller="GrandChildCtrl">
            <p>Good {{timeOfDay}}, {{name}}!</p>
          </div>
        </div>
      </div>
    </div>
  </body>
</html>

3つのng-controllerディレクティブがテンプレート内に入れ子になっている事に注目してください。 結果的にビュー内に4つのスコープが作成されます。

  • ルートスコープ
  • timeOfDayとnameプロパティを持つMainCtrlスコープです。
  • ChildCtrlスコープはtimeOfDayプロパティを継承しますが、nameプロパティは上書きします。
  • GrandChildCtrlスコープはMainCtrlで定義されたtimeOfDayプロパティ、ChildCtrlで定義されたnameプロパティの両方を上書きします。

メソッドの継承も、プロパティと同じです。 そのした前述した例であれば、全てのプロパティを文字列の値を返すメソッドに置き換えることが出来ます。

コントローラーのテスト

コントローラーをテストする方法は数多くありますが、 最良な方法の慣習に下記に示すように、$rootScopeと$controllerの注入を含めるという事が挙げられます。

コントローラー定義
var myApp = angular.module('myApp',[]);

myApp.controller('MyController', function($scope) {
  $scope.spices = [{"name":"pasilla", "spiciness":"mild"},
                   {"name":"jalapeno", "spiceiness":"hot hot hot!"},
                   {"name":"habanero", "spiceness":"LAVA HOT!!"}];
  $scope.spice = "habanero";
});
コントローラーのテスト
describe('myController function', function() {

  describe('myController', function() {
    var $scope;

    beforeEach(module('myApp'));

    beforeEach(inject(function($rootScope, $controller) {
      $scope = $rootScope.$new();
      $controller('MyController', {$scope: $scope});
    }));

    it('should create "spices" model with 3 spices', function() {
      expect($scope.spices.length).toBe(3);
    });

    it('should set the default value of spice', function() {
      expect($scope.spice).toBe('habanero');
    });
  });
});

もし、入れ子のコントローラーのテストを必要とする場合は、既存のDOM内でのテストで同じスコープ階層を作成する必要があります。

describe('state', function() {
    var mainScope, childScope, grandChildScope;

    beforeEach(module('myApp'));

    beforeEach(inject(function($rootScope, $controller) {
        mainScope = $rootScope.$new();
        $controller('MainCtrl', {$scope: mainScope});
        childScope = mainScope.$new();
        $controller('ChildCtrl', {$scope: childScope});
        grandChildScope = childScope.$new();
        $controller('GrandChildCtrl', {$scope: grandChildScope});
    }));

    it('should have over and selected', function() {
        expect(mainScope.timeOfDay).toBe('morning');
        expect(mainScope.name).toBe('Nikki');
        expect(childScope.timeOfDay).toBe('morning');
        expect(childScope.name).toBe('Mattie');
        expect(grandChildScope.timeOfDay).toBe('evening');
        expect(grandChildScope.name).toBe('Gingerbreak Baby');
    });
});

 Back to top

© 2017 Google
Licensed under the Creative Commons Attribution License 3.0.

このページは、ページトップのリンク先のAngularJS公式ドキュメント内のページを翻訳した内容を基に構成されています。 下記の項目を確認し、必要に応じて公式のドキュメントをご確認ください。 もし、誤訳などの間違いを見つけましたら、 @tomofまで教えていただければ幸いです。

  • AngularJSの更新頻度が高いため、元のコンテンツと比べてドキュメントの情報が古くなっている可能性があります。
  • "訳注:"などの断わりを入れた上で、日本人向けの情報やより分かり易くするための追記を行っている事があります。