Help us understand the problem. What is going on with this article?

その使い方はもう古いかも?AngularJS老化チェック(ディレクティブ篇)

More than 3 years have passed since last update.

こんにちは、らこです。AngularJSのバージョン1.5.0がリリースされましたね!

http://angularjs.blogspot.jp/2016/02/angular-150-ennoblement-facilitation.html

コードネームは ennoblement-facilitation、ざっくり訳すなら「高尚化促進」でしょうか。コンポーネント志向の「高尚」なアプリケーション設計への移行をサポートするバージョンだということでしょう。

1.5.0ではcomponentの追加をメインに、Angular2へのスムーズな移行を行うための足がかりとなるアップデートです。いい機会なので、 今の自分のAngularJSの使い方がどれくらい古いのか をチェックし、どのようにモダンにしていけばいいのかを知っておきましょう。

ちなみに、1.4までに関しては AngularJSモダンプラクティス - Qiita を参考にするとよいでしょう。
このモダンプラクティスに従ったコードになっていればcomponent()対応はあっという間のはずです。

到達目標 - component()

1.5.0から可能になるcomponent()によるカスタム要素の定義は次のように記述します。

  <body>
    <my-app></my-app>
    <script src="app.js"></script>
  </body>
const app = angular.module("app", []);

class MyAppCtrl {
}

app.component("myApp", {
  template: `<greeting name="'World'"></greeting>`,
  controller: MyAppCtrl
});

class GreetingCtrl {

  get upperName() {
    return this.name.toUpperCase();
  }
}

app.component("greeting", {
  bindings: {
    name: "="
  },
  template: `<h1>Hello {{$ctrl.upperName}}!</h1>`,
  controller: GreetingCtrl
});

angular.bootstrap(document.body, [app.name]);

サンプル

ポイントは次の3点です

  • controllerにES6 Classを渡している
  • $scopeを使っていない
  • componentのテンプレート内で$ctrlを使っている

ES6での記述は機能とは直接関係はありませんが、Angular2を見据えたコードにするならばクラスにしておいて損はありません。フィールドの初期化もコンストラクタがあるので明確です。

データバインディングに$scopeを使っていないことも重要です。componentを使うと、デフォルトでcontrollerAs: "$ctrl"の状態になります。コンポーネント内の閉じたスコープが自動的に与えられ、コントローラのフィールドや関数に直接バインドすることができます。

なお、component()になってもdirective()と同じく定義するカスタム要素の名前はキャメルケースで myApp のようにします。そしてHTML側ではハイフンケースで my-app のように宣言します。

モダンなAngularJS - directive + scope + bTC + controllerAs

1.4まではcomponent()がないので、directive()でしかカスタム要素は定義できません。しかしscopeプロパティとbindToController (bTC)、controllerAsを使うことで同じようなコンポーネントを定義することができます。

function MyAppCtrl() {
}

app.directive("myApp", () => {
  return {
    restrict: "E",
    scope: {},
    template: `<greeting name="'World'"></greeting>`,
    controller: MyAppCtrl
  };
});

function GreetingCtrl() {
  this.upperName = () => this.name.toUpperCase();
}

app.directive("greeting", () => {
  return {
    restrict: "E",
    scope: {},
    bindToController: {
      name: "="
    },
    template: `<h1>Hello {{$ctrl.upperName()}}!</h1>`,
    controller: GreetingCtrl,
    controllerAs: "$ctrl"
  };
});

サンプル

実は1.5のcomponent()はこれと同じようなdirective()の呼び出しをラップしているだけです。また、bTCにオブジェクトを渡せるようになったのは1.4からです。

ナウいAngularJS - directive + scope + controllerAs

1.3まではbTCにオブジェクトが渡せないのでscopeと併用しないといけません。

function MyAppCtrl() {
}

app.directive("myApp", function() {
  return {
    restrict: "E",
    scope: {},
    template: "<greeting name=\"'World'\"></greeting>",
    controller: MyAppCtrl
  };
});

function GreetingCtrl() {
  this.upperName = function() {
    return this.name.toUpperCase();
  }
}

app.directive("greeting", function() {
  return {
    restrict: "E",
    scope: {
      name: "="
    },
    bindToController: true,
    template: "<h1>Hello {{$ctrl.upperName()}}!</h1>",
    controller: GreetingCtrl,
    controllerAs: "$ctrl"
  };
});

サンプル

1.4のコードと殆ど変わらないですね。ここまでできていればほとんどコンポーネント化できていると言えるでしょう

古い - directive + scope + controllerAs

1.2以前はbTCがないので、親スコープからのデータバインディングは$scopeを介さないと受け取ることができません。しかし、controllerAsを使っているのでまだ自身のプロパティに関してはthisで扱えます。

function MyAppCtrl() {
}

app.directive("myApp", function () {
  return {
    restrict: "E",
    scope: {},
    template: "<greeting name=\"'World'\"></greeting>",
    controller: MyAppCtrl
  };
});

function GreetingCtrl($scope) {
  this.upperName = function() {
     return $scope.name.toUpperCase();
  };
}

app.directive("greeting", function() {
  return {
    restrict: "E",
    scope: {
      name: "="
    },
    template: "<h1>Hello {{$ctrl.upperName()}}!</h1>",
    controller: GreetingCtrl,
    controllerAs: "$ctrl"
  };
});

サンプル

ここまでは比較的すんなりcomponent()に移行しやすい(スコープの考え方がだいたい合ってる)コードだと思います。

時代遅れ - directive + scope + controller

controllerAsが導入されていないのでテンプレート中で変数名が直接登場し、コントローラのthisがスコープで参照されなくなりました。ただしscopeを使っていてisolated scopeがあります。また、コントローラでビューとロジックの分離がギリギリ出来ている状態です。

function MyAppCtrl() {
}

app.directive("myApp", function() {
  return {
    restrict: "E",
    scope: {},
    template: "<greeting name=\"'World'\"></greeting>",
    controller: MyAppCtrl
  };
});

function GreetingCtrl($scope) {
  $scope.upperName = function() {
    return $scope.name.toUpperCase();
  };
}

app.directive("greeting", function() {
  return {
    restrict: "E",
    scope: {
      name: "="
    },
    template: "<h1>Hello {{upperName()}}!</h1>",
    controller: GreetingCtrl,
  };
});

サンプル

このコードは1.5関係なく、早めにcontrollerAsを使ったスタイルに直すべきです。controllerAsについての議論は2014年あたりには決着がついており、

この辺りを読めばcontrollerAsを使うことによるメリット、使わないことによるデメリットがわかるはずです。

化石 - directive + link

directiveに紐付いたスコープが存在しないケースです。link関数やcompile関数は完全にcomponent()と互換性があるわけではないですが、カスタム要素の場合はほとんどのケースでlinkcompileも必要ないでしょう。directiveとcontrollerでロジックの分離もできていないのでとてもメンテ性の低いコードです。

app.directive("myApp", function() {
  return {
    restrict: "E",
    template: "<greeting name=\"World\"></greeting>"
  };
});

app.directive("greeting", function() {
  return {
    restrict: "E",
    template: "<h1>Hello {{upperName()}}!</h1>",
    link: function(scope, element, attrs) {
      scope.name = attrs.name;
      scope.upperName = function() {
        return scope.name.toUpperCase();
      };
    }
  };
});

サンプル

論外 - html + ng-controller

カスタム要素によるコンポーネント化が全くなされていないコードです。

    <div id="myApp" ng-controller="MyAppCtrl">
      <div id="greeting" ng-controller="GreetingCtrl">
        <h1>Hello {{upperName()}}!</h1>
      </div>
    </div>
app.controller("MyAppCtrl", ["$scope", function($scope) {
    $scope.name = "World";
}]);

app.controller("GreetingCtrl", ["$scope", function($scope) {
  $scope.upperName = function() {
    return $scope.name.toUpperCase();
  };
}]);

サンプル

綺麗にAngularJSを使うつもりがあればng-controllerは無くしましょう。せめてasを使って親スコープからの暗黙の継承を防ぎましょう。

これからやるべきこと

自分のAngularJSの現在地がなんとなくわかったでしょうか。それぞれの段階ごとに、適切な順序でコンポーネント志向なAngularJSにしていきましょう。

html + ng-controllerが残ってる場合

  1. まずはng-controller="SomeCtrl as some"にする
  2. ng-controllerのある要素をdirectiveに置き換える

すべてのコントローラをcontrollerAsにするのが最初の最優先課題です。
それができればあとは一番外側のコントローラから順番にdirectiveにしていきましょう。

directiveがcontrollerを使っていない場合

  1. directiveのlinkcompileはコントローラで代用
  2. directiveのscopeでスコープを分離する

次に最優先すべきはscopeを使ってdirectiveごとにスコープを分離することです。

bTCを使っていない場合

  1. AngularJS1.4以降に上げる 2.scopeに渡していたオブジェクトをbTCに渡し、scopeを空オブジェクトにする

次は$scopeを使わずにすむようにdirectiveのスコープをコントローラにバインドしましょう。

1.4でbTCを使っていた場合

  1. AngularJS 1.5に上げる
  2. directive()の代わりにcomponent()を使う
  3. できればコントローラはES6 Classにする

完成です。ここまでくればAngular2も今いる場所の延長線上にあることがわかってくると思います。

まとめ

1.2から1.3、1.4とコンポーネント志向なdirectiveを作るための機能が少しずつ増えてきており、それらのベストプラクティスとして生まれたのがcomponent()です。
1.5以降はもうAngularJSとしての新しい機能は増えず、Angular2への移行サポートがメインになることがわかっています。
component()対応後にちゃぶ台を返される心配はないので、安心して1.5に向けたマイグレーションを行いましょう。

lacolaco
I play Angular and pray for Angular. No Breaking Changes, No Life.
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした