Angular CDK drag-and-drop の紹介
こんにちは。
この記事ではAngular CDKの次期アップデートで提供される、 drag-and-drop 機能を紹介します。 執筆時点ではまだnpmパッケージとして公開されていないので、一般に利用できるまでにはもうしばらくかかりますが、 もし早く使いたい方は、次のコマンドで開発版ビルドをインストールしましょう。 なお、開発版ビルドですので自己責任でお願いします。
$ yarn add angular/cdk-builds
CDK drag-and-drop
drag-and-dropはその名のとおり、UI上でのドラッグアンドドロップ操作をサポートするものです。
@angular/cdk/drag-drop
パッケージから提供される DragDropModule
をインポートすると、次の2つのディレクティブ、コンポーネントが利用できます。
cdkDrag
ディレクティブ
cdkDrag
ディレクティブは、ドラッグされる要素を指定するディレクティブです。このディレクティブを付けられた要素は画面上で自由に位置を変えられます。
たとえば ng new
直後のテンプレートHTMLで、 li
要素に cdkDrag
ディレクティブを付与すると、次のようになります。
(わかりやすさのために li
要素にCSSでスタイルを付与しています)
<ul> <li cdkDrag> <h2><a target="_blank" rel="noopener" href="https://angular.io/tutorial">Tour of Heroes</a></h2> </li> <li cdkDrag> <h2><a target="_blank" rel="noopener" href="https://github.com/angular/angular-cli/wiki">CLI Documentation</a></h2> </li> <li cdkDrag> <h2><a target="_blank" rel="noopener" href="https://blog.angular.io/">Angular blog</a></h2> </li> </ul>
cdkDrag
ディレクティブだけを使うと、何の制約もなく自由に移動することができました。
cdk-drop
コンポーネント
このままでは動いて面白い以上の意味がないので、cdk-drop
コンポーネントを使います。
cdk-drop
コンポーネントは、cdkDrag
ディレクティブをグルーピングし、動きに制限をつけて、限られた領域内でだけ移動できるようにします。
たとえば、ul
要素の外側に <cdk-drop>
コンポーネントを配置すると、ul
の内部でだけ移動できるようになり、移動中は並び替えが行われるようになります。
<cdk-drop> <ul> <li cdkDrag> <h2><a target="_blank" rel="noopener" href="https://angular.io/tutorial">Tour of Heroes</a></h2> </li> <li cdkDrag> <h2><a target="_blank" rel="noopener" href="https://github.com/angular/angular-cli/wiki">CLI Documentation</a></h2> </li> <li cdkDrag> <h2><a target="_blank" rel="noopener" href="https://blog.angular.io/">Angular blog</a></h2> </li> </ul> </cdk-drop>
見てのとおり、 <cdk-drop>
タグの内側でだけ並べ替えが行われるようになりましたが、ドロップしてしまうともとの状態に戻ります。
これはcdk-drop
コンポーネントの仕様で、ドラッグアンドドロップが終了すると、その内部のcdkDrag
の順序は復元されます。
ただし、ドラッグアンドドロップ終了時にはdropped
イベントが発行されていて、このイベントをもとにコンポーネント側からデータモデルを更新することで、
ドラッグアンドドロップによる並べ替えを実現できます。
並べ替え
並べ替えをおこなうためには、リストをコンポーネント側で管理する必要があります。これまでは適当なli
要素を使っていましたが、AppComponentに次のようなlist
プロパティをもたせます。
@Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { list = ['まぐろ', 'サーモン', 'えび']; }
そしてテンプレートを次のように変更し、list
プロパティの要素を繰り返し表示します。先程までと同じように、<cdk-drop>
タグのなかで繰り返される並べ替えの対象にcdkDrag
ディレクティブを付与します。
<h2>好きなネタ</h2> <cdk-drop [data]="list" (dropped)="drop($event)"> <ul> <li *ngFor="let item of list" cdkDrag> <h2>{{item}}</h2> </li> </ul> </cdk-drop>
ポイントは <cdk-drop [data]="list" (dropped)="drop($event)">
です。[data]
プロパティには並べ替えの対象となるデータモデルを渡します。
次に、(dropped)="drop($event)"
では、dropped
イベントハンドラでdrop
メソッドを呼び出しています。
drop
メソッドは次のように記述します。
import { CdkDragDrop, moveItemInArray, } from '@angular/cdk/drag-drop'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { list = ['まぐろ', 'サーモン', 'えび']; drop(event: CdkDragDrop<string[]>) { moveItemInArray( event.container.data, event.previousIndex, event.currentIndex ); } }
CdkDragDrop<string[]>
は、drop
イベントの引数の型です。ジェネリックのstring[]
は並べ替え対象の配列の型を表しています。
moveItemInArray
関数は、基本的な配列の並べ替えを行ってくれるCDKの機能です。中身は単なるJavaScriptの配列の並べ替えですが、Angularチームによる実装にまかせておくのが安心だと思います。
export function moveItemInArray<T = any>(array: T[], fromIndex: number, toIndex: number): void { const from = clamp(fromIndex, array.length - 1); const to = clamp(toIndex, array.length - 1); if (from === to) { return; } const target = array[from]; const delta = to < from ? -1 : 1; for (let i = from; i !== to; i += delta) { array[i] = array[i + delta]; } array[to] = target; }
これで、drop
イベントによって配列を並べ替えられるようになりました。
複数のcdk-drop
でグルーピングをおこなう
複数のグループを跨いだ並べ替えも可能です。先程のAppComponentを次のように変更します。list
プロパティをlike
プロパティに改名し、新しくunlike
プロパティを追加します。
@Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { like = ['まぐろ', 'サーモン', 'えび']; unlike = ['数の子', 'たくあん']; drop(event: CdkDragDrop<string[]>) { moveItemInArray( event.container.data, event.previousIndex, event.currentIndex ); } }
テンプレートでは、like
プロパティとunlike
プロパティの両方で同じようにcdk-drop
による並べ替えができるようにします。
<h2>好きなネタ</h2> <cdk-drop [data]="like" (dropped)="drop($event)"> <ul> <li *ngFor="let item of like" cdkDrag> <h2>{{item}}</h2> </li> </ul> </cdk-drop> <h2>好きじゃないネタ</h2> <cdk-drop [data]="unlike" (dropped)="drop($event)"> <ul> <li *ngFor="let item of unlike" cdkDrag> <h2>{{item}}</h2> </li> </ul> </cdk-drop>
ここまでは先程と変わりません。ここから、この2つのグループを結合します。
並べ替えグループを結合するには、cdk-drop
のconnectTo
プロパティを使います。このプロパティに結合の対象となるグループの参照を渡します。
<h2>好きなネタ</h2> <cdk-drop #dropLike [data]="like" (dropped)="drop($event)" [connectedTo]="[dropUnlike]"> <ul> <li *ngFor="let item of like" cdkDrag> <h2>{{item}}</h2> </li> </ul> </cdk-drop> <h2>好きじゃないネタ</h2> <cdk-drop #dropUnlike [data]="unlike" (dropped)="drop($event)" [connectedTo]="[dropLike]"> <ul> <li *ngFor="let item of unlike" cdkDrag> <h2>{{item}}</h2> </li> </ul> </cdk-drop>
さらに、AppComponentのdrop
メソッドで、グループを跨いでいた場合の処理を追加します。
この場合も汎用的なグループ移動機能をサポートするtransferArrayItem
関数が提供されているので、それを使います。
グループを跨いだ移動かどうかは event.previousContainer
と event.container
を比較して判定できます。
次のように書けば、一致する場合は配列内での移動を、一致しない場合はグループを越えた移動をおこないます。
drop(event: CdkDragDrop<string[]>) { if (event.previousContainer === event.container) { moveItemInArray( event.container.data, event.previousIndex, event.currentIndex ); } else { transferArrayItem( event.previousContainer.data, event.container.data, event.previousIndex, event.currentIndex ); } }
これで複数のグループを跨いだドラッグアンドドロップによる並べ替えができるようになりました。
CSSによるスタイリング
最後に、CDKのdrag-dropが提供するスタイリングのためのCSSクラスを紹介します。
.cdk-drag.placeholder
.cdk-drag.placeholder
クラスは、ドラッグされている要素のプレースホルダ部分につけられるCSSクラスです。たとえばここを次のように見えなくすることで自然な挿入を演出できます。
.cdk-drag-placeholder { opacity: 0; }
.cdk-drag-preview
.cdk-drag-preview
クラスは、ドラッグされている要素のプレビュー部分(動かしている部分)につけられるCSSクラスです。たとえば次のように半透明にすることで自然な挿入を演出できます。
.cdk-drag-preview { box-sizing: border-box; opacity: 0.5; }
この他にもいくつかCSSクラスがあります。詳しくはスタイリングに関するドキュメントを参照してください。
まとめ
- Component Dev Kitの次期アップデートでドラッグアンドドロップがサポートされる
cdkDrag
ディレクティブとcdk-drop
コンポーネントで並べ替えやグルーピングが簡単に実装できる
CDKのアップデートはAngular v7のリリースと合わせておこなわれるだろうと見られています。 楽しみに待ちましょう。
ng-sake #12 を開催しましたレポート
8月6日にng-sake #12を開催しました!
数字
今回の出席者数は 12人 + スタッフ 3人 という構成でした。
参加費は¥1,500、今回もKyashによる集金をオプションで用意しましたが、6人がKyashで送金してもらえました。ご協力ありがとうございました!
内容
会場
株式会社トレタさんのオフィスを使わせていただきました。初めてのトレタでしたが広くてよかったです。さすが五反田バレー。
フード・ドリンク
お酒はいつもどおりカクヤスでプレミアムモルツと氷結やほろよいなどを用意しました。
フードは、今回は宅配ピザをやめて、サンドイッチを提供してみました。 みなさんピザはもう飽きてると思うんですよ。
写真撮ろうと思ったらいくつか食べられてました。
サンドイッチはフレッシュデリというお店のデリバリーを使いました。おいしかったです。
ライトニングトーク
今回は3人がライトニングトークを発表しました。
- kouさん: ngrx/effectsの話
- t2さん: Angularをlit-htmlで書けるようにする
- laco: jsx-to-ivyの続報
いつもよりもさらに一般受けしない話題で、ng-sakeらしいLTでした。
ディスカッション
今回のディスカッションボードはこちら
反省点
- YouTube配信しようと思ったけど機材の準備に失敗した
- 家で練習したので次回はうまくいけばいいな…
- ソフトドリンクを用意してなかった
- カクヤスの注文にいれておきたい
最近ng-sakeの開催頻度が低下しているので、もともとの隔月くらいに戻したいなと思っています。 できれば10月〜11月くらいにまたやりますので、興味があれば ng-japan Slackの #ng-sake チャンネルで更新情報をウォッチしてください!
Angularコンポーネントのスタイルにemotionを使う
型安全にCSSのオブジェクトを書きたいというだけならNgStyleとcsstype
を使うだけでもよさそうだ。
emotionを使うことによる利点は、
くらいなものか。
今日の境界遊び。CSS in JSをAngularでやりたかった。 常識のある方は真似しないほうがよい。
今回使ったのは https://emotion.sh/.
Angularで時々困るのはstylesの中にデータバインディングを置きたいケース。 たとえば、フォームの入力に応じて動的にフォントサイズを変えるようなケースを考える。
emotionの css
関数は、与えたCSSスタイルシートをシリアライズしてユニークなCSSクラス名に変換してくれる。
AngularのコンポーネントはHTML要素と1:1に対応するので、 [className]
プロパティにバインディングすればemotionで生成したクラスを適用できる。
helloClassName$
プロパティは、フォントサイズに応じたCSSスタイルシートをCSSクラス名に変換したObservableである。
helloClassName$ = this.form.valueChanges.pipe( map(({ fontSize }) => css({ fontSize })) );
これをテンプレート中で次のように使えば、emotionによって動的に生成されたクラスを任意の要素に適用できる。
<hello [className]="helloClassName$ | async" [name]="name"></hello>
ところで、Angularの CommonModule
(@angular/common) は、 NgClass
と NgStyle
という2つのディレクティブを提供している。
https://angular.io/api/common/NgStyle
https://angular.io/api/common/NgClass
className
を使わずとも、次のように書くこともできる。NgClass
は文字列以外にも文字列の配列やオブジェクトを受け取れる以外には、本質的にclassName
と何も違いはない。
<hello [ngClass]="helloClassName$ | async" [name]="name"></hello>
NgStyle
を使う場合は、emotionではなく生のスタイルシートっぽいオブジェクトを渡すことになる。
本来AngularだけでCSS-in-JSやろうとするとこのAPIになるわけだが、emotionだとcss
関数の引数オブジェクトにTypeScript型定義もあるし嬉しいのでは?という目論見がある。
あとemotionなら同じスタイルなら同じクラスになり、キャッシュの仕組みが強いっぽいので、パフォーマンス良くなるかもしれない。
https://emotion.sh/docs/typescript
helloStyle$ = this.form.valueChanges.pipe( map(({ fontSize }) => ({ fontSize: `${fontSize}px` })) ); <hello [ngStyle]="helloStyle$ | async" [name]="name"></hello>
すべて同じ動きとなるので好みで選べばよいが、個人的にはReactとの対称性を考えて[className]
でよいのでは?と感じる。
実際に動くサンプルは次の通り。
Reactの場合、classNameはHTML要素に対応するコンポーネントにしか使えないが、Angularの場合すべてのコンポーネントはHTML要素に対応付けられるので、テンプレート中で親からclassNameプロパティにバインディングするだけで子コンポーネント側でなにもしなくてもよいのは、比較的楽だなと思った。 しかしemotionで一番やりたいstyled-componentがReactしか使えないので、これをどうにかしてみたい。
GraphQLとRESTfulについて今日考えてたこと Backend for Usecase/Resourceについて
DISCLAIMER: これは本当にただのメモ書きで、これがベストプラクティスだとかいう話ではないので、同じようなことを考えてる人いたら今度議論しましょうよ、って程度の話の種。
GraphQLを使うべきスポット、RESTfulが好ましいスポットについて今日ぼんやり考えていて、なんとなく言語化ができる気がするので文字起こししてみる。
Backend for UsecaseとBackend for Resource
バックエンドのAPIには2種類あって、
- 「データ」を構成する「リソース」を提供するもの
- アプリケーションの「ユースケース」がもつシナリオのなかで登場する「データ」部分を埋めるためのもの
を区別することが必要そう、と思っている。
まず前者を Backend for Resource (BFR)と呼ぶことにする。これはわかりやすくて、これはまさしくRESTfulそのもの。 RDBやそうじゃないDB、あるいはファイルストレージかもしれない永続化されたリソースにURIを付与し、外部からアクセス可能にするのが役目。 BFRの設計はリソースのスキーマに依存する。
次の後者を Backend for Usecase (BFU)と呼ぶことにする。これがGraphQLが向いていそうなところ。 フロントエンドのユースケースのなかで、永続化されたデータが必要になった時の問い合わせ先。 つまり、BFUの設計はフロントエンドのユースケースに依存する。
なんで分けたいのか
BFFの文脈でいろんな理由が挙げられているとは思うが、個人的にはそのシステム自体のライフサイクルの違いに合わせた分割が、柔軟な開発サイクルを支えてくれるんじゃないかと思っている。
フロントエンドはユーザーが触れる最前線にあるので、UXの改善、ユースケースの再設計は成長段階のプロダクトにおいて頻繁に発生する。 フロントエンドについては一旦諦めるとして、バックエンドの中には、そうしたユースケースの再設計に巻き込まれてもしょうがない部分とそうでもなさそうな部分がある。 前者が、「ユースケースに対するデータプロバイダー」としてのバックエンド、つまり上述のBFUで、後者が「リソースコンテナ」としてのバックエンド、つまりBFRじゃないかと思う。
ユースケースが再設計されたとしても、リソース自体に変化があるわけじゃないなら、その部分は残したい。ユースケースに依存する範囲を明確に分けたい、というのが僕のBFU/BFRに対するモチベーションの大きな部分。 その境界でバックエンドのサーバーごと分けてしまうというのは、昨今のコンテナ化の流れにもマッチしてそうな気がする。
なんでBFUはGraphQLがよさそうか
フロントエンドのユースケースの中では目的が必ず存在してデータ要求が生まれる。 すごく単純化したら「ID=2のUserのデータがほしい」ではなく「ID=2のユーザーの名前を表示するためのデータがほしい」という話で、そのユースケースにとってはユーザーの名前がUserテーブルのNameカラムに保存されているとかそんなことはどうでもよく、ただユーザーの名前がほしい、というニーズだけがある。
BFUがない場合は、フロントエンドのなかでBFRから受け取ったデータをユースケースに合った形に自分で整形することになる。あるいはBFRが拡張されてユースケースを受け入れはじめるかのどちらか。
BFUがある場合は、フロントエンドはBFUにユースケースを投げつける。ユースケースを定義するのはフロントエンドの仕事なので、GraphQLのようにフロントエンドでほしいデータの形を定義するのがマッチするはず。
フロントエンド、BFU、BFRが分かれたとき、それぞれのシステムのライフサイクルは次のようになる。
悲しいことにフロントエンドはまっさきに壊されるが、それは宿命として諦めるとして、BFUがあることでBFRは長生きすることができる。
やっちゃいけないこと
BFUは「ユースケースに対するデータプロバイダー」としてのバックエンドであるから、常に実装の要件はフロントエンドのユースケースによって決定される。 そのため、次のことはやってはいけない。
BFUは常にアプリケーションのために存在するが、BFRはアプリケーションが無くても成立する。
とりあえずここまで。今日10分くらい会社でryopekoさんと雑談してたネタなのであまりしっかり練った考えではないけど、こんな感じのシステムを作って試してみたい。
Kaizen Platformに入社して1年が経ちました(在職エントリ)
Inspired by
おはようございます、lacoです。 早いもので、Kaizen Platformに入社してから1年になります。
これは退職したときの記事です。
そしてこれが入社後3ヶ月の記事ですね。こんな記事書いてたこと完全に忘れてました。
1年間なにやったか
AngularJSアプリケーションのチューニング
入社後の初めてのプルリクエストはAngularJSのチューニングで、めっちゃ遅かったドロップダウンリストの描画を7倍くらい速くしました。
その後もしばらく、パフォーマンスに問題がある画面の計測と改修に手を付けていました。
フロントエンド開発チームの体制づくり
僕が入って4人になったフロントエンドチームですが、新規の小規模アプリ開発をいくつか並行して進めることにしました。 4人がそれぞれ個人戦のようになる上で、守るべき約束事を決めようということで、「フロントエンドマイクロアプリケーション憲章」というものを制定しました。
その中で定めたコードフォーマットの決まりのために、Prettierの設定を共通化して公開しました。 いまでも大活躍しています。
この憲章は4月に行ったフロントエンドチームの福岡合宿で半年ぶりにアップデートされました。その記事はKaizen Platformのエンジニアブログでまとめられています。
憲章があることで、逆に心理的に安全に冒険できる環境が作れているんじゃないかと思っています。「公園には柵があるから自由に遊べる」、というのが最近の口癖。
AngularでSPA開発
当然といえば当然ですが僕もSPA開発やってまして、さっき書いた 新規の小規模アプリ開発
ってやつをAngular使って2つほど開発してます。
設計を試行錯誤したり、アプリケーションの要件が変わったりいろいろありますが、TypeScriptでよかった〜となる場面がとても多いですね。
コンパイラによる静的検査がなければ安心してリファクタリングできません。
ワークフローづくり
2つのうち今年の頭から始まったものは、実験的にバックエンドとフロントエンドの間の通信にGraphQLを採用しています。
API Spec中心開発
と銘打って、リモートワークを前提とした非同期的な分業をスムーズにするためのワークフローを考えて、実践してみたのは楽しかったし学びも多かったです。
同じような文脈で、フロントエンドとバックエンドの開発者間だけじゃなく、デザイナーとの分業においても仕事を型化してワークフローを定義するとよいんじゃないか、ということでそういったことも今年に入って取り組んでいます。
デザイナーの中に閉じてブラックボックス化していたデザインプロセスを分解、整理して、プロダクト設計原則
という形に再構成しました。
ここ1ヶ月くらいは、この設計原則に従って、いままでだったらデザイナーに頼んでいたタスクを、僕が代わりに原則に沿って進めることを試しています。 ステップごとに進めばいいのでデザイン初心者でも安心して仕事ができます。やはり型があると安心。
ver 1.0としているので、うまくいくところ、いかないところをもとにバージョンアップを続けていくつもりです。
オープンコミュニティ活動
おそらくお気づきかとは思いますが1年間まったく滞ることなくAngular日本ユーザー会をはじめとしたいろんなオープンコミュニティの活動つづけてました。 大きめのイベントだけ拾ってみてもこんだけあちこちいろいろやってて、露出の多い1年でした。
本の監修なんかも経験しました。
やりたかったこと
1年前の記事で、今後やりたいことを書いていました。
Angular日本ユーザー会をはじめとしたコミュニティ活動は今後も継続したい
できていますね
OSSコントリビューション増やしていきたい
できているんじゃないかと思います
JavaScriptを駆逐したい
きびしい
Flutterに夢を見たい
リリースプレビューおめでとう!!!!!!!!!!🎉🎉🎉🎉
ということで概ね達成している気がします。
1年やってきて
思ってた以上にKaizen Platformという会社は馴染んでいて、日々楽しく仕事をしています。 プログラミングするのも好きなんだけど、どちらかというとエンジニアリングというか、どうやったら生産性を向上できるのかっていうのを考えるのが好きなんだなーというのに、この1年で気づきました。 知らない自分を発見できた感じ。
これはチームのマネージャーになりたいとかそういうわけじゃないんですよね。 むしろデベロッパーがどうすれば生産性上がるかってことは、デベロッパー自身も当事者として考えなきゃいけないところだと思うし、考えなきゃいけないっていうか、考えると楽しいですよ。 自分を歯車に見立てて、うまい回し方や他の歯車との噛み合わせ方を考える、パズルゲームみたいな感じがします。
って感じで、脳みその使い方が1年間でだいぶ変わったなーと、自分なりに思います。
これから
特になにか節目ということでもないので今やってることは引き続きやっていきます。 あと、自分の働き方をいろいろ実験してみようと思ってて、有り体に言えば副業っぽいんですが、今年のテーマとして「OSSのように働く」ってのを目指してます。
OSSにコントリビューションするのって、面白そうなリポジトリを見つけて、自分が貢献できそうな部分を見つけて、そこにアプローチしていくじゃないですか。 そんな感じで、面白そうな事業を見つけて、自分が貢献できそうかどうか自分からアプローチして、コントリビューションするように仕事ができたら楽しいんじゃないかって考えてます。
せっかく新しい自由な働き方を推進してる会社に所属しているので、自分自身で試行錯誤していきます。
まだ実際にできているわけじゃないんですけど、もしそういうことができたら楽しいなーと思ってるし、続報があればまたブログ書きます。
ところで、試しにPatreon登録してみました。$1から投げられるのでぜひよろしくおねがいします。
はじめた経緯などはこちらの記事に書いています。
それでは。