Angular のソート フィルターをアップグレードする方法

はじめに

AngularJS の初期の提供で最も便利な機能の 1 つは、テンプレート変数とフィルターのみを使用してページ上のデータをフィルタリングおよびソートできることでした。 しかし今日、多くのフロントエンド開発者は一方向のデータ バインディングを好み、それらの orderBy および filter フィルターは Angular の出現により過去のものとなっています。 (注意: この記事では、1.x を参照するために「AngularJS」を、2 以上を参照するために「Angular」だけを使用します。)

しかし、どのようにして同じ効果を達成することになっているのでしょうか。 ngUpgrade プロジェクトを見て、これを行う方法を学びましょう!

Step 1 – Pull Code from Git

新しく書き直されたコンポーネントのテンプレートを更新するステップを説明します。 次に、ソートとフィルタリングを追加して、AngularJS で持っていた機能をすべて復元します。

始めるには、これから使用するサンプル プロジェクトをクローンします (publicserver の両方のフォルダーで npm install を実行することを忘れないでください)。

git checkout 9daf9ab1e21dc5b20d15330e202f158b4c065bc3

このサンプル プロジェクトは AngularJS 1.6 と Angular 4 の両方を使用する ngUpgrade のハイブリッド プロジェクトです。 Express API が動作し、開発および本番用の Webpack がビルドされています。 自由に探索し、フォークして、自分のプロジェクトでパターンを使用してください。 このプロジェクトのAngular 5を使用したバージョンを見たい場合は、このレポをチェックしてください。 このチュートリアルの目的では、2 つのバージョン間の違いは重要ではありません (マイナーなものは指摘します)。

ステップ 2 – AngularJS 構文の置き換え

アプリケーションのこの段階で、注文コンポーネントは Angular で書き直され、すべての依存関係が注入および解決されています。 しかし、アプリケーションを実行しようとすると、テンプレートに問題があることを示すエラーがコンソールに表示されます。 最初に修正する必要があるのはこの点です。 受注テンプレートのAngularJS構文(orders/orders.html)を置き換えて、ルートをロードし、ページに受注を表示できるようにします。 次にフィルタリングとソートを修正します。

最初に行う必要があるのは、このテンプレート内の $ctrl のインスタンスをすべて削除することです。 これらはもはやAngularでは必要ありません。 検索と置換で $ctrl. を探し(ドットに注意)、何もない状態に置き換えることができます。

次に、13 行目のボタンの data-ng-click を置換しましょう。 Angularでは、ng-clickの代わりにclickイベントを使用し、括弧でイベントであることを示します。 大括弧は入力を、括弧は出力またはイベントを表します。

<button type="button" (click)="goToCreateOrder()" class="btn btn-info">Create Order</button>

私たちはここで、クリック イベントで、注文コンポーネントの goToCreateOrder 関数を実行することを述べています。 注文を読み込む div 全体 (行 17 以降) をコメントアウトします。 アプリケーションを実行するには、ターミナルを開き、次のコマンドを実行します:

cd servernpm start

これで Express サーバーが起動します。 Webpack 開発サーバーを実行するには、別のターミナルを開いて次のコマンドを実行します:

cd publicnpm run dev

(このチュートリアルの残りの期間、これらのプロセスを実行し続けることができます。)

アプリケーションが再びロードされていることを確認できます。 受注ルートに移動すると、受注コンポーネントが正しく表示されていることがわかります。

App screenshot

また、注文の作成ボタンをクリックすると、注文の作成ルートとフォームに正しく移動することができます。 div のコメントを解除します (このアプリはまた壊れてしまいます)。

残りのインスタンス data-ng-click をすべて (click) イベント ハンドラで置き換えてみましょう。 検索」 & 「置換」を使用するか、エディターのショートカットですべての出現箇所を選択します (Windows 版 VS Code では Ctrl+Shift+L)。

次に、data-ng-show*ngIf にすべて置換します。 実はAngularにはng-showに直接相当するものはありませんが、それでもかまいません。 なぜなら、そうすれば、単に要素を隠したり表示したりするのではなく、実際にDOMに要素を追加したり削除したりすることになるからです。 つまり、data-ng-show を見つけて *ngIf に置き換えるだけでよいのです。

最後に、テーブル本体を修正するために 2 つのことを行う必要があります。 まず、data-ng-repeat*ngFor="let order of orders"に置き換える。 その行の orderByfilter のフィルターも削除して、tr 全体が次のようになることに注意してください:

<tr *ngFor="let order of orders">

次に、注文詳細ルートへの href リンクの前の data-ng プレフィックスを削除します。 AngularJS はここでもルーティングを処理しますが、これは Angular テンプレートになったので、このプレフィックスを使用する必要はありません。

アプリケーションをもう一度見てみると、注文が画面に正しくロードされていることがわかります。 ソート リンクはもはや機能しませんし、通貨は、Angular の通貨パイプが AngularJS の対応するものとわずかに異なるため、ちょっと混乱しています。 これについては後ほど説明します。 今のところ、これは素晴らしい兆候です。なぜなら、データがコンポーネントに届き、ページ上で読み込まれることを意味するからです。 これで、このテンプレートの基本がAngularに変換されました。

Step 3 – ソートを追加する

注文が画面に読み込まれましたが、それらを並べ替えたりする方法がまだありません。 AngularJS では、ページ上のデータを並べ替えるために組み込みの orderBy フィルターを使用するのが一般的でした。 AngularにはもうorderByフィルターはありません。 これは、この種のビジネスロジックをテンプレートに持たせるのではなく、コンポーネントに移動させることが強く推奨されるようになったからです。 というわけで、ここではそれを行うことにします。 (注意: ここでは、リアクティブフォームのアプローチではなく、ごく普通の関数とイベントを使用する予定です。 これは、このようなことを理解するための第一歩を踏み出そうとしているからです。 基本を押さえたら、observable を使ってさらに進めてください!)

Sorting in the Component

ng-repeat から orderBy フィルターを削除し、*ngFor に変更したときにすでに行いました。 今度は、ordersコンポーネントでソート機能を作ってみます。 テーブル ヘッダーのクリック イベントを使用してこの関数を呼び出し、並べ替えを行うプロパティを渡します。

Orders コンポーネント (./orders/orders.component.ts) を開き、クラスに 2 つのパブリック プロパティを追加しましょう。 これらは、テンプレートがすでに参照している 2 つのプロパティと一致させるつもりです。 最初のものは、型 stringsortType です。 2つ目は boolean 型の sortReverse で、デフォルト値を false に設定します。 sortReverseプロパティは、単に順序を反転させるかどうかを記録しているだけで、昇順や降順の同義語とは考えないでください。

では、クラス内のタイトルの宣言の後に次のように記述します。

sortType: string;sortReverse: boolean = false;

次に、JavaScript の Array.sort プロトタイプ関数で使用する関数を追加します。 goToCreateOrder 関数の後(ただし、まだクラス内)にこれを追加します:

dynamicSort(property) { return function (a, b) { let result = (a < b) ? -1 : (a > b) ? 1 : 0; return result; } }

この動的ソート関数は、配列内のオブジェクトのプロパティ値を比較します。 ネストされた3項関数は一見すると少しわかりにくいかもしれませんが、基本的には「Aのプロパティの値がBより小さければ、-1を返す」というだけのものです。 そうでない場合は、より大きければ1を返します。 等しい場合は0を返します。

さて、これは超高度な、または深い比較ではありません。 ソートするために書くことができるもっと洗練されたヘルパー関数がありますし、これをどのように破ることができるか自由に試してみてください。 このロジックを好きなカスタム ソート ロジックと交換することができます。 Array プロトタイプの sort 関数は、配列内のアイテムを比較するために使用できる関数を渡すことができます。 新しい dynamicSort 関数を使用して、それを利用する sortOrders という関数をクラスで作成してみましょう:

sortOrders(property) { }

最初に行う必要があるのは、クラスの sortType プロパティを渡されたプロパティに等しく設定することです。 次に、sortReverse プロパティをトグルさせます。

sortOrders(property) { this.sortType = property; this.sortReverse = !this.sortReverse;}

ここで、this.orderssort 関数を呼び出すことができますが、プロパティで動的ソート関数を渡すことができます。

sortOrders(property) { this.sortType = property; this.sortReverse = !this.sortReverse; this.orders.sort(this.dynamicSort(property));}

そして、最後にもう1つ必要なことがあります。 配列の昇順と降順を逆にできるように、dynamicSort 関数を少し修正する必要があります。 そのために、dynamicSort の結果をクラスの sortReverse プロパティに結びつけます。

最初にやることは、変数の宣言です。

let sortOrder = -1;

そして、クラスのsortReverseプロパティが真か偽かをチェックします。 True の場合、ソート順変数を 1 に設定します。

if (this.sortReverse) { sortOrder = 1; }

このように関数を結び付けているのは、デモのためにソート関数でトグルを行っているからです。 もっと徹底して言えば、別のアプローチとして、sortReverseの代わりにsortDescendingという変数を用意して、別の関数で制御することもできます。 このルートでは、逆のことをします。sortOrdersortDescendingが真でなければ1になります。

最後の2つを組み合わせて3項式にすることもできますが、わかりやすくするために、もう少し冗長なままにしておきます。 そして、結果を通常とは逆にするには、resultsortOrderをかければよいのです。

 dynamicSort(property) { let sortOrder = -1; if (this.sortReverse) { sortOrder = 1; } return function(a, b) { let result = a < b ? -1 : a > b ? 1 : 0; return result * sortOrder; }; }

繰り返しますが、これはソートのデモ実装であり、コンポーネント上でカスタム ソート関数を使用する際の主要な概念を理解するためのものです。

これらの関数が機能しているかどうかを確認するために、ngOnInit 関数にデフォルトの並べ替えを追加します。

forkJoin サブスクリプションの内部で、顧客名プロパティを追加する forEach の後に、this.sortOrders を呼び出して合計アイテム数プロパティを渡してみます。

では、テーブル ヘッダー リンクから sortOrders 関数を呼び出して、テンプレートにこの並べ替えを実装する必要があるだけです。

Add sorting to the template

注文コンポーネントで sortOrders 関数が正しく動作しているので、それをテンプレートに追加して、テーブル ヘッダーを再びクリックできるようにする準備が整いました。

その前に、ngOnInit 関数のデフォルトの並べ替えを ID だけに変更してみましょう:

this.sortOrders('id');

これは、合計アイテムを使用するより少し普通ですね。

これでテンプレートに手を加えることができる。 最初に行いたいのは、すべてのクリック イベントで sortOrders 関数を呼び出すことです。 sortType = のインスタンスを選択し、sortOrders( に置き換えることができます。 次に、; sortReverse = !sortReverseのインスタンスを)に置き換えることができます。

また、ここで渡しているプロパティ名のうち、*ngIfのインスタンスにある2つを修正する必要があります。 orderIdの3つのインスタンスをidに、customernameの3つのインスタンスをcustomerNameに置き換えてください。

最後に、ヘッダー内の href タグをそれぞれ括弧で囲み、Angular が引き継いでこれらのリンクが実際にはどこにも移動しないようにします。 クリックイベントが発生することになります。 つまり、ヘッダーは次のパターンに従っている必要があります:

<th> <a ="" (click)="sortOrders('id')"> Order Id <span *ngIf="sortType == 'id' && !sortReverse" class="fa fa-caret-down"></span> <span *ngIf="sortType == 'id' && sortReverse" class="fa fa-caret-up"></span> </a></th>

ブラウザに移動して、すべてのテーブルヘッダーリンクをテストしてください。 各プロパティが昇順と降順の両方でソートされることが確認できるはずです。 素晴らしい!

これは素晴らしいことですが、1 つのことを失いました – カーソルはポインターではなくセレクタです。 いくつかの CSS でそれを修正しましょう。

Fix the Cursor

注文ページで正しく機能しているソートがありますが、カーソルがポインターではなくセレクタになり、イライラさせられます。

これを修正するために CSS を使用できるいくつかの異なる方法があります。

  • メイン アプリの SCSS ファイルにクラスを作成する。
  • インライン CSS を記述することもできますが、これはほとんど望ましいことではありません。
  • Component Decorator の styles オプションを使用して Angular のスコープされた CSS を利用する。

この特定のコンポーネント用のスタイルに 1 つのルールを追加するだけでよいので、最後のオプションを使用することにします。

Orders コンポーネント クラスをもう一度開いてください。 コンポーネント デコレーターで、styles という新しいプロパティを追加できます。 Styles は文字列の配列ですが、この文字列は CSS ルールです。 カーソルを修正するために必要なことは、テーブルの行で、リンクがある場合は、カーソルプロパティをポインタに変更するというルールを書き出すことです。 デコレーターは次のようになります。

@Component({ selector: 'orders', template: template, styles: })

これで、行のヘッダーにカーソルを置くと、ポインター カーソルが表示されることがわかります。 この方法の優れている点は、この CSS ルールが他のコンポーネントに影響を与えないことです。 注文コンポーネントにのみ適用されます。

さて、フィルタリングについて何かできるかどうか見てみましょう。 その「フィルター filter」は Angular から削除されたので、創造力を発揮して、私たちのコンポーネントにそれを実装する方法を考え出す必要がありそうです。

Step 4 – Add Filtering

AngularJS フィルターを使用して、検索する文字列に基づいてオーダー コレクションを検索していたフィルター ボックスを置き換える準備が整いました。 AngularJS フィルターはテンプレート上にあり、コントローラやコンポーネントにコードを必要としませんでした。 現在では、このようなテンプレート内のロジックは推奨されません。

Add a Filter Function

コンポーネントに戻って、filteredOrders という名前の新しい注文の配列を作成します。 次に、orders 配列を filteredOrders 配列に設定するフィルタ関数に渡します。 最後に、テンプレート上の *ngFor で、元の配列の代わりに filteredOrders を使用します。

filteredOrders: Order;

次に、注文の元の配列を設定する forkJoin で、注文配列に filteredOrders の初期状態を設定できます。

this.filteredOrders = this.orders;

さて、実際にフィルタリングを行う関数を追加する準備が整いました。 コンポーネントの下部にあるソート関数のすぐ後に、この関数を貼り付けます。 まず、この関数にsearchという文字列のプロパティを与えています。 そして、オーダーをループして、オブジェクトのキーをすべて見つけます。 すべてのキーについて、検索用語に一致するプロパティの some 値があるかどうかを確認します。 このJavaScriptの部分は、最初は少しわかりにくいかもしれませんが、基本的にはこのような処理が行われています。

if ステートメントでは、文字列を明示的にテストしていることに注意してください。 今の例では、クエリーを文字列に限定しています。 ネストされたプロパティや数値のプロパティなどを処理するつもりはありません。 検索語は顧客名プロパティにマッチし、住所や他の文字列プロパティを表示することを選択した場合は、それらも含めて検索します。

もちろん、数字をテストしたり、入れ子オブジェクトの別の層を調べるためにこの関数を変更することもできます。 ソートと同様に、デモの実装から始めて、想像力を働かせてより複雑にしてください。

sortOrders 関数といえば、次に進む前に、コンポーネントで最後にもう 1 つ行う必要があります。 フィルタをソートより優先させたいので、sortOrders を修正して、元の orders ではなく filteredOrders を使用するようにします。

sortOrders(property) { this.sortType = property; this.sortReverse = !this.sortReverse; this.filteredOrders.sort(this.dynamicSort(property));}

これで、テンプレートにこのフィルタリングを実装する準備ができました。

Add Filtering to the Template

テンプレートに戻り、フィルタリングを使用するように修正します。

最初に行う必要があるのは、data-ng-model を置き換えることです。 その代わりに、keyupイベントを使用するつもりなので、「キーアップ」と書いて、括弧で囲みます((keyup))。 これはAngularの組み込みイベントで、入力のキーアップで関数を実行することができます。 関数の名前はfilterOrdersとしましたが、これはAngularJSのフィルタに渡すプロパティの名前だったので、その横に括弧を追加するだけです。 入力は今のところ次のようになります:

<input type="text" class="form-control" placeholder="Filter Orders (keyup)="filterOrders()">

しかし、フィルターオーダー関数に何を渡せばよいのでしょうか? デフォルトでは、イベントは$eventと呼ばれるものを渡します。 この中にはtargetというものが含まれており、この中に入力の値が含まれています。 $eventを使うには一つ問題がある。 target.value は本当に何でもあり得るので、これらの漠然とした型を追跡するのは非常に困難である。 これでは、デバッグするのも、期待される値の型を知るのも難しい。 その代わりに、Angularには本当に気の利いたことがあります。それは、この入力にテンプレート変数を割り当てることです。

幸運なことに、Angular はこれを行うためのメソッドを提供しています。 入力タグの後に、ハッシュ記号(#)と、目的のモデルの名前を追加します。 ここでは #ordersFilter と呼ぶことにします。 タグのどこに書いてもいいし、どう呼んでもいいのですが、私は入力の後に書いて、どのモデルがどの入力に関連付けられているかを、ページを見下ろしたときにキャッチできるようにしたいのです。

ここで、その変数を keyup イベントで filterOrders 関数に渡すことができます。 その前にハッシュ記号は必要ありませんが、.valueを追加する必要があります。 これはモデル全体ではなく、モデルの実際の値を渡すことになります。 完成した入力は次のようになります:

<input #ordersFilter type="text" class="form-control" placeholder="Filter Orders" (keyup)="filterOrders(ordersFilter.value)">

最後に、通常の orders 配列ではなく filteredOrders 配列を使用するように *ngFor を修正する必要があります:

<tr *ngFor="let order of filteredOrders">

Inspect the Product

フィルタリングとソートがコンポーネント内にあるため、テンプレートがどれほどきれいになったかがわかると思います:

次に、ブラウザ上でこれを確認してみましょう。 ボックスに「sally」などのテキストを入力すると、注文が変更され、その上で並べ替えが動作していることがわかります。

さて、このコンポーネントで最後に行う必要があるのは、通貨パイプを修正することです。

Step 5 – 通貨パイプを修正する

最後の仕上げは、以前の通貨フィルターを更新することです。 AngularJS では指定する必要がなかったいくつかのパラメーターをテンプレート内のパイプに追加するだけです。 この部分は、Angular 4 または Angular 5 を使用している場合は異なります:.

Angular 4 では次のようにします:
<td>{{order.totalSale | currency:'USD':true}}</td>

Angular 5+ では次のようにします:
<td>{{order.totalSale | currency:'USD':'symbol'}}</td>

最初のオプションは通貨コードです(たくさんあります、米ドルに限定されていません!)。 2つ目はシンボル表示です。 Angular 4 では、これは通貨記号を使用するかコードを使用するかを示すブール値です。 Angular 5+では、選択肢は文字列としてsymbolcode、またはsymbol-narrowです。

これで期待通りの記号が表示されるはずです。
Screenshot of Total Sale column of app

そして完了しました!

そして、私たちは、この記号が表示されるようになりました。

Conclusion

あなたは最後までよく頑張りました! このガイドで達成したことは以下のとおりです。

  1. AngularJS テンプレート構文を Angular 構文に置き換える
  2. ソートをコンポーネントに移動する
  3. Using scoped CSS styles
  4. フィルタリングをコンポーネントに移動する
  5. AngularJS 通貨フィルターを Angular 通貨パイプに置き換える

ここからどこに行くべきでしょうか。

  • Make the sorting more sophisticated (for example: should the ordering reset or stay the same when the user clicks a new header?)
  • Make the filtering more sophisticated (search for numbers or nested properties)
  • Change to a reactive approach. keyup 関数の代わりに値の変化の observable をリッスンして、そこでソートとフィルタリングを行うことができます。 observable を使用すると、入力のデバウンスのような本当にクールなこともできるようになります!