diff --git a/README.md b/README.md index 9ab133a..a13482b 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,8 @@ import { getObservableLifecycle } from 'ngx-observable-lifecycle'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class ChildComponent { - @Input() input: number | undefined | null; + @Input() input1: number | undefined | null; + @Input() input2: string | undefined | null; constructor() { const { @@ -106,9 +107,12 @@ export class ChildComponent { ngAfterViewInit, ngAfterViewChecked, ngOnDestroy, - } = getObservableLifecycle(this); + } = + // specifying the generics is only needed if you intend to + // use the `ngOnChanges` observable, this way you'll have + // typed input values instead of just a `SimpleChange` + getObservableLifecycle(this); - ngOnChanges.subscribe(() => console.count('onChanges')); ngOnInit.subscribe(() => console.count('onInit')); ngDoCheck.subscribe(() => console.count('doCheck')); ngAfterContentInit.subscribe(() => console.count('afterContentInit')); @@ -116,6 +120,19 @@ export class ChildComponent { ngAfterViewInit.subscribe(() => console.count('afterViewInit')); ngAfterViewChecked.subscribe(() => console.count('afterViewChecked')); ngOnDestroy.subscribe(() => console.count('onDestroy')); + + ngOnChanges.subscribe(changes => { + console.count('onChanges'); + + // do note that we have a type safe object here for `changes` + // with the inputs from our component and their associated values typed accordingly + + changes.input1?.currentValue; // `number | null | undefined` + changes.input1?.previousValue; // `number | null | undefined` + + changes.input2?.currentValue; // `string | null | undefined` + changes.input2?.previousValue; // `string | null | undefined` + }); } } diff --git a/projects/ngx-observable-lifecycle/src/lib/ngx-observable-lifecycle.ts b/projects/ngx-observable-lifecycle/src/lib/ngx-observable-lifecycle.ts index d6a57c1..9f27b0b 100644 --- a/projects/ngx-observable-lifecycle/src/lib/ngx-observable-lifecycle.ts +++ b/projects/ngx-observable-lifecycle/src/lib/ngx-observable-lifecycle.ts @@ -26,24 +26,47 @@ export type LifecycleHookKey = keyof AllHooks; type AllHookOptions = Record; type DecorateHookOptions = Partial; +export interface TypedSimpleChange { + previousValue: Data; + currentValue: Data; + firstChange: boolean; +} + +/** + * FIRST POINT: + * the key is made optional because an ngOnChanges will only give keys of inputs that have changed + * SECOND POINT: + * the value is associated with `| null` as if an input value is defined but actually retrieved with + * an `async` pipe, we'll initially get a `null` value + * + * For both point, feel free to check the following stackblitz that demo this + * https://stackblitz.com/edit/stackblitz-starters-s5uphw?file=src%2Fmain.ts + */ +export type TypedSimpleChanges = { + [Key in Keys]?: TypedSimpleChange | null; +}; + // none of the hooks have arguments, EXCEPT ngOnChanges which we need to handle differently -export type DecoratedHooks = Record, Observable> & { - ngOnChanges: Observable[0]>; +export type DecoratedHooks = Record< + Exclude, + Observable +> & { + ngOnChanges: Observable>; }; export type DecoratedHooksSub = { [k in keyof DecoratedHooks]: DecoratedHooks[k] extends Observable ? Subject : never; }; -type PatchedComponentInstance = Pick & { - [hookSubject]: Pick; +type PatchedComponentInstance = Pick & { + [hookSubject]: Pick; constructor: { prototype: { - [hooksPatched]: Pick; + [hooksPatched]: Pick; }; }; }; -function getSubjectForHook(componentInstance: PatchedComponentInstance, hook: LifecycleHookKey): Subject { +function getSubjectForHook(componentInstance: PatchedComponentInstance, hook: LifecycleHookKey): Subject { if (!componentInstance[hookSubject]) { componentInstance[hookSubject] = {}; } @@ -87,10 +110,12 @@ function getSubjectForHook(componentInstance: PatchedComponentInstance, hoo /** * Library authors should use this to create their own lifecycle-aware functionality */ -export function getObservableLifecycle(classInstance: any): DecoratedHooks { - return new Proxy({} as DecoratedHooks, { - get(target: DecoratedHooks, p: LifecycleHookKey): Observable { - return getSubjectForHook(classInstance, p).asObservable(); +export function getObservableLifecycle( + classInstance: Component, +): DecoratedHooks { + return new Proxy({} as DecoratedHooks, { + get(target: DecoratedHooks, p: LifecycleHookKey): Observable { + return getSubjectForHook(classInstance as unknown as PatchedComponentInstance, p).asObservable(); }, }); } diff --git a/src/app/child/child.component.ts b/src/app/child/child.component.ts index 49a1934..87ccecd 100644 --- a/src/app/child/child.component.ts +++ b/src/app/child/child.component.ts @@ -7,7 +7,8 @@ import { getObservableLifecycle } from 'ngx-observable-lifecycle'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class ChildComponent { - @Input() input: number | undefined | null; + @Input() input1: number | undefined | null; + @Input() input2: string | undefined | null; constructor() { const { @@ -19,9 +20,12 @@ export class ChildComponent { ngAfterViewInit, ngAfterViewChecked, ngOnDestroy, - } = getObservableLifecycle(this); + } = + // specifying the generics is only needed if you intend to + // use the `ngOnChanges` observable, this way you'll have + // typed input values instead of just a `SimpleChange` + getObservableLifecycle(this); - ngOnChanges.subscribe(() => console.count('onChanges')); ngOnInit.subscribe(() => console.count('onInit')); ngDoCheck.subscribe(() => console.count('doCheck')); ngAfterContentInit.subscribe(() => console.count('afterContentInit')); @@ -29,5 +33,18 @@ export class ChildComponent { ngAfterViewInit.subscribe(() => console.count('afterViewInit')); ngAfterViewChecked.subscribe(() => console.count('afterViewChecked')); ngOnDestroy.subscribe(() => console.count('onDestroy')); + + ngOnChanges.subscribe(changes => { + console.count('onChanges'); + + // do note that we have a type safe object here for `changes` + // with the inputs from our component and their associated values typed accordingly + + changes.input1?.currentValue; // `number | null | undefined` + changes.input1?.previousValue; // `number | null | undefined` + + changes.input2?.currentValue; // `string | null | undefined` + changes.input2?.previousValue; // `string | null | undefined` + }); } }