顯示具有 angular 標籤的文章。 顯示所有文章
顯示具有 angular 標籤的文章。 顯示所有文章

2021/08/20

Form Validator In Angular

Angular 的 ReactiveFormsModule 相當強大,包含了 two way binding data 以及好用的 validator,以下的範例示範怎麼針對單一欄位以及整體表單自製 validator,如果 username 填寫 chan 的話表示帳號重複,年齡大於 100 的話則錯誤,group 的部分兩者都要填寫。

ts
import { Component, OnInit } from '@angular/core';
import { AbstractControl, FormBuilder, ValidationErrors, ValidatorFn } from '@angular/forms';

@Component({
  selector: 'app-form-validator',
  templateUrl: './form-validator.component.html',
  styleUrls: ['./form-validator.component.scss']
})
export class FormValidatorComponent implements OnInit {
  public form = this.fb.group({
    username: ['', [this.userNameChecker()]],
    age: ['', [this.numberChecker()]]
  }, { validators: this.bothRequired() });
  public ageLimit: number = 100;

  constructor(
    private fb: FormBuilder
  ) { }

  ngOnInit(): void {
  }

  get username(): AbstractControl {
    return this.form.controls.username;
  }

  get age(): AbstractControl {
    return this.form.controls.age;
  }

  private userNameChecker(): ValidatorFn {
    return (control: AbstractControl): ValidationErrors | null => {
      if (control.value === '') {
        return null;
      }

      if (control.value === 'chan') {
        return { userNameExists: true };
      }

      return null;
    }
  }

  private numberChecker(): ValidatorFn {
    return (control: AbstractControl): ValidationErrors | null => {
      if (control.value === '') {
        return null;
      }

      if (control.value > this.ageLimit) {
        return { tooBig: true };
      }

      return null;
    }
  }

  private bothRequired(): ValidatorFn {
    return (group: AbstractControl): ValidationErrors | null => {
      return (group.get('username')?.value === '' || group.get('age')?.value === '') ? { bothRequired: true } : null;
    }
  }
}
html
<h3>{{ form.value | json }}</h3>
<div [formGroup]="form">
  <div>
    username: <input type="text" formControlName="username">
    <div *ngIf="username.errors?.userNameExists">username exists</div>
  </div>
  <div>
    age: <input type="number" formControlName="age">
    <div *ngIf="age.errors?.tooBig">age should lower than {{ ageLimit }}</div>
  </div>
  <div>
    <button [disabled]="form.invalid">submit</button>
  </div>
</div>

2021/08/11

Input And Output In Angular

Angular 的 @Input 以及 @Output 修飾器是用來讓父與子元件可以跨元件溝通資訊,應用範圍很廣,譬如說有些固定的表單功能我們可以把他切細,提供給各單元的表單套入使用,可以減少重複造輪子的問題,下面示範如何讓子元件改變父元件表單的內容。

父 ts
import { Component, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms';

@Component({
  selector: 'app-example',
  templateUrl: './example.component.html',
  styleUrls: ['./example.component.scss']
})
export class ExampleComponent implements OnInit {
  public form = this.fb.group({
    username: 'chan',
    amount: 100
  });

  constructor(
    private fb: FormBuilder
  ) { }

  ngOnInit(): void {
  }

  public patchAmount(amount: number): void {
    this.form.patchValue({
      amount: amount
    })
  }
}
父 html
<h1>Input Output Example</h1>
<h3>{{ form.value | json }}</h3>
<form [formGroup]="form">
  <div>
    <input type="text" formControlName="username">
  </div>
  <div>
    <app-example-number [amount]="form.get('amount')?.value" (patchParentAmount)="patchAmount($event)"></app-example-number>
  </div>
</form>
子 ts
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';

@Component({
  selector: 'app-example-number',
  templateUrl: './example-number.component.html',
  styleUrls: ['./example-number.component.scss']
})
export class ExampleNumberComponent implements OnInit {
  @Input() amount: number = 0;
  @Output() patchParentAmount = new EventEmitter<number>();

  constructor() { }

  ngOnInit(): void {
  }

  public updateAmount(): void {
    this.patchParentAmount.emit(this.amount);
  }
}
子 html
<input type="number" [(ngModel)]="amount" (ngModelChange)="updateAmount()">

父元件傳了預設的 amount 過去給子元件,子元件透過監聽 ngModelChange 將改變的數字 emit 到父元件執行 patch form value 的動作,就可以做到互動更新了。

2021/08/09

Dynamic Form Field In Angular

Angular 的 form builder 蠻強大的,two way binding data 以及 validator 相當好用,其中有一個特性 FormArray 可以做許多運用,來做一個動態產生表單欄位的範例。

ts
import { Component, OnInit } from '@angular/core';
import { FormArray, FormBuilder, FormGroup } from '@angular/forms';

@Component({
  selector: 'app-dy-demo',
  templateUrl: './dy-demo.component.html',
  styleUrls: ['./dy-demo.component.scss']
})
export class DyDemoComponent implements OnInit {
  public form = this.fb.group({
    username: 'chan',
    family: this.fb.array([])
  })

  constructor(
    private fb: FormBuilder
  ) { }

  ngOnInit(): void {
  }

  public newLine(): FormGroup {
    return this.fb.group({
      name: '',
      age: ''
    })
  }

  public family(): FormArray {
    return this.form.get('family') as FormArray;
  }

  public add(): void {
    return this.family().push(this.newLine());
  }

  public remove(i: number): void {
    this.family().removeAt(i);
  }
}
html
<h3>{{ form.value | json }}</h3>
<div>
  <button (click)="add()">add</button>
</div>
<form action="" [formGroup]="form">
  <div>
    <input type="text" formControlName="username">
  </div>
  <div formArrayName="family">
    <ng-container *ngFor="let f of family().controls; let i=index" [formGroupName]="i">
      <div>
        username: <input type="text" formControlName="name"> age: <input type="number" formControlName="age">
        <button (click)="remove(i)">remove</button>
      </div>
    </ng-container>
  </div>
</form>

目前覺得 Angular 的內容真是易學難精啊。

2021/08/05

Array Object In Angular

angular 使用了 typescript,強型別是 ts 大殺器之一,除了簡單的 stringnumber 那些定義以外,array object 使用度也很高,最常見的作法如下。

interface Member {
    username: string;
    password: string;
}

const members: Member[] = [
    {
        username: 'chan',
        password: '123456'
    }
];

console.log(members);

這樣做沒什麼大問題,但設計上我覺得有點卡,因為 interface Member 就是一個單一物件,另一種寫法比較囉唆一點,但我覺得比較好:

interface Member {
    username: string;
    password: string;
}

interface Members extends Array<Member> {}

const members: Members = [
    {
        username: 'chan',
        password: '123456'
    }
];

console.log(members);

這樣調用 Members 時很明確就是要用陣列,而 Member 則可以給 member detail 使用,職責比較清楚,detail 理論上應該是最小單位,但如果 list 部分有自己的需求也可以自行擴充,type alias 也有相同的作法:

type Member = {
    username: string;
    password: string;
}

type Members = Member[];

const members: Members = [
    {
        username: 'chan',
        password: '123456'
    }
];

console.log(members);

2021/07/27

Specific String On Typescript

今天在寫 anguar 的時候想到一個問題,一般我們在 parameter 後面帶的會是型別,像是:

const tester = (params: { name: string }): void => {
    console.log(params.name);
}

tester({name: 'a'}); // a

那是否可以直接限定想要的內容,答案是可以的:

const tester = (params: { name: 'a' | 'b' }): void => {
    console.log(params.name);
}
  
tester({name: 'a'}); // a

此時如果你下 tester({name: 'c'}); 會得到 TS2322: Type '"c"' is not assignable to type '"a" | "b"'. 的警告,不過多寫了幾個案例以後發現一個雷,不能用變數帶入:

const tester = (params: { name: 'a' | 'b' }): void => {
    console.log(params.name);
}
const params = {
    name: 'a'
};

tester(params); // a

上方的寫法會得到警告 TS2345: Argument of type '{ name: string; }' is not assignable to parameter of type '{ name: "a" | "b"; }'. Types of property 'name' are incompatible. Type 'string' is not assignable to type '"a" | "b"'.,最簡單的解法是:

const tester = (params: { name: 'a' | 'b' }): void => {  
    console.log(params.name);
}
const params = {
    name: 'a' as const
};

tester(params); // a

不過上社群討論後,最好的解法應該是用 enum

enum TesterParam {
    A = 'a',
    B = 'b'
}

const tester = (params: { name: TesterParam }): void => {
    console.log(params.name);
}
const params = {
    name: TesterParam.A
};

tester(params); // a

這樣的架構對維護來說是最好的。

2021/04/09

FormArray And Checkbox

最近在學 Angular,他的 Form 相關模組挺有趣的,不過為了實現某些功能還是必須透過 JS 加工,這邊記錄一下怎麼綁 checkbox 的 cheked 結果到 FormBuilder 上。

html
<h3>{{ form.value | json }}</h3>  
<div [formGroup]="form">  
 <input type="text" formControlName="name">  
 <ng-container *ngFor="let hobby of hobbies">  
 <label> <input type="checkbox" [value]="hobby.value" (click)="updateHobbies($event)">  
  {{ hobby.name }}  
  </label>  
 </ng-container></div>
ts
import { Component, OnInit } from '@angular/core';
import { FormArray, FormBuilder, FormControl } from '@angular/forms';

@Component({
  selector: 'app-checkbox',
  templateUrl: './checkbox.component.html',
  styleUrls: ['./checkbox.component.scss']
})
export class CheckboxComponent implements OnInit {
  public form = this.fb.group({
    name: [''],
    hobbies: this.fb.array([])
  });
  public hobbies: object[] = [
    { name: 'basketball', value: 'basketball' },
    { name: 'baseball', value: 'baseball' },
    { name: 'tennis', value: 'tennis' },
  ];

  constructor(
    private fb: FormBuilder
  ) { }

  ngOnInit(): void {
  }

  public updateHobbies(e): void {
    const hobbies: FormArray = this.form.get('hobbies') as FormArray;

    if (e.target.checked) {
      hobbies.push(this.fb.control(e.target.value));
    } else {
      let i = 0;

      hobbies.controls.forEach((control: FormControl) => {
        if (control.value === e.target.value) {
          hobbies.removeAt(i);

          return;
        }

        ++i;
      });
    }
  }
}