1月メモ・リンク集

1月に調べたことのメモです。

Angular関連

Date系ライブラリとIANA

最近はLuxonを利用しています。Intlのブラウザ対応状況、他のライブラリのタイムゾーン関連のサポートについて気になったので、そのあたりを調べた時のメモです。

複数のプロジェクタで共有したいロジックとコンポーネントをパッケージ化した

必要な前提知識

最終的に上記環境を利用し、簡単にパッケージ作成ができるようになったのだが、実際には色々とつまづいた。原因は、前提となる基礎知識がかけていたから。
(基本的な公式ドキュメントはしっかり読みましょうという話)

ReactのHOCs

その他

AngularのCDK Tableのコードを読みながら、Viewの組み立て方について調べる

段階的に実装してみたサンプルコードを晒します。完成版とおまけを除くと8ステップにわかれています。

Viewの組み立て部分を参考にしたかったので、DataSourceやIterableDiffers, TrackByFunctionなどを利用したコードはありません。

CDK Tableのドキュメント

0: 完成版

f:id:ryotah:20180124234812p:plain

1: コンポーネントを動的にaddする

2: 対象のコンポーネント内にaddする

3: コンポーネントのaddからテンプレートを取得してaddする方式に変更

f:id:ryotah:20180124232835p:plain

4: table.ts, row.ts, cell.tsを用意

  • angular-cdk-datatable-04-celldefs - StackBlitz
    • 各セルの内容を定義するために、CellDefという考え方を導入
    • この段階では実態は export class CellDef { constructor(public template: TemplateRef<any>) {} } というDirective

5: 行定義を追加(RowDef)

app.component.html

<my-table [data]="data">
    <!-- cellを定義 -->
    <ng-container *cellDef="let data;">
        <span style="border: 1px solid cyan;">[cell-1] {{data}}</span>
    </ng-container>
    <ng-container *cellDef="let data;">
        <span style="border: 1px solid yello;">[cell-2] {{data}}</span>
    </ng-container>
    <ng-container *cellDef="let data;">
        <span style="border: 1px solid blue;">[cell-3] {{data}}</span>
    </ng-container>
  
</my-table>  

table.html

<!-- rowを定義 -->
<ng-container *rowDef>
    <div style="border: 1px solid red; background-color: rgba(255,0,0,.2)">[row]
        <ng-container cellOutlet></ng-container>
    </div>
</ng-container>

6: ヘッダー用の行定義を追加(HeaderRowDef)

f:id:ryotah:20180124234656p:plain

app.component.html

<my-table [columns]="columns" [data]="data">

  <!-- header cellを定義 -->
    <ng-container *headerCellDef="let data;">
        <span style="border: 1px solid cyan;">[ttl-1]{{data.title}}</span>
    </ng-container>
    <ng-container *headerCellDef="let data;">
        <span style="border: 1px solid green;">[ttl-2]{{data.title}}</span>
    </ng-container>
    <ng-container *headerCellDef="let data;">
        <span style="border: 1px solid blue;">[ttl-2]{{data.title}}</span>
    </ng-container>

    <!-- cellを定義 -->
    <ng-container *cellDef="let data;">
        <span style="border: 1px solid cyan;">[cell-1] {{data}}</span>
    </ng-container>
    <ng-container *cellDef="let data;">
        <span style="border: 1px solid green;">[cell-2] {{data}}</span>
    </ng-container>
    <ng-container *cellDef="let data;">
        <span style="border: 1px solid blue;">[cell-3] {{data}}</span>
    </ng-container>
  
</my-table>  

table.html

<!-- header rowを定義 -->
<ng-container *headerRowDef>
    <div style="border: 1px solid magenta; background-color: rgba(255,0,255,.2)">[row]
        <ng-container cellOutlet></ng-container>
    </div>
</ng-container>

<!-- rowを定義 -->
<ng-container *rowDef>
    <div style="border: 1px solid red; background-color: rgba(255,0,0,.2)">[row]
        <ng-container cellOutlet></ng-container>
    </div>
</ng-container>

7: accessorを追加

8: header cell, cellの定義をcolumnの定義でまとめる

f:id:ryotah:20180124235744p:plain

app.component.html

<my-table [data]="data">

    <!-- [name] columnを定義 -->
    <ng-container columnDef="name">
        <ng-container *headerCellDef>
            <span style="border: 1px solid cyan;">Name</span>
        </ng-container>
        <ng-container *cellDef="let row;">
            <span style="border: 1px solid cyan;">{{row.name}}</span>
        </ng-container>
    </ng-container>
    <!-- [age] columnを定義 -->
    <ng-container columnDef="age">
        <ng-container *headerCellDef>
            <span style="border: 1px solid green;">Age</span>
        </ng-container>
        <ng-container *cellDef="let row;">
            <span style="border: 1px solid green;">{{row.age}}</span>
        </ng-container>
    </ng-container>
    <!-- [id] columnを定義 -->
    <ng-container columnDef="id">
        <ng-container *headerCellDef>
            <span style="border: 1px solid blue;">Id</span>
        </ng-container>
        <ng-container *cellDef="let row;">
            <span style="border: 1px solid blue;">{{row.id}}</span>
        </ng-container>
    </ng-container>

</my-table>

9: おまけ

12月メモ・リンク集

12月に調べたことのメモです。

Angular関連

View作成や再利用について

f:id:ryotah:20180107131908p:plain f:id:ryotah:20180107131911p:plain f:id:ryotah:20180107131913p:plain
<div [myItem]="item1" #dir="myItem">
  {{dir.item.header}}: {{dir.item.content}} | <button (click)="dir.onRemove()">remove</button>
</div>

Form関連

f:id:ryotah:20180107131717p:plain
private markFormGroupTouched(formGroup: FormGroup) {
  (<any>Object).values(formGroup.controls).forEach(control => {
    control.markAsTouched();
    if (control.controls) {
      control.controls.forEach(c => this.markFormGroupTouched(c));
    }
  });
}

Dateライブラリ

VSCode

コードフォーマット

{
  "singleQuote": true,
  "trailingComma": "es5"
}
  • tslint --fix
    • 今まで利用してなかったけど、便利すぎた
    • TypeScript Hero のSort and organize your imports (sort and remove unused)をわざわざする必要なくなった
"tslint.autoFixOnSave": true,
"no-unused-variable": true,
"ordered-imports": true,

Go環境用意

  • インストール
    • brew install go
export GOPATH=$HOME/foo/bar
export PATH=$GOPATH/bin:$PATH
  • GOPATH以下に開発環境を構築する
    • go get などのコマンドを実行するとGOPATHフォルダにダウンロードされる
  • PATHを設定しておくとコマンドうつのが楽

Reactチュートリアル

Tutorial

  • Tutorial: Intro To React - React
  • in React apps to use on names for the attributes and handle for the handler methods.

  • Why Immutability Is Important
    • Easier Undo/Redo and Time Travel
    • Tracking Changes
    • Determining When to Re-render in React
  • It’s strongly recommended that you assign proper keys whenever you build dynamic lists.

  • react-tutorial-tic-tac-toe - StackBlitz
    • 自分でやったサンプル

その他

Google Analytics

コンポーネントを設計するときに

f:id:ryotah:20180107131350p:plainf:id:ryotah:20180107131359p:plain
  • 「飛行機のパーツ」とみるか、「2×4のブロック」とみるか
  • 細かくつくる
  • 汎用化と抽象化
  • Viewにどこまで関係するのか(テンプレートにどこまで関係するのか)
    • それは「ロジック」ではないか
    • それは「振る舞い」ではない

moment -> Luxon

Momentと比較してみて

使ってみて1週間ほど経た感想です。v0.2.9を利用。

気に入ったところ

  • Immutability
    • momentのようにcloneメソッドが不要
      • const bar = foo.clone().add(1, 'month')
  • 月を1から12で計算できる
    • momentはネイティブのDateと同様に0スタート
  • ネイティブのIntl APIを利用している
    • moment-timezoneが不要
      • 各localeファイルが不要
      • 柔軟なフォーマットで表示できる(曜日も追加したい、など)
  • オブジェクト作成時に元データ(元フォーマット)に応じて複数のメソッドが用意されている
  • 厳格なパース
  • 単純な値の取得はgetterで統一
    • dateTime.year()に対してdateTime.year
  • 出力フォーマットの拡充

なるほどと思ったところ

  • setterを集約
    • dateTime.year(2016).month(4)からdateTime.set({year: 2016, month: 4})

その他のmomentとの違い

準備

TypeScriptの場合。

型定義ファイル

まだ用意されていないので、このあたりからUse Flow or Typescript · Issue #73 · moment/luxon · GitHub拾ってくる。

npm i @types/luxon

インポート

import { DateTime } from 'luxon';

基本的な書き方

作成

// 現在時刻
const now = DateTime.local();

// 特定の時刻
const dt = DateTime.local(2017, 5, 15, 8, 30);

// 特定フォーマットから作成
DateTime.fromISO('2017-05-15')
DateTime.fromMillis('1494774000000')
// など

https://moment.github.io/luxon/docs/manual/tour.html#creating-a-datetime
https://moment.github.io/luxon/docs/manual/parsing.html

変更

DateTime.fromISO('2017-05-15').plus({months: 2, days: 6}).toISODate();
//=> '2017-07-21'

https://moment.github.io/luxon/docs/manual/tour.html#transforming-your-datetime
https://moment.github.io/luxon/docs/manual/math.html

出力

dt.toISODate();
//=> '2017-04-20'

dt.valueOf();
//=> 1494774000000

dt.toJSDate();
//=> Native Date Object

dt.toLocaleString(DateTime.DATETIME_FULL);
//=> 'April 20, 2017, 11:32 AM EDT'

https://moment.github.io/luxon/docs/manual/formatting.html

日本語にする

import { DateTime, Settings } from 'luxon';

DateTime.fromMillis(1494774000000, { locale: 'ja' }).toLocaleString(DateTime.DATETIME_HUGE);
// -> 2017年5月15日月曜日 0:00 日本標準時

// デフォルトを変更
Settings.defaultLocale = 'ja';

https://moment.github.io/luxon/docs/manual/intl.html

タイムゾーンの変更

import { DateTime, Settings } from 'luxon';

const tz = 'America/Los_Angeles';

DateTime.fromMillis(1494774000000, { locale: 'ja', zone: tz }).toLocaleString(DateTime.DATETIME_HUGE);
// -> 2017年5月14日日曜日 8:00 アメリカ太平洋夏時間

// デフォルトを変更
Settings.defaultZoneName = tz;

// 作成後の変更
const dt = DateTime.local();
const rezoned = local.setZone('America/Los_Angeles');

// ローカルタイムゾーン、デフォルトタイムゾーンに戻す
// https://github.com/moment/luxon/blob/f70e17c5134f221b2534b8fb035374a50e220398/src/impl/util.js#L183
const rezonedDefault = rezoned.setZone();
const rezonedLocal = rezoned.setZone('local');

https://moment.github.io/luxon/docs/manual/zones.html

今後の懸念

  • Intl APIのポリフィルがちょっと不安。

デモ

https://stackblitz.com/edit/angular-date-pipe-and-luxon

Refs

Refs

Parcelを利用してReact × Typescriptの環境を用意

ちょっとしたモック作成や動作確認をしたいときに使ってみようかと思います。
Parcel v1.2.0を利用。

準備

Parcelをインストール

npm install -g parcel-bundler

tsconfig.jsonを用意

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "strict": true,
    "jsx": "react"
  }
}

Index.html

<html>
<body>
  <div id="root"></div>
  <script src="./index.tsx"></script>
</body>
</html>

Index.tsx

import * as React from 'react';
import * as ReactDOM from 'react-dom';

ReactDOM.render(
  <h1>Hello, world!</h1>,
  document.getElementById('root')
);

起動

parcel index.html

参考

ローダーと通信エラーの実装

f:id:ryotah:20171214221323p:plainf:id:ryotah:20171214221325p:plain

Angularアプリのローダーと通信時エラーの実装について。
(Angular 5.0.1を利用)

ローディング

ローダーの種類

  • 通信時につねに表示するもの (A)
  • 通信時に画面をブロックしたいもの (B)

Aの実装方針

  • HTTP_INTERCEPTORSを利用して自動で実行させる

http-interceptor-loader.ts

@Injectable()
export class HttpInterceptorLoader implements HttpInterceptor {
  constructor(private store: Store<formRoot.AppState>) {}

  intercept(
    req: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {

    // 今回はngrxを利用
    // load開始時と終了時にカウントの増減をする
    this.store.dispatch(new LoadCountAdded());
    return next.handle(req).pipe(
      finalize(() => {
        setTimeout(() => this.store.dispatch(new LoadCountRemoved()), 0);
      })
    );
  }
}

actionとstate

/**
 * action
 */
export class LoadCountAdded implements Action {
  readonly type = LOAD_COUNT_ADDED;

  // `withBlocker`があるのは B タイプのローダーでも利用するため
  constructor(public withBlocker = false) { }
}

export class LoadCountRemoved implements Action {
  readonly type = LOAD_COUNT_REMOVED;
  constructor(public withBlocker = false) { }
}
/**
 * state
 */
const initialState: State = {
  loadCount: {
    withBlocker: 0,
    withoutBlocker: 0,
  },
};

stateをsubscribe

export class AppComponent {
  constructor(private store: Store<fromRoot.AppState>) {

    // http://ricostacruz.com/nprogress/を利用

    NProgress.configure({ showSpinner: false });
    this.store.select(fromRoot.getProgressNeeded)
      .subscribe(needed => {
        needed ? NProgress.start() : NProgress.done();
      }
    );
  }
}

Bの実装方針

  • 必要なタイミングでdispatchすればいい

利用例

 @Effect()
  load$: Observable<any> = this.actions$
    .ofType<actions.LoadData>(actions.LOAD_DATA)
    .map(action => action.payload)
    .switchMap(key => {

      // `withBlocker`フラグをtrueにする
      this.store.dispatch(new layout.LoadCountAdded(true));

      return this.service.getData(key)
        .catch(() => {
          // TODO
        })
        .finally(
          () => this.store.dispatch(new layout.LoadCountRemoved(true))
        );
    });

stateをsubscribe

// app-blockerというブロッカーコンポーネントの表示非表示をコントロールする
@Component({
  selector: 'app-root',
  template: `<router-outlet></router-outlet>
  <app-blocker *ngIf="blockerNeeded$ | async"></app-blocker>`,
  styles: [],
})
export class AppComponent {
  blockerNeeded$: Observable<boolean>;
  constructor(private store: Store<fromRoot.AppState>) {
    this.blockerNeeded$ = this.store.select(fromRoot.getBlockerNeeded);
  }
}

BlockerComponent

@Component({
  selector: 'app-blocker',
  template: `
  // bootstrap4のクラス
  // やっていること -> 中央にコンテンツを配置
  <div class="d-flex justify-content-center align-items-center h-100">
    <app-spinner size="lg"></app-spinner>
  </div>
  `,
  styles: [`
  :host {
    position: fixed;

    // 状況に応じて変更
    // z-index: 1000;

    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background-color: rgba(255,255,255,.38);
  }`]
})
export class BlockerComponent {
}

通信エラー

実装方針

  • エラー表示ロジックを実装したHttServiceをつくる
  • HttServiceではエラー表示ロジックのみ実装し、エラーの解決はしない
    • エラーの解決は利用状況により異なることが想定されるので、各Feature Serviceにまかせる
@Injectable()
export abstract class HttpService {
  protected apiUrl = environment.apiUrl;
  constructor(
    protected http: HttpClient,
    private injector: Injector,
  ) { }

  // resolve cyclic dependency
  get ngbModal(): NgbModal {
    return this.injector.get(NgbModal);
  }

  protected handleError(err: HttpErrorResponse | Error): ErrorObservable {
    const modalRef = this.ngbModal.open(NgbmAlertComponent, {
      backdrop: 'static'
    });

    // エラー処理
    if (err instanceof Error || err.error instanceof Error) {

      const msg = err instanceof Error ? err.message : err.error.message;

      // A client-side or network error occurred
      Object.assign(modalRef.componentInstance, {
        title: 'Error',
        message: `An error occurred: ${err.message}`
      });
    } else {

      // The backend returned an unsuccessful response code.
      Object.assign(modalRef.componentInstance, {
        title: 'Error',
        message: `Backend returned code ${err.status}, message was: ${err.message}`
      });
    }
    return Observable.throw(err);
  }
}

利用例

@Injectable()
export class ApiService extends HttpService {
  getHero(): Observable<Hero> {
    return this.http
      .get(`${this.apiUrl}/hero.json`)
      .pipe(
        map(json => /* ... */),
        catchError(err => this.handleError(err))
      );
  }
}

(WIP) createEmbeddedViewとcreateComponen

templateを利用して動的にコンポーネントを生成する

this.viewContainer.createEmbeddedView(this.templateRef);
  • 何が起きているか
    • Angularが生成した<ng-template>を利用して、embedded viewを作成。作成したembedded viewをホスト要素に隣接するview containerに差し込む
    • 隣接とは

ViewContainerRef

  • createEmbeddedView
    • templateRefからEmbeddedViewRefを生成してinsert
  • createComponent
    • ComponentFactoryからComponentRefを生成してinsert
  • insert(viewRef: ViewRef, index?: number): ViewRef
  • clear(): void

TemplateRef

  • 2つの利用方法
    • <ng-template>に配置されたディレクティブ(or directive prefixed with *)
      • TemplateRefがinjectされる
    • Query

EmbeddedViewRef

サンプル

import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';

@Directive({
  selector: '[appUnless]'
})
export class UnlessDirective {
  private hasView = false;

  constructor(
    private templateRef: TemplateRef<any>,
    private viewContainer: ViewContainerRef) { }

  @Input() set appUnless(condition: boolean) {
    if (!condition && !this.hasView) {

      // メモ: コンポーネントから生成する場合
      // this.viewContainer.createComponent(
      //   this.componentFactoryResolver.resolveComponentFactory(component)
      // );

      this.viewContainer.createEmbeddedView(this.templateRef);
      this.hasView = true;
    } else if (condition && this.hasView) {
      this.viewContainer.clear();
      this.hasView = false;
    }
  }
}

参考