久々に Angular を触ってとても簡単なものを作ったのだけど、これがまた結構はまりました。いつもにもまして自分のためのブログになっていますので、ご了承ください。
目標のイメージ
こんなレイアウトを単純に作りたかったのですが、結構時間を使ってしまいました。結城さんの一連の投稿に影響を受けて、自分の理解を振り返って何が足りないのか分析してみます。
使っているライブラリ
Nebular 世間だとみんなReactや、Vue.js に乗り換えている感じですが、メインタスクじゃないので使ったことがあるやつ、TypeScriptのサポートがほしいと同僚がいっていたという二つの理由でサクッと作るつもり、、、だったのですが、結構はまりました。Angular 5.x が入っていることもあって、スターターキットを素直に使うことにしました。凄くインパクトのあるサンプルになっているのですが、ひさびさにTypeScriptをよむ身としては、少々複雑な感じはしました。
4つのカードで折り返す
こんなの楽勝だろうと思っていた、4つのカードで折返すということに関してかなりのハマりました。まず、最初のポイントは、親子関係になっているのでデータの引渡しです。
子へのデータの引き渡し。
親のダッシュボードのコードはこうなっています。[chartValue]
を親がわに書いておくと、
<ng-container *ngFor="let t of rows.teams">
<div class="col">
<ngx-solar [chartValue]="t.uppercent" [teamName]="t.name" [uptime]="t.uptime" [point]="t.point"></ngx-solar>
</div>
</ng-container>
そこに書いている名前を子のコンポーネントの所に、こんな感じでコードを書いておきます。
export class SolarComponent implements AfterViewInit, OnDestroy {
private teamUptime = 0;
@Input("uptime")
set uptime(value: number) {
this.teamUptime = value;
}
Input
のアノテーションのついた関数に値が引き渡されるので、子側のテンプレートでその値を参照できるようになるわけです。
<nb-card size="xsmall" class="solar-card">
<nb-card-header>{{teamName}} Status</nb-card-header>
<nb-card-body>
<div echarts [options]="option" class="echart">
</div>
<div class="info">
<div class="value">{{teamPoint}} points</div>
<div class="details"><span>uptime</span> {{teamUptime}} min</div>
</div>
</nb-card-body>
</nb-card>
ちなみに、最初のコードはこんなのでした。
<ng-container *ngFor="let t of rows.teams">
<div class="col">
<ngx-solar [chartValue]="{{t.uppercent}}" [teamName]="{{t.name}}" [uptime]="{{t.uptime}}" [point]="{{t.point}}"></ngx-solar>
</div>
</ng-container>
このようにしていると、つぎのエラーが出ます。メッセージだけではなんやわかりませんが、結局の所、[charValue]
の記法と、{{t.uppercent}}
の記法が同じ所に書けないようです。
Parser Error: Got interpolation ({{}}) where expression was expected at column 0 in [{{t.uppercent}}] in ng:///DashboardModule/DashboardComponent.html@4:19 ("
<ng-container *ngFor="let t of rows.teams">
<div class="col">
折り返しの方法がわからない
当初は楽勝と思っていました。たとえば4行目に改行を入れるなら、((index+1) % 4)
の値を見て、ゼロの時になんらかのオペレーションをすればいいだけです。最初は、ngFor
で index 付きでループする方法がわかりませんでしたが、こんな感じでやれるとわかりました。
<div *ngFor="let t of teams; let i = index" [attr.data-index]="i">
ところがまだまだ甘かったのです。
レイアウトが崩れる。
実際にそういうコードを書いて見ます。今回のフレームワークでは、<div class="row"></div>
とかを書いてあげると、そのタグが終われば改行されます。しかし、うまくは行きませんでした。理想的には、HTLM はこんなのになって欲しい。
<div class="row">
<div class="col">
<ngx-solar [chartValue]="90" [teamName]="Team1" [uptime]="30" [point]="250">
</ngx-solar>
</div>
<div class="col">
<ngx-solar [chartValue]="90" [teamName]="Team2" [uptime]="30" [point]="250">
</ngx-solar>
</div>
<div class="col">
<ngx-solar [chartValue]="90" [teamName]="Team3" [uptime]="30" [point]="250">
</ngx-solar>
</div>
<div class="col">
<ngx-solar [chartValue]="90" [teamName]="Team4" [uptime]="30" [point]="250">
</ngx-solar>
</div>
</div>
<div class="row">
<div class="col">
<ngx-solar [chartValue]="90" [teamName]="Team1" [uptime]="30" [point]="250">
</ngx-solar>
</div>
<div class="col">
<ngx-solar [chartValue]="90" [teamName]="Team2" [uptime]="30" [point]="250">
</ngx-solar>
</div>
<div class="col">
<ngx-solar [chartValue]="90" [teamName]="Team3" [uptime]="30" [point]="250">
</ngx-solar>
</div>
<div class="col">
<ngx-solar [chartValue]="90" [teamName]="Team4" [uptime]="30" [point]="250">
</ngx-solar>
</div>
</div>
ところが、実際に、下記のようにすると
<div class = "row">
<div *ngFor="let t of teams" ;let i = index" [attr.data-index]="i">
<div class="col">
<ngx-solar [chartValue]="t.uppercent" [teamName]="t.name" [uptime]="t.uptime" [point]="t.point"></ngx-solar>
</div>
<div ngIf="isRowEnd(i)> // (i +1) % 4 で改行の判断
</div>
<div class="row">
</div>
</div>
</div>
:
ngFor
や、ngIf
もタグとして現れてしまいます。制御がしたいだけで、本来ならタグは不要です。インターネットで検索してしても、ng-repeat-start
ng-repeat-end
でできるとか書いてあったのですが、うまく動きません。結局どうだったかというと、ng-container という要素名にすることによって、HTMLタグが表示されないというテクニックがつかえます。先のng-repeat-start
などのテクニックは、Angular1世代のもののようです。
<ng-container *ngFor="let rows of viewTeams">
<div class = "row">
<ng-container *ngFor="let t of rows.teams">
<div class="col">
<ngx-solar [chartValue]="t.uppercent" [teamName]="t.name" [uptime]="t.uptime" [point]="t.point"></ngx-solar>
</div>
</ng-container>
</div>
</ng-container>
View のデータモデルの変更
さて、この過程で、せっかくng-container
がつかえて余計なタグが出力されなくなっても、問題があります。先ほどの作戦でngIf
などで改行のための、タグ追加とかすると、タグの階層が異なるので結局うまく表示されなくなります。うーむ。困った。あるブログを読んでいると、ViewControllerを作って、そこで、データ構造を返還すれば良いとありました。CQRSのようですね。まずは、私はサンプルを作って見ました。最初のが普通にRESTから取れるデータイメージだとすると、二番目のハードコードされたデータ構造をあるタイミングで変換して、ダブルループで回せるようにしてあげればいいです。コードは全くエレガントではありませんが、正しく変換されます。このコードの中で、先ほどの、ロジックをつかえば、しっかりと最初のデータ構造の子として4ついないのデータの構造になります。そうすると簡単になりうまく行きました。
よりエレガントなコードをかける人は是非コメントいただけると学びになって嬉しいです。
var teams = [
{"name" : "Team1",
"uptime": 30,
"uppercent": 50,
"point": 120},
{"name" : "Team2",
"uptime": 120,
"uppercent": 90,
"point": 530},
{"name" : "Team3",
"uptime": 100,
"uppercent": 90,
"point": 120},
{"name" : "Team4",
"uptime": 120,
"uppercent": 90,
"point": 530
},
{"name" : "Team5",
"uptime": 30,
"uppercent": 50,
"point": 120},
{"name" : "Team6",
"uptime": 120,
"uppercent": 90,
"point": 530},
{"name" : "Team7",
"uptime": 100,
"uppercent": 90,
"point": 120},
{"name" : "Team8",
"uptime": 120,
"uppercent": 90,
"point": 530
}
]
var row = 0;
var viewModels:[{[k:string]: any}] = [{}]; // typescript doesn't allow the ambigous type.
viewModels = [{
"row": "0",
"detail": [{"name" : "Team8",
"uptime": 120,
"uppercent": 90,
"point": 530
}, {"name" : "Team7",
"uptime": 100,
"uppercent": 90,
"point": 120}]
},
{
"row": "1",
"detail": [ {"name" : "Team8",
"uptime": 120,
"uppercent": 90,
"point": 530
}, {"name" : "Team7",
"uptime": 100,
"uppercent": 90,
"point": 120}]}
];
for (let row of viewModels) {
console.log(row.row);
for (let team of row.detail) {
console.log(team);
}
}
console.log("*********************");
var numberOfRow = 4;
var viewTeams:{[k:string]: any}[] = []; // initialize the type of array.
var localTeams = [];
var lastRow = -1;
teams.forEach((team, index) => {
var row = Math.floor(index / numberOfRow);
if (row != lastRow && lastRow != -1) {
viewTeams.push(
{
"row": lastRow,
"temas": localTeams
}
)
localTeams = [];
lastRow = row;
}
if (lastRow = -1) {
lastRow = row;
}
localTeams.push(team);
});
viewTeams.push(
{"row" : lastRow,
"teams": localTeams});
console.log(viewTeams);
データの構造の初期化
最初TypeScript でつぎのようなコードを書いているとエラーになりました。
var viewTeams:[{[k:string]: any}] = []
どうやったら初期化できるかというとこうかくといけます。
var viewTeams:[{[k:string]: any}] = [{}]
しかしこれには大きな問題があって、Array の中に空の要素が一つできてしまうために、空のArrayではなくなる事です。正しい書き方はこれ。
var viewTeams:{[k:string]: any}[] = [];
自分は何をわかっていなかったのか?何がわかっていれば解けたのか?
当初は、データ構造を変換させるアイデアは出てきませんでしたが、正解でした。ng-continer
みたいなものがあると知らず、タグができてしまうのは仕方ないと考えていました。ちなみに、最初のサンプルを解析するのも時間がかかりました。親子とかも知りませんでした。
結局のところ、「Angular2」の文法がをちゃんと知らないからという自分的な結論になりました。typescript
の基礎力もなさそうです。基礎がないのです。だから、対策としては、「typescriptとか、Angular2のビデオで、大体の機能をおぼえてしまう、リファレンスを引かなくてもいいように、基本的な構文はちゃんと調べて、色々試して、仕組みを理解してる、おぼえてしまうのがいい気がしています。空いた時間で、Pluralsight か、Safari books online あたりで、Angular2 を探して勉強してして見ます。