前提
AngularJSでは、コンポーネント(またはディレクティブ)を定義すると、属性から内部スコープ変数を作成することが可能です。 そのための API はかなり複雑です。
bindings: {
attr1: '@',
attr2: '<',
attr3: '=',
attr4: '&'
}
使用するたびに頭を包むという代償を払うことに飽きたので、この記事では 4 つの記号の違いをきっぱりと解剖します。
具体的には、以下のようになります…
- 文字列の渡し方 (
@
) - 動的式の渡し方 (
<
) - 出力を受け取る方法 (
&
) - 2つのセットアップ方法について学ぶ(
=
) - 上記 4 つのうちどれかを使わずにすべてを行う方法を学ぶ
- なぜ
<
が他の 3 つのお尻を蹴飛ばすのかを学ぶ
Reading an attribute as text
まずは @
から始めましょう。 この4つのうち最も単純なのは、属性をテキストとして読み取ることです。
app.component("readingstring", {
bindings: { text: '@' },
template: '<p>text: <strong>{{$ctrl.text}}</strong></p>'
});
このコンポーネントがあるとします:
<readingstring text="hello"></readingstring>
そして、次のようにレンダリングします:
@
を使用すると、指定した属性のテキストを格納した内部変数が生成されます。 これは、コンポーネントの初期構成として機能すると言えます。
式として属性を評価する
より興味深いのは、属性を式として評価し、式が変わるたびに再評価させる必要があるということです。 動的な入力!
<dynamicinput in="outervariable"></dynamicinput>
これをできるようにしたい……そして、outervariable
の評価を dynamicinput
に渡します。5以前は、このための構文は=
:
app.component("dynamicinput",{
bindings: { in: '=' },
template: '<p>dynamic input: <strong>{{$ctrl.in}}</strong></p>'
});
だけでした。=
の欠点は、一方向しか必要ないのに、双方向のデータバインディングが作成されることでした。 これはまた、渡す式が変数でなければならないことを意味します。
しかし、AngularJS 1.5 では <
となり、これは一方通行のデータバインディングを意味します。
<dynamicinput in="calculateSomething()"></dynamicinput>
コンポーネントの実装は、=
を <
に変更する以外はまったく同じです。
出力をキャッチする
そろそろ話を変えましょう。 下の小さなアプリを見てください。ボタンは子でレンダリングされ、それらがクリックされると、それに応じて外側の値を更新したいのです。
ここで &
が登場します。 これは、属性をステートメントとして解釈し、それを関数でラップします。 コンポーネントはその関数を自由に呼び出すことができ、ステートメント内の変数に値を入れることができます。 親への出力!
Outer value: {{count}}
<output out="count = count + amount"></output>
外側の html が次のような場合、&
を使用した output
の実装は次のようになります:
app.component("output",{
bindings: { out: '&' },
template: `
<button ng-click="$ctrl.out({amount: 1})">buy one</button>
<button ng-click="$ctrl.out({amount: 5})">buy many</button> `
});
注: 入力する変数をオブジェクトで渡していることに注目してください。
- 使用する属性名
- 魔法のように作成される変数名
&
が非常に複雑なため、多くの場合は出力を行うために =
を使用しています。 操作する変数を渡すことで…
Outer value: {{count}}
<output out="count"></output>
…
app.component("output",{
bindings: { out: '=' },
template: `<div>
<button ng-click="$ctrl.out = $ctrl.out + 1;">buy one</button>
<button ng-click="$ctrl.out = $ctrl.out + 5;">buy many</button>
</div>`
});
これは本当にきれいではありません。
- 私たちは一方向しか必要としないにもかかわらず、再び双方向のデータバインディングを行っています
- 出力を保存せず、単にそれを処理したい場合もあります
上記すべてよりも優れた解決策は、コールバックに渡すことにより出力を作成するのに <
を使うことです!
外部コントローラでコールバックを作成し…
$scope.callback = function(amount){
$scope.count += amount;
}
…そしてそれをコンポーネントに渡します。
余談ですが、このパターンはまさにコンポーネントからの出力が React でどのように動作するかを示しています。
双方向データバインディング
これは、通常 =
が AngularJS の申し子として輝けるところです。 このアプリを例にとると、
Outer: <input ng-model="value">
<twoway connection="value"></twoway>
のようにレンダリングすると、=
を使用して twoway
を次のように実装できます。 多くの場合、実際に必要なのは入力と出力です。
ここで、<
だけを使用して双方向バインディングを実装する方法を紹介します! 再び外部コントローラでコールバック関数を作成し…
$scope.callback = function(newval){
$scope.value = newval;
}
… 値とコールバックの両方を渡すと…
<twoway value="value" callback="callback"></twoway>
…component は次のように作成されます:
app.component("twowayin",{
bindings: {
value: '<',
callback: '<'
},
template: `
<input ng-model="$ctrl.value" ng-change="$ctrl.callback($ctrl.value)">
`
});
私たちは、双方向データ結合を実現しましたが、一方向のデータフローに従ったままになっています。 Better karma!
シンボルを完全に置き去りにする
Fact is, the four symbols are just shortcuts.これは、4つのシンボルはショートカットに過ぎない。
こんな風にレンダリングした文字列渡しアプリは……
<readingstring text="hello"></readingstring>
…….
……
<readingstring text="hello"></readingstring>
……。は、$element
サービスにアクセスすることで実装できます:
app.component("readingstring", {
controller: function($element){
this.text = $element.attr("text");
},
template: '<p>text: <strong>{{$ctrl.text}}</strong></p>'
});
または指示文で、link
に渡される attrs
を使用します:
app.directive("readingstring", function(){
return {
restrict: 'E',
scope: {},
link: function(scope,elem,attrs){
scope.text = attrs.text;
},
template: '<p>text: <strong>{{text}}</strong></p>'
};
});
動的入力 app.は、次のようになります。…
…このようにレンダリング…
<dynamicinput in="outervariable"></dynamicinput>
…は、親スコープで .$watch
呼び出しを使用して実装できます:
app.component("dynamicinput",{
controller: ($scope,$element) => {
let expression = $element.attr("in");
$scope.$parent.$watch(expression, newVal => $scope.in = newVal);
},
template: '<p>dynamic input: <strong>{{in}}</strong></p>'
});
出力アプリ…
… このようにレンダリング…
<output out="count = count + amount"></output>
…は、…は、親スコープで $scope.$apply
を呼び出すことで実装できます。
app.component("output",{
controller: ($scope,$element,$timeout) => {
let statement = $element.attr("out");
$scope.increaseBy = by => {
$timeout(function(){
$scope.$parent.$apply(`amount = ${by}; ${statement}`);
});
}
},
template: `
<button ng-click="increaseBy(1)">buy one</button>
<button ng-click="increaseBy(5)">buy many</button>`
});
これは、親スコープを amount
変数で汚染しているので、&
とまったく同じではありませんが、それでも、コンセプトを十分に示すものです。
最終的に、
…=
のようにレンダリングされた双方向アプリケーション…
<twoway connection="value"></twoway>
…。は、親と子の両方のスコープで $watch
を設定することで実装できます。
app.component("twoway",{
controller: ($scope,$element,$timeout) => {
let variable = $element.attr("connection");
$scope.$parent.$watch(variable, newVal => $scope.inner = newVal;
$scope.$watch('inner', (newVal='') => $timeout( () => {
$scope.$parent.$apply(`${variable} = "${newVal}";`);
}));
},
template: `inner: <input ng-model="inner">`
});
これは、バインドされた値が常に文字列であると仮定しているので、少し不正行為ですが、要点はまだそこにあります!
まとめ
この旅が教育的であり、@
、<
、=
および &
がそれほど威圧的に感じないことを望みます。
どちらも文字列を読むのに多少不器用ですが (<
は文字列の中に文字列を必要とし、=
は代理変数を必要とします)、バニラを作るのは簡単なので @
はあまり生意気にならないようにしましょう。