diff --git a/Day001-Installation.md b/Day001-Installation.md index 947d01a..13bc18b 100644 --- a/Day001-Installation.md +++ b/Day001-Installation.md @@ -74,7 +74,7 @@ Lúc này các bạn sẽ cần trả lời 1 số câu hỏi về routing, styl ## Youtube Video -https://youtu.be/NS6P1fpU77o +[![Day 01](https://img.youtube.com/vi/NS6P1fpU77o/0.jpg)](https://youtu.be/NS6P1fpU77o) ## HASHTAG diff --git a/Day002-AngularApp.md b/Day002-AngularApp.md index 3706999..c2494bc 100644 --- a/Day002-AngularApp.md +++ b/Day002-AngularApp.md @@ -27,9 +27,9 @@ export class HelloComponent {} Nó là một TS class rất đơn giản phải không, bây giờ chúng ta sẽ gắn meta-data cho nó như sau. ```typescript -import { Component } from "@angular/core"; +import { Component } from '@angular/core'; @Component({ - selector: "app-hello", + selector: 'app-hello', template: `

Hello there!

`, }) export class HelloComponent {} @@ -59,8 +59,9 @@ error NG8001: 'app-hello' is not a known element: Declarables must belong to exactly one module. The compiler emits an error if you try to declare the same class in more than one module. Be careful not to declare a class that is imported from another module. Yeah, chính là nó đó, giờ chỉ việc import component lên đầu và thêm HelloComponent vào declarations là xong. + ```typescript -import { HelloComponent } from './hello.component' +import { HelloComponent } from './hello.component'; ``` ```typescript @@ -80,7 +81,7 @@ Các bạn hãy thử tìm hiểu cấu trúc ứng dụng và tạo thêm nhi ## Youtube Video -https://youtu.be/jgFw8tAgKNs +[![Day 02](https://img.youtube.com/vi/jgFw8tAgKNs/0.jpg)](https://youtu.be/jgFw8tAgKNs) ## Link tham khảo @@ -89,7 +90,7 @@ Link document các bạn cần tìm hiểu trong Day 2 - https://angular.io/guide/architecture - https://angular.io/guide/architecture-modules - https://angular.io/guide/architecture-components - + Mục tiêu Day 3 sẽ là về **data binding**. ## Author diff --git a/Day003-DataBinding.md b/Day003-DataBinding.md index 4205670..3f70059 100644 --- a/Day003-DataBinding.md +++ b/Day003-DataBinding.md @@ -11,9 +11,9 @@ Nó có thể hiểu là hãy tính toán cái expression này, nếu có trả Chỉ đơn giản thế. Giờ các bạn có thể phun data về tên tuổi của một người thành cái profile đơn giản như sau: ```typescript -import { Component } from "@angular/core"; +import { Component } from '@angular/core'; @Component({ - selector: "app-hello", + selector: 'app-hello', template: `

Hello there!

Your name: {{ user.name }}

@@ -22,7 +22,7 @@ import { Component } from "@angular/core"; }) export class HelloComponent { user = { - name: "Tiep Phan", + name: 'Tiep Phan', age: 30, }; } @@ -37,11 +37,11 @@ Sau khi parse xong sẽ có một object (node) thuộc type HTMLInputElement đ ```typescript obj = { - type: 'text', - value: 'something', - attributes: [] // thuộc type NamedNodeMap, dạng như một array - // … các thuộc tính, method khác -} + type: 'text', + value: 'something', + attributes: [], // thuộc type NamedNodeMap, dạng như một array + // … các thuộc tính, method khác +}; ``` Như bạn có thể thấy, attribute được dùng để chỉ những gì các bạn đặt vào phần opening tag của một tag, còn lại là property của object (node). @@ -67,7 +67,7 @@ Câu trả lời chính là Event binding. Để gắn event listener vào một ```typescript @Component({ - selector: "app-hello", + selector: 'app-hello', template: `

Hello there!

@@ -75,7 +75,7 @@ Câu trả lời chính là Event binding. Để gắn event listener vào một }) export class HelloComponent { showInfo() { - alert("Inside Angular Component method"); + alert('Inside Angular Component method'); } } ``` @@ -119,13 +119,12 @@ Mục tiêu Day 4 sẽ là về cấu trúc `if else` ## Youtube Video -https://youtu.be/WrMywdbnQfk +[![Day 03](https://img.youtube.com/vi/WrMywdbnQfk/0.jpg)](https://youtu.be/WrMywdbnQfk) ## Author [Tiep Phan](https://github.com/tieppt) - ## HASHTAG `#100DaysOfCodeAngular` `#100DaysOfCode` `#AngularVietNam100DoC_Day3` diff --git a/Day004-Structure-Directive-If-Else.md b/Day004-Structure-Directive-If-Else.md index 2c4e5d0..f80ec81 100644 --- a/Day004-Structure-Directive-If-Else.md +++ b/Day004-Structure-Directive-If-Else.md @@ -9,19 +9,17 @@ Trong Angular để thêm, xóa, thay đổi structure (structure HTML chẳng h ```typescript @Component({ - selector: "app-hello", + selector: 'app-hello', template: `

Hello there!

Your name: {{ user.name }}

Your name: {{ user.age }}

-
- Bạn có thể xem nội dung PG-13 -
+
Bạn có thể xem nội dung PG-13
`, }) export class HelloComponent { user = { - name: "Tiep Phan", + name: 'Tiep Phan', age: 30, }; } @@ -32,24 +30,16 @@ Với những directive được cung cấp sẵn (built-in) bởi Angular, gi Vậy nếu chúng ta muốn dùng IF-ELSE thì thế nào. Có thể các bạn sẽ nghĩ ngay đến phủ định mệnh đề IF là được ELSE thôi. Điều này hoàn toàn được. ```html -
- Bạn có thể xem nội dung PG-13 -
-
- Bạn không thể xem nội dung PG-13 -
+
Bạn có thể xem nội dung PG-13
+
Bạn không thể xem nội dung PG-13
``` Hoặc chúng ta có cách hay ho khác, đó là dùng đến ng-template. Tag ng-template là một tag được cung cấp bởi Angular, nó sẽ lưu trữ một Template được định nghĩa bên trong cặp thẻ mở/đóng của nó. Những gì được định nghĩa bên trong đó sẽ không được hiển thị ra view, nhưng chúng ta có thể sử dụng Template đó để render bằng code. Đoạn code phía trên có thể được chuyển đổi tương đương: ```html -
- Bạn có thể xem nội dung PG-13 -
+
Bạn có thể xem nội dung PG-13
-
- Bạn không thể xem nội dung PG-13 -
+
Bạn không thể xem nội dung PG-13
``` @@ -59,9 +49,7 @@ Với cú pháp sử dụng dấu `*` ở trên, có thể các bạn sẽ thấ ```html -
- Bạn có thể xem nội dung PG-13 -
+
Bạn có thể xem nội dung PG-13
``` @@ -77,7 +65,7 @@ Mục tiêu Day 5 sẽ là về cấu trúc lặp `ngForOf` ## Youtube Video -https://youtu.be/Yujs6hi-l4w +[![Day 04](https://img.youtube.com/vi/Yujs6hi-l4w/0.jpg)](https://youtu.be/Yujs6hi-l4w) ## Author diff --git a/Day005-Structure-Directive-NgFor.md b/Day005-Structure-Directive-NgFor.md index 23f9f31..3826b96 100644 --- a/Day005-Structure-Directive-NgFor.md +++ b/Day005-Structure-Directive-NgFor.md @@ -9,19 +9,19 @@ Giả sử chúng ta có một danh sách các tác giả của các cuốn sác authors = [ { id: 1, - firstName: "Flora", - lastName: "Twell", - email: "ftwell0@phoca.cz", - gender: "Female", - ipAddress: "99.180.237.33", + firstName: 'Flora', + lastName: 'Twell', + email: 'ftwell0@phoca.cz', + gender: 'Female', + ipAddress: '99.180.237.33', }, { id: 2, - firstName: "Priscella", - lastName: "Signe", - email: "psigne1@berkeley.edu", - gender: "Female", - ipAddress: "183.243.228.65", + firstName: 'Priscella', + lastName: 'Signe', + email: 'psigne1@berkeley.edu', + gender: 'Female', + ipAddress: '183.243.228.65', }, // more data ]; @@ -101,9 +101,7 @@ Ví dụ: ```html
-
- More code -
+
More code
``` @@ -111,14 +109,10 @@ Giả sử nếu bạn không được phép hoặc không muốn sinh ra một ```html
- - More code - + More code
- - More code - + More code
``` @@ -137,7 +131,7 @@ Mục tiêu Day 6 sẽ là về `Attribute directive` để áp dụng style, cl ## Youtube Video -https://youtu.be/q7CQPEPSkD0 +[![Day 05](https://img.youtube.com/vi/q7CQPEPSkD0/0.jpg)](https://youtu.be/q7CQPEPSkD0) ## Author diff --git a/Day006-Attribute-Directive-Class-Style.md b/Day006-Attribute-Directive-Class-Style.md index 726e9c6..ea7d5ec 100644 --- a/Day006-Attribute-Directive-Class-Style.md +++ b/Day006-Attribute-Directive-Class-Style.md @@ -9,9 +9,7 @@ Trong các ứng dụng thực tế, có thể chúng ta cần thay đổi (thê Ví dụ, nếu chúng ta đang chọn một tab nào đó để hiển thị, thì tab đó sẽ có thêm class tab-active, các tab khác sẽ không có. Lúc này chúng ta sẽ sử dụng cú pháp: ```html -
- some content -
+
some content
``` Nhìn qua thì nó chỉ là property binding, với giá trị của isTabActive trả về true thì classList của div đó sẽ tồn tại class tab-active, còn nếu trả về false thì sẽ không tồn tại. @@ -78,7 +76,7 @@ Mục tiêu Day 7 sẽ là Input cho component. ## Youtube Video -https://youtu.be/Zh36WRD3MMQ +[![Day 06](https://img.youtube.com/vi/Zh36WRD3MMQ/0.jpg)](https://youtu.be/Zh36WRD3MMQ) ## Author diff --git a/Day007-Component-Interaction-01.md b/Day007-Component-Interaction-01.md index 7b1d411..b5650eb 100644 --- a/Day007-Component-Interaction-01.md +++ b/Day007-Component-Interaction-01.md @@ -61,10 +61,10 @@ Khi đã có component và khai báo input rồi thì làm sao để sử dụng Đây là component progress-bar của chúng ta. ```typescript -import { Component, OnInit, Input } from "@angular/core"; +import { Component, OnInit, Input } from '@angular/core'; @Component({ - selector: "app-progress-bar", + selector: 'app-progress-bar', template: `
{{ author.firstName }} {{ author.lastName }} @@ -87,7 +87,7 @@ export class AuthorDetailComponent implements OnInit { ```typescript @Component({ - selector: "app-author-list", + selector: 'app-author-list', template: ` - Your name: {{ name }} -

+

Your name: {{ name }}

- + ``` + **app.component.ts** + ```ts @Component({ selector: 'my-app', templateUrl: './app.component.html', - styleUrls: [ './app.component.css' ] + styleUrls: ['./app.component.css'], }) -export class AppComponent { +export class AppComponent { name = 'Tiep Phan'; } ``` @@ -50,7 +52,7 @@ Giờ đây mỗi lần bạn thay đổi giá trị của input thì thẻ `p` Thực chất cách viết trên là viết tắt của property binding và event binding như sau: ```html - + ``` Vậy nên để tạo custom Two-way binding thì bạn chỉ cần tạo @Input và @Output với @Output có suffix là change là được, ví dụ `value` và `valueChange`. @@ -66,52 +68,57 @@ ng g c toggle Phần code của component sẽ được implement như sau: **toggle.component.ts** + ```ts import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; @Component({ selector: 'app-toggle', templateUrl: './toggle.component.html', - styleUrls: ['./toggle.component.css'] + styleUrls: ['./toggle.component.css'], }) export class ToggleComponent implements OnInit { @Input() checked = false; @Output() checkedChange = new EventEmitter(); - constructor() { } + constructor() {} - ngOnInit() { - } + ngOnInit() {} toggle() { this.checked = !this.checked; this.checkedChange.emit(this.checked); } - } ``` **toggle.component.html** + ```html -
+
``` + **toggle.component.css** + ```css .toggle-wrapper { display: flex; align-items: center; justify-content: center; cursor: pointer; - transition: all .2s; + transition: all 0.2s; width: 100px; height: 100px; border-radius: 50%; background-color: #fe4551; box-shadow: 0 20px 20px 0 rgba(#fe4551, 0.3); - - } .toggle-wrapper:active { @@ -124,7 +131,6 @@ export class ToggleComponent implements OnInit { width: 17px; } - .toggle { transition: all 0.2s ease-in-out; height: 20px; @@ -133,19 +139,16 @@ export class ToggleComponent implements OnInit { border: 10px solid #fff; border-radius: 50%; cursor: pointer; - animation: red .7s linear forwards; + animation: red 0.7s linear forwards; } - .toggle-wrapper.checked { background-color: #48e98a; box-shadow: 0 20px 20px 0 rgba(#48e98a, 0.3); - } .toggle-wrapper.checked:active { box-shadow: 0 15px 15px 0 rgba(#48e98a, 0.5); - } .toggle-wrapper.checked .toggle { @@ -153,17 +156,19 @@ export class ToggleComponent implements OnInit { background-color: #fff; border-color: transparent; border-radius: 30px; - animation: green .7s linear forwards !important; + animation: green 0.7s linear forwards !important; } ``` + Component trên của chúng ta không có quá khác biệt so với những component trước đây, nó đều sử dụng những concept cơ bản như @Input, @Output, class binding, style css. Chỉ với việc sử dụng suffix `change` cho property `checked` chúng ta có thể sử dụng component này ở bất cứ đâu với cách dùng giống như `ngModel`: + ```html ``` -![Toggle](assets/100doc-day9.gif) +![Toggle](assets/100doc-day9.gif) ## SUMMARY @@ -176,7 +181,7 @@ Như vậy trong Day 9, chúng ta sẽ phải tìm hiểu cách để tạo ra c ## Youtube Video -https://youtu.be/U8UCOKInmu8 +[![Day 09](https://img.youtube.com/vi/U8UCOKInmu8/0.jpg)](https://youtu.be/U8UCOKInmu8) ## Code sample diff --git a/Day010-template-variable-viewchild-viewchildren.md b/Day010-template-variable-viewchild-viewchildren.md index ccf9abf..c0344e9 100644 --- a/Day010-template-variable-viewchild-viewchildren.md +++ b/Day010-template-variable-viewchild-viewchildren.md @@ -3,6 +3,7 @@ Nếu bạn cần trỏ tới một phần tử (HTMLElement/component/directive) ở trong template và thao tác trực tiếp lên nó thì sao. Có cách nào để chúng ta tạo ra một `variable` ở trong template và sử dụng nó không? Câu hỏi trên sẽ được trả lời trong ngày thứ 10 này. ## Parent interacts with child via local variable + Giả sử chúng ta có `AppComponent` có nhúng một phần template như sau: ```html @@ -14,7 +15,7 @@ Nhưng thay vì click vào `ToggleComponent` để thay đổi trạng thái c ```html -
+
``` @@ -26,7 +27,7 @@ Lúc này bạn sẽ có thể sử dụng đến Template variable như một g ```html -
+
``` @@ -36,13 +37,9 @@ Cú pháp của Template variable chính là sử dụng `#varName` và bạn c Như trước đây làm với ví dụ về `NgIf-Else` chúng ta cũng đã sử dụng Template variable để lấy về một instance của `ng-template` và truyền vào cho `NgIfElse` như sau: ```html -
- Bạn có thể xem nội dung PG-13 -
+
Bạn có thể xem nội dung PG-13
-
- Bạn không thể xem nội dung PG-13 -
+
Bạn không thể xem nội dung PG-13
``` @@ -56,16 +53,20 @@ Trong một số trường hợp bạn cần lấy chính xác một instance c ```html
- + [(ngModel)]="model.name" + name="name" + #name="ngModel" + />
``` Ở template trên chúng ta đã tạo ra 2 Template variable là: + - `nameForm`: mong muốn lấy instance của directive có `exportAs` là `ngForm` - `name`: mong muốn lấy instance của directive có `exportAs` là `ngModel` @@ -79,14 +80,14 @@ Lúc này chúng ta có thể query một Template variable ở trong Component ```html -
-
+
+
``` ```ts -export class AppComponent { +export class AppComponent { @ViewChild('toggleComp') toggleComp: ToggleComponent; toggleInside() { this.toggleComp.toggle(); @@ -101,7 +102,7 @@ Nếu bạn sử dụng ViewChild cho một HTMLElement thì chúng ta sẽ nh ``` ```ts -export class AppComponent { +export class AppComponent { @ViewChild('chartContainer') container: ElementRef; } ``` @@ -121,6 +122,7 @@ ViewChild(selector: string | Function | Type, opts?: { ``` Trong đó các `selector` có thể là: + - Any class with the @Component or @Directive decorator - A template reference variable as a string (e.g. query with @ViewChild('cmp')) - Any provider defined in the child component tree of the current component (e.g. @ViewChild(SomeService) someService: SomeService) @@ -131,11 +133,14 @@ Trong đó các `selector` có thể là: ```html
- + [(ngModel)]="model.name" + name="name" + #name="ngModel" + />
``` @@ -143,25 +148,25 @@ Trong đó các `selector` có thể là: ```ts export class NameFormComponent implements OnInit { model = { - name: 'Tiep Phan' + name: 'Tiep Phan', }; @ViewChild('nameForm', { read: ElementRef, - static: true - }) form: ElementRef; - constructor() { } + static: true, + }) + form: ElementRef; + constructor() {} ngOnInit() { - console.log(this.form) + console.log(this.form); } - } ``` Ở trong trường hợp trên, nếu chúng ta không khai báo `read` thì sẽ lấy về NgForm instance, nhưng do khai báo là một ElementRef nên nó sẽ query khác với variable ở ngoài template. -`opts.static` nếu `selector` không nằm trong if/else hay một structure directive nào thì chúng ta có thể gọi nó là `static: true`, tức là nó không thay đổi trong suốt thời gian sống của component. Lúc này Angular (v9 trở lên) sẽ chạy phần `resolve query result` (tiến trình) trước khi chạy *Change Detection* nên chúng ta có thể truy cập nó ở trong `ngOnInit` như ở trên, nếu `static: false` (giá trị mặc định) thì tiến trình trên sẽ chạy sau khi chạy *Change Detection* nên bạn không thể dùng nó ở `ngOnInit` mà phải chạy ở `ngAfterViewInit`. +`opts.static` nếu `selector` không nằm trong if/else hay một structure directive nào thì chúng ta có thể gọi nó là `static: true`, tức là nó không thay đổi trong suốt thời gian sống của component. Lúc này Angular (v9 trở lên) sẽ chạy phần `resolve query result` (tiến trình) trước khi chạy _Change Detection_ nên chúng ta có thể truy cập nó ở trong `ngOnInit` như ở trên, nếu `static: false` (giá trị mặc định) thì tiến trình trên sẽ chạy sau khi chạy _Change Detection_ nên bạn không thể dùng nó ở `ngOnInit` mà phải chạy ở `ngAfterViewInit`. ## Parent class with ViewChildren @@ -171,9 +176,10 @@ ViewChildren sẽ trả về một QueryList trước khi `ngAfterViewInit` đư ```html -
+
``` + ```ts @ViewChildren(ToggleComponent) toggleList: QueryList; @@ -197,7 +203,7 @@ Cũng trong Day 10 chúng ta học thêm một component lifecycle khác là `ng ## Youtube Video -https://youtu.be/Wd_644YBQUM +[![Day 10](https://img.youtube.com/vi/Wd_644YBQUM/0.jpg)](https://youtu.be/Wd_644YBQUM) ## Code sample diff --git a/Day011-typescript-data-type.md b/Day011-typescript-data-type.md index ab9c031..6a95749 100644 --- a/Day011-typescript-data-type.md +++ b/Day011-typescript-data-type.md @@ -211,7 +211,7 @@ Mục tiêu của Day 12 là **Một số kỹ thuật TypeScript Nâng Cao**. ## Youtube Video -https://youtu.be/ozHjDLuusVU +[![Day 11](https://img.youtube.com/vi/ozHjDLuusVU/0.jpg)](https://youtu.be/ozHjDLuusVU) ## Author diff --git a/Day012-typescript-advanced-type.md b/Day012-typescript-advanced-type.md index effc8f9..3d4f1cf 100644 --- a/Day012-typescript-advanced-type.md +++ b/Day012-typescript-advanced-type.md @@ -286,7 +286,7 @@ Mục tiêu của Day 13 là **Content Projection trong Angular**. ## Youtube Video -https://youtu.be/4tcajihANZQ +[![Day 12](https://img.youtube.com/vi/4tcajihANZQ/0.jpg)](https://youtu.be/4tcajihANZQ) ## Author diff --git a/Day013-content-projection-in-angular.md b/Day013-content-projection-in-angular.md index 77c8939..8ec4dde 100644 --- a/Day013-content-projection-in-angular.md +++ b/Day013-content-projection-in-angular.md @@ -168,7 +168,7 @@ Một số link mà các bạn cần tìm hiểu: ## Youtube Video -https://youtu.be/-vN52YVbcgk +[![Day 13](https://img.youtube.com/vi/-vN52YVbcgk/0.jpg)](https://youtu.be/-vN52YVbcgk) ## Code sample diff --git a/Day014-ng-template-ng-template-outlet-ng-container.md b/Day014-ng-template-ng-template-outlet-ng-container.md index af723ee..cd734cf 100644 --- a/Day014-ng-template-ng-template-outlet-ng-container.md +++ b/Day014-ng-template-ng-template-outlet-ng-container.md @@ -5,13 +5,9 @@ Trong [bài viết thứ tư][day4] của series, anh @tiepphan đã nói có nói đến một trường hợp dùng `ng-template`. Đó là khi dùng `*ngIf` với điều kiện else, chúng ta có thể truyền vào một template reference đc định nghĩa thông qua cú pháp `#templateReferenceName` để render lên UI. ```html -
- Bạn có thể xem nội dung PG-13 -
+
Bạn có thể xem nội dung PG-13
-
- Bạn không thể xem nội dung PG-13 -
+
Bạn không thể xem nội dung PG-13
``` @@ -20,7 +16,6 @@ Thông qua ví dụ trên, chắc các bạn cũng đã nhận ra được đôi - Khi code HTML của bạn dc bao quanh bởi `ng-template`, phần HTML đó sẽ không dc render lên UI ngay lập tức. Mà chỉ dc render trong một số trường hợp, ví dụ như khi `*ngIf else tmpl` hoặc thông qua `ngTemplateOutlet` mà chúng ta sẽ đề cập đến ở phần sau của bài viết. - Tên gọi của ng-template cũng phần nào nói lên đc ý nghĩa của nó. Template hiểu nôm na là mẫu, dạng. Dịch ra tiếng Việt hơi khó, tuy nhiên khi kết hợp nhiều template với nhau thì chúng ta có thể có một UI đầy đủ. - Từ những điểm trên, có thể định nghĩa `ng-template` là một thành phần của Angular để render HTML code. Và phần HTML code nằm trong `ng-template` không bao giờ được hiển thị trực tiếp ở nơi nó được định nghĩa ### Khi nào thì nên dùng ng-template? @@ -31,21 +26,24 @@ Một số trường hợp hay cần dùng đến ng-template theo như kinh ngh #### 2. Khi một số UI element trong một component bị lặp lại trong chính component đó, nhưng phần code đó quá nhỏ để tách ra làm một component riêng. -Ví dụ như bạn có một component có chứa biến một biển tên là `counter`. Phần UI của counter này sẽ đc lặp lại ở trong component của bạn vài lần với UI giống nhau. +Ví dụ như bạn có một component có chứa biến một biển tên là `counter`. Phần UI của counter này sẽ đc lặp lại ở trong component của bạn vài lần với UI giống nhau. Đây là cách bình thường chúng ta hay làm. Copy and paste code. ```html
-
- You have selected {{ counter }} items. -
-
- There are {{ counter }} items was selected. -
- +
+ You have selected + {{ counter }} items. +
+
+ There are {{ counter }} items was + selected. +
+
``` @@ -53,25 +51,28 @@ Và đây là cách chúng ta có thể viết lại bằng cách dùng `ng-temp ```html
-
- You have selected . -
-
- There are was selected. -
- +
+ You have selected + . +
+
+ There are was + selected. +
+
- {{ counter }} items + {{ counter }} items ``` Khi viết lại code dùng `ng-template`, ưu điểm dễ nhận thấy đó là: -- Nếu cần sửa lại UI cho counter. Thay vì phải sửa ở 3 nơi, bây giờ ta chỉ cần sửa ở một vị trí đó là `ng-template` của counter thôi. Tránh những lỗi typo hay find and replace bị thiếu. +- Nếu cần sửa lại UI cho counter. Thay vì phải sửa ở 3 nơi, bây giờ ta chỉ cần sửa ở một vị trí đó là `ng-template` của counter thôi. Tránh những lỗi typo hay find and replace bị thiếu. - Vì phần template này chỉ gói gọn trong đúng một dòng code nên dùng ng-template tiện hơn hẳn là phải tách phần counter ra một component mới. #### 3. Dùng ng-template để pass vào component khác. Hỗ trợ override template có sẵn trong component. @@ -83,16 +84,16 @@ Ví dụ mình có component `tab-container`, mặc định sẽ render tab vớ selector: 'tab-container', template: ` -
- ... -
+
...
- + ... rest of tab container component ... - ` + `, }) export class TabContainerComponent { - @Input() headerTemplate: TemplateRef; // Custom template provided by parent + @Input() headerTemplate: TemplateRef; // Custom template provided by parent } ``` @@ -101,7 +102,7 @@ Tuy nhiên, khi dùng `tab-container` bạn hoàn toàn có thể pass vào temp ```ts @Component({ selector: 'app-root', - template: ` + template: `
- + ` }) ``` @@ -121,7 +122,7 @@ Tuy nhiên, khi dùng `tab-container` bạn hoàn toàn có thể pass vào temp Qua ví dụ trên thì có thể thấy ngay `ngTemplateOutlet` là cách dùng để render một template được tạo ra bởi `ng-template` lên UI. Cú pháp như sau -- *ngTemplateOutlet="templateRef"(chú ý dấu sao `*` nhé, không có là không chạy đâu đấy) +- _ngTemplateOutlet="templateRef"(chú ý dấu sao `_` nhé, không có là không chạy đâu đấy) - [ngTemplateOutlet]="templateRef" Tuy nhiên, như component có `@Input()` để truyền data từ bên ngoài vào, thì ng-template cũng có cú pháp tương tự để truyền data. Đó chính là `ngTemplateOutletContext` @@ -129,36 +130,39 @@ Tuy nhiên, như component có `@Input()` để truyền data từ bên ngoài v Ví dụ như mình muốn reuse lại một button với những tên khác nhau, ngoài ra, một button mình muốn có icon, và một button mình ko muốn có icon. Thì bạn hoàn toàn cũng có thể dùng kết hợp giữa `ng-template`, `ngTemplateOutlet` và `ngTemplateOutletContext` để viết code. ```html - + ``` Hoàn toàn có thể được viết lại thành. ```html - - + + - + - + ``` @@ -167,24 +171,22 @@ Tuy nhiều code hơn, nhưng giờ chúng ta có thể reuse lại button này Vài điểm chú ý: - Khi define ra `ng-template`, bạn có thể config cho template đó nhận vào value bằng cách dùng cú pháp `let-name="name"`. -Trong đó name ở bên trái dấu bằng là variable bạn có thể access được ở trong ng-template, còn name ở bên phải dấu bằng là tên của object property khi pass qua `ngTemplateOutletContext`. Hai cái name này hoàn toàn có thể khác nhau, không có vấn đề gì cả. + Trong đó name ở bên trái dấu bằng là variable bạn có thể access được ở trong ng-template, còn name ở bên phải dấu bằng là tên của object property khi pass qua `ngTemplateOutletContext`. Hai cái name này hoàn toàn có thể khác nhau, không có vấn đề gì cả. - Nếu bạn không đặt tên cho variable ở vế phải của dấu bằng, chỉ viết là `let-name`, thì khi truyền data qua context, property của object truyền vào sẽ là `$implicit`. Ví dụ cụ thể: ```html - - + + - + ``` @@ -197,17 +199,15 @@ ng-container là một custom html tag để khi render trên UI sẽ không có ```html
-
+ [ngTemplateOutletContext]="{ label: 'Click here', class: 'btn-primary', icon: null }" +>
``` Tuy nhiên khi render ra UI, sẽ bị thừa một thẻ div bao ngoài. ```html
- +
``` @@ -221,14 +221,14 @@ Một số bài viết khác bạn có thể đọc thêm. - https://alligator.io/angular/reusable-components-ngtemplateoutlet/ - https://angular.io/guide/structural-directives#the-ng-template -- [Angular render recursive view using *ngFor and ng-template](https://trungk18.com/experience/angular-recursive-view-render/) +- [Angular render recursive view using \*ngFor and ng-template](https://trungk18.com/experience/angular-recursive-view-render/) - https://blog.angular-university.io/angular-ng-template-ng-container-ngtemplateoutlet/ Mục tiêu của Day 15 là **Giới thiệu Dependency Injection trong Angular**. ## Youtube Video -https://youtu.be/3JM8pDR-MaU +[![Day 14](https://img.youtube.com/vi/3JM8pDR-MaU/0.jpg)](https://youtu.be/3JM8pDR-MaU) ## Author @@ -236,4 +236,4 @@ https://youtu.be/3JM8pDR-MaU `#100DaysOfCodeAngular` `#100DaysOfCode` `#AngularVietNam100DoC_Day14` -[day4]: Day004-Structure-Directive-If-Else.md \ No newline at end of file +[day4]: Day004-Structure-Directive-If-Else.md diff --git a/Day015-introduction-dependency-injection-in-angular.md b/Day015-introduction-dependency-injection-in-angular.md index 6ed499e..3858861 100644 --- a/Day015-introduction-dependency-injection-in-angular.md +++ b/Day015-introduction-dependency-injection-in-angular.md @@ -121,7 +121,7 @@ Ví dụ về cách provide `CartService`: ```ts @Injectable({ - providedIn: "root", + providedIn: 'root', }) export class CartService { // properties and methods @@ -130,9 +130,9 @@ export class CartService { ```ts @Component({ - selector: "app-product", - templateUrl: "./product.component.html", - styleUrls: ["./product.component.css"], + selector: 'app-product', + templateUrl: './product.component.html', + styleUrls: ['./product.component.css'], }) export class ProductComponent implements OnInit { constructor(private cartService: CartService) {} @@ -182,7 +182,7 @@ Hoặc có thể override vào `@Injectable` của service ```ts @Injectable({ - providedIn: "root", + providedIn: 'root', useClass: CartExtService, }) export class CartService { @@ -203,7 +203,7 @@ Như vậy, trong Day 15 chúng ta đã làm quen về khái niệm cũng như c ## Youtube Video -https://youtu.be/_JnUGhVhq_o +[![Day 15](https://img.youtube.com/vi/_JnUGhVhq_o/0.jpg)](https://youtu.be/_JnUGhVhq_o) ## Code sample diff --git a/Day016-dependency-injection-in-angular-part-2.md b/Day016-dependency-injection-in-angular-part-2.md index 395c941..cfc23ef 100644 --- a/Day016-dependency-injection-in-angular-part-2.md +++ b/Day016-dependency-injection-in-angular-part-2.md @@ -27,21 +27,21 @@ Và trong mỗi component của Angular chúng ta sẽ được cung cấp sẵn Đến thời điểm hiện tại, bạn hoàn toàn có thể sử dụng `EventEmitter` để notify cho parent component biết được các thời điểm tương ứng. Nhưng cũng có một cách khác, đó chính là inject parent component vào child component và thực hiện các hành động tương ứng. Giả sử bạn cài đặt **tab-group.component.ts** như sau: + ```ts @Component({ selector: 'app-tab-group', templateUrl: './tab-group.component.html', - styleUrls: ['./tab-group.component.css'] + styleUrls: ['./tab-group.component.css'], }) export class TabGroupComponent implements OnInit { tabPanelList: TabPanelComponent[] = []; @Input() tabActiveIndex = 0; @Output() tabActiveChange = new EventEmitter(); - constructor() { } + constructor() {} - ngOnInit() { - } + ngOnInit() {} selectItem(idx: number) { this.tabActiveIndex = idx; @@ -65,19 +65,22 @@ export class TabGroupComponent implements OnInit { if (index !== -1) { this.selectItem(0); } - } - } ``` Và đây là phần UI cho **tab-group.component.html**: + ```html
-
@@ -88,22 +91,23 @@ Và đây là phần UI cho **tab-group.component.html**:
``` + Việc của chúng ta bây giờ chỉ là inject và call các method để register và remove: ```ts @Component({ selector: 'app-tab-panel', template: ` - - - + + + `, - styles: [''] + styles: [''], }) export class TabPanelComponent implements OnInit, OnDestroy { @Input() title: string; - @ViewChild(TemplateRef, {static: true}) panelBody: TemplateRef; - constructor(private tabGroup: TabGroupComponent) { } + @ViewChild(TemplateRef, { static: true }) panelBody: TemplateRef; + constructor(private tabGroup: TabGroupComponent) {} ngOnInit() { this.tabGroup.addTabPanel(this); @@ -111,7 +115,6 @@ export class TabPanelComponent implements OnInit, OnDestroy { ngOnDestroy() { this.tabGroup.removeTabPanel(this); } - } ``` @@ -122,6 +125,7 @@ Như bạn có thể thấy, tab group của chúng ta ở trên có UI cực k Đây là nơi tỏa sáng của DI. Bạn chỉ cần đơn giản là provide một provider để override là được. **bs-tab-group.component.ts** + ```ts @Component({ selector: 'app-bs-tab-group', @@ -130,20 +134,31 @@ Như bạn có thể thấy, tab group của chúng ta ở trên có UI cực k providers: [ { provide: TabGroupComponent, - useExisting: BsTabGroupComponent - } - ] + useExisting: BsTabGroupComponent, + }, + ], }) -export class BsTabGroupComponent extends TabGroupComponent { -} +export class BsTabGroupComponent extends TabGroupComponent {} ``` + **bs-tab-group.component.html** + ```html
@@ -156,6 +171,7 @@ export class BsTabGroupComponent extends TabGroupComponent { ``` Template khi sử dụng: + ```html content tab 1 @@ -179,22 +195,20 @@ Bây giờ giả sử bạn tách phần provider ở decorator ra một variabl ```ts const BsTabGroupProvider = { provide: TabGroupComponent, - useExisting: BsTabGroupComponent -} + useExisting: BsTabGroupComponent, +}; @Component({ selector: 'app-bs-tab-group', templateUrl: './bs-tab-group.component.html', styleUrls: ['./bs-tab-group.component.css'], - providers: [ - BsTabGroupProvider - ] + providers: [BsTabGroupProvider], }) -export class BsTabGroupComponent extends TabGroupComponent { -} +export class BsTabGroupComponent extends TabGroupComponent {} ``` Bạn sẽ nhận được một Error như sau: + > Error in src/app/bs-tab-group/bs-tab-group.component.ts (6:16) > > Class 'BsTabGroupComponent' used before its declaration. @@ -204,19 +218,23 @@ Do đó chúng ta cần dùng đến closure, đó là tạo một function nó ```ts const BsTabGroupProvider = { provide: TabGroupComponent, - useExisting: forwardRef(() => BsTabGroupComponent) -} + useExisting: forwardRef(() => BsTabGroupComponent), +}; ``` + Bạn có thể thắc mắc là tại sao sử dụng trực tiếp trong decorator thì lại không lỗi? Câu trả lời là vì bản thân Class Decorator sẽ được call sau khi mà bạn đã tạo xong class. Bạn có thể tưởng tượng nó sẽ hoạt động giống như sau: + ```ts @SomeDecorator class SomeClass {} ``` + Sẽ tương đương với call một function như sau. + ```ts -let SomeClass = class SomeClass {} +let SomeClass = class SomeClass {}; SomeClass = SomeDecorator(SomeClass); ``` @@ -227,6 +245,7 @@ Như chúng ta đã tìm hiểu qua thì chúng ta có các cách provide một > Lưu ý: code phía dưới đây sẽ tương tự cho `@NgModule`, `@Component`, `@Directive`. - useClass: + ```ts @NgModule({ providers: [SomeClass] @@ -234,6 +253,7 @@ Như chúng ta đã tìm hiểu qua thì chúng ta có các cách provide một ``` Tương đương với cú pháp: + ```ts @NgModule({ providers: [{ provide: SomeClass, useClass: SomeClass}] @@ -241,6 +261,7 @@ Tương đương với cú pháp: ``` - useExisting: + ```ts @Component({ providers: [ @@ -253,6 +274,7 @@ Tương đương với cú pháp: ``` - useFactory: + ```ts @Component({ providers: [ @@ -267,6 +289,7 @@ Tương đương với cú pháp: ``` - useValue: + ```ts @Component({ providers: [ @@ -277,6 +300,7 @@ Tương đương với cú pháp: ] }) ``` + ## Summary Như vậy, trong Day 16 này bạn sẽ cần tìm hiểu một số kỹ thuật sử dụng DI trong Angular, như thế sẽ giúp bạn hiểu sâu hơn về DI trong Angular và có thể tạo những phần code dễ reuse, flexible hơn. @@ -294,6 +318,10 @@ https://stackblitz.com/edit/angular-ivy-100-days-of-code-day-16?file=src%2Fapp%2 Mục tiêu của Day 17 là **ContentChild & ContentChildren**. +## Youtube Video + +[![Day 16](https://img.youtube.com/vi/hTsn6L8vcVg/0.jpg)](https://youtu.be/hTsn6L8vcVg) + ## Author [Tiep Phan](https://github.com/tieppt) diff --git a/Day017-contentchild-contentchildren.md b/Day017-contentchild-contentchildren.md index 44f6e1a..8e0cfe5 100644 --- a/Day017-contentchild-contentchildren.md +++ b/Day017-contentchild-contentchildren.md @@ -23,6 +23,7 @@ Giả sử chúng ta sẽ sử dụng Tab Component đã được tạo từ Day ``` + Thật bất ngờ, chúng ta hi vọng rằng chỉ có 1 instance của counter, nhưng thực thế chúng ta đang có đến 4 instances, chỉ là có 1 instances được hiển thị. Vậy nếu trong trường hợp các tabs của chúng ta có những component có phần phức tạp, và chúng ta mong muốn chúng được lazy initialize thì làm thế nào? @@ -40,28 +41,31 @@ ContentChild(selector: string | Function | Type, opts?: { ``` Trước tiên, chúng ta sẽ tạo một directive đã. + ```ts import { Directive } from '@angular/core'; @Directive({ - selector: 'ng-template[tabPanelContent]' + selector: 'ng-template[tabPanelContent]', }) export class TabPanelContentDirective { - constructor() { } + constructor() {} } ``` Directive sẽ giúp chúng ta thêm các tính năng lên một phần tử (DOM Node, Component chẳng hạn), chúng ta có thể thấy directive ở trên muốn target đến bất kỳ thẻ `ng-template` nào có kèm thêm attribute `[tabPanelContent]`. Và đây là cách sử dụng `ContentChild` để lấy về directive đó. + ```ts export class TabPanelComponent implements OnInit, OnDestroy { @Input() title: string; - @ViewChild(TemplateRef, {static: true}) panelBody: TemplateRef; + @ViewChild(TemplateRef, { static: true }) panelBody: TemplateRef; - @ContentChild(TabPanelContentDirective, {static: true}) explicitBody: TemplateRef; + @ContentChild(TabPanelContentDirective, { static: true }) + explicitBody: TemplateRef; - constructor(private tabGroup: TabGroupComponent) { } + constructor(private tabGroup: TabGroupComponent) {} ngOnInit() { this.tabGroup.addTabPanel(this); @@ -69,11 +73,11 @@ export class TabPanelComponent implements OnInit, OnDestroy { ngOnDestroy() { this.tabGroup.removeTabPanel(this); } - } ``` Và chúng ta có thể thay đổi cách sử dụng `TabPanelComponent` như sau: + ```html @@ -98,14 +102,14 @@ Bây giờ chúng ta chỉ cần thêm thắt một chút là Tab component sẽ ```ts export class TabPanelComponent implements OnInit, OnDestroy { @Input() title: string; - @ViewChild(TemplateRef, {static: true}) implicitBody: TemplateRef; + @ViewChild(TemplateRef, { static: true }) implicitBody: TemplateRef; - @ContentChild(TabPanelContentDirective, {static: true, read: TemplateRef}) explicitBody: TemplateRef; + @ContentChild(TabPanelContentDirective, { static: true, read: TemplateRef }) + explicitBody: TemplateRef; get panelBody(): TemplateRef { return this.explicitBody || this.implicitBody; } - } ``` @@ -116,7 +120,6 @@ export class TabPanelComponent implements OnInit, OnDestroy { content tab 1 - @@ -147,38 +150,37 @@ Quay trở lại với Tab component ở trên, nếu chúng ta không sử dụ ```ts export class TabGroupComponent implements OnInit { - @Input() tabActiveIndex = 0; @Output() tabActiveChange = new EventEmitter(); - @ContentChildren(TabPanelComponent) tabPanelList: QueryList + @ContentChildren(TabPanelComponent) + tabPanelList: QueryList; - constructor() { } + constructor() {} - ngOnInit() { - } + ngOnInit() {} selectItem(idx: number) { this.tabActiveIndex = idx; this.tabActiveChange.emit(idx); } - } ``` ```ts export class TabPanelComponent { @Input() title: string; - @ViewChild(TemplateRef, {static: true}) implicitBody: TemplateRef; + @ViewChild(TemplateRef, { static: true }) implicitBody: TemplateRef; - @ContentChild(TabPanelContentDirective, {static: true, read: TemplateRef}) explicitBody: TemplateRef; + @ContentChild(TabPanelContentDirective, { static: true, read: TemplateRef }) + explicitBody: TemplateRef; get panelBody(): TemplateRef { return this.explicitBody || this.implicitBody; } - } ``` + Chỉ cần có thế là chúng ta đã có thể query được tất cả theo yêu cầu. > Lưu ý: `ContentChildren` does not retrieve elements or directives that are in other components' templates, since a component's template is always a black box to its ancestors. @@ -189,16 +191,15 @@ Chỉ cần có thế là chúng ta đã có thể query được tất cả the ```ts export class TabGroupComponent implements OnInit, AfterContentInit { - @Input() tabActiveIndex = 0; @Output() tabActiveChange = new EventEmitter(); - @ContentChildren(TabPanelComponent) tabPanelList: QueryList + @ContentChildren(TabPanelComponent) + tabPanelList: QueryList; - constructor() { } + constructor() {} - ngOnInit() { - } + ngOnInit() {} ngAfterContentInit() { this.tabPanelList.changes.subscribe(() => { @@ -212,7 +213,6 @@ export class TabGroupComponent implements OnInit, AfterContentInit { this.tabActiveIndex = idx; this.tabActiveChange.emit(idx); } - } ``` @@ -225,7 +225,6 @@ Câu trả lời cho bạn đây: - Content: là phần template được project vào thông qua cặp thẻ mở/đóng của một component/directive. Nó không trực tiếp quản lý. (Nó còn được gọi với tên light DOM). - ## Summary Như vậy, trong Day 17 bạn sẽ cần tìm hiểu sự khác biệt giữa view và content, làm thế nào để query một hoặc một số element được project vào component/directive đó. @@ -246,6 +245,10 @@ Như vậy, trong Day 17 bạn sẽ cần tìm hiểu sự khác biệt giữa v Mục tiêu của Day 18 là **Pipe trong Angular**. +## Youtube Video + +[![Day 17](https://img.youtube.com/vi/m3ZgeVGLZag/0.jpg)](https://youtu.be/m3ZgeVGLZag) + ## Author [Tiep Phan](https://github.com/tieppt) diff --git a/Day018-pipes.md b/Day018-pipes.md index 7402c97..f2fbee8 100644 --- a/Day018-pipes.md +++ b/Day018-pipes.md @@ -33,16 +33,17 @@ Mình có một biến tên là `now` ở trong component. ```ts export class PipeExampleComponent implements OnInit { - now = "2020-06-24T09:00:00.000Z"; + now = '2020-06-24T09:00:00.000Z'; } ``` Và đây là cách mình hiển thị với built in pipe [Date][date] trong Angular ```html -
{{ now | date }}
//Jun 24, 2020 -
{{ now | date:'medium'}}
//Jun 24, 2020, 5:00:00 -PM +
{{ now | date }}
+//Jun 24, 2020 +
{{ now | date:'medium'}}
+//Jun 24, 2020, 5:00:00 PM ``` Chú ý phần giữa hai dấu ngoặc nhọn `{{ }}`, ngoài việc truyền vào variable bạn muốn hiển thì thì có thêm dấu xổ dọc `|`. Đó là pipe operator, sau đó là tên của pipe bạn đã định nghĩa. Tất cả pipe đều hoạt động theo cách này. @@ -131,7 +132,7 @@ interface PipeTransform { ```ts export class AppTitlePipe implements PipeTransform { transform(resourceId: string): string { - return resourceId ? "Edit" : "Add"; + return resourceId ? 'Edit' : 'Add'; } } ``` @@ -144,11 +145,11 @@ Giống như component có decorator `@Component`. Pipe cũng có decorator `@Pi ```ts @Pipe({ - name: 'appTitle' + name: 'appTitle', }) export class AppTitlePipe implements PipeTransform { transform(resourceId: string): string { - return resourceId ? "Edit" : "Add"; + return resourceId ? 'Edit' : 'Add'; } } ``` @@ -208,12 +209,12 @@ Với pipe `appTitle` ở trên, vì mình truyền vào giá trị string cho a ```ts export class PipeExampleComponent implements OnInit { - userIdChangeAfterFiveSeconds = "14324"; + userIdChangeAfterFiveSeconds = '14324'; time$: Observable = timer(0, 1000).pipe( map((val) => 5 - (val + 1)), startWith(5), finalize(() => { - this.userIdChangeAfterFiveSeconds = ""; + this.userIdChangeAfterFiveSeconds = ''; }), takeWhile((val) => val >= 0) ); @@ -262,7 +263,7 @@ Mình có một pipe tên là `isAdult`, để filter ra những user lớn hơn ```ts @Pipe({ - name: "isAdult", + name: 'isAdult', }) export class IsAdultPipe implements PipeTransform { transform(arr: User[]): User[] { @@ -331,6 +332,7 @@ Bây giờ thì bạn thấy list người lớn cũng đã được update khi ### 2. Set impure Pipe Nếu bạn muốn trigger pipe khi có thay đổi value của một phần tử trong array, hay khi một property của object bị thay đổi. Bạn có thể cấu hình pipe của bạn với thuộc tính `pure` với giá trị `false` trong decorator. Mặc định, `pure` luôn có giá trị true. + ```ts @Pipe({ name: 'isAdult', @@ -358,6 +360,10 @@ https://stackblitz.com/edit/angular-100-days-of-code-day-18-pipes Mục tiêu của Day 19 là **Giới thiệu RxJS và Observable**. +## Youtube Video + +[![Day 18](https://img.youtube.com/vi/4BJ2Vk67f6A/0.jpg)](https://youtu.be/4BJ2Vk67f6A) + ## Author [Trung Vo](https://github.com/trungk18) diff --git a/Day019-intro-rxjs-observable.md b/Day019-intro-rxjs-observable.md index ab25b5e..5f224e9 100644 --- a/Day019-intro-rxjs-observable.md +++ b/Day019-intro-rxjs-observable.md @@ -4,9 +4,9 @@ Khi bạn tìm hiểu về Angular, bạn sẽ thấy rằng nó có phụ thu Đây vừa là một điểm mạnh, cũng vừa là điểm yếu của Angular. Vì RxJS xử lý asynchronous rất mạnh, nhưng bù lại bạn sẽ phải học thêm một số các concept khác xoay quanh stream. Thinking in streams. -> RxJS is a library for composing asynchronous and event-based programs by using observable sequences. [RxJS Overview][RxJSOverview] +> RxJS is a library for composing asynchronous and event-based programs by using observable sequences. [RxJS Overview][rxjsoverview] -> In RxJS and in reactive programming in general, the fundamental unit of work is the stream. Think in terms of streams (think reactively) and design code in a way that, instead of holding on to data, you allow it to flow through and apply transformations along the way until it reaches your desired state. [RxJS in Action][RxJSinAction] +> In RxJS and in reactive programming in general, the fundamental unit of work is the stream. Think in terms of streams (think reactively) and design code in a way that, instead of holding on to data, you allow it to flow through and apply transformations along the way until it reaches your desired state. [RxJS in Action][rxjsinaction] ![Everything is a stream](assets/everything-is-a-stream.jpg) @@ -30,15 +30,15 @@ Observable có thể coi là một Array của các value theo thời gian: Hiện tại Observable chưa chính thức tồn tại trong JS, nhưng bạn có thể sử dụng RxJS để có thể có những thành phần chính như `Observable`, `Observer`, `Subject`, etc. Và một loạt các `operators` đi kèm để xử lý stream được dễ dàng hơn. - ## Use-case throttle Thông thường để xử lý một số event xảy ra quá nhanh và nhiều, trong khi chúng ta có thể bỏ qua một số value ở trung gian. Chẳng hạn bạn tạo một ứng dụng, ứng dụng đó có một button và bạn không muốn người dùng click vào button đấy nhanh quá (e.g: 500ms), nếu họ click quá nhanh thì bỏ qua và chỉ tương tác khi lần click gần nhất đã cách đó hơn khoảng thời gian quy định ở trên. Bây giờ chúng ta thử implement giải pháp thông thường và giải pháp với RxJS xem sao. Các bạn hãy khoan đi vào chi tiết những phần code RxJS dưới đây: + ```ts -const btnjsThrottle = document.querySelector("#jsThrottle"); -const btnrxjsThrottle = document.querySelector("#rxjsThrottle"); +const btnjsThrottle = document.querySelector('#jsThrottle'); +const btnrxjsThrottle = document.querySelector('#rxjsThrottle'); // PURE JS version let count = 0; let rate = 500; @@ -57,9 +57,9 @@ import { throttleTime, scan } from 'rxjs/operators'; fromEvent(btnrxjsThrottle, 'click') .pipe( throttleTime(500), - scan(count => count + 1, 0) + scan((count) => count + 1, 0) ) - .subscribe(count => console.log(`RxJS: Clicked ${count} times`)); + .subscribe((count) => console.log(`RxJS: Clicked ${count} times`)); ``` Với cách sử dụng RxJS cho bài toán trên quả không tệ, và nếu cần transform thêm nữa thì chúng ta hoàn toàn có thể handle được. @@ -67,24 +67,33 @@ Với cách sử dụng RxJS cho bài toán trên quả không tệ, và nếu c ## RxJS core concepts ### Observable + - Observable: đại diện cho ý tưởng về một tập hợp các giá trị hoặc các sự kiện trong tương lai. Khi các giá trị hoặc sự kiện phát sinh trong tương lai, Observable sẽ điều phối nó đến Observer. - Observable chỉ là một function (class) mà nó có một số yêu cầu đặc biệt. Nó nhận đầu vào là một Function, mà Function này nhận đầu vào là một Observer và trả về một function để có thể thực hiện việc cancel quá trình xử lý. Thông thường (RxJS 5 trở lên) chúng ta đặt tên function đó là unsubscribe. -> Observables are functions that tie an observer to a producer. That’s it. They don’t necessarily set up the producer, they just set up an observer to listen to the producer, and generally return a teardown mechanism to remove that listener. The act of subscription is the act of “calling” the observable like a function, and passing it an observer. [Ben Lesh: Hot vs Cold Observables][BenLeshHotandCold] + > Observables are functions that tie an observer to a producer. That’s it. They don’t necessarily set up the producer, they just set up an observer to listen to the producer, and generally return a teardown mechanism to remove that listener. The act of subscription is the act of “calling” the observable like a function, and passing it an observer. [Ben Lesh: Hot vs Cold Observables][benleshhotandcold] ### Observer + - Observer là một tập hợp các callbacks tương ứng cho việc lắng nghe các giá trị (next, error, hay complete) được gửi đến bởi Observable. ### Subscription + - Subscription là kết quả có được sau khi thực hiện một Observable, nó thường dùng cho việc hủy việc tiếp tục xử lý. ### Operators + - Operators là các pure functions cho phép lập trình functional với Observable. + ### Subject + - Subject để thực hiện việc gửi dữ liệu đến nhiều Observers (multicasting). + ### Schedulers + - Một scheduler sẽ điều khiển khi nào một subscription bắt đầu thực thi, và khi nào sẽ gửi tín hiệu đi. ## Working with Observables + ### Creating Observables Để create một Observable chúng ta chỉ cần gọi constructor và truyền vào một function (gọi là **subscribe**), trong đó **subscribe function** sẽ nhận đầu vào là một Observer. @@ -108,11 +117,12 @@ const observable = new Observable(function subscribe(observer) { }, 1000); return function unsubscribe() { clearTimeout(id); - } + }; }); ``` ### Invoking Observable + Các Observable hầu hết sẽ giống như một function, tức là nếu bạn có một Observable thì nó chỉ như khai báo một function, do đó những gì bên trong function sẽ không được chạy cho đến khi bạn invoke function đó (lazy computation). Để invoke một Observable bạn chỉ cần `subscribe` vào nó là được. Và sau khi subscribe thì nó sẽ trả về một Subscription. @@ -127,7 +137,7 @@ const subscription = observable.subscribe({ }, complete: () => { console.log('Done'); - } + }, }); ``` @@ -147,18 +157,17 @@ Có ba kiểu giá trị mà một Observable Execution có thể gửi đi: Next notifications thường được sử dụng rộng rãi, nó cực kỳ quan trọng, vì nó gửi đi dữ liệu cần thiết cho một Observer. -Error và Complete notifications có thể chỉ xảy ra duy nhất một lần trong một Observable Execution. +Error và Complete notifications có thể chỉ xảy ra duy nhất một lần trong một Observable Execution. > Lưu ý rằng, chỉ có 1 trong 2 loại tín hiệu trên được gửi đi, nếu đã complete thì không có error, nếu có error thì không có complete. (Chúng không thuộc về nhau :D). Và nếu đã gửi đi complete, hoặc error signal, thì sau đó không có dữ liệu nào được gửi đi nữa. Tức là stream đã close. > In an Observable Execution, zero to infinite Next notifications may be delivered. If either an Error or Complete notification is delivered, then nothing else can be delivered afterwards. - ### Disposing Observable Executions -Bởi vì quá trình thực thi Observable có thể lặp vô hạn, hoặc trong trường hợp nào đó bạn muốn thực hiện hủy việc thực thi vì việc này không còn cần thiết nữa - dữ liệu đã lỗi thời, có dữ liệu khác thay thế. Các bạn có thể liên tưởng tới việc *close websocket stream*, *removeEvenListener* cho một element nào đó đã bị loại bỏ khỏi DOM chẳng hạn. +Bởi vì quá trình thực thi Observable có thể lặp vô hạn, hoặc trong trường hợp nào đó bạn muốn thực hiện hủy việc thực thi vì việc này không còn cần thiết nữa - dữ liệu đã lỗi thời, có dữ liệu khác thay thế. Các bạn có thể liên tưởng tới việc _close websocket stream_, _removeEvenListener_ cho một element nào đó đã bị loại bỏ khỏi DOM chẳng hạn. -Observable có cơ chế tương ứng, cho phép chúng ta hủy việc thực thi. Đó là khi subscribe được gọi, một Observer sẽ bị gắn với một *Observable execution* mới được tạo, sau đó nó sẽ trả về một object thuộc type Subscription. Kiểu dữ liệu này có một method `unsubscribe` khi chúng ta gọi đến, nó sẽ thực hiện cơ chế để hủy việc thực thi. +Observable có cơ chế tương ứng, cho phép chúng ta hủy việc thực thi. Đó là khi subscribe được gọi, một Observer sẽ bị gắn với một _Observable execution_ mới được tạo, sau đó nó sẽ trả về một object thuộc type Subscription. Kiểu dữ liệu này có một method `unsubscribe` khi chúng ta gọi đến, nó sẽ thực hiện cơ chế để hủy việc thực thi. > Lưu ý: nếu bạn tự tạo Observable (bằng new Observable chẳng hạn) thì bạn phải tự thiết lập cơ chế để hủy. @@ -174,31 +183,34 @@ const subscription = observable.subscribe({ }, complete: () => { console.log('Done'); - } + }, }); setTimeout(() => { subscription.unsubscribe(); -}, 500) +}, 500); ``` - ## Observers + Observer là một Consumer những dữ liệu được gửi bởi Observable. Observer là một object chứa một tập 3 callbacks tương ứng cho mỗi loại notification được gửi từ Observable: `next`, `error`, `complete`. Một Observer có dạng như sau: + ```ts const observer = { - next: x => console.log('Observer got a next value: ' + x), - error: err => console.error('Observer got an error: ' + err), + next: (x) => console.log('Observer got a next value: ' + x), + error: (err) => console.error('Observer got an error: ' + err), complete: () => console.log('Observer got a complete notification'), }; ``` Observer được cung cấp là tham số đầu vào của subscribe để kích hoạt Observable execution. + ```ts observable.subscribe(observer); ``` + > Observers are just objects with three callbacks, one for each type of notification that an Observable may deliver. Observe có thể chỉ có một số callbacks trong bộ 3 callbacks kể trên (có thể là một object không có callback nào trong bộ kể trên, trường hợp này ít dùng đến). @@ -206,18 +218,19 @@ Observe có thể chỉ có một số callbacks trong bộ 3 callbacks kể tr Ngoài cách dùng như trên, `observable.subscribe` sẽ chuẩn hóa các callbacks thành Observer object tương ứng, bạn có thể truyền vào các hàm rời rạc nhau, nhưng cần lưu ý truyền đúng thứ tự callback. Cách dùng này hiện tại không khuyến cáo sử dụng, chỉ dùng cách truyền function nếu bạn có một hàm để handle `Next` notification. + ```ts observable.subscribe( - x => console.log('Observer got a next value: ' + x), - err => console.error('Observer got an error: ' + err), + (x) => console.log('Observer got a next value: ' + x), + (err) => console.error('Observer got an error: ' + err), () => console.log('Observer got a complete notification') ); // tương đương với const observer = { - next: x => console.log('Observer got a next value: ' + x), - error: err => console.error('Observer got an error: ' + err), + next: (x) => console.log('Observer got a next value: ' + x), + error: (err) => console.error('Observer got an error: ' + err), complete: () => console.log('Observer got a complete notification'), }; @@ -225,27 +238,28 @@ observable.subscribe(observer); ``` > Lưu ý: Nếu bạn không muốn truyền error handler function vào, hãy truyền `null`/`undefined`: + ```ts observable.subscribe( - x => console.log('Observer got a next value: ' + x), + (x) => console.log('Observer got a next value: ' + x), null, () => console.log('Observer got a complete notification') ); ``` # Subscription + Subscription là một object đại diện cho một nguồn tài nguyên có khả năng hủy được, thông thường trong Rxjs là hủy Observable execution. Subscription có chứa một method quan trọng unsubscribe (từ Rxjs 5 trở lên), khi method này được gọi, execution sẽ bị hủy. Ví dụ: chúng ta có một đồng hồ đếm thời gian, mỗi giây sẽ gửi đi một giá trị, giả sử sau khi chạy 5s chúng ta cần hủy phần thực thi này. ```ts const observable = interval(1000); -const subscription = observable.subscribe(x => console.log(x)); +const subscription = observable.subscribe((x) => console.log(x)); setTimeout(() => { subscription.unsubscribe(); }, 5000); - ``` > A Subscription essentially just has an unsubscribe() function to release resources or cancel Observable executions. @@ -258,8 +272,8 @@ Một Subscription có thể chứa trong nó nhiều Subscriptions con, khi Sub const foo = interval(500); const bar = interval(700); -const subscription = foo.subscribe(x => console.log('first: ' + x)); -const childSub = bar.subscribe(x => console.log('second: ' + x)); +const subscription = foo.subscribe((x) => console.log('first: ' + x)); +const childSub = bar.subscribe((x) => console.log('second: ' + x)); subscription.add(childSub); @@ -268,6 +282,7 @@ setTimeout(() => { subscription.unsubscribe(); }, 2000); ``` + ## Summary Vậy là qua Day 19 hy vọng các bạn đã hiểu được cơ bản về RxJS và Observable. @@ -286,14 +301,16 @@ https://stackblitz.com/edit/rxjs-racgao?file=index.ts Mục tiêu của Day 20 là **RxJS Creation Operators**. +## Youtube Video + +[![Day 19](https://img.youtube.com/vi/lRfyUh4ex38/0.jpg)](https://youtu.be/lRfyUh4ex38) + ## Author [Tiep Phan](https://github.com/tieppt) `#100DaysOfCodeAngular` `#100DaysOfCode` `#AngularVietNam100DoC_Day19` -[RxJSOverview]: https://rxjs.dev/guide/overview - -[RxJSinAction]: https://freecontent.manning.com/reactive-fundamentals-thinking-in-streams/#:~:text=In%20RxJS%20and%20in%20reactive,it%20reaches%20your%20desired%20state. - -[BenLeshHotandCold]: https://medium.com/@benlesh/hot-vs-cold-observables-f8094ed53339#:~:text=Observables%20are%20functions%20that%20tie,mechanism%20to%20remove%20that%20listener. \ No newline at end of file +[rxjsoverview]: https://rxjs.dev/guide/overview +[rxjsinaction]: https://freecontent.manning.com/reactive-fundamentals-thinking-in-streams/#:~:text=In%20RxJS%20and%20in%20reactive,it%20reaches%20your%20desired%20state. +[benleshhotandcold]: https://medium.com/@benlesh/hot-vs-cold-observables-f8094ed53339#:~:text=Observables%20are%20functions%20that%20tie,mechanism%20to%20remove%20that%20listener. diff --git a/Day020-rxjs-creation.md b/Day020-rxjs-creation.md index f8ac1d0..f641b5e 100644 --- a/Day020-rxjs-creation.md +++ b/Day020-rxjs-creation.md @@ -294,6 +294,10 @@ Với `defer()`, chúng ta đã có 3 giá trị khác nhau cho mỗi lần subs Mục tiêu của Day 21 là **RxJS Transformation Operators**. +## Youtube Video + +[![Day 20](https://img.youtube.com/vi/OWvK8ZB_Wrc/0.jpg)](https://youtu.be/OWvK8ZB_Wrc) + ## Author [Chau Tran](https://github.com/nartc) diff --git a/Day021-rxjs-transformation.md b/Day021-rxjs-transformation.md index a88aebf..65a1763 100644 --- a/Day021-rxjs-transformation.md +++ b/Day021-rxjs-transformation.md @@ -9,24 +9,17 @@ Một Pipeable Operator là một function nó nhận đầu vào là một Obse Cú pháp: ```ts -observableInstance.pipe( - operator1(), - operator2(), -) +observableInstance.pipe(operator1(), operator2()); ``` -Với cú pháp trên thì `observableInstance` có *pipe* bao nhiêu operator đi nữa thì nó vẫn không đổi, và cuối cùng chúng ta sẽ nhận lại một Observable nên để có thể sử dụng thì chúng ta cần gán lại, hoặc thực hiện subscribe ngay sau khi *pipe*: +Với cú pháp trên thì `observableInstance` có _pipe_ bao nhiêu operator đi nữa thì nó vẫn không đổi, và cuối cùng chúng ta sẽ nhận lại một Observable nên để có thể sử dụng thì chúng ta cần gán lại, hoặc thực hiện subscribe ngay sau khi _pipe_: ```ts -const returnObservable = observableInstance.pipe( - operator1(), - operator2(), -) +const returnObservable = observableInstance.pipe(operator1(), operator2()); ``` Nếu bạn dùng với RxJS version < 5.5 thì có thể các bạn sẽ thấy cú pháp sử dụng khác là prototype method chain, nhưng nếu bạn dùng từ version 5.5 trở lên thì nên dùng pipe operators, dựa theo một số giải thích ở đây: [pipeable operators](https://rxjs.dev/guide/v6/pipeable-operators) - Pipeable Operators có thể chia thành nhiều category khác nhau, trong ngày hôm nay chúng ta sẽ tìm hiểu về **Transformation Operators**. ## Transformation Operators @@ -35,18 +28,30 @@ Chắc hẳn các bạn đã quá quen với làm việc cùng Array trong JS, c ```ts const users = [ - {id: 'ddfe3653-1569-4f2f-b57f-bf9bae542662', username: 'tiepphan', firstname: 'tiep', lastname: 'phan'}, - {id: '34784716-019b-4868-86cd-02287e49c2d3', username: 'nartc', firstname: 'chau', lastname: 'tran'}, + { + id: 'ddfe3653-1569-4f2f-b57f-bf9bae542662', + username: 'tiepphan', + firstname: 'tiep', + lastname: 'phan', + }, + { + id: '34784716-019b-4868-86cd-02287e49c2d3', + username: 'nartc', + firstname: 'chau', + lastname: 'tran', + }, ]; -const usersVm = users.map(user => { +const usersVm = users.map((user) => { return { ...user, - fullname: `${user.firstname} ${user.lastname}` - } + fullname: `${user.firstname} ${user.lastname}`, + }; }); ``` + Kết quả có được sẽ có dạng như sau: + ```ts usersVm = [ { @@ -54,25 +59,24 @@ usersVm = [ username: 'tiepphan', firstname: 'tiep', lastname: 'phan', - fullname: 'tiep phan' + fullname: 'tiep phan', }, { id: '34784716-019b-4868-86cd-02287e49c2d3', username: 'nartc', firstname: 'chau', lastname: 'tran', - fullname: 'chau tran' - } -] + fullname: 'chau tran', + }, +]; ``` Như vậy qua một lần biến đổi, chúng ta sẽ có được dữ liệu như ý muốn. Vậy với Observable thì sao. Giả sử chúng ta đang có một hệ thống tracking xem những ai đăng nhập vào hệ thống. Do đó ở một số thời điểm sẽ có một/một vài người đăng nhập, và mỗi lần như thế hệ thống sẽ gửi cho chúng ta một event để biết. Bây giờ chúng ta cũng làm nhiệm vụ tương tự như `map` ở trên thì sao. - ```ts -import { Observable } from 'rxjs'; +import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; interface User { @@ -84,8 +88,18 @@ interface User { const source = new Observable((observer) => { const users = [ - {id: 'ddfe3653-1569-4f2f-b57f-bf9bae542662', username: 'tiepphan', firstname: 'tiep', lastname: 'phan'}, - {id: '34784716-019b-4868-86cd-02287e49c2d3', username: 'nartc', firstname: 'chau', lastname: 'tran'}, + { + id: 'ddfe3653-1569-4f2f-b57f-bf9bae542662', + username: 'tiepphan', + firstname: 'tiep', + lastname: 'phan', + }, + { + id: '34784716-019b-4868-86cd-02287e49c2d3', + username: 'nartc', + firstname: 'chau', + lastname: 'tran', + }, ]; setTimeout(() => { @@ -98,12 +112,11 @@ const source = new Observable((observer) => { }); const observer = { - next: value => console.log(value), - error: err => console.error(err), + next: (value) => console.log(value), + error: (err) => console.error(err), complete: () => console.log('completed'), }; source.subscribe(observer); - ``` Khi chạy chương trình bạn sẽ thấy rằng, sau 1 giây thì sẽ emit ra user đầu tiên, và sau đó 2 giây thì sẽ emit ra user thứ hai kèm theo complete signal. @@ -121,23 +134,25 @@ Cách đơn giản nhất là bạn sẽ vào hàm next để thực hiện tín ```ts import { map } from 'rxjs/operators'; -source.pipe( - map(user => { - return { - ...user, - fullname: `${user.firstname} ${user.lastname}` - }; - }) -).subscribe(observer); +source + .pipe( + map((user) => { + return { + ...user, + fullname: `${user.firstname} ${user.lastname}`, + }; + }) + ) + .subscribe(observer); ``` Hoặc giả sử yêu cầu của chúng ta giờ đây thay đổi, chỉ cần trả về id của user mỗi khi được emit. + ```ts -source.pipe( - map(user => user.id) -).subscribe(observer); +source.pipe(map((user) => user.id)).subscribe(observer); ``` -Cách dùng map này *khá giống* cách dùng map của array ở trên phải không??? + +Cách dùng map này _khá giống_ cách dùng map của array ở trên phải không??? ![RxJS map](assets/rxjs-map.png) @@ -146,13 +161,13 @@ Cách dùng map này *khá giống* cách dùng map của array ở trên phải `pluck(...properties: string[]): OperatorFunction` Đối với yêu cầu map ra một property trong một object như vừa rồi, bạn có thể sử dụng một cách khác là `pluck`: + ```ts import { pluck } from 'rxjs/operators'; -source.pipe( - pluck('id') -).subscribe(observer); +source.pipe(pluck('id')).subscribe(observer); ``` + ![RxJS pluck](assets/rxjs-pluck.png) ### mapTo @@ -174,12 +189,8 @@ const mouseover$ = fromEvent(element, 'mouseover'); const mouseleave$ = fromEvent(element, 'mouseleave'); const hover$ = merge( - mouseover$.pipe( - mapTo(true), - ), - mouseleave$.pipe( - mapTo(false), - ) + mouseover$.pipe(mapTo(true)), + mouseleave$.pipe(mapTo(false)) ); hover$.subscribe(observer); @@ -202,17 +213,28 @@ const button = document.querySelector('#add'); const click$ = fromEvent(button, 'click'); -click$.pipe( - scan((acc, curr) => acc + 1, 0) -).subscribe(observer); +click$.pipe(scan((acc, curr) => acc + 1, 0)).subscribe(observer); ``` + Count số bài đăng của những người dùng đăng nhập theo thời gian: ```ts const users$ = new Observable((observer) => { const users = [ - {id: 'ddfe3653-1569-4f2f-b57f-bf9bae542662', username: 'tiepphan', firstname: 'tiep', lastname: 'phan', postCount: 5}, - {id: '34784716-019b-4868-86cd-02287e49c2d3', username: 'nartc', firstname: 'chau', lastname: 'tran', postCount: 22}, + { + id: 'ddfe3653-1569-4f2f-b57f-bf9bae542662', + username: 'tiepphan', + firstname: 'tiep', + lastname: 'phan', + postCount: 5, + }, + { + id: '34784716-019b-4868-86cd-02287e49c2d3', + username: 'nartc', + firstname: 'chau', + lastname: 'tran', + postCount: 22, + }, ]; setTimeout(() => { @@ -224,10 +246,9 @@ const users$ = new Observable((observer) => { }, 3000); }); -users$.pipe( - scan((acc, curr) => acc + curr.postCount, 0) -).subscribe(observer); +users$.pipe(scan((acc, curr) => acc + curr.postCount, 0)).subscribe(observer); ``` + ![RxJS scan](assets/rxjs-scan.png) ### reduce @@ -237,9 +258,7 @@ users$.pipe( Operator này khá giống `scan` là nó sẽ reduce value overtime, nhưng nó sẽ đợi đến khi source complete rồi thì nó mới emit một giá trị cuối cùng và gửi đi `complete`. ```ts -users$.pipe( - reduce((acc, curr) => acc + curr.postCount, 0) -).subscribe(observer); +users$.pipe(reduce((acc, curr) => acc + curr.postCount, 0)).subscribe(observer); ``` ![RxJS reduce](assets/rxjs-reduce.png) @@ -251,18 +270,15 @@ users$.pipe( Giả sử bạn cần collect toàn bộ các value emit bởi stream rồi lưu trữ thành một array, sau đó đợi đến khi stream complete thì emit một array và complete. Lúc này bạn hoàn toàn có thể sử dụng `reduce`: ```ts -users$.pipe( - reduce((acc, curr) => [...acc, curr], []) -).subscribe(observer); +users$.pipe(reduce((acc, curr) => [...acc, curr], [])).subscribe(observer); ``` Nhưng có một cách viết khác ngắn gọn hơn đó là dùng `toArray`. ```ts -users$.pipe( - toArray() -).subscribe(observer); +users$.pipe(toArray()).subscribe(observer); ``` + ### buffer `buffer(closingNotifier: Observable): OperatorFunction` @@ -274,20 +290,15 @@ const interval$ = interval(1000); const click$ = fromEvent(document, 'click'); -const buffer$ = interval$.pipe( - buffer(click$) -); - +const buffer$ = interval$.pipe(buffer(click$)); -const subscribe = buffer$.subscribe( - val => console.log('Buffered Values: ', val) +const subscribe = buffer$.subscribe((val) => + console.log('Buffered Values: ', val) ); // output có dạng -"Buffered Values: " -[0, 1] -"Buffered Values: " -[2, 3, 4, 5, 6] +'Buffered Values: '[(0, 1)]; +'Buffered Values: '[(2, 3, 4, 5, 6)]; ``` ![RxJS buffer](assets/rxjs-buffer.png) @@ -317,6 +328,7 @@ const bufferTimeSub = bufferTime.subscribe( [4, 5] ... ``` + ![RxJS bufferTime](assets/rxjs-bufferTime.png) ## Summary @@ -330,6 +342,10 @@ Như vậy trong Day 21 chúng ta đã tìm hiểu cơ bản về một số **T Mục tiêu của Day 22 là **RxJS Filtering Operators**. +## Youtube Video + +[![Day 21](https://img.youtube.com/vi/AG97A7_NCLE/0.jpg)](https://youtu.be/AG97A7_NCLE) + ## Author [Tiep Phan](https://github.com/tieppt) diff --git a/Day022-rxjs-filtering.md b/Day022-rxjs-filtering.md index c666e50..7cb8056 100644 --- a/Day022-rxjs-filtering.md +++ b/Day022-rxjs-filtering.md @@ -377,6 +377,10 @@ Mục tiêu ngày 23 sẽ là **RxJS Combination Operators** - [RxJS Overview](https://rxjs.dev/guide/overview) - [LearnRxJS](https://www.learnrxjs.io/) +## Youtube Video + +[![Day 22](https://img.youtube.com/vi/KEBpdRL11Nw/0.jpg)](https://youtu.be/KEBpdRL11Nw) + ## Author [Chau Tran](https://github.com/nartc) diff --git a/Day023-rxjs-combination.md b/Day023-rxjs-combination.md index cb9098a..5200d6b 100644 --- a/Day023-rxjs-combination.md +++ b/Day023-rxjs-combination.md @@ -564,6 +564,10 @@ Mục tiêu ngày 24 sẽ là **RxJS Error handling/conditional Operators** - [RxJS Overview](https://rxjs.dev/guide/overview) - [LearnRxJS](https://www.learnrxjs.io/) +## Youtube Video + +[![Day 23](https://img.youtube.com/vi/qChj6nScvl0/0.jpg)](https://youtu.be/qChj6nScvl0) + ## Author [Chau Tran](https://github.com/nartc) diff --git a/Day024-rxjs-error-handling-conditional.md b/Day024-rxjs-error-handling-conditional.md index b6cb804..239c70e 100644 --- a/Day024-rxjs-error-handling-conditional.md +++ b/Day024-rxjs-error-handling-conditional.md @@ -27,14 +27,14 @@ const observer = { `catchError>(selector: (err: any, caught: Observable) => O): OperatorFunction>` ```ts -import { of } from "rxjs"; -import { map, catchError } from "rxjs/operators"; +import { of } from 'rxjs'; +import { map, catchError } from 'rxjs/operators'; const cached = [4, 5]; of(1, 2, 3, 4, 5) .pipe( - map(n => { + map((n) => { if (cached.includes(n)) { - throw new Error("Duplicated: " + n); + throw new Error('Duplicated: ' + n); } return n; }), @@ -43,10 +43,9 @@ of(1, 2, 3, 4, 5) .subscribe(observer); /** -* Output: -* --1--2--3--(next: Error)--| -*/ - + * Output: + * --1--2--3--(next: Error)--| + */ ``` Trong trường hợp trên nếu chúng ta không bắt error thì `observer.error` sẽ là nơi đón Error, nhưng vì chúng ta trả về là một `next: Error` nên error này đã được handle bởi `observer.next`. @@ -54,31 +53,24 @@ Trong trường hợp trên nếu chúng ta không bắt error thì `observer.er Một ví dụ trong ứng dụng là khi các bạn làm việc với `forkJoin` [Day 23](Day023-rxjs-combination.md), lúc này nếu một stream nào đó emit error thì toàn bộ stream sẽ bị văng ra error. Trong trường hợp các bạn muốn nó vẫn tiếp tục chạy hết và chúng ta sẽ tách Error ra ở pipe tiếp theo thì chỉ cần `catchError` lại như trên là được. ```ts -forkJoin([ - of(1), - of(2), - throwError(new Error('401')), -]).subscribe(observer); +forkJoin([of(1), of(2), throwError(new Error('401'))]).subscribe(observer); /** -* Output: -* --(x: Error 401)-- -*/ - + * Output: + * --(x: Error 401)-- + */ // with catchError forkJoin([ of(1), of(2), - throwError(new Error('401')).pipe( - catchError(err => of(err)) - ), + throwError(new Error('401')).pipe(catchError((err) => of(err))), ]).subscribe(observer); /** -* Output: -* --(next: [1, 2, Error 401])|-- -*/ + * Output: + * --(next: [1, 2, Error 401])|-- + */ ``` ![RxJS catchError](assets/rxjs-catchError.png) @@ -89,9 +81,9 @@ Nếu bạn muốn retry kèm theo giới hạn về số lần, chúng ta có t ```ts of(1, 2, 3, 4, 5) .pipe( - map(n => { + map((n) => { if (cached.includes(n)) { - throw new Error("Duplicated: " + n); + throw new Error('Duplicated: ' + n); } return n; }), @@ -101,9 +93,9 @@ of(1, 2, 3, 4, 5) .subscribe(observer); /** -* Output: -* --1--2--3--1--2--3--1| -*/ + * Output: + * --1--2--3--1--2--3--1| + */ ``` Ngoài ra, trong catchError bạn hoàn toàn có thể throw về một error để pipe phía sau có thể handle tiếp. @@ -122,9 +114,9 @@ Nó khá hữu ích khi bạn muốn retry HTTP request chẳng hạn. Lưu ý c const cached = [4, 5]; of(1, 2, 3, 4, 5) .pipe( - map(n => { + map((n) => { if (cached.includes(n)) { - throw new Error("Duplicated: " + n); + throw new Error('Duplicated: ' + n); } return n; }), @@ -133,9 +125,9 @@ of(1, 2, 3, 4, 5) .subscribe(observer); /** -* Output: -* --1--2--3--1--2--3--1--2--3--1--2--3--(x: Error) -*/ + * Output: + * --1--2--3--1--2--3--1--2--3--1--2--3--(x: Error) + */ ``` ![RxJS retry](assets/rxjs-retry.png) @@ -162,9 +154,9 @@ export function retryBackoff( defer(() => { let index = 0; return source.pipe( - retryWhen(errors => + retryWhen((errors) => errors.pipe( - concatMap(error => { + concatMap((error) => { const attempt = index++; return iif( () => attempt < maxRetries && shouldRetry(error), @@ -189,6 +181,7 @@ export function retryBackoff( ## RxJS Error Conditional Operators ### defaultIfEmpty/throwIfEmpty + `defaultIfEmpty(defaultValue: R = null): OperatorFunction` `throwIfEmpty(errorFactory: () => any = defaultErrorFactory): MonoTypeOperatorFunction` @@ -200,16 +193,17 @@ Giả sử, chúng ta cần làm yêu cầu nếu người dùng không click v ```ts import { fromEvent, timer } from 'rxjs'; import { throwIfEmpty, takeUntil } from 'rxjs/operators'; - + const click$ = fromEvent(document, 'click'); - -click$.pipe( - takeUntil(timer(1000)), - throwIfEmpty( - () => new Error('the document was not clicked within 1 second') - ), -) -.subscribe(observer); + +click$ + .pipe( + takeUntil(timer(1000)), + throwIfEmpty( + () => new Error('the document was not clicked within 1 second') + ) + ) + .subscribe(observer); ``` ![RxJS throwIfEmpty](assets/rxjs-throwIfEmpty.png) @@ -225,31 +219,32 @@ Opeator này sẽ trả về true nếu tất cả các value emit của source > Lưu ý nếu source không complete thì sẽ không có gì emit ra cả. ```ts -of(1, 2, 3, 4, 5, 6).pipe( - every(x => x < 5), -) -.subscribe(observer); +of(1, 2, 3, 4, 5, 6) + .pipe(every((x) => x < 5)) + .subscribe(observer); /** -* Output: -* ------false| -*/ + * Output: + * ------false| + */ ``` + ![RxJS every](assets/rxjs-every.png) Các method của Array trong JS, có cả `every` và `some`, nếu các bạn muốn có một operator giống some ở trong RxJS thì có thể dùng `first` kèm theo predicate function. Giống như trong phần [Router của Angular](https://github.com/angular/angular/blob/10.0.x/packages/router/src/operators/check_guards.ts#L74-L76) ```ts -of(1, 2, 3, 14, 5, 6).pipe( - first(x => x > 10, false), - map(v => Boolean(v)) -) -.subscribe(observer); +of(1, 2, 3, 14, 5, 6) + .pipe( + first((x) => x > 10, false), + map((v) => Boolean(v)) + ) + .subscribe(observer); /** -* Output: -* ------true| -*/ + * Output: + * ------true| + */ ``` ### iif @@ -266,31 +261,25 @@ Opeartor này cho phép chúng ta lựa chọn Observable tương ứng với h > If you have more complex logic that requires decision between more than two Observables, `defer` will probably be a better choice. Actually `iif` can be easily implemented with `defer` and exists only for convenience and readability reasons. - ```ts import { iif, of } from 'rxjs'; - + let subscribeToFirst; -const firstOrSecond = iif( - () => subscribeToFirst, - of('first'), - of('second'), -); - +const firstOrSecond = iif(() => subscribeToFirst, of('first'), of('second')); + subscribeToFirst = true; -firstOrSecond.subscribe(value => console.log(value)); - +firstOrSecond.subscribe((value) => console.log(value)); + // Logs: // "first" - + subscribeToFirst = false; -firstOrSecond.subscribe(value => console.log(value)); - +firstOrSecond.subscribe((value) => console.log(value)); + // Logs: // "second" ``` - ## Summary Như vậy ngày hôm nay chúng ta đã tăng cường thêm nội lực (💪) về RxJS qua một số operators: Error Handling và Conditional. Hẹn gặp lại các bạn vào ngày mai. @@ -303,9 +292,12 @@ Mục tiêu ngày 25 sẽ là **RxJS Higher-order Observable & Utility** - [LearnRxJS](https://www.learnrxjs.io/) - [rxmarbles](https://rxmarbles.com/) +## Youtube Video + +[![Day 24](https://img.youtube.com/vi/UnfiFpY5VtQ/0.jpg)](https://youtu.be/UnfiFpY5VtQ) + ## Author [Tiep Phan](https://github.com/tieppt) - `#100DaysOfCodeAngular` `#100DaysOfCode` `#AngularVietNam100DoC_Day24` diff --git a/Day025-rxjs-hoo-utility.md b/Day025-rxjs-hoo-utility.md index e9c98e7..430f78c 100644 --- a/Day025-rxjs-hoo-utility.md +++ b/Day025-rxjs-hoo-utility.md @@ -451,6 +451,10 @@ Mục tiêu của ngày 26 sẽ là **RxJS Subject và Multicasting** - [RxJS Overview](https://rxjs.dev/guide/overview) - [LearnRxJS](https://www.learnrxjs.io/) +## Youtube Video + +[![Day 25](https://img.youtube.com/vi/5SD2YIxMBBM/0.jpg)](https://youtu.be/5SD2YIxMBBM) + ## Author [Chau Tran](https://github.com/nartc) diff --git a/Day026-rxjs-subject-multicast.md b/Day026-rxjs-subject-multicast.md index a878f05..dfdea72 100644 --- a/Day026-rxjs-subject-multicast.md +++ b/Day026-rxjs-subject-multicast.md @@ -9,9 +9,7 @@ Như chúng ta đã biết, đối với các **Observable** thông thường, m Ví dụ, chúng ta có một observable như sau: ```ts -const observable = interval(500).pipe( - take(5) -); +const observable = interval(500).pipe(take(5)); const observerA = { next: (val) => console.log(`Observer A: ${val}`), @@ -36,9 +34,7 @@ Observer A complete Giả sử chúng ta subscribe thêm một observer mới sau một khoảng thời gian (ví dụ: 2s) ```ts -const observable = interval(500).pipe( - take(5) -); +const observable = interval(500).pipe(take(5)); const observerA = { next: (val) => console.log(`Observer A: ${val}`), @@ -136,15 +132,15 @@ const hybridObserver = { this.observers.push(observer); }, next(value) { - this.observers.forEach(observer => observer.next(value)); + this.observers.forEach((observer) => observer.next(value)); }, error(err) { - this.observers.forEach(observer => observer.error(err)); + this.observers.forEach((observer) => observer.error(err)); }, complete() { - this.observers.forEach(observer => observer.complete()); - } -} + this.observers.forEach((observer) => observer.complete()); + }, +}; hybridObserver.subscribe(observerA); @@ -181,8 +177,8 @@ Với phương pháp kể trên, chúng ta đã cơ bản chuyển đổi từ m - multicast: cũng là hai người (có thể nhiều hơn) vào xem video ở Youtube, nhưng video đó đang phát Live (theo dõi một show truyền hình, hay một trận bóng đá Live chẳng hạn). Lúc này Youtube sẽ phát video Live, và những người vào xem video đó sẽ có cùng một thời điểm của video đó (cùng thời điểm của trận đấu đang diễn ra chẳng hạn). - ## Subject + Do Subject vừa là một Observable (chúng ta có thể subscribe vào nó), vừa là một Observer (có các method để chúng ta tự control khi nào gửi notification). Nên nó khá hay được sử dụng trong ứng dụng, ví dụ để làm Event Bus chẳng hạn. Dưới đây là một ví dụ về type ahead: @@ -191,22 +187,24 @@ Dưới đây là một ví dụ về type ahead: @Component({ selector: 'my-app', templateUrl: './app.component.html', - styleUrls: [ './app.component.css' ] + styleUrls: ['./app.component.css'], }) -export class AppComponent implements OnInit { - +export class AppComponent implements OnInit { searchTerm$ = new Subject(); ngOnInit() { - this.searchTerm$.asObservable().pipe( - throttleTime(250, undefined, { - leading: true, - trailing: true, - }), - distinctUntilChanged(), - ).subscribe({ - next: value => console.log(value) - }); + this.searchTerm$ + .asObservable() + .pipe( + throttleTime(250, undefined, { + leading: true, + trailing: true, + }), + distinctUntilChanged() + ) + .subscribe({ + next: (value) => console.log(value), + }); } onInput(event: Event) { @@ -228,14 +226,14 @@ Một trong những vấn đề khi làm việc với Subject đó là tình hu const subject = new Subject(); subject.subscribe({ - next: v => console.log("observerA: " + v) + next: (v) => console.log('observerA: ' + v), }); subject.next(1); subject.next(2); subject.subscribe({ - next: v => console.log("observerB: " + v) + next: (v) => console.log('observerB: ' + v), }); subject.next(3); @@ -249,6 +247,7 @@ observerA: 3 observerB: 3 */ ``` + Để giải quyết vấn đề này, chúng ta có một trong các biến thể của **Subject** đó là **BehaviorSubject**, nó là biến thế có khái niệm về "the current value". **BehaviorSubject** lưu trữ lại giá trị mới emit gần nhất để khi một Observer mới subscribe vào, nó sẽ emit giá trị đó ngay lập tức cho Observer vừa rồi. > A variant of Subject that requires an initial value and emits its current value whenever it is subscribed to. [BehaviorSubject](https://rxjs.dev/api/index/class/BehaviorSubject) @@ -263,14 +262,14 @@ Lưu ý: BehaviorSubject yêu cầu phải có giá trị khởi tạo khi tạo const subject = new BehaviorSubject(0); // 0 is the initial value subject.subscribe({ - next: (v) => console.log('observerA: ' + v) + next: (v) => console.log('observerA: ' + v), }); subject.next(1); subject.next(2); subject.subscribe({ - next: (v) => console.log('observerB: ' + v) + next: (v) => console.log('observerB: ' + v), }); subject.next(3); @@ -288,6 +287,7 @@ observerB: 3 ``` ### ReplaySubject + Một ReplaySubject tương tự như một BehaviorSubject khi nó có thể gửi những dữ liệu trước đó cho Observer mới subscribe, nhưng nó có thể lưu giữ nhiều giá trị (có thể là toàn bộ giá trị của stream từ thời điểm ban đầu). Tham số đầu vào của ReplaySubject có thể là: @@ -301,7 +301,7 @@ Tham số đầu vào của ReplaySubject có thể là: const subject = new ReplaySubject(3); // buffer 3 values for new subscribers subject.subscribe({ - next: (v) => console.log('observerA: ' + v) + next: (v) => console.log('observerA: ' + v), }); subject.next(1); @@ -310,7 +310,7 @@ subject.next(3); subject.next(4); subject.subscribe({ - next: (v) => console.log('observerB: ' + v) + next: (v) => console.log('observerB: ' + v), }); subject.next(5); @@ -329,13 +329,14 @@ observerA: 5 observerB: 5 */ ``` + Hoặc kết hợp buffer với `windowTime`: ```ts const subject = new ReplaySubject(100, 500 /* windowTime */); subject.subscribe({ - next: (v) => console.log('observerA: ' + v) + next: (v) => console.log('observerA: ' + v), }); let i = 1; @@ -343,7 +344,7 @@ const id = setInterval(() => subject.next(i++), 200); setTimeout(() => { subject.subscribe({ - next: (v) => console.log('observerB: ' + v) + next: (v) => console.log('observerB: ' + v), }); }, 1000); @@ -385,7 +386,7 @@ AsyncSubject khá giống Promise đấy chứ. const subject = new AsyncSubject(); subject.subscribe({ - next: (v) => console.log('observerA: ' + v) + next: (v) => console.log('observerA: ' + v), }); subject.next(1); @@ -394,7 +395,7 @@ subject.next(3); subject.next(4); subject.subscribe({ - next: (v) => console.log('observerB: ' + v) + next: (v) => console.log('observerB: ' + v), }); subject.next(5); @@ -409,6 +410,7 @@ observerB: 5 ``` ### Subject Completion + Khi BehaviorSubject complete, thì các Observers subscribe vào sau đó sẽ chỉ nhận được complete signal. Khi ReplaySubject complete, thì các Observers subscribe vào sau đó sẽ được emit tất cả các giá trị lưu trữ trong buffer, sau đó mới thực thi complete của Observer. @@ -420,7 +422,7 @@ const subject = new BehaviorSubject(0); // 0 is the initial value subject.subscribe({ next: (v) => console.log('observerA: ' + v), - complete: () => console.log('observerA: done') + complete: () => console.log('observerA: done'), }); subject.next(1); @@ -428,7 +430,7 @@ subject.next(2); subject.subscribe({ next: (v) => console.log('observerB: ' + v), - complete: () => console.log('observerB: done') + complete: () => console.log('observerB: done'), }); subject.next(3); @@ -437,7 +439,7 @@ subject.complete(); subject.subscribe({ next: (v) => console.log('observerC: ' + v), - complete: () => console.log('observerC: done') + complete: () => console.log('observerC: done'), }); /** @@ -454,12 +456,13 @@ observerB: done observerC: done */ ``` + ```ts const subject = new ReplaySubject(3); subject.subscribe({ next: (v) => console.log('observerA: ' + v), - complete: () => console.log('observerA: done') + complete: () => console.log('observerA: done'), }); let i = 1; @@ -470,7 +473,7 @@ setTimeout(() => { clearInterval(id); subject.subscribe({ next: (v) => console.log('observerB: ' + v), - complete: () => console.log('observerB: done') + complete: () => console.log('observerB: done'), }); }, 1000); @@ -489,12 +492,13 @@ observerB: 5 observerB: done */ ``` + ```ts const subject = new AsyncSubject(); subject.subscribe({ next: (v) => console.log('observerA: ' + v), - complete: () => console.log('observerA: done') + complete: () => console.log('observerA: done'), }); subject.next(1); @@ -507,7 +511,7 @@ subject.complete(); subject.subscribe({ next: (v) => console.log('observerB: ' + v), - complete: () => console.log('observerB: done') + complete: () => console.log('observerB: done'), }); /** Output: @@ -520,12 +524,11 @@ observerB: done ``` ## Multicasting + Quay trở lại vấn đề ban đầu khi chúng ta mong muốn multicast. Chúng ta mong muốn cả hai observer đều chạy cùng một execution. ```ts -const observable = interval(500).pipe( - take(5) -); +const observable = interval(500).pipe(take(5)); const subject = new Subject(); @@ -608,7 +611,9 @@ Multicast nhận vào một `subjectOrSubjectFactory`, như trong ví dụ trên ```ts connectableObservable.connect(); ``` + Nó tương đương với việc subscribe internal Subject vào Observable như ban đầu: + ```ts observable.subscribe(subject); ``` @@ -687,6 +692,7 @@ setTimeout(() => { connectSub.unsubscribe(); }, 3000); ``` + Lúc này bạn chỉ cần `connectSub.unsubscribe()` là sẽ unsubscribe internal Subject do đó không cần chạy `sub.unsubscribe()` cũng được. #### refCount @@ -697,7 +703,7 @@ Việc phải connect và disconnect manually khá là low level. Do đó `Conne const subject = new Subject(); const connectableObservable = interval(500).pipe( - tap(x => console.log('log.info: ' + x)), + tap((x) => console.log('log.info: ' + x)), multicast(subject) ) as ConnectableObservable; @@ -738,7 +744,7 @@ Trong trường hợp Subject bị complete, nó sẽ không thể next thêm m ```ts const connectableObservable = interval(500).pipe( take(10), - tap(x => console.log('log.info: ' + x)), + tap((x) => console.log('log.info: ' + x)), multicast(new Subject()) ) as ConnectableObservable; @@ -766,7 +772,6 @@ setTimeout(() => { setTimeout(() => { const subA2 = sharedObservable.subscribe(observerA); }, 6000); - ``` Sau 5s `sharedObservable` đã emit complete nên khi ở thời điểm 6s chúng ta tiếp tục subscribe thì nó không thể nhận được các value được nữa. Lúc này để có thể tiếp tục trigger chúng ta cần phải tạo ra một Subject mới, đó chính là lúc bạn sẽ có thể dùng đến SubjectFactory. @@ -774,7 +779,7 @@ Sau 5s `sharedObservable` đã emit complete nên khi ở thời điểm 6s chú ```ts const connectableObservable = interval(500).pipe( take(10), - tap(x => console.log('log.info: ' + x)), + tap((x) => console.log('log.info: ' + x)), multicast(() => new Subject()) ) as ConnectableObservable; ``` @@ -810,8 +815,8 @@ Việc sử dụng `multicast(new Subject())` có thể được viết gọn l ```ts const connectableObservable = interval(500).pipe( - tap(x => console.log('log.info: ' + x)), - publish(), + tap((x) => console.log('log.info: ' + x)), + publish() ) as ConnectableObservable; const observerA = { @@ -843,6 +848,7 @@ setTimeout(() => { subB.unsubscribe(); // ref from 1 => 0 }, 5000); ``` + ![RxJS publish](assets/rxjs-publish.png) Ngoài ra, giống như Subject có các biến thể thì publish cũng có các biến thể tương ứng với một số loại Subject. @@ -861,8 +867,8 @@ Việc sử dụng `multicast(() => new Subject()) + refCount` khá phổ biến ```ts const sharedObservable = interval(500).pipe( - tap(x => console.log('log.info: ' + x)), - share(), + tap((x) => console.log('log.info: ' + x)), + share() ); const observerA = { @@ -939,19 +945,19 @@ export class JokeService { private cache$: Observable>; private reload$ = new Subject(); - constructor(private http: HttpClient) { } + constructor(private http: HttpClient) {} // This method is responsible for fetching the data. - // The first one who calls this function will initiate + // The first one who calls this function will initiate // the process of fetching data. get jokes() { if (!this.cache$) { // Set up timer that ticks every X milliseconds const timer$ = timer(0, REFRESH_INTERVAL); - + // For each timer tick make an http request to fetch new data - // We use shareReplay(X) to multicast the cache so that all - // subscribers share one underlying source and don't re-create + // We use shareReplay(X) to multicast the cache so that all + // subscribers share one underlying source and don't re-create // the source over and over again. We use takeUntil to complete // this stream when the user forces an update. this.cache$ = timer$.pipe( @@ -972,9 +978,9 @@ export class JokeService { // Helper method to actually fetch the jokes private requestJokes() { - return this.http.get(API_ENDPOINT).pipe( - map(response => response.value) - ); + return this.http + .get(API_ENDPOINT) + .pipe(map((response) => response.value)); } } ``` @@ -989,6 +995,10 @@ Mục tiêu của ngày 27 sẽ là **Giới thiệu Router trong Angular** https://stackblitz.com/edit/angular-ivy-wbyobn?file=src%2Fapp%2Fapp.component.ts +## Youtube Video + +[![Day 26](https://img.youtube.com/vi/8nWosjgcI5k/0.jpg)](https://youtu.be/8nWosjgcI5k) + ## References - [RxJS Overview](https://rxjs.dev/guide/overview) diff --git a/Day027-router.md b/Day027-router.md index d0c7308..9524915 100644 --- a/Day027-router.md +++ b/Day027-router.md @@ -84,17 +84,15 @@ Khi tạo ứng dụng mới theo step ở trên thì CLI đã mặc định t ```ts @NgModule({ - declarations: [ - AppComponent - ], + declarations: [AppComponent], imports: [ BrowserModule, - AppRoutingModule // Đây chính là AppRoutingModule được tạo tự động bằng CLI + AppRoutingModule, // Đây chính là AppRoutingModule được tạo tự động bằng CLI ], providers: [], - bootstrap: [AppComponent] + bootstrap: [AppComponent], }) -export class AppModule { } +export class AppModule {} ``` Có ba thành phần chính khi làm việc với Router @@ -114,6 +112,7 @@ export class AppRoutingModule {} Chú ý là `AppRoutingModule` vừa import và export `RouterModule`. Điều này có nghĩa là khi bạn import `AppRoutingModule` vào các module khác, bạn ko cần import lại `RouterModule` để sử dụng nữa vì nó đã được đc re-export từ `AppRoutingModule`. RouterModule mặc định sẽ provide hai method là `forRoot` và `forChild`. Hai method này đều dùng để config routes, tuy nhiên. + - `forRoot`, dc gọi một lần duy nhất khi bạn config route trong `AppRoutingModule`. forRoot cũng dùng để configures/initializes router. - `forChild`, dc gọi trong các module khác để config routes. @@ -128,11 +127,11 @@ Với yêu cầu của mình và hai component vừa được tạo, thì cấu ```ts const routes: Routes = [ { - path: "detail", + path: 'detail', component: ArticleDetailComponent, }, { - path: "", + path: '', component: ArticleListComponent, }, ]; @@ -178,22 +177,22 @@ Bây giờ mình sẽ render ra một danh sách các bài viết dựa vào dat ```ts const Articles: Article[] = [ { - id: "1", - slug: "bai-viet-1", - title: "Bai viet 1", - content: "Day la noi dung bai viet 1", - updateAt: "2020-07-06T13:26:31.785Z", + id: '1', + slug: 'bai-viet-1', + title: 'Bai viet 1', + content: 'Day la noi dung bai viet 1', + updateAt: '2020-07-06T13:26:31.785Z', }, { - id: "2", - slug: "bai-viet-2", - title: "Bai viet 2", - content: "Day la noi dung bai viet 2 nhe", - updateAt: "2020-07-15:00:00.000Z", + id: '2', + slug: 'bai-viet-2', + title: 'Bai viet 2', + content: 'Day la noi dung bai viet 2 nhe', + updateAt: '2020-07-15:00:00.000Z', }, ]; @Injectable({ - providedIn: "root", + providedIn: 'root', }) export class ArticleService { getArticles(): Observable { @@ -219,9 +218,7 @@ export class ArticleListComponent implements OnInit {
-
- {{ article.title }} -
+
{{ article.title }}

{{ article.content }}

@@ -242,11 +239,11 @@ Giờ phần việc còn lại là config để route detail có thể nhận dc ```ts const routes: Routes = [ { - path: ":slug", + path: ':slug', component: ArticleDetailComponent, }, { - path: "", + path: '', component: ArticleListComponent, }, ]; @@ -262,7 +259,7 @@ export class ArticleDetailComponent implements OnInit { constructor(private _route: ActivatedRoute, private _api: ArticleService) {} ngOnInit(): void { - let slug = this._route.snapshot.paramMap.get("slug"); + let slug = this._route.snapshot.paramMap.get('slug'); this.article$ = this._api.getArticleBySlug(slug); } } @@ -293,6 +290,10 @@ Mục tiêu của Day 28 là Feature Module. https://stackblitz.com/edit/angular-100-days-of-code-day-27-router-basic +## Youtube Video + +[![Day 27](https://img.youtube.com/vi/D0Tv5BaNTa8/0.jpg)](https://youtu.be/D0Tv5BaNTa8) + ## Author [Trung Vo](https://github.com/trungk18) diff --git a/Day028-router-feature-child-services.md b/Day028-router-feature-child-services.md index 383e1b7..9d85ce8 100644 --- a/Day028-router-feature-child-services.md +++ b/Day028-router-feature-child-services.md @@ -19,12 +19,10 @@ import { ArticleListComponent } from './article-list/article-list.component'; import { ArticleDetailComponent } from './article-detail/article-detail.component'; @NgModule({ - imports: [ - CommonModule, - ], - declarations: [ArticleListComponent, ArticleDetailComponent] + imports: [CommonModule], + declarations: [ArticleListComponent, ArticleDetailComponent], }) -export class ArticleModule { } +export class ArticleModule {} ``` Tiếp theo, chúng ta sẽ config RouterModule giống như đã từng làm với AppRoutingModule, nhưng thay vì gọi `forRoot` thì chúng ta sẽ gọi `forChild` (nguyên nhân tại sao thì các bạn quay trở lại Day 27). @@ -39,22 +37,19 @@ import { ArticleDetailComponent } from './article-detail/article-detail.componen const routes: Routes = [ { path: 'article', - component: ArticleListComponent + component: ArticleListComponent, }, { path: 'article/:slug', - component: ArticleDetailComponent - } + component: ArticleDetailComponent, + }, ]; @NgModule({ - imports: [ - CommonModule, - RouterModule.forChild(routes), - ], - declarations: [ArticleListComponent, ArticleDetailComponent] + imports: [CommonModule, RouterModule.forChild(routes)], + declarations: [ArticleListComponent, ArticleDetailComponent], }) -export class ArticleModule { } +export class ArticleModule {} ``` Như thế là chúng ta đã tạo xong Feature Module kèm theo Router, bây giờ chúng ta cần import nó vào AppModule để có thể sử dụng. @@ -67,12 +62,12 @@ import { ArticleModule } from './article/article.module'; BrowserModule, FormsModule, ArticleModule, // <== lưu ý thứ tự import này - AppRoutingModule + AppRoutingModule, ], - declarations: [ AppComponent ], - bootstrap: [ AppComponent ] + declarations: [AppComponent], + bootstrap: [AppComponent], }) -export class AppModule { } +export class AppModule {} ``` Bây giờ chúng ta có thể vào app với path `article` để xem Article List. @@ -80,6 +75,7 @@ Bây giờ chúng ta có thể vào app với path `article` để xem Article L ![App Feature Route](assets/day28-router-1.gif) ## Route config redirect + Để redirect người dùng khi vào một route nào đó đến một route khác, bạn có thể config như sau: ```ts @@ -87,8 +83,8 @@ const routes: Routes = [ { path: '', redirectTo: 'article', - pathMatch: 'full' - } + pathMatch: 'full', + }, ]; ``` @@ -104,6 +100,7 @@ Với config `pathMatch`, chúng ta sẽ có thể có 2 strategy là `full` và Code sample cho phần này có đầy đủ tại đây: https://stackblitz.com/edit/angular-100-days-of-code-day-28-router-feature-1?file=src%2Fapp%2Farticle%2Farticle.module.ts ## Routing Module + Có một kỹ thuật trong Angular Router đó là Routing Module, được dùng để tách phần routing ra thành một module riêng, và được sử dụng kèm với một NgModule thông thường. Trường hợp của AppRoutingModule là một ví dụ. Bạn hoàn toàn có thể áp dụng kỹ thuật này với các Feature Module như sau. @@ -112,23 +109,23 @@ Bạn hoàn toàn có thể áp dụng kỹ thuật này với các Feature Modu const routes: Routes = [ { path: 'article', - component: ArticleListComponent + component: ArticleListComponent, }, { path: 'article/:slug', - component: ArticleDetailComponent - } + component: ArticleDetailComponent, + }, ]; @NgModule({ imports: [ CommonModule, - RouterModule.forChild(routes) // <== config routing + RouterModule.forChild(routes), // <== config routing ], declarations: [], - exports: [RouterModule] // <== exports this NgModule + exports: [RouterModule], // <== exports this NgModule }) -export class ArticleRoutingModule { } +export class ArticleRoutingModule {} ``` Ở đây chúng ta sẽ config routing với `RouterModule.forChild(routes)`, sau đó chúng ta exports `RouterModule` ra ngoài để `ArticleModule` có thể sử dụng những directives/components mà `RouterModule` cung cấp mà không cần imports `RouterModule`. @@ -137,13 +134,10 @@ export class ArticleRoutingModule { } import { ArticleRoutingModule } from './article-routing.module'; @NgModule({ - imports: [ - CommonModule, - ArticleRoutingModule - ], - declarations: [ArticleListComponent, ArticleDetailComponent] + imports: [CommonModule, ArticleRoutingModule], + declarations: [ArticleListComponent, ArticleDetailComponent], }) -export class ArticleModule { } +export class ArticleModule {} ``` Full code: https://stackblitz.com/edit/angular-100-days-of-code-day-28-router-feature-2?file=src%2Fapp%2Farticle%2Farticle-routing.module.ts @@ -153,16 +147,17 @@ Full code: https://stackblitz.com/edit/angular-100-days-of-code-day-28-router-fe Nhìn vào config phía dưới đây các bạn sẽ thấy rằng có một phần prefix khá giống nhau. Vậy chúng ta có cấu trúc nào cho dạng parent-child hay không? **Cách 1** + ```ts const routes: Routes = [ { path: 'article', - component: ArticleListComponent + component: ArticleListComponent, }, { path: 'article/:slug', - component: ArticleDetailComponent - } + component: ArticleDetailComponent, + }, ]; ``` @@ -179,9 +174,9 @@ const routes: Routes = [ }, { path: ':slug', - component: ArticleDetailComponent - } - ] + component: ArticleDetailComponent, + }, + ], }, ]; ``` @@ -202,22 +197,23 @@ const routes: Routes = [ }, { path: ':slug', - component: ArticleDetailComponent - } - ] + component: ArticleDetailComponent, + }, + ], }, ]; ``` Full code: https://stackblitz.com/edit/angular-100-days-of-code-day-28-router-feature-3?file=src%2Fapp%2Farticle%2Farticle-routing.module.ts - ## ActivatedRoute Service. > Provides access to information about a route associated with a component that is loaded in an outlet. Use to traverse the RouterState tree and extract information from nodes. [ActivatedRoute Service](https://angular.io/api/router/ActivatedRoute) Service này cung cấp một số public API cho phép chúng ta biết được thông tin về route đang activated và component đã được loaded (activated). + ### Retrieve params + Ví dụ trong Day 27, chúng ta muốn lấy thông tin của `params`, lúc đó chúng ta đã inject service này vào `ArticleDetailComponent` như sau: ```ts @@ -241,8 +237,8 @@ export class ArticleDetailComponent implements OnInit { ngOnInit(): void { this.article$ = this._route.paramMap.pipe( - map(params => params.get('slug')), - switchMap(slug => this._api.getArticleBySlug(slug)) + map((params) => params.get('slug')), + switchMap((slug) => this._api.getArticleBySlug(slug)) ); } } @@ -277,13 +273,15 @@ Full code: - https://stackblitz.com/edit/angular-100-days-of-code-day-28-router-feature-6?file=src%2Fapp%2Farticle%2Farticle-detail%2Farticle-detail.component.ts ### Retrieve config: query params, route data, etc + Ngoài việc cung cấp API cho `params`, ActivatedRoute Service cũng cho phép bạn lấy/observe query params thông qua `queryParamMap`. Ví dụ bạn vào một URL là `tiepphan.com/page/2?sort=createdDate`, thì bạn có thể lấy về `sort` query qua `snapshot.queryParamMap.get('sort')` hoặc + ```ts -queryParamMap.subscribe(query => { +queryParamMap.subscribe((query) => { console.log(query.get('sort')); -}) +}); ``` Tương tự chúng ta có thể lấy về `route data`, các bạn có thể tìm hiểu kỹ hơn ở đây: https://angular.io/api/router/ActivatedRoute @@ -314,18 +312,23 @@ class SomeComponent { Ngoài ra bạn có thể observe Router Event để làm gì đó: ```ts -this.router.events.pipe( - filter(e => e instanceof NavigationEnd) -).subscribe(e => { - console.log(e); -}); +this.router.events + .pipe(filter((e) => e instanceof NavigationEnd)) + .subscribe((e) => { + console.log(e); + }); ``` ## Summary + Day 28 này cũng đã có nhiều concept về Angular Router, đây đều là những concept không thể thiếu khi bạn phát triển một ứng dụng thực tế, vì thế các bạn nên đọc thêm nhiều về code của nó trên github, cũng như documentation từ Angular.io Mục tiêu của ngày 29 sẽ là **Angular Router Lazy Loading** +## Youtube Video + +[![Day 28](https://img.youtube.com/vi/D0Tv5BaNTa8/0.jpg)](https://youtu.be/D0Tv5BaNTa8) + ## References Các bạn có thể đọc thêm ở các bài viết sau diff --git a/Day029-router-lazy-load.md b/Day029-router-lazy-load.md index cb63541..79b3d4e 100644 --- a/Day029-router-lazy-load.md +++ b/Day029-router-lazy-load.md @@ -25,15 +25,15 @@ export class ArticleModule {} ```ts const routes: Routes = [ { - path: "article", + path: 'article', component: ArticleComponent, children: [ { - path: "", + path: '', component: ArticleListComponent, }, { - path: ":slug", + path: ':slug', component: ArticleDetailComponent, }, ], @@ -87,11 +87,11 @@ Giờ chúng ta bắt tay vào code nhé. Mình cũng sẽ tạo ra một `Admin ```ts const routes: Routes = [ { - path: "admin", + path: 'admin', component: AdminComponent, children: [ { - path: "", + path: '', component: AdminArticleListComponent, }, ], @@ -188,14 +188,12 @@ Phần bundle của `main.js` giờ đã không còn AdminModule nữa rồi, ch Còn đây là phân tích của `admin-admin-module.js`. Phần import của Form và các component của AdminModule đã nằm trong bundle này. Quá xuất sắc. - ## Lazy load syntax `import('...')` syntax được khuyến cáo sử dụng từ Angular version 8. Ngoài cách dùng `import('...')` syntax, chúng ta có một cách dùng từ Angular version 7 trở xuống như sau: `loadChildren: './admin/admin.module#AdminModule'`. Đó là một magic string, để chỉ ra file path đến file mà chứa NgModule có kèm Router cần load. - ### Preloading Lazy Module Việc tách ra thành các lazy loading module rất có lợi cho lần tải trang của user. Với việc browser phải download file JS nhỏ hơn, nên thời gian chờ đợi để tương tác với website sẽ giảm. Tuy nhiên nhiều khi các lazy loading module có thể có size rất lớn. Dẫn đến việc khi user bấm vào một link, sẽ mất thời gian để tải. Ví dụ vẫn là lazy loading `AdminModule` ở trên nhưng giờ mình giả lập là mạng điện thoại, xem tình hình thế nào nhé. @@ -209,18 +207,18 @@ Có một số module mà mình biết rằng thường là khi user mở ứng Để enable preloading cho tất cả các lazy loaded modules, các bạn cần import `PreloadAllModules` từ package `@angular/router` và cấu hình nó ở trong AppRoutingModule, đoạn forRoot. ```ts -import { PreloadAllModules } from "@angular/router"; +import { PreloadAllModules } from '@angular/router'; const routes: Routes = [ { - path: "admin", + path: 'admin', loadChildren: () => - import("./admin/admin.module").then((m) => m.AdminModule), + import('./admin/admin.module').then((m) => m.AdminModule), }, { - path: "", - redirectTo: "article", - pathMatch: "full", + path: '', + redirectTo: 'article', + pathMatch: 'full', }, ]; @@ -253,6 +251,10 @@ Mục tiêu của Day 30 là **Angular Router - Guards and Resolvers** https://stackblitz.com/edit/angular-100-days-of-code-day-29-router-lazy +## Youtube Video + +[![Day 29](https://img.youtube.com/vi/D0Tv5BaNTa8/0.jpg)](https://youtu.be/D0Tv5BaNTa8) + ## References Các bạn có thể đọc thêm ở các bài viết sau diff --git a/Day030-router-guards-resolvers.md b/Day030-router-guards-resolvers.md index 1dc7ded..93d34ad 100644 --- a/Day030-router-guards-resolvers.md +++ b/Day030-router-guards-resolvers.md @@ -20,10 +20,10 @@ Khi nhận một URL, Angular Router sẽ thực hiện các hành động sau: 4. Activates all the needed components 5. Manages navigation - Để demo cho các nội dung tiếp theo, ứng dụng của chúng ta bao gồm các config routing như sau: **app-routing.module.ts** + ```ts const routes: Routes = [ { @@ -34,12 +34,13 @@ const routes: Routes = [ { path: '', redirectTo: 'article', - pathMatch: 'full' - } + pathMatch: 'full', + }, ]; ``` **article-routing.module.ts** + ```ts const routes: Routes = [ { @@ -52,14 +53,15 @@ const routes: Routes = [ }, { path: ':slug', - component: ArticleDetailComponent - } - ] + component: ArticleDetailComponent, + }, + ], }, ]; ``` **admin-routing.module.ts** + ```ts const routes: Routes = [ { @@ -76,6 +78,7 @@ const routes: Routes = [ ``` ### Navigation + Operation đầu tiên chính là **Navigation** hay **Applies redirects**. Đối với thẻ `a href` thông thường, hành vi mặc định của nó sẽ gửi request đến URL được chỉ định. Do đó Angular Router cung cấp một directive là `routerLink` để thay thế hành vi đó. Directive đó là cách declarative để thực hiện navigation. Ngoài ra chúng ta cũng có các cách imperative như dùng `Router.navigate()` hoặc `Router.navigateByUrl()`. @@ -104,7 +107,7 @@ Sau bước này Angular Router sẽ emit event **RoutesRecognized**. Đây chính là Operation mà chúng ta sẽ tìm hiểu chủ yếu trong ngày hôm nay. -Ở thời điểm này, chúng ta sẽ có *future router state*, Router sẽ kiểm tra xem nơi mà chúng ta sắp đến này có được phép hay không. +Ở thời điểm này, chúng ta sẽ có _future router state_, Router sẽ kiểm tra xem nơi mà chúng ta sắp đến này có được phép hay không. Chúng ta có thể apply nhiều Guards khác nhau, Router sẽ check nếu tất cả các Guards đều trả về `true` hoặc `Promise<>` hoặc `Observable<>` thì sẽ cấp cho giấy thông hành. Ngược lại, nếu bạn trả về các giá trị `false` hoặc `Promise<>` hoặc `Observable<>` hoặc một [`UrlTree`](https://angular.io/api/router/UrlTree) thì sẽ không cấp phép. @@ -123,6 +126,7 @@ Sau bước này, Router sẽ update thêm những phần của resolvers vào ` Sau khi đã chạy hết tất cả các resolvers đã được thiết lập từ trước, Router sẽ tiến hành activate component vào các router-outlet tương ứng trong config đã được set trước đó. ### Activating Components + Ở thời điểm này, Router sẽ activate các components đã được liên kết với các activated route. Đây sẽ là thời điểm khởi tạo mới hoặc reuse các components sau đó render chúng vào các `router-outlet` tương ứng. Default sẽ là primary outlet - tức là `` mà không có `name`. Các events tương ứng là **ActivationStart**, **ActivationEnd**, **ChildActivationStart**, **ChildActivationEnd**. @@ -137,7 +141,6 @@ Từ đây, Router lại tiếp tục observe, nếu có một yêu cầu nào ![Navigation Logs](assets/day30-router-03.png) - ## Route Guards Route Guards để giải quyết câu hỏi, liệu tôi có được phép redirect đến URL này hay không. @@ -150,25 +153,59 @@ Angular Router cung cấp một số guards như sau: ```ts interface CanActivate { - canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable | Promise | boolean | UrlTree + canActivate( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot + ): + | Observable + | Promise + | boolean + | UrlTree; } ``` + ```ts interface CanActivateChild { - canActivateChild(childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable | Promise | boolean | UrlTree + canActivateChild( + childRoute: ActivatedRouteSnapshot, + state: RouterStateSnapshot + ): + | Observable + | Promise + | boolean + | UrlTree; } ``` - Deactivate components: + ```ts interface CanDeactivate { - canDeactivate(component: T, currentRoute: ActivatedRouteSnapshot, currentState: RouterStateSnapshot, nextState?: RouterStateSnapshot): Observable | Promise | boolean | UrlTree + canDeactivate( + component: T, + currentRoute: ActivatedRouteSnapshot, + currentState: RouterStateSnapshot, + nextState?: RouterStateSnapshot + ): + | Observable + | Promise + | boolean + | UrlTree; } ``` + - Load children (lazy loading route): + ```ts interface CanLoad { - canLoad(route: Route, segments: UrlSegment[]): Observable | Promise | boolean | UrlTree + canLoad( + route: Route, + segments: UrlSegment[] + ): + | Observable + | Promise + | boolean + | UrlTree; } ``` @@ -188,13 +225,13 @@ const routes: Routes = [ }, { path: ':slug', - component: ArticleDetailComponent + component: ArticleDetailComponent, }, { path: ':slug/edit', - component: ArticleEditComponent - } - ] + component: ArticleEditComponent, + }, + ], }, ]; ``` @@ -207,16 +244,26 @@ Giờ đây bạn có thể tạo một service, sau đó kiểm tra các quyề ```ts import { Injectable } from '@angular/core'; -import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree } from '@angular/router'; +import { + CanActivate, + ActivatedRouteSnapshot, + RouterStateSnapshot, + UrlTree, +} from '@angular/router'; import { Observable } from 'rxjs'; @Injectable({ - providedIn: 'root' // you can change to any level if needed + providedIn: 'root', // you can change to any level if needed }) export class CanEditArticleGuard implements CanActivate { canActivate( next: ActivatedRouteSnapshot, - state: RouterStateSnapshot): Observable | Promise | boolean | UrlTree { + state: RouterStateSnapshot + ): + | Observable + | Promise + | boolean + | UrlTree { return true; // replace with actual logic } } @@ -234,9 +281,9 @@ const routes: Routes = [ { path: ':slug/edit', component: ArticleEditComponent, - canActivate: [CanEditArticleGuard] // <== this is an array, we can have multiple guards - } - ] + canActivate: [CanEditArticleGuard], // <== this is an array, we can have multiple guards + }, + ], }, ]; ``` @@ -247,14 +294,13 @@ Giả định rằng, chúng ta có một service để biết được user hi import { Injectable } from '@angular/core'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class UserService { currentUser = { - username: 'TiepPhan' + username: 'TiepPhan', }; - constructor() { } - + constructor() {} } ``` @@ -262,19 +308,32 @@ Guard của chúng ta sẽ có thể có logic như sau: ```ts @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class CanEditArticleGuard implements CanActivate { - constructor(private userService: UserService, private articleService: ArticleService) {} + constructor( + private userService: UserService, + private articleService: ArticleService + ) {} canActivate( next: ActivatedRouteSnapshot, - state: RouterStateSnapshot): Observable | Promise | boolean | UrlTree { - return this.articleService.getArticleBySlug(next.paramMap.get('slug')).pipe( - map(article => article.author === this.userService.currentUser.username) - ); + state: RouterStateSnapshot + ): + | Observable + | Promise + | boolean + | UrlTree { + return this.articleService + .getArticleBySlug(next.paramMap.get('slug')) + .pipe( + map( + (article) => article.author === this.userService.currentUser.username + ) + ); } } ``` + Thành quả có được là chúng ta không thể vào page edit của `bai-viet-2`, vì author không phải là user đang đăng nhập. ![Apply Guard](assets/day30-router-04.gif) @@ -282,6 +341,7 @@ Thành quả có được là chúng ta không thể vào page edit của `bai-v Tương tự như `CanActivate`, chúng ta có cách hoạt động của `CanActivateChild`, nhưng được apply cho các children của một route. ## Summary + Day 30 này có khá nhiều concept về Angular Router Navigation Lifecycle mà các bạn nên biết, ngoài ra chúng ta đã thực hành một guard thường được sử dụng là CanActivate, hi vọng sẽ không làm khó được các bạn. Mục tiêu của ngày 31 sẽ là **Angular Router - Guards and Resolvers Part 2** @@ -291,8 +351,11 @@ Mục tiêu của ngày 31 sẽ là **Angular Router - Guards and Resolvers Part - https://stackblitz.com/edit/angular-100-days-of-code-day-30?file=src%2Fapp%2Farticle%2Farticle.service.ts - https://stackblitz.com/edit/angular-100-days-of-code-day-30-01?file=src%2Fapp%2Farticle%2Farticle-routing.module.ts -## References +## Youtube Video + +[![Day 30](https://img.youtube.com/vi/STzxk1vOGqw/0.jpg)](https://youtu.be/STzxk1vOGqw) +## References Các bạn có thể đọc thêm ở các bài viết sau - https://angular.io/guide/router diff --git a/Day031-router-guards-resolvers-2.md b/Day031-router-guards-resolvers-2.md index 2ebaff2..d521c71 100644 --- a/Day031-router-guards-resolvers-2.md +++ b/Day031-router-guards-resolvers-2.md @@ -266,6 +266,10 @@ Mục tiêu của ngày 32 sẽ là **Angular Router - Guards and Resolvers Part - https://stackblitz.com/edit/angular-100-days-of-code-day-31-02?file=src%2Fapp%2Farticle%2Farticle-edit%2Farticle-edit.component.ts - https://stackblitz.com/edit/angular-100-days-of-code-day-31-03?file=src%2Fapp%2Fcan-load-admin.guard.ts +## Youtube Video + +[![Day 31](https://img.youtube.com/vi/VsUjev5-pTU/0.jpg)](https://youtu.be/VsUjev5-pTU) + ## References Các bạn có thể đọc thêm ở các bài viết sau diff --git a/Day032-router-guards-resolvers-3.md b/Day032-router-guards-resolvers-3.md index ae312af..856c13b 100644 --- a/Day032-router-guards-resolvers-3.md +++ b/Day032-router-guards-resolvers-3.md @@ -19,8 +19,8 @@ export class ArticleDetailComponent implements OnInit { ngOnInit(): void { this.article$ = this._route.paramMap.pipe( - map(params => params.get('slug')), - switchMap(slug => this._api.getArticleBySlug(slug)) + map((params) => params.get('slug')), + switchMap((slug) => this._api.getArticleBySlug(slug)) ); } } @@ -40,24 +40,28 @@ Với những trường hợp đơn giản như trên, chúng ta hoàn toàn có ```ts interface Resolve { - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable | Promise | T + resolve( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot + ): Observable | Promise | T; } ``` Resolver chỉ là một service, có implement interface `Resolve` ở trên. Các Resolvers sẽ được call hàm `resolve` và Router sẽ đợi đến khi nào data được `resolved` xong xuôi thì mới activate components. - Ví dụ, chúng ta có thể tạo một resolver để lấy về thông tin một Article như sau: ```ts @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ArticleResolver implements Resolve
{ + constructor(private articleService: ArticleService) {} - constructor(private articleService: ArticleService) { } - - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable
| Promise
| Article { + resolve( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot + ): Observable
| Promise
| Article { const slug = route.paramMap.get('slug'); return this.articleService.getArticleBySlug(slug); } @@ -80,10 +84,10 @@ const routes: Routes = [ path: ':slug', component: ArticleDetailComponent, resolve: { - article: ArticleResolver, // <== key: value (service or Dependency injection token) - } + article: ArticleResolver, // <== key: value (service or Dependency injection token) + }, }, - ] + ], }, ]; ``` @@ -100,9 +104,7 @@ export class ArticleDetailComponent implements OnInit { // map(params => params.get('slug')), // switchMap(slug => this._api.getArticleBySlug(slug)) // ); - this.article$ = this._route.data.pipe( - map(data => data.article) - ); + this.article$ = this._route.data.pipe(map((data) => data.article)); } } ``` @@ -113,7 +115,6 @@ Bây giờ chúng ta chạy thử sẽ thấy, thay vì hiển thị loading ở Full demo: https://stackblitz.com/edit/angular-100-days-of-code-day-32-02?file=src%2Fapp%2Farticle-resolver.service.ts - ## Lưu ý Đây là điều mà bạn cần phải nhớ khi quyết định dùng Resolvers: @@ -133,6 +134,7 @@ getArticleBySlug(slug: string): Observable
{ ); } ``` + Nếu bạn sử dụng trong component thông thường, chúng sẽ hiển thị rất bình thường, nhưng trong Resolvers thì chỉ khi nào Observable trả về bởi `getArticleBySlug` complete, lúc đó bạn mới navigate vào page được. Full demo: https://stackblitz.com/edit/angular-100-days-of-code-day-32-03?file=src%2Fapp%2Farticle%2Farticle.service.ts @@ -142,6 +144,7 @@ Do đó, đối với những stream trả về nhiều value, thì bạn không Quan điểm của mình, thì chúng ta chỉ nên dùng Resolver để lấy về một phần dữ liệu, sau đó component sẽ thực thi tiếp các connection khác. ## Summary + Day 32 này chúng ta đã biết thêm về Route Resolvers, chúng ta cũng đã tìm hiểu một số trade-off của việc dùng Resolvers, hi vọng các bạn sẽ có thể thấy nhiều điều hữu ích khác của nó khi dùng trong dự án. Mục tiêu của ngày 33 sẽ là **Angular Forms: Template-driven Forms** @@ -152,6 +155,10 @@ Mục tiêu của ngày 33 sẽ là **Angular Forms: Template-driven Forms** - https://stackblitz.com/edit/angular-100-days-of-code-day-32-02?file=src%2Fapp%2Farticle-resolver.service.ts - https://stackblitz.com/edit/angular-100-days-of-code-day-32-03?file=src%2Fapp%2Farticle%2Farticle.service.ts +## Youtube Video + +[![Day 32](https://img.youtube.com/vi/YAAv4f85s7A/0.jpg)](https://youtu.be/YAAv4f85s7A) + ## References Các bạn có thể đọc thêm ở các bài viết sau diff --git a/Day033-template-driven-forms.md b/Day033-template-driven-forms.md index 3e6dc6a..73377de 100644 --- a/Day033-template-driven-forms.md +++ b/Day033-template-driven-forms.md @@ -420,6 +420,9 @@ Mục tiêu của ngày 34 sẽ là **Angular Forms: Template-driven Forms Part - https://github.com/tieppt/100-doc-angular/tree/day33 - https://stackblitz.com/edit/100-days-of-angular-day-33?file=src%2Fapp%2Fsign-in%2Fsign-in.component.html +## Youtube Video + +[![Day 35](https://img.youtube.com/vi/0kbEVtO79Xw/0.jpg)](https://youtu.be/0kbEVtO79Xw) ## References Các bạn có thể đọc thêm ở các bài viết sau diff --git a/Day034-template-driven-forms-2.md b/Day034-template-driven-forms-2.md index acf4ef2..23abc30 100644 --- a/Day034-template-driven-forms-2.md +++ b/Day034-template-driven-forms-2.md @@ -220,6 +220,10 @@ Mục tiêu của ngày 35 sẽ là **Angular Forms: Reactive Forms** - https://github.com/tieppt/100-doc-angular/tree/day34 - https://stackblitz.com/edit/100-days-of-angular-day-34?file=src%2Fapp%2Fsign-in%2Fsign-in.component.html +## Youtube Video + +[![Day 34](https://img.youtube.com/vi/45VnmzfV_MI/0.jpg)](https://youtu.be/45VnmzfV_MI) + ## References Các bạn có thể đọc thêm ở các bài viết sau: diff --git a/Day035-reactive-forms.md b/Day035-reactive-forms.md index e38603a..3ab000e 100644 --- a/Day035-reactive-forms.md +++ b/Day035-reactive-forms.md @@ -42,11 +42,11 @@ ng g c sign-in-rf ```ts const routes: Routes = [ { - path: "sign-in", + path: 'sign-in', component: SignInComponent, }, { - path: "sign-in-rf", + path: 'sign-in-rf', component: SignInRfComponent, }, ]; @@ -89,7 +89,7 @@ Sau khi khởi chạy ứng dụng với lệnh `ng serve` bạn có thể visit Để sử dụng được **Reactive Forms** ở trong ứng dụng, chúng ta cần imports một `NgModule` là `ReactiveFormsModule` vào NgModule quản lý component của chúng ta - trong trường hợp của component hiện tại là `AppModule`. ```ts -import { ReactiveFormsModule } from "@angular/forms"; +import { ReactiveFormsModule } from '@angular/forms'; @NgModule({ declarations: [ @@ -118,8 +118,8 @@ Thông thường, mỗi một form sẽ bắt đầu bởi một **FormGroup**, ```ts export class SignInRfComponent implements OnInit { signInForm = new FormGroup({ - username: new FormControl(""), // <== default value - password: new FormControl(""), // <== default value + username: new FormControl(''), // <== default value + password: new FormControl(''), // <== default value rememberMe: new FormControl(false), // <== default value }); constructor() {} @@ -186,15 +186,15 @@ export class SignInRfComponent implements OnInit { ngOnInit(): void { this.signInForm = this.fb.group({ - username: "", - password: "", + username: '', + password: '', rememberMe: false, }); } } ``` -## Cập nhật giá trị cho Reactive Forms qua pathValue hoặc setValue +## Cập nhật giá trị cho Reactive Forms qua patchValue hoặc setValue Có 2 phương thức để cập nhật giá trị cho form control được mô tả bởi class `AbstractControl` là `setValue` và `patchValue`. Chúng là các abstract method, vậy nên các class dẫn xuất sẽ phải implement riêng cho chúng. @@ -251,6 +251,10 @@ Mục tiêu của ngày 36 sẽ là **Angular Forms: Reactive Forms Part 2** - https://github.com/tieppt/100-doc-angular/tree/day35 - https://stackblitz.com/edit/100-days-of-angular-day-35 +## Youtube Video + +[![Day 35](https://img.youtube.com/vi/oTwukyGa_qY/0.jpg)](https://youtu.be/oTwukyGa_qY) + ## References Các bạn có thể đọc thêm ở các bài viết sau diff --git a/Day037-form-async-validator.md b/Day037-form-async-validator.md index 785a414..4cffe7a 100644 --- a/Day037-form-async-validator.md +++ b/Day037-form-async-validator.md @@ -248,7 +248,7 @@ submitForm() { Có thể thấy là mình vẫn bấm được nút Register trong khi đang validate username 😂 Để fix lỗi này thì mình có tham khảo [một câu trả lời trên stackoveflow][stack]. -Ý tưởng là thay vì ngSubmit sẽ trigger thẳng hàm submit, thay vào đó mình sẽ tạo ra một Subject tên là `formSubmit$` và handle chỉ khi nào status của form chuyển thành `VALID` thì `formSubmit$` mới emit một value, từ đó mới call hàm `submitForm`. +Ý tưởng là thay vì `ngSubmit` sẽ trigger thẳng hàm `submit`, mình sẽ tạo ra một `Subject` tên là `formSubmit$` và handle chỉ khi nào status của form chuyển thành `VALID` thì `formSubmit$` mới emit một value, từ đó mới call hàm `submitForm`. ```ts this.formSubmit$ diff --git a/README.md b/README.md index cf53113..d270d21 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ Bọn mình chân thành cảm ơn sự ủng hộ và đóng góp của các b | [Day 32: Angular Router - Guards and Resolvers Part 3][day32] | | [Youtube][day32-video] | | [Day 33: Template-driven Forms Trong Angular][day33] | | [Youtube][day33-video] | | [Day 34: Template-driven Forms Trong Angular Part 2][day34] | | [Youtube][day34-video] | -| [Day 35: Reactive Forms Trong Angular][day35] | | | +| [Day 35: Reactive Forms Trong Angular][day35] | | [Youtube][day35-video] | | [Day 36: Reactive Forms Trong Angular Part 2][day36] | | | | [Day 37: Angular Form Async Validator][day37] | | | | [Day 38: Dynamic Component][day38] | | | @@ -194,6 +194,7 @@ https://www.youtube.com/playlist?list=PLVmX3uPQtp3vXOXUOl8gDIA_43_pmIdFN [day32-video]: https://youtu.be/YAAv4f85s7A [day33-video]: https://youtu.be/0kbEVtO79Xw [day34-video]: https://youtu.be/45VnmzfV_MI +[day35-video]: https://youtu.be/oTwukyGa_qY [tieppt]: https://github.com/tieppt [nartc]: https://github.com/nartc [trungk18]: https://github.com/trungk18