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;

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

今後の懸念

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

デモ

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

Refs

Refs

Remove all ads

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;
    }
  }
}

参考

ControlValueAccessorでRadioボタンを含むコンポーネントを実装するメモ

  • (change)を利用
  • ViewChildrenを利用
  • ref.nativeElement.checked = true;でViewを更新

RadioComponent

import { Component, ElementRef, QueryList, ViewChildren } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';

@Component({
  selector: 'app-radio',
  template: `
  <label *ngFor="let value of values">
    <input type="radio"
      (change)="onChange($event.target.value)"
      name="radio" [value]="value" #radio>{{value}}
  </label>
  <pre></pre>
 `,
  styles: [`:host {
    display: block;
    background-color: #fcc;
  }`],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: RadioComponent,
      multi: true
    }
  ]
})
export class RadioComponent implements ControlValueAccessor {
  @ViewChildren('radio') private radios: QueryList<ElementRef>;
  values = ['foo', 'bar', 'baz'];

  onChange: (value: string) => void;
  onTouched: () => void;

  // ControlValueAccessor
  writeValue(value: string) {
    if (value) {
      const target = this.radios.find(radio =>
        radio.nativeElement.value
      );
      if (target) {
        toChecked(target);
      }
    }
  }
  registerOnChange(fn: any) { 
    this.onChange = fn;
  }
  registerOnTouched(fn: any) {
    this.onTouched = fn;
  }
}

function toChecked(ref: ElementRef) {
  ref.nativeElement.checked = true;
}

利用例

<app-radio name="radio" [ngModel]="(form$ | async).radio"></app-radio>

11月メモ・リンク集

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

ngrx関連

各Feature Module(Page Module)のステートを、画面遷移時にリセットする

  • meta reducerを使って以下のように実装
  • LEAVE時にFeatureAのステートをundefinedにする -> 各reducersでinitial stateを設定する

feature-a.module

// ...

StoreModule.forFeature(
  'feature-a',
  fromFeatureA.reducers,
  {  metaReducers: [fromFeatureA.resetReducer]  }
),

reducers/index.ts

// ...

export function resetReducer(reducer: ActionReducer<FeatureAState, routeActions.Actions>) {
  return function (state, action) {
    if (action.type === routeActions.LEAVE) {
      return reducer(undefined, action);
    } else {
      return reducer(state, action);
    }
  };
}

Angular関連

Component/Directive関連

アプリケーションに依存するデータを表示するコンポーネントのインターフェース(@Input)について

コンポーネントで必要とするデータと、実データ(e.g. アプリ上で利用しているデータ型)の違いがある場合。柔軟なインターフェースにしたほうがいい場合。

  • @Inputに渡すデータの型をどうするか
    • データの形式を固定してしまうと、いつか苦労する
      • APIによってサーバから送られるデータが違う場合
      • サーバからもらうデータと、クライアントで作成したデータが同じ構造になるとは限らない
  • @Inputの数 = 更新タイミングの数
    • 不要に増やしたくはない

show-data.component.ts

 export interface IShowDataProps {
  a: number;
  b: numebr;
  c?: string;
  d?: string;
  e?: boolean;
}

// ...

export class ShowDataComponent {
  /**
   * `appData`か`props`のどちらかを受け取り
   * `_props`をつくる
   */
  _props: IShowDataProps;
  @Input() set params(
    params: { appData?: AppData, props?: IShowDataProps, options?: any }
  ) {
    if (params.appData) {
      this._props = this.toProps(params.appData);
    } else if (params.props) {
      this._props = params.props;
    }
  }
  get props() { return this._props; }

  // ...

}

page.html

<show-data [params]="{ appData: appData }"></show-data>
<show-data [params]="{ appData: appData, options: {...} }"></show-data>
<show-data [params]="{
  props: {
    a: 100,
    b: 1000',
    c: 'c',
    e: true,
  }
}">

Form関連

CSS関連