Last active
January 24, 2022 14:47
-
-
Save mynameislau/2388064a46a3a86bd3bfcc080322ed18 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { | |
| Component, | |
| ChangeDetectionStrategy, | |
| NgModule, | |
| Input, | |
| } from '@angular/core'; | |
| import { Observable } from 'rxjs'; | |
| import { WithObservableModule } from '.'; | |
| @Component({ | |
| selector: 'tt-dummy', | |
| changeDetection: ChangeDetectionStrategy.OnPush, | |
| template: ` | |
| <div> | |
| <ng-template let-error #pendingState> | |
| <div>pending...</div> | |
| </ng-template> | |
| <ng-template let-error #errorState> | |
| <div>{{ error }} error</div> | |
| </ng-template> | |
| <!-- prettier-ignore --> | |
| <div *withObservable="let value from obs$; onError errorState; onPending pendingState"> | |
| Value: {{value}} | |
| </div> | |
| </div> | |
| `, | |
| }) | |
| export class WithObservableExampleComponent { | |
| @Input() obs$!: Observable<string>; | |
| } | |
| @NgModule({ | |
| imports: [WithObservableModule], | |
| declarations: [WithObservableExampleComponent], | |
| exports: [WithObservableExampleComponent], | |
| }) | |
| export class WithObservableExampleModule {} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { CommonModule } from '@angular/common'; | |
| import { | |
| Directive, | |
| EmbeddedViewRef, | |
| Input, | |
| NgModule, | |
| OnDestroy, | |
| OnInit, | |
| TemplateRef, | |
| ViewContainerRef, | |
| } from '@angular/core'; | |
| import { Observable, Subscription } from 'rxjs'; | |
| type NoContext = { $implicit: undefined }; | |
| type ErrorContext = { $implicit: unknown }; | |
| type Context<T> = { $implicit: T }; | |
| @Directive({ | |
| // eslint-disable-next-line @angular-eslint/directive-selector | |
| selector: '[withObservable][withObservableFrom]', | |
| }) | |
| export class WithObservableFromDirective<T> implements OnInit, OnDestroy { | |
| subscription: Subscription | null = null; | |
| initiated = false; | |
| observable: Observable<T> | null = null; | |
| context: { $implicit: T } | null = null; | |
| _fromViewRef: EmbeddedViewRef<Context<T>> | null = null; | |
| _errorViewRef: EmbeddedViewRef<ErrorContext> | null = null; | |
| _pendingViewRef: EmbeddedViewRef<NoContext> | null = null; | |
| @Input() set withObservableFrom(obs$: Observable<T>) { | |
| if (obs$ === this.observable) { | |
| return; | |
| } | |
| this.observable = obs$; | |
| if (this.initiated) { | |
| this.setupObservable(); | |
| } | |
| } | |
| @Input() withObservableOnError: TemplateRef<{ $implicit: unknown }> | null = | |
| null; | |
| @Input() withObservableOnPending: TemplateRef<{ | |
| $implicit: undefined; | |
| }> | null = null; | |
| constructor( | |
| private templateRef: TemplateRef<Context<T>>, | |
| private viewContainer: ViewContainerRef, | |
| ) {} | |
| ngOnInit(): void { | |
| this.initiated = true; | |
| this.setupObservable(); | |
| } | |
| setupObservable(): void { | |
| if (!this.observable) { | |
| return; | |
| } | |
| this.subscription?.unsubscribe(); | |
| if (this.withObservableOnPending) { | |
| if (!this._pendingViewRef) { | |
| this.viewContainer.clear(); | |
| this._pendingViewRef = this.viewContainer.createEmbeddedView( | |
| this.withObservableOnPending, | |
| { | |
| $implicit: undefined, | |
| }, | |
| ); | |
| } | |
| } | |
| this.subscription = this.observable.subscribe({ | |
| next: data => { | |
| if (!this.context) { | |
| this.context = { $implicit: data }; | |
| } else { | |
| this.context.$implicit = data; | |
| } | |
| if (!this._fromViewRef) { | |
| this._pendingViewRef = null; | |
| this._errorViewRef = null; | |
| this.viewContainer.clear(); | |
| this._fromViewRef = this.viewContainer.createEmbeddedView<Context<T>>( | |
| this.templateRef, | |
| this.context, | |
| ); | |
| this._fromViewRef.markForCheck(); | |
| } | |
| }, | |
| error: (err: unknown) => { | |
| if (!this._errorViewRef) { | |
| if (this.withObservableOnError) { | |
| this._pendingViewRef = null; | |
| this._errorViewRef = null; | |
| this.viewContainer.clear(); | |
| const viewRef = this.viewContainer.createEmbeddedView( | |
| this.withObservableOnError, | |
| { | |
| $implicit: err, | |
| }, | |
| ); | |
| viewRef.markForCheck(); | |
| } | |
| } | |
| }, | |
| }); | |
| } | |
| ngOnDestroy(): void { | |
| this.subscription?.unsubscribe(); | |
| } | |
| static ngTemplateContextGuard<T>( | |
| _dir: WithObservableFromDirective<T>, | |
| _ctx: unknown, | |
| ): _ctx is { $implicit: T; error: unknown | undefined } { | |
| return true; | |
| } | |
| } | |
| @NgModule({ | |
| imports: [CommonModule], | |
| declarations: [WithObservableFromDirective], | |
| exports: [WithObservableFromDirective], | |
| }) | |
| export class WithObservableModule {} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { Meta, Story } from '@storybook/angular'; | |
| import { time } from 'console'; | |
| import { Observable, of, timer } from 'rxjs'; | |
| import { delay, tap } from 'rxjs/operators'; | |
| import { WithObservableExampleModule } from './with-observable-example'; | |
| const meta: Meta = { | |
| title: 'Core/with-observable', | |
| }; | |
| export default meta; | |
| type Args = { obs$: Observable<unknown> }; | |
| const Template: Story<Args> = args => ({ | |
| props: { ...args }, | |
| template: ` | |
| <tt-dummy [obs$]="obs$"></tt-dummy> | |
| `, | |
| moduleMetadata: { | |
| imports: [WithObservableExampleModule], | |
| }, | |
| }); | |
| export const Default = Template.bind({}); | |
| Default.args = { | |
| obs$: of('coucou').pipe(delay(2000)), | |
| }; | |
| export const Erroring = Template.bind({}); | |
| Erroring.args = { | |
| obs$:timer(2000).pipe( | |
| tap(() => { | |
| throw new Error('oups'); | |
| }), | |
| ), | |
| }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment