[Angular] Dùng providedIn root services in custom validators/operators

volamfan

Support
Moderator
Như các bạn cũng đã biết từ Angular 6, @Injectable có nhận thêm vào 1 option object có property là "providedIn". Với giá trị: "providedIn: root", những Injectable này sẽ được Angular làm những việc sau:
1. Store reference đến những injectable này
2. Khi những injectable này được inject (trong constructor) ở bất cứ đâu trong application, lần đầu tiên, thì Angular sẽ instantiate những injectable này 1 lần duy nhất, sau đó sẽ store lại instance này => singleton và lazy load.

Quay lại vấn đề: Custom Validators và Custom Operators.
Validators: Những validators thông thường sẽ là những functions và return value là Validator hoặc AsyncValidator. Phần lớn, các validators này thường được viết dưới dạng: Static Method hoặc Pure Function mà ít khi (hoặc không bao giờ) được viết dưới dạng Instance Method. Lí do là vì những hàm này sẽ được Angular Form thực thi với "this" context khác nên dùng Instance Method sẽ không bảo đảm cho lắm vì API Angular có thể thay đổi, bạn ko thể cứ dùng .bind() được. Ví dụ như sau:

JavaScript:
export class CustomValidators {
   static nonEmpty() {
      return control => {...}
   }
}

Là Static Method, nên sẽ ko phụ thuộc vào "this". Điêu tương tự áp dụng cho Operators

JavaScript:
export class CustomOperators {
   static logErrorAndRethrow() {
      return catchError(err => {
         console.error(err);
         // TODO: call appInsightService to log error to ApplicationInsight
         return throwError(err);
      });
   }
}

Trên đây là 2 ví dụ Custom Validators và Custom Operators (rxjs) và cả 2 đều là Static Method.
Vấn đề xảy ra ở đây là bây giờ bạn sẽ không có được access đến những services được inject vào. Ví dụ như dòng TODO ở trên, mình muốn gọi 1 service và service này sẽ log error này vào ApplicationInsight (dịch vụ log của Microsoft Azure), mình ko có dùng : this.appInsightService.log() được., mặc dù service này được providedIn: root

Solution (chỉ work với providedIn root): Đó là tạo 1 static class để keep track thằng root Injector.

JavaScript:
export class RootInjector {
  private static rootInjector: Injector;
  private static injectorReady = new BehaviorSubject(false);

  static setInjector(injector: Injector) {
    if (this.rootInjector) {
      return;
    }

    this.rootInjector = injector;
    this.injectorReady.next(true);
  }

  static get injectorReady$(): Observable<boolean> {
    return this.injectorReady.asObservable();
  }

  static get<T>(token: Type<T> | InjectionToken<T>, notFoundValue?: T, flags?: InjectFlags): T {
    try {
      return this.rootInjector.get(token);
    } catch (e) {
      console.error(
        `Error getting ${token} from RootInjector. This is likely due to RootInjector is undefined. Please check RootInjector.rootInjector value.`,
      );
      return null;
    }
  }
}


Trên đây là implementation detail của RootInjector. Vậy có bạn sẽ hỏi, vậy gọi setInjector khi nào?

Câu trả lời là ở "main.ts". Hàm "boostrapModule()" trả về 1 Promise với resolved value là ModuleRef. Trên ModuleRef sẽ có 1 property là injector, đây chính là root Injector của Angular, nói đúng hơn là Injector của AppModule (hoặc module dùng để bootstrap). Các bạn chỉ cần .then() rồi gọi hàm `RootInjector.setInjector(moduleRef.injector)` để track lại reference của injector này.

JavaScript:
platformBrowserDynamic()
  .bootstrapModule(AppModule)
  .then(ngModuleRef => {
RootInjector.setInjector(ngModuleRef.injector);
  })
  .catch(err => console.error(err));

Bây giờ thì các bạn có thể truy xuất được đến các services được providedIn: root trong static methods. Quay lại ví dụ "logAndRethrowError()"

JavaScript:
export class CustomOperators {
   static logErrorAndRethrow() {
      const appInsightService = RootInjector.get(AppInsightService);
      return catchError(err => {
         console.error(err);
         appInsightService.log(err);
         return throwError(err);
      });
   }
}

"RootInjector.get(AppInsightService) giống như:

JavaScript:
constructor(private readonly appInsightService: AppInsightService) {}


getter "injectorReady$" observable dùng khi bạn có flow dùng tới những operators có sử dụng RootInjector bên trong AppComponent hoặc Resolver/Guard/ErrorHandler/APP_INITIALIZER khi app bắt đầu chay. Lý do là những thằng trên đều có thể khiến cho AppModule được bootstrap sau => RootInjector chưa set được injector từ ModuleRef. Lưu ý nhé
 
Top