12/08/2018, 16:34

5 Rookie Mistakes to Avoid with Angular 2

Mistake #1: Binding to the native "hidden" property Trong AngularJS, nếu bạn muốn thay đổi sự hiển thị của một element, có thể bạn sẽ sử dụng một trong số các directive của Angular là ng-show hoặc ng-hide: Angular 1 example <div ng-show="showGreeting"> Hello, there! </div> ...

Mistake #1: Binding to the native "hidden" property

Trong AngularJS, nếu bạn muốn thay đổi sự hiển thị của một element, có thể bạn sẽ sử dụng một trong số các directive của Angular là ng-show hoặc ng-hide:

Angular 1 example

<div ng-show="showGreeting">
   Hello, there!
</div>

Trong Angular 2, cú pháp template làm cho nó có thể liên kết với bất kỳ property nào của một element. Điều này cực kỳ mạnh mẽ và mở ra một số khả năng. Một trong số những khả năng đó là liên kết với hidden property, tương tự như ng-show.

Angular 2 [hidden] example (not recommended)

<div [hidden]="!showGreeting">
   Hello, there!
</div>

Thoạt nhìn, trông nó có vẻ giống với ng-showcủa Angular 1. Tuy nhiên, nó lại có sự khác biệt: ng-show và ng-hide quản lý khả năng hiển thị bằng cách chuyển một CSS class "ng-hide" ở element, nói đơn giản là set display property thành "none". Quan trọng nhất, Angular kiểm soát style này và postscript bằng "!important" để đảm bảo nó luôn luôn ghi đè bất kỳ style hiển thị nào khác được đặt trên element đó.

Mặt khác, các style "display: none" gắn liền với native hidden property được thực hiện bởi trình duyệt. Hầu hết các trình duyệt làm như vậy mà không có một postscript "!important". Do đó, các style low specificity, vô tình có thể dễ bị dàng ghi đè, đơn giản như là thêm bất kỳ display style nào vào element mà bạn đang cố hide. Nói cách khác, nếu bạn đặt "display: flex" ở element trong stylesheet, nó sẽ vượt ra ngoài style được set bởi hidden và element sẽ luôn hiển thị (xem ví dụ).

Vì lý do này, thì ta nên sử dụng *ngIf.

Angular 2 ngIf example (recommended)

<div *ngIf="showGreeting">
   Hello, there!
</div>

Không giống như hidden property, directive ngIf của Angular không phụ thuộc vào các ràng buộc về style. Luôn luôn an toàn khi sử dụng bất kể stylesheet nào của bạn. Tuy nhiên, cần lưu ý rằng nó không tương đương về chức năng. Thay vì chuyển đổi display property, nó hoạt động bằng cách thêm hoặc loại bỏ các template element khỏi DOM.

Một alternative sẽ set một global style trong ứng dụng của bạn để config "display: none !important" cho tất cả các hidden attributes. Bạn có thể hỏi lý do tại sao framework không handle mặc định cái này. Câu trả lời là chúng ta không thể giả định rằng global style này là sự lựa chọn tốt nhất cho mọi ứng dụng. Bởi vì nó có khả năng phá vỡ các ứng dụng phụ thuộc vào trạng thái bình thường của hidden, chúng ta để nó cho các developer quyết định những gì là đúng cho từng trường hợp sử dụng của họ.

Mistake #2: Calling DOM APIs directly

Có rất ít trường hợp khi thao tác trực tiếp với DOM mà nó là cần thiết. Angular 2 cung cấp một tập hợp các API mạnh mẽ như là các query mà người dùng có thể sử dụng như một cách thay thế. Sử dụng các API này mang lại một vài ưu điểm khác nhau:

  • Có thể test ứng dụng của bạn mà không cần phải động vào DOM, để loại bỏ sự phức tạp khỏi test của bạn và giúp các bài test của bạn chạy nhanh hơn.
  • Nó tách riêng code của bạn khỏi trình duyệt, cho phép bạn chạy ứng dụng của mình trong bất kỳ ngữ cảnh nào.

Xuất phát từ mô hình Angular 1 có một vài tình huống mà bạn có thể bị "dụ dỗ" để làm việc trực tiếp với DOM. Hãy xem lại một vài tình huống, để chứng minh và làm thế nào để refactor chúng sử dụng các query.

Scenario 1: You need a reference to an element in your component's template

Hãy tưởng tượng bạn có một văn bản input trong template của component và bạn muốn nó auto-focus khi load component.

Có thể bạn biết rằng @ViewChild/@ViewChildren có thể cung cấp quyền truy cập vào component được lồng bên trong template của component . Nhưng trong trường hợp này, bạn cần reference đến một HTML element mà không được gắn vào một component cụ thể. Suy nghĩ đầu tiên của bạn có thể là chèn ElementRef của component như vậy:

Working with the ElementRef directly (not recommended)

@Component({
    selector: 'my-comp',
    template: `
        <input type="text" />
        <div> Some other content </div>
  `
})
export class MyComp {
    constructor(el: ElementRef) {
        el.nativeElement.querySelector('input').focus();
    }
}

Tuy nhiên, loại workaround này là không cần thiết. Giải pháp: ViewChild + local template variable Những gì mà các developer thường không nhận ra là nó cũng có thể truy vấn bằng biến local ngoài component. Vì bạn có thể kiểm soát view của component, nên bạn có thể thêm biến local vào thẻ input (ví dụ: "ref-myInput" hoặc "#myInput") và pass tên biến vào truy vấn @ViewChild dưới dạng string. Sau đó, khi view được khởi tạo, bạn có thể sử dụng renderer để gọi method và focus vào input đó.

Working with ViewChild and local variable (recommended)

@Component({
    selector: 'my-comp',
    template: `
        <input #myInput type="text" />
        <div> Some other content </div>
    `
})
export class MyComp implements AfterViewInit {
    @ViewChild('myInput') input: ElementRef;

    constructor(private renderer: Renderer) {}

    ngAfterViewInit() {
        this.renderer.invokeElementMethod(this.input.nativeElement, 'focus');
    }
}

Scenario 2: You need a reference to an element a user projects into your component

Điều gì sẽ xảy ra nếu bạn cần một reference đến một element không có trong template của component? Ví dụ: hãy tưởng tượng bạn có một danh sách các component, mà nó accepts danh sách tùy chỉnh các item thông qua content projection và bạn muốn theo dõi số mục trong danh sách.

Bạn có thể sử @ContentChildren để tìm kiếm content của từng component (nghĩa là các nút được dự báo trong component), nhưng vì content là tùy ý, nên bạn không thể gắn nhãn các nút bằng các biến local như trong ví dụ trước. Có một option là yêu cầu người dùng gắn nhãn cho mỗi item trong danh sách của họ bằng một biến đã thoả thuận trước, như "# list-item". Trong trường hợp đó, cách tiếp cận giống như ví dụ trước: ContentChildren and local variable (not recommended)

// user code
<my-list>
   <li *ngFor="let item of items" #list-item> {{item}} </li>
</my-list>

// component code
@Component({
  selector: 'my-list',
  template: `
    <ul>
      <ng-content></ng-content>
    </ul>
  `
})
export class MyList implements AfterContentInit {
  @ContentChildren('list-item') items: QueryList<ElementRef>;

  ngAfterContentInit() {
     // do something with list items
  }
}

Tuy nhiên, giải pháp này không phải là lý tưởng vì nó đòi hỏi người dùng phải viết thêm một số boilerplate. Bạn có thể thích một API với thẻ <li> thông thường và không có thuộc tính. Làm thế nào chúng ta có thể làm nó hoạt động? Giải pháp: ContentChildren + directive with li selector Một giải pháp tuyệt vời là khai thác lợi thế của selector @Directive decorator. Bạn đơn giản xác định một directive cho các <li> element, sau đó sử dụng một @ContentChildren để lọc tất cả các <li> element, chỉ những content children của component.

ContentChildren and directive (recommended)

// user code
<my-list>
   <li *ngFor="let item of items"> {{item}} </li>
</my-list>

@Directive({ selector: 'li' })
export class ListItem {}

// component code
@Component({
  selector: 'my-list'
})
export class MyList implements AfterContentInit {
  @ContentChildren(ListItem) items: QueryList<ListItem>;

  ngAfterContentInit() {
     // do something with list items
  }
}

Lưu ý: Có vẻ như nó chỉ có thể hoạt động để select cho các phần tử <li> trong thẻ <my-list> (ví dụ: "my-list li"), nhưng điều quan trọng cần lưu ý là các parent-child selectors chưa được hỗ trợ. Nếu bạn muốn giới hạn kết quả cho children của component, thì sử dụng các truy vấn để filter là cách tốt nhất.

Mistake #3: Checking for query results in the constructor

Khi lần đầu tiên làm việc với các query, thật dễ dàng để rơi vào bẫy này:

Logging query in constructor (broken)

@Component({...})
export class MyComp {
  @ViewChild(SomeDir) someDir: SomeDir;

  constructor() {
    console.log(this.someDir);       // undefined
  }
}

Khi kết quả console log là "undefined", bạn có thể giả sử query không hoạt động hoặc bạn khởi tạo nó không chính xác. Trên thực tế, bạn chỉ cần kiểm tra kết quả quá sớm trong lifecycle của component. Đó là chìa khóa để nhớ rằng kết quả query chưa có khi contructor thực hiện. May mắn thay, lifecycle mới của Angular giúp bạn dễ hiểu hơn khi bạn cần kiểm tra từng loại query.

  • Nếu bạn đang thực hiện view query, các kết quả sẽ available sau khi view được khởi tạo. Sử dụng tiện ích ngAfterViewInit lifecycle hook.
  • Nếu bạn đang tiến hành content query, kết quả sẽ available sau khi content được khởi tạo. Sử dụng ngAfterContentInit lifecycle hook.

Vì vậy, chúng ta có thể sửa code của chúng ta ở trên thành:

Logging query in ngAfterViewInit hook (recommended)

@Component({...})
export class MyComp implements AfterViewInit {
  @ViewChild(SomeDir) someDir: SomeDir;

  ngAfterViewInit() {
    console.log(this.someDir);       // SomeDir {...}
  }
}

Mistake #4: Using ngOnChanges to detect query list changes

Trong Angular 1, nếu bạn muốn được thông báo khi một giá trị thay đổi, bạn phải đặt một $scope.$watch và kiểm tra theo cách thủ công cho mỗi digest cycle. Trong Angular 2, ngOnChanges giúp đơn giản hoá quá trình này. Một khi bạn define phương thức ngOnChanges trong component, nó sẽ được gọi bất cứ khi nào các input của component thay đổi. Điều này rất tiện lợi.

Tuy nhiên, phương thức ngOnChanges chỉ thực hiện khi các input của component thay đổi - cụ thể là những mục bạn đã đưa vào trong input array hoặc được gắn nhãn với một @Input decorator. Nó sẽ không được gọi khi các item được thêm vào hoặc gỡ bỏ khỏi list query @ViewChildren hoặc @ContentChildren.

Nếu bạn muốn được thông báo về các thay đổi trong một list query, đừng sử dụng ngOnChanges. Thay vào đó hãy subscribe list query được xây dựng trong observable, nó là thuộc tính "changes". Miễn là bạn làm như vậy trong proper lifecycle hook, không phải là hàm khởi tạo, bạn sẽ được thông báo bất cứ khi nào một mục được thêm vào hoặc xoá đi.

Ví dụ: Code sẽ giống như sau:

Using 'changes' observable to subscribe to query list changes (recommended)

@Component({ selector: 'my-list' })
export class MyList implements AfterContentInit {
  @ContentChildren(ListItem) items: QueryList<ListItem>;

  ngAfterContentInit() {
    this.items.changes.subscribe(() => {
       // will be called every time an item is added/removed
    });
  }
}

Mistake #5: Constructing ngFor incorrectly

Trong Angular 2, chúng ta có khái niệm "structural directives" để thêm hoặc xóa các phần tử khỏi DOM dựa trên các biểu thức. Không giống như các directive khác, structural directive phải được sử dụng với một template element, một template attribute hoặc một dấu hoa thị. Với cú pháp mới này, nó có xu hướng gây ra những sai lầm mới.

Bạn có thể phát hiện ra lỗi thông thường dưới đây không ?

Incorrect ngFor code

// a: 
<div *ngFor="#item in items">
   <p> {{ item }} </p>
</div>

// b: 
<template *ngFor let-item [ngForOf]="items">
   <p> {{ item }} </p>
</template>

// c:
<div *ngFor="let item of items; trackBy=myTrackBy; let i=index">
   <p>{{i}}: {{item}} </p>
</div>

Hãy sửa các lỗi trên, từng cái một.

5a: Using outdated syntax

// incorrect 
<div *ngFor="#item in items">
   <p> {{ item }} </p>
</div>

Có hai lỗi ở đây. Thứ nhất là một cái bẫy thông thường mà các developer có thể rơi vào nếu họ đã làm việc với AngularJS. Trong AngularJS, bộ lặp tương đương sẽ đọc ng-repeat = "item in items". Angular 2 chuyển từ "in" sang "of" để giống với vòng lặp for-of trong ES6. Nó có thể giúp ghi nhớ rằng input property thực tế của ngFor là ngForOf chứ không phải ngForIn.

Lỗi thứ hai là "#". Trong các phiên bản cũ của Angular 2, "#" được sử dụng để tạo ra một biến local template trong ngFor, nhưng ở phiên bản beta.17, ngFor sử dụng tiền tố "let".

Một nguyên tắc tốt là sử dụng tiền tố "let" nếu bạn muốn tạo một biến có sẵn trong template của ngFor và sử dụng tiền tố "#" hoặc "ref" nếu bạn muốn get một reference đến một element bên ngoài một ngFor (như trong Mistake 2).

// correct 
<div *ngFor="let item of items">
   <p> {{ item }} </p>
</div>

*5b: Mixing sugared and de-sugared template syntax *

// incorrect
<template *ngFor let-item [ngForOf]="items">
   <p> {{ item }} </p>
</template>

Không cần thiết include cả template tag và dấu hoa thị - và trên thực tế, khi sử dụng cả hai thì sẽ không hoạt động. Một khi bạn thêm tiền tố vào một directive với dấu hoa thị, Angular coi nó như một template attribute chứ không phải là một directive thông thường. Cụ thể, trình phân tích cú pháp lấy giá trị string của ngFor, prefixes với directive name, và sau đó parses nó như là một template attribute. Nói cách khác, giống như: <div *ngFor="let item of items">

Nó xử lý giống như

<div template="ngFor let item of items">

Về mặt chức năng, khi bạn sử dụng cả hai, nó giống như việc viết:

<template template="ngFor" let-item [ngForOf]="items">

Do đó, khi template attribute được parse, tất cả giá trị chứa là "ngFor". Nếu không có bộ sưu tập nguồn hoặc biến local cho "item" trong chuỗi đó, nó không thể được xử lý chính xác và sẽ không có gì xảy ra.

Mặt khác, thẻ template không còn có một ngFor directive kèm theo, do đó code sẽ throw một lỗi. Nếu không có ngFor directive, ngForOf property binding không có component class nào để ràng buộc.

Lỗi có thể được khắc phục bằng cách gỡ bỏ dấu hoa thị, hoặc chuyển đổi hoàn toàn sang phiên bản ngắn của cú pháp.

// correct 
<template ngFor let-item [ngForOf]="items">
   <p> {{ item }} </p>
</template>

// correct
<p *ngFor="let item of items">
   {{ item }}
</p>

*5c: Using the wrong operator in syntax *

// incorrect
<div *ngFor="let item of items; trackBy=myTrackBy; let i=index">
   <p>{{i}}: {{item}} </p>
</div>

Để giải thích những gì đang xảy ra ở đây, chúng ta hãy bắt đầu bằng cách viết lại code theo cú pháp long-form template:

// correct
<template ngFor let-item [ngForOf]="items" [ngForTrackBy]="myTrackBy" let-i="index">
   <p> {{i}}: {{item}} </p>
</template>

Trong dạng này, dễ hiểu hơn về cấu trúc của directive. Để phá vỡ nó:

  1. Chúng ta sử dụng input property để truyền hai phần thông tin sang ngFor:
    • Source collection của chúng ta (items) bị ràng buộc bởi ngForOf property
    • Custom function myTrackBy bị ràng buộc với ngForTrackBy property
  2. Chúng ta đang declare hai biến local template bằng cách sử dụng tiền tố "let": "i" và "item". Directove ngFor set các biến này khi nó lặp lại các item trong list.
    • "i" được set thành zero-based index của list items
    • "item" được set thành element hiện tại trong danh sách tại index "i"

Khi chúng ta rút ngắn code để sử dụng cú pháp dấu hoa thị, chúng ta phải tuân theo các quy tắc nhất định mà trình parse sẽ hiểu:

  • Tất cả các config phải xảy ra trong chuỗi giá trị của các thuộc tính *ngFor
  • Chúng ta đặt các biến local bằng toán tử =
  • Chúng ta set các input property bằng cách sử dụng toán tử :
  • Chúng ta bỏ tên tiền tố directive name từ input property ("ngForOf" → "of")
  • Chúng ta separate statements bằng dấu chấm phẩy

Theo các quy tắc này, ta có kết quả:

// correct
<p *ngFor="let item; of:items; trackBy:myTrackBy; let i=index"> 
   {{i}}: {{item}}
</p>

Các dấu phân cách và dấu hai chấm thực sự không bắt buộc vì chúng bị bỏ qua bởi trình parse. Chúng được sử dụng chủ yếu là để dễ đọc. Do đó, chúng ta có thể dọn sạch code của chúng ta để flow một cách tự nhiên hơn:

// correct
<p *ngFor="let item of items; trackBy:myTrackBy; let i=index"> 
   {{i}}: {{item}}
</p>

Hy vọng những lưu ý trên sẽ giúp ích được cho mọi người. Bản dịch còn nhiều sai sót rất mong nhận được sự ghóp ý (bow).

References

http://angularjs.blogspot.com/2016/04/5-rookie-mistakes-to-avoid-with-angular.html

0