(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関連

AngularのForm(応用)

f:id:ryotah:20171202160340p:plainf:id:ryotah:20171202160319p:plain

これは何か

AngularConnect 2017のセッション「Angular Forms - Advanced Topics」を自分用にまとめたものです。 セッションではCustom Form Controls、ControlValueAccessor、Formを入れ子にする場合のベストプラクティスなどについて話されています。

Angular v5.0.2で動作確認しています。

Custom Form Controls

  • 自作のフォームパーツをつくる
  • ControlValueAccessorを利用する
  • バリデーションが必要な場合はValidatorも利用する
    • 外部から(利用するテンプレートから)requiredなど既存のバリデーションを付与することも可能だが、コンポーネントの機能として必須な条件があれば、内部で定義した方がいい

NG_VALUE_ACCESSORを利用する

@Component({
  // ...
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: FooComponent,
      multi: true
    },
    {
      provide: NG_VALIDATORS,
      useExisting: FooComponent,
      multi: true
    }
  ]
})
export class FooComponent implements ControlValueAccessor, Validator {

  // ControlValueAccessor
  writeValue(obj: any) {
  }
  registerOnChange(fn: any) { 
  }
  registerOnTouched(fn: any) {
  }
  setDisabledState(disabled: boolean) {
  }

  // Validator
  validate(ctrl: AbstractControl) {
  }
}

NgControlを利用する

f:id:ryotah:20180515164825p:plain

  • コンポーネント内でcontrolにアクセスしたい場合(control.errorsを利用したい)
    • [control]=dir.control #dir="ngModel"[control]=form.get('foo') formControlName="foo"でもできるが、毎回書くのは煩わしい
    • NgModelFormControlNameは利用しないでNgControlを利用する(Template-driven、Reactive両方で利用できるようにするため)
  • provide: ...は不要になる
  • @Selfにする意味
export class FooNgControlComponent implements ControlValueAccessor {

  constructor(@Self() public controlDir: NgControl) {
    controlDir.valueAccessor = this;
  }

  ngOnInit() {
    const control = this.controlDir.control;
    const validators = control.validator
      ? [control.validator, Validators.required]
      : Validators.required;
    control.setValidators(validators);
    control.updateValueAndValidity();
  }

  // ControlValueAccessor
  writeValue(obj: any) {
  }
  registerOnChange(fn: any) { 
  }
  registerOnTouched(fn: any) {
  }
  setDisabledState(disabled: boolean) {
  }
}

サンプルコード

メモ

フォームの入れ子

ControlValueAccessorを利用

  • FormGroupの値を利用して、データの受け渡しをする
  • Template-drivenとReactive両方で利用できる
@Component({
  selector: 'app-address',
  template: `
  <div [formGroup]="form">
    <input type="text" formControlName="street">
    <input type="text" formControlName="city">
  </div>`,
  providers: [
    { provide: NG_VALUE_ACCESSOR, /* ... */ },
    { provide: NG_VALIDATORS, /* ... */ },
  ]
})
export class AddressComponent  implements OnInit, ControlValueAccessor {
  form: FormGroup;
  constructor(private fb: FormBuilder) { }

  ngOnInit() {
    this.form = this.fb.group({
      street: '',
      city: ''
    });
  }

  // ControlValueAccessor
  writeValue(obj: any) {
    if (obj) {
      this.form.setValue(obj, { emitEvent: false });
    }
  }
  registerOnChange(fn: any) {
    this.form.valueChanges.subscribe(fn);
  }
  registerOnTouched(fn: any) {
  }
}

Template-driven専用

@Component({
  selector: 'app-address-template',
  template: `
  <div ngModelGroup="address">
    <input type="text" name="street" ngModel>
    <input type="text" name="city" ngModel>
  </div>
    `,
  viewProviders: [
    {
      provide: ControlContainer,
      useExisting: NgForm
    }
  ]
})
export class AddressTemplateComponent {
}

Reactive専用

  • シンプルですぐ実装可能
  • FormGroupに子コンポーネントが自分でコントローラを登録する
    • ちょっと奇妙な書き方に感じる
@Component({
  selector: 'app-address-reactive',
  template: `
  <div formGroupName="address">
    <input type="text" formControlName="street">
    <input type="text" formControlName="city">
  </div>
    `,
  viewProviders: [
    {
      provide: ControlContainer,
      useExisting: FormGroupDirective
    }
  ]
})
export class AddressReactiveComponent {

  constructor(private parent: FormGroupDirective) {
  }

  ngOnInit() {
    // FormGroupを取得
    const form = this.parent.form;

    // 自身で、親のFormGroupにコントローラを登録する
    form.addControl('address', new FormGroup({
      street: new FormControl(''),
      city: new FormControl(''),
    }));
  }
}

サンプルコード

  • angular-form-nested-forms - StackBlitz
    • AddressComponent
      • ControlValueAccessorを利用
      • Template-driven、Reactive両方で利用可能
    • AddressTemplateComponent
      • Template-drivenで利用可能
    • AddressReactiveComponent
      • Reactive両方で利用可能

メモ

Error Aggregator

  • フォーム内の各コントローラのエラーを集約して表示させたい場合
  • formをprojectionするのはさせよう
    • Template-driven、Reactive、どちらかでしか利用できないような設計になる
    • 難しくなるし、バグが発生しやすくなる
    • もうすこし良い設計があるはず
@Component({
  selector: 'error-aggregator',
  template: `
  <div *ngFor="let error of errors">{{error | json}}</div>`,
})
export class ErrorAggregatorComponent implements OnInit {
  @Input() form: FormGroup;
  errors;
  constructor() { }
  ngOnInit() {
    // NgFormを間違って渡していないか確認
    if (this.form instanceof FormGroup) {
      this.form.statusChanges.subscribe(val => {
        this.errors = this.reduceErrors();
      });
    }
  }  
  private reduceErrors() {
    return pickErrors(this.form);
  }
}

// for demo
function pickErrors(formGroup: FormGroup) {
  debugger;
  const keys = Object.keys(formGroup.controls);
  return keys.map(key => {
    const control = formGroup.controls[key];
    if (control instanceof FormControl) {
      return control.errors;
    } else if (control instanceof FormGroup) {
      return pickErrors(control);
    }
  });
}

サンプルコード

参考

メモ

  • 以前のコード
// ...
@Input() model?: string;
@Input() form: NgForm;
@ViewChild('ngModel') ngModel: NgModel;
ngOnInit() {
  this.form.addControl(this.ngModel);
}
// ...

10月メモ・リンク集

これは何か

  • 10月に調べたことのメモ
  • 基本形式は、「参考URLとそれに関するメモとコメント」

ngrx関連

ngrxとフォーム

Introducing @ngrx/entity – @ngrx – Medium

状態のリセット

// foo.module.ts
...
StoreModule.forFeature(
  'foo',
  fromCalendar.reducers,
  {
    metaReducers: [fromFoo.resetReducer]
  }
),
...

// reducers/index.ts
export function resetReducer(reducer: ActionReducer<FooState, reset.Actions>) {
  return function (state, action) {
    if (action.type === RESET) {
      return reducer(undefined, action);
    } else {
      return reducer(state, action);
    }
  };
}

How to share state between reducers best practice · Issue #159 · ngrx/example-app · GitHub

Rx関連

unsubscribe

その他

Angular関連

view操作

ngOnInit() {
  const componentFactory = this.componentFactoryResolver.resolveComponentFactory(SpinnerComponent);
  const componentRef = componentFactory.create(this.viewContainer.injector);
  this.renderer.appendChild(
    this.viewContainer.element.nativeElement,
    componentRef.location.nativeElement
  );
}

フォーム

export class ChildComponent implements OnInit {
  ...
  @ViewChild("model") ngModel: NgModel;

  ngOnInit() {
    this.parentForm.addControl(this.ngModel); // <== add this
    this.parentForm.valueChanges.subscribe(changes => {
      console.log(JSON.stringify(changes));
    });
  }
}

その他

null vs undefined

TypeScript

スニペット

Bootstrap4, デスクトップファースト

// Media of at most the maximum breakpoint width. No query for the largest breakpoint.
// Makes the @content apply to the given breakpoint and narrower.
@mixin media-breakpoint-down($name, $breakpoints: $grid-breakpoints) {
  $max: breakpoint-max($name, $breakpoints);
  @if $max {
    @media (max-width: $max) {
      @content;
    }
  } @else {
    @content;
  }
}

Htmlの<base href>を取得

// <base href="/ja/">
const bases = document.getElementsByTagName('base');
if (bases.length > 0) {
  const baseHref = bases[0].href;
  // -> 'https://foo.com/ja/'
}

Bugsnag

import { ErrorHandler, Injectable } from '@angular/core';
import { environment } from '../../environments/environment';

@Injectable()
export class GlobalErrorHandler implements ErrorHandler {
  constructor() {
    Bugsnag.releaseStage = environment.stage;
  }
  handleError(error) {
    Bugsnag.notifyException(error);
    throw error;
  }
}

ローダーのオンオフ、簡易版

function disable() {
  const elm: HTMLElement | null = document.getElementById('blocker');
  if (elm) {
    elm.classList.add('active');
  }
}

function enable() {
  const elm: HTMLElement | null = document.getElementById('blocker');
  if (elm) {
    elm.classList.remove('active');
  }
}

Clickメモ

Array.apply(null, document.querySelectorAll('input[type=radio][value="3"]')).forEach(elm => elm.click());

Observable.throw()が返すオブジェクトの型はErrorObservable

import { Observable } from 'rxjs/Observable';
import { ErrorObservable } from 'rxjs/Observable/ErrorObservable';

// ...

handleError(err): Observable<any>| ErrorObservable {
  if (/* 何か */) {
    return Observable.of('ok');
  } else {
    return Observable.throw(err);
  }
}

「テキスト入力 -> 自動で検索開始」を実現するためのRx使い方

シュミレート用サンプルコード

テキスト入力の代わりに、画面クリックで一連の流れが確認できる簡易版コードです。 ブラウザのコンソールで実行させれば、動作確認できます。

http://reactivex.io/rxjs/などで実行してみてください。)

const sub = new Rx.BehaviorSubject();
const http = () => Rx.Observable.of('{ data }').delay(500);

document.addEventListener('click', (e) => {
  sub.next(e);
});

sub.debounceTime(1000)
  .distinctUntilChanged()
  .switchMap(() => {
    console.log('start http');
    const next$ = sub.skip(1);
    return http().takeUntil(next$);
  })
  .subscribe(val => console.log('get', val));

説明

  • .debounceTime
    • 入力中に送信されないようにする
  • .distinctUntilChanged
    • 前回と同じ値を送信しないようにする
  • .switchMap
    • 未完了のhttp通信があった場合、無視する
  • .takeUntil(next$)
    • debounceTimeで遅延させている間にhttp通信が完了する場合があるので、何かしらの入力変更があったらhttp通信を無視できるようにする