Thực Hành Content Projection, Lifecycle Liên Quan và QueryList Changes Event Trong Angular

Thực Hành Content Projection, Lifecycle Liên Quan và QueryList Changes Event Trong Angular

Bài viết này là bài tiếp theo trong series "Thử Nghiệm Với Angular", trong bài này sẽ giúp các bạn củng cố lại kiến thức từ bài trước Thử Nghiệm Với Angular Phần 12: Content Projection Trong Angular.
Thực Hành Content Projection, Lifecycle Liên Quan và QueryList Changes Event Trong Angular

1: Giới thiệu

Content Projection là một concept khá hay trong Angular, giúp chúng ta có thể dễ dàng tạo các component có khả năng tái sử dụng cao. Bài học này chúng ta sẽ tạo các Component để tìm hiểu chi tiết hơn về Content Projection và các Lifecycle liên quan, cùng với đó chúng ta cũng sẽ tìm hiểu về QueryList Changes Event cho việc áp dụng các phần logic khi list content thay đổi.

2: Các thành phần cơ bản của app

Trong bài học này chúng ta sẽ tạo mới hai component là CollapseGroupComponentCollapseComponent. Trong đó CollapseGroupComponent sẽ chịu trách nhiệm quản lý các component CollapseComponent như sau:

<tp-collapse-group [multiple]="false">
  <tp-collapse [title]="'First block'" [selected]="true">
    <span>
      Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
      Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
    </span>
  </tp-collapse>
  <tp-collapse [title]="'Second block'" [selected]="false">
    <span>
      Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
      Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
    </span>
  </tp-collapse>
</tp-collapse-group>

3: Cài đặt

Cài đặt chi tiết của các component như sau:

3.1: CollapseGroupComponent

collapse-group.component.html

<div class="collapsible">
  <ng-content select="tp-collapse"></ng-content>
</div>

collapse-group.component.ts

import { Component, OnInit, AfterContentInit, Input, ContentChildren, QueryList, OnDestroy } from '@angular/core';
import { CollapseComponent } from "../collapse/collapse.component";
import { Subscription } from "rxjs/Rx";

@Component({
  selector: 'tp-collapse-group',
  templateUrl: './collapse-group.component.html',
  styleUrls: ['./collapse-group.component.scss']
})
export class CollapseGroupComponent implements OnInit, AfterContentInit, OnDestroy {
  @ContentChildren(CollapseComponent) collapses: QueryList<CollapseComponent>;
  @Input() multiple: boolean = true;

  private _subscriptions: Subscription[] = [];
  constructor() { }

  ngOnInit() {
  }

  ngAfterContentInit() {
    this.collapses.forEach(collapse => {
      let subscription = collapse.selectedChange.subscribe(coll => {
        if (!this.multiple && coll.selected) {
          this.toggleCollapse(coll);
        }
      });
      this._subscriptions.push(subscription);
    });
  }

  toggleCollapse(collapse) {
    this.collapses.forEach(c => {
      if (c.collapseId != collapse.collapseId) {
        c.selected = false;
      }
    });
  }

  ngOnDestroy() {
    if (this._subscriptions && this._subscriptions.length) {
      this._subscriptions.forEach(sub => sub.unsubscribe());
    }
    this._subscriptions = [];
  }

}

Ở dòng 11, chúng ta sử dụng @ContentXXX để query các phần tử được nhúng vào thông qua ng-content mà chúng ta đã cài đặt ở template. Trong trường hợp ở trên, chúng ta muốn query tất cả các phần tử là CollapseComponent.

Bây giờ, để sử dụng các element, chúng ta cần hook vào lifecycle là ngAfterContentInit, kết quả là chúng ta có một mảng element để thao tác.

Về mặt logic, lúc này chúng ta kiểm tra xem Component này có cho phép multiple CollapseComponent được open cùng một lúc không, nếu không thì chúng ta phải thực hiên việc đóng các component khác mà đang open khi muốn open một thằng khác.

Dòng 22 chúng ta subscribe vào event emitter của CollapseComponent để thực hiện việc quản lý trạng thái của chúng.

Khi component CollapseGroupComponent bị hủy, chúng ta cũng thực hiện xóa bỏ các phần dữ liệu không cần thiết như: hủy các event listener đang tồn tại chẳng hạn.

3.2: CollapseComponent

collapse.component.html

<header class="collapsible-header" (click)="toggleSelected()">
    { {  title }}
</header>
<section class="collapsible-body" [class.active]="selected">
  <ng-content></ng-content>
</section>

collapse.component.ts

import { Component, OnInit, Input, ViewEncapsulation, Output, EventEmitter } from '@angular/core';

export interface DataCollapseChange {
  collapseId: string;
  selected: boolean;
}

let uuid: number = 1;

@Component({
  selector: 'tp-collapse',
  templateUrl: './collapse.component.html',
  styleUrls: ['./collapse.component.scss'],
  encapsulation: ViewEncapsulation.None
})
export class CollapseComponent implements OnInit {
  @Input() title: string = '';
  @Input() selected: boolean = false;
  @Output() selectedChange: EventEmitter<DataCollapseChange> = new EventEmitter<DataCollapseChange>();
  private _id: number;
  constructor() { }

  ngOnInit() {
    this._id = ++uuid;
  }

  get collapseId() {
    return 'tp-collapse-' + this._id;
  }

  toggleSelected() {
    this.selected = !this.selected;
    this.selectedChange.emit({
      collapseId: this.collapseId,
      selected: this.selected
    });
  }
}

Dòng 3 đến 6 chúng ta định nghĩa một kiểu dữ liệu để thực hiện trả về khi component thay đổi trạng thái của nó. Component này đơn giản chỉ nhận đầu vào, hiển thị và có thể gửi lại một event là selectedChange.

3.3: ContentChild vs ContentChildren vs ViewChild vs ViewChildren

Content: là các phần tử được truyền vào theo ng-content hay content projection.

View: là các phần tử nội tại của component đó, có thể hiểu là phần DOM mà bạn cài đặt.

Ví dụ trong CollapseComponent thì phần header là view, còn phần được truyền vào qua ng-content là content.

XXXChild vs XXXChildren: thì XXXChild nó sẽ trả về 1 phần tử, còn XXXChildren trả về một list các phần tử.

4: QueryList Changes Event Trong Angular

Với phiên bản code phía trên liệu rằng khi chúng ta làm việc với dữ liệu async thì app của chúng ta sẽ chạy đúng. Lúc này, chúng ta gặp phải một vấn đề đó là mặc dù chúng ta đã để [multiple]="false" nhưng app lại chạy không như chúng ta mong muốn.
app.component.html

<tp-collapse-group [multiple]="false">
  <tp-collapse
    *ngFor="let post of posts" [title]="post.title" [selected]="false">
    <div>
        { {  post.body }}
    </div>
  </tp-collapse>
</tp-collapse-group>

app.component.ts

import { Component, OnInit } from '@angular/core';
import { POSTS } from './services/post';
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
  posts: any[] = [];
  ngOnInit() {
    setTimeout(() => {
      this.posts = POSTS.slice();
    }, 500);
  }
}

Bây giờ, chúng ta cần quan sát đối tượng của QueryList thay đổi như thế nào để có các hành động tương ứng bằng việc subscribe vào Changes Event của đối tượng đó như sau:

this.collapses.changes.subscribe((object) => {
  // do something here
});

Ví dụ như trong app của chúng ta đang phát triển, chúng ta cần quan sát sự thay đổi của list CollapseComponent để thực hiện quản lý ở mode accordion như sau:

this.collapses.changes.subscribe(() => {
  this.clearListener();
  this.initListener();
});

Việc quan sát này thực sự cẩn thiết khi chúng ta cần phải áp dụng một phần logic nào đó để Component của chúng ta chạy đúng, chẳng hạn như mong muốn của chúng ta là CollapseGroupComponent chỉ thực hiện cho phép một CollapseComponent được phép mở ở tại một thời điểm nếu chúng ta set cho mode của group là accordion.

Thêm vào đó, khi thực hiện subscribe bằng code như trên, chúng ta đã tạo ra một object của class Subscription, vậy nên khi Component bị destroy thì chúng ta nên (phải) hủy object đó đi như sau:

// some method
this._changeSubs = this.collapses.changes.subscribe(() => {
  this.clearListener();
  this.initListener();
});

// on destroy
ngOnDestroy() {
  if (this._changeSubs) {
    this._changeSubs.unsubscribe();
  }
}

Đơn giản phải không nào!
Và code hoàn thiện của CollapseGroupComponent như sau:

import { Component, OnInit, AfterContentInit, Input, ContentChildren, QueryList, OnDestroy } from '@angular/core';
import { CollapseComponent } from "../collapse/collapse.component";
import { Subscription } from "rxjs/Rx";

@Component({
  selector: 'tp-collapse-group',
  templateUrl: './collapse-group.component.html',
  styleUrls: ['./collapse-group.component.scss']
})
export class CollapseGroupComponent implements OnInit, AfterContentInit, OnDestroy {
  @ContentChildren(CollapseComponent) collapses: QueryList<CollapseComponent>;
  @Input() multiple: boolean = true;

  private _subscriptions: Subscription[] = [];
  private _changeSubs: Subscription;
  constructor() { }

  ngOnInit() {
  }

  ngAfterContentInit() {
    this.initListener();
    this._changeSubs = this.collapses.changes.subscribe(() => {
      this.clearListener();
      this.initListener();
    });
  }

  initListener() {
    this.collapses.forEach(collapse => {
      let subscription = collapse.selectedChange.subscribe(coll => {
        if (!this.multiple && coll.selected) {
          this.toggleCollapse(coll);
        }
      });
      this._subscriptions.push(subscription);
    });
  }

  toggleCollapse(collapse) {
    this.collapses.forEach(c => {
      if (c.collapseId != collapse.collapseId) {
        c.selected = false;
      }
    });
  }

  clearListener() {
    if (this._subscriptions && this._subscriptions.length) {
      this._subscriptions.forEach(sub => sub.unsubscribe());
    }
    this._subscriptions = [];
  }

  ngOnDestroy() {
    this.clearListener();
    if (this._changeSubs) {
      this._changeSubs.unsubscribe();
    }
  }

}

5: Video bài học


6: Tham khảo

Blog post:

http://www.tiepphan.com/thu-nghiem-voi-angular-thuc-hanh-content-projection-va-lifecycle-angular/
http://www.tiepphan.com/thu-nghiem-voi-angular-querylist-changes-event-trong-angular/

Source code:

https://github.com/tieppt/try-angular-2/tree/lesson-13
https://github.com/tieppt/try-angular-2/tree/lesson-14

Docs:

https://angular.io/docs/ts/latest/guide/lifecycle-hooks.html
https://angular.io/docs/ts/latest/api/core/index/ContentChild-decorator.html
https://angular.io/docs/ts/latest/api/core/index/ContentChildren-decorator.html
https://angular.io/docs/ts/latest/api/core/index/ViewChild-decorator.html
https://angular.io/docs/ts/latest/api/core/index/ViewChildren-decorator.html
https://angular.io/docs/ts/latest/api/core/index/Query-class.html
https://angular.io/docs/ts/latest/api/core/index/QueryList-class.html

Bình luận


White
{{ comment.user.name }}
Bỏ hay Hay
{{comment.like_count}}
Male avatar
{{ comment_error }}
Hủy
   

Hiển thị thử

Chỉnh sửa

White

Tiep Phan

7 bài viết.
21 người follow
Kipalog
{{userFollowed ? 'Following' : 'Follow'}}
Cùng một tác giả
White
19 6
Giới thiệu series học Lập trình Angular (Angular 2) Xin chào các bạn, trong thời gian vừa qua cộng đồng Javascript lại đón nhận một đứa con mới đến...
Tiep Phan viết hơn 1 năm trước
19 6
White
12 3
Thử Nghiệm Với Angular – Forms Trong Angular Hầu hết các ứng dụng web hiện đại đều làm việc với forms để thu thập dữ liệu từ người dùng. Angular c...
Tiep Phan viết 1 năm trước
12 3
White
9 6
Tháng 32017, Angular team đã phát hành Angular 4, vậy Angular 4 có gì mới, có những gì thay đổi mà chúng ta cần lưu ý. Bài này sẽ giới thiệu cho cá...
Tiep Phan viết hơn 1 năm trước
9 6
Bài viết liên quan
White
0 3
Các @Input ở component con chỉ khởi tạo giá trị ban đâu, do đó, các biến ở component cha thay đổi thì các @Input ở component con sẽ không thay đổi ...
Vani viết 5 tháng trước
0 3
White
19 6
Giới thiệu series học Lập trình Angular (Angular 2) Xin chào các bạn, trong thời gian vừa qua cộng đồng Javascript lại đón nhận một đứa con mới đến...
Tiep Phan viết hơn 1 năm trước
19 6
{{like_count}}

kipalog

{{ comment_count }}

bình luận

{{liked ? "Đã kipalog" : "Kipalog"}}


White
{{userFollowed ? 'Following' : 'Follow'}}
7 bài viết.
21 người follow

 Đầu mục bài viết

Vẫn còn nữa! x

Kipalog vẫn còn rất nhiều bài viết hay và chủ đề thú vị chờ bạn khám phá!