Skip to content

Instantly share code, notes, and snippets.

@mynameislau
Last active January 24, 2022 14:47
Show Gist options
  • Select an option

  • Save mynameislau/2388064a46a3a86bd3bfcc080322ed18 to your computer and use it in GitHub Desktop.

Select an option

Save mynameislau/2388064a46a3a86bd3bfcc080322ed18 to your computer and use it in GitHub Desktop.
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 {}
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 {}
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