Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DONE] Day040 content: Custom Structural Directive #18

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 36 additions & 16 deletions Day038-dynamic-component.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ Hay ở tình huống khác, chúng ta muốn người dùng phải là
Vậy việc load động 1 component khác trong lúc runtime được thực hiện như thế nào?
Điều đó dẫn ta đến bài hôm nay, **Dynamic Component** sẽ là câu trả lời phù hợp để làm việc này.

Dưới đây là kết quả của project code hôm nay
![Dynamic Component Demo](assets/day038.gif)

## Coding Practice

### Step 1: Khởi tạo project
Expand Down Expand Up @@ -99,22 +102,38 @@ export class ExampleContainerComponent implements OnInit {
Flow chính:

1. Tạo 1 ViewChild trong template. Ở đây là thẻ div **#dynamicComponent**. Đây sẽ là nơi chúng ta load những components vào ở runtime.
2. Connect **#dynamicComponent** thông qua @ViewChild. Chúng ta sẽ có 1 [ViewContainerRef](###ViewContainerRef)

```html
<div #dynamicComponent></div>
```

2. Connect **#dynamicComponent** thông qua **@ViewChild**. Chúng ta sẽ có 1 [ViewContainerRef](###ViewContainerRef)

```typescript
@ViewChild("dynamicComponent", { read: ViewContainerRef, static: true })
containerRef: ViewContainerRef;
```

3. Inject [CompanyFactoryResolver](###ComponentFactoryResolver) của Angular vào component ExampleContainerComponent.

```typescript
constructor(private cfr: ComponentFactoryResolver) {}
```

4. Dùng Resolver connect với component nào chúng ta muốn load dynamic.
=> Kết quả sẽ trả về 1 [Component Factory](###ComponentFactory)

```typescript
const componentFactory = this.cfr.resolveComponentFactory(
DynamicContentOneComponent
);
```
```typescript
const componentFactory = this.cfr.resolveComponentFactory(
DynamicContentOneComponent
);
```

Dùng **ViewContainerRef** với Component Factory chúng ta vừa tạo ở trên để load **Dynamic Component**.
5. Dùng **ViewContainerRef** với Component Factory chúng ta vừa tạo ở trên để load **Dynamic Component**.

```typescript
const componentRef = this.containerRef.createComponent(componentFactory);
```
```typescript
const componentRef = this.containerRef.createComponent(componentFactory);
```

### Step 4: Add các dynamic components vào entryComponents

Expand Down Expand Up @@ -186,11 +205,11 @@ addDynamicCompOne() {
Hiện tại code sử dụng entryComponents đã cũ và với **Angular Ivy**, chúng ta hoàn toàn không cần sử dụng nữa. Ngoài ra chúng ta có thể sử dụng **Angular Ivy** để **lazy load** các components dynamic.
Code sẽ sửa như sau:

### Step 7.1: Xóa entryComponents setting in app.module.ts
#### Step 7.1: Xóa entryComponents setting in app.module.ts

Các bạn hãy vào file app.module.ts xóa đi config đã set ở step 4.

### Step 7.2: Update code ở container component
#### Step 7.2: Update code ở container component

- Remove 2 cái import components ở đầu file.
- Sửa 2 hàm addDynamicComp
Expand All @@ -216,8 +235,9 @@ Code sẽ như sau:
componentRef.instance.data = "INPUT DATA 2";
}
```

Vậy là đã xong, các bạn đã thực hiện thành công việc lazy load các dynamic components mà không phải add trực tiếp vào như ở những step đầu.
**Lưu ý**: Đối với những bạn nào dùng Angular phiên bản cũ thì nhớ update angular để sử dụng tính năng Angular Ivy.
**Lưu ý**: Đối với những bạn nào dùng Angular phiên bản cũ thì nhớ update angular để sử dụng tính năng Angular Ivy.

## Concepts

Expand All @@ -226,7 +246,7 @@ Vậy là đã xong, các bạn đã thực hiện thành công việc
Nó là một cái container từ đó có thể tạo ra Host View (component khi được khởi tạo sẽ tạo ra view tương ứng), và Embedded View (được tạo từ TemplateRef). Với các view được tạo đó sẽ có nơi để gắn vào (container).

Container có thể chứa các container khác (ng-container chẳng hạn) tạo nên cấu trúc cây. Hay hiểu đơn giản thì nó giống như 1 DOM Element, khi đó có thể add thêm các view khác (Component, Template) vào đó.
![TiepPhan](https://www.tiepphan.com/angular-trong-5-phut-dynamic-component-rendering/)
[TiepPhan](https://www.tiepphan.com/angular-trong-5-phut-dynamic-component-rendering/)

### ComponentFactory

Expand All @@ -236,7 +256,7 @@ Container có thể chứa các container khác (ng-container chẳng hạn) t

Đây là 1 class nhận vào các component để load dynamic và tạo ra 1 component factory của component đó. ViewContainerRef sẽ dùng **ComponentFactory** đó để load dynamic các components.

## Exercies
## Exercise
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@januaryofmine Nếu em dùng VSCode để viết markdown thì có mấy cái extension khá hữu ích, có khi em cài rồi nhưng anh cứ note ra.

  1. https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker - Để check chính tả và spelling. Tránh lỗi như em phải sửa sai spelling như ở dòng này.

  2. https://marketplace.visualstudio.com/items?itemName=yzhang.markdown-all-in-one - Để format, tạo table of content etc cho markdown. Nhưng hình như khi turn on thì type tiếng Việt ko có được 😂

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@trungk18
dạ, em cảm ơn a. v có gì a giúp em xem day 39 với day 41 với. Em cảm ơn anh.


### 1. Replace component, not Add

Expand All @@ -252,7 +272,7 @@ Cũng như thử emit event từ ViewChild và nhận, xử lý sự ki

Day 38 chúng ta đã học được những concepts liên quan đến Dynamic Component. Đây là 1 tính năng quan trọng có tính ứng dụng cao. Các bạn có thể thực hành nhiều hơn thông qua các bài tập mình đưa cũng như các nguồn tài liệu mình để dưới đây.

Mục tiêu của ngày 39 sẽ là
Mục tiêu của ngày 39 sẽ là **Custom Attribute Directive**.

## Code sample

Expand Down
231 changes: 231 additions & 0 deletions Day039-custom-attribute-directive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
# Day 39: Custom Attribute Directive trong Angular

## Introduction

Qua các bài trước, các bạn cũng biết những directives trong Angular như ngFor, ngIf, ngStyle.
Trước khi bước vào bài mới hôm nay, mình xin tổng quát lại về các directives như sau:

Có 3 loại directives trong Angular:

**Components**: Đây là directive phổ biến nhất, cũng thường gặp nhất trong Angular. Directive này giúp chúng ta đóng gói (encapsulated), và tái sử dụng (reusable). Đây là directive với template, view.

**Structural directives**: Đây là directive dùng để thay đổi cấu trúc của views bằng cách thêm hoặc ẩn, xóa bớt phần tử trong DOM. Ví dụ: ngFor, ngIf.

**Attribute directives**: Đây là directive dùng để thay đổi diện mạo (style, appearance) hoặc hành vi (behavior) của các phần tử DOM hay components. Ví dụ: ngStyle, ngClass

Và bài hôm nay của chúng ta sẽ là tạo 1 **Custom Attribute Directive** để chúng ta có thể tái sử dụng nhiều nơi trong ứng dụng.

Dưới đây là kết quả của project code hôm nay

![Dynamic Component Demo](assets/day039.gif)

## Coding Practice

### Step 1: Khởi tạo project

```sh
ng new custom-attribute-directive-demo
```

### Step 2: Tạo các components và directives

```sh
ng g c example-component
```

```sh
ng generate directive custom-directive-demo
```

Một lưu ý nhỏ ở đây như sau, để sử dụng được directive này chúng ta cần khai báo nó vào module giống như cách chúng ta khai báo component. Câu lệnh generate trên đã auto khai báo directive trên declarations của AppModule cho chúng ta.

Sau đó chúng ta add example-component vào template của app.component.html như sau:

```html
<app-example-container></app-example-container>
```

### Step 3: Code directive logic

Chúng ta sẽ inject **ElementRef** vào constructor của directive như sau:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@januaryofmine Em có ví dụ nào thực tế hơn chút không? Vì directive này thật ra thực tế 99% là mình sẽ ko dùng đến. Nên nếu lấy đc một ví dụ nào đó em đã từng dùng thì sẽ dể để mọi người hình dung hơn. Chứ một directive để hiện lên một đoạn demo text và hover với color khác thì nếu là anh, anh sẽ viết CSS cho nhanh 😂

Đa phần custom directive anh viết thì để wrap cái element với một thư viện nào đấy chưa có sẵn cho Angular. Thì anh nghĩ đây là một use case khá phổ biến.

@Directive({ selector: "[jsTree]" })
export class JsTreeDirective implements AfterViewInit {
  @Input() jsTree: any;

  constructor(private el: ElementRef, private zone: NgZone) { 
    this.jsTree = this.jsTree || {}
  }

  ngAfterViewInit(): void {
    try {
      runOutsideAngular(this.zone, () => {
        let jsTreeElement = $(this.el.nativeElement);
        jsTreeElement.jstree(this.jsTree).bind("select_node.jstree", function (e, data) {
          var href = data.node.a_attr.href;
          if (href.indexOf('#') === -1)
            document.location.href = href;
        });
        jsTreeElement.removeClass("hidden");
      })
    } catch (error) { }
  }
}

@nartc @tieppt Các đại ca thấy sao?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dạ. Em trong quá trình research về attribute directive, em thường thấy các use-cases demo là dùng để thay đổi styling hay attributes có sẵn của thẻ. em nghĩ directive này 1 ví dụ. Trong lúc xây dựng ví dụ, em tập trung vào sự dễ hiểu, để các bạn có thể nhanh chóng grab idea và ứng dụng theo ý các bạn. Vậy nên bản thân ví dụ sẽ đơn giản, không có tính sử dụng trực tiếp nhưng sẽ có tính tham khảo để custom nhanh theo business. Ngoài ra sự khác biệt của attribute directive và css em nghĩ đó là tính reuseable. Em thấy directive sẽ reuse dễ dàng hơn.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Em cũng bám khá sát ví dụ từ official docs của Angular. Ở đây nó build 1 example tương tự thế này nè anh: https://angular.io/guide/attribute-directives

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ừ anh hiểu ý, nhưng vì mình làm series nên nếu ví dụ sát thực tế hơn tý cũng đẹp trai hơn mà. Vì nhiều ví dụ trên Angular docs anh thấy tính thực tiễn cũng hơi thấp 🤣 Anw ý kiến chủ quan của anh thôi, chứ content hiện tại cũng rất là tốt rồi nhé.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ví dụ gì mà dùng jquery thế này....

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dạ, ý của a @trungk18 là wrap thư viện lại. Còn project của em không có dùng jquery.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

một directive khá phổ biến là để truyền vào permissions, sau đó check list available rồi xem có render một template hay ko. https://github.com/AlexKhymenko/ngx-permissions

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dạ a. v giờ em sửa lại demo ha a @tieppt.


```typescript
constructor(private el: ElementRef) {
this.el.nativeElement.innerText = "DEMO TEXT";
this.el.nativeElement.style.color = "blue";
}
```

Ở đây chúng ta dùng **ElementRef** để tương tác và thay đổi các properties của DOM element. Cụ thể là **innerText** hay **style.color**.

### Step 4: Apply directive

Chúng ta apply attribute directive bằng cách add nó vào như mọi attribute bình thường khác. Tên của attribute này sẽ nằm ở decorator @Directive. (Chúng ta hoàn toàn có thể sửa lại tên chúng ta muốn. Ở đây mình đã sửa từ appCustomDirectiveDemo -> appCustomDirective)

```typescript
@Directive({
selector: "[appCustomDirective]",
})
```

Chúng ta vào file **example-component.component.html** sửa lại như sau để apply custom directive chúng ta vừa tạo vào component này.

```html
<p appCustomDirective></p>
```

Vậy là chúng ta đã hoàn thành cách tạo và sử dụng 1 custom attribute directive đơn giản.

### Step 5: Using Render2

Ở step 3, chúng ta đã dùng **ElementRef** để tương tác và thay đổi các properties của DOM element. Điều này đã được warning trong official doc của Angular là không nên vì nó sẽ tạo nguy cơ về cho XSS attacks.
Và ở chính warning đó, Angular cũng giới thiệu đến **Render2** layer. **Render2** cung cấp những apis để dùng tương tác với DOM element 1 cách an toàn. Vậy nên chúng ta sẽ update code sử dụng **Render2** như sau:

```typescript
export class CustomDirectiveDemoDirective {
constructor(private el: ElementRef, private renderer: Renderer2) {
this.customContent("DEMO TEXT", "blue");
}
private customContent(text: string, color: string) {
this.renderer.setProperty(this.el.nativeElement, "innerText", text);
this.renderer.setStyle(this.el.nativeElement, "color", color);
}
}
```

### Step 6: Interact with event of Host Element

Khi chúng ta sử dụng 1 **Custom Directive** cho 1 HTML element, thì element đó sẽ được gọi là **Host Element**.( Ở trường hợp của chúng ta Host Element chính thẻ p ở template của example component )

Vậy hiện tại chúng ta muốn xử lý event của Host Element ở directive thì chúng ta sẽ làm thế nào? Ví dụ, khi hover chuột thì đổi màu text. Ở trường hợp nào chúng ta sẽ tương tác thông qua **HostListener**.

Code chúng ta sẽ update như sau để lắng nghe và xử lý 2 event **mouseenter** and **mouseleave** trên Host Element:

```typescript
export class CustomDirectiveDemoDirective {
constructor(private el: ElementRef, private renderer: Renderer2) {
this.customContent("DEMO TEXT", "blue");
}

@HostListener("mouseenter") onMouseEnter() {
this.customContent("ON MOUSE ENTER", "orange");
}

@HostListener("mouseleave") onMouseLeave() {
this.customContent("DEMO TEXT", "blue");
}

private customContent(text: string, color: string) {
this.renderer.setProperty(this.el.nativeElement, "innerText", text);
this.renderer.setStyle(this.el.nativeElement, "color", color);
}
}
```

### Step 7: Pass values into the directive

Tiếp theo, chúng ta sẽ truyền xử lý data truyền từ Host Element vào directive. Cụ thể ở case này chúng ta sẽ truyền màu sẽ thay đổi khi hover từ thẻ p vào.

Ở thẻ p, chúng ta sẽ truyền vào như 1 attribute bình thường.

```html
<p appCustomDirective hoverColor="green"></p>
```

Ở directive, chúng ta sẽ dùng **@Input** để nhận và xử lý.

```typescript
import {
Directive,
ElementRef,
Renderer2,
HostListener,
Input,
} from "@angular/core";

@Directive({
selector: "[appCustomDirective]",
})
export class CustomDirectiveDemoDirective {
@Input() hoverColor: string;

constructor(private el: ElementRef, private renderer: Renderer2) {
this.customContent("DEMO TEXT", "blue");
}

@HostListener("mouseenter") onMouseEnter() {
this.customContent("ON MOUSE ENTER", this.hoverColor);
}

@HostListener("mouseleave") onMouseLeave() {
this.customContent("DEMO TEXT", "blue");
}

private customContent(text: string, color: string) {
this.renderer.setProperty(this.el.nativeElement, "innerText", text);
this.renderer.setStyle(this.el.nativeElement, "color", color);
}
}
```

Vậy là đã xong, các bạn đã thực hiện thành công việc tạo và sử dụng 1 **Custom Attribute Directive** trong Angular.

## Concepts

### ElementRef

Đây là 1 class trong **@angular/core** dùng để tương tác với các DOM element trong template. Tuy nhiên chúng ta không nên sử dụng trực tiếp vì vấn đề security.

### Renderer2

Đây là 1 class cung cấp những apis để tương tác với DOM Element 1 cách an toàn. Chúng ta sẽ dùng nó gọi đến ElementRef.

### HostListener

Đây là 1 decorator được định nghĩa cho việc lắng nghe 1 event của DOM. Chúng ta sẽ dùng để viết hàm xử lý khi event đó diễn ra.

Example:

```typescript
constructor(private el: ElementRef, private renderer: Renderer2) {
this.renderer.setProperty(this.el.nativeElement, "innerText", "DEMO");
}
```

## Exercise

### 1. @Input and @Input alias

Hiện tại code đang truyền vào thẻ p 2 attribute là **appCustomDirective** và **hoverColor** cho 2 mục đích khác nhau. Hãy sử dụng **property binding** để chỉ cần truyền 1 cái và làm gọn code đi.
Tham khảo: [**Pass value into directive**](https://angular.io/guide/attribute-directives#pass-values-into-the-directive-with-an-input-data-binding)

### 2. Interact more with Render2

Hãy sử dụng các apis của Render2 để biến đổi nhiều properties và styles hơn.

## Summary

Day 39 chúng ta đã học được những concepts và cách làm liên quan đến cách **Custom attribute directive**.

Mục tiêu của ngày 40 sẽ là **Custom structural directive**.

## Code sample

- https://github.com/januaryofmine/angular-custom-attribute-directive-example

## References

Các bạn có thể đọc thêm ở các bài viết sau

- https://angular.io/guide/attribute-directives#build-a-simple-attribute-directive
- https://medium.com/@nishu0505/custom-directives-in-angular-5a843f76bb96
- https://angular.io/api/core/ElementRef

## Author

[Khanh Tiet](https://github.com/januaryofmine)

`#100DaysOfCodeAngular` `#100DaysOfCode` `#AngularVietNam100DoC_Day39`

[day34]: Day034-template-driven-forms-2.md
[day38]: Day038-dynamic-component.md
Loading