Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(template): implement rxLet template triggers #1374

Merged
merged 16 commits into from
Aug 28, 2022
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions apps/demos/src/app/features/template/rx-let/rx-let.menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ export const MENU_ITEMS = [
label: 'Template Bindings',
link: 'template-bindings',
},
{
label: 'Template Triggers',
link: 'template-triggers',
},
{
label: 'Preloading Techniques',
link: 'preloading-images',
Expand Down
7 changes: 7 additions & 0 deletions apps/demos/src/app/features/template/rx-let/rx-let.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ export const ROUTES: Routes = [
(m) => m.LetTemplateBindingModule
),
},
{
path: 'template-triggers',
loadChildren: () =>
import('./template-triggers/template-triggers.module').then(
(m) => m.TemplateTriggersModule
),
},
{
path: 'ng-if-hack',
loadChildren: () =>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<rxa-visualizer>
<div visualizerHeader>
<h2>rxLet Template Triggers</h2>
<div class="d-flex">
<rxa-strategy-select
(strategyChange)="strategy$.next($event)"
></rxa-strategy-select>
<div class="ml-2">
<div class="mb-2">
<strong>Stream</strong>
</div>
<rxa-value-provider
#valueProvider
[buttons]="true"
></rxa-value-provider>
</div>
<div class="ml-2">
<div class="mb-2"><strong>Trigger</strong></div>
<rxa-trigger-provider #triggerProvider></rxa-trigger-provider>
</div>
</div>
</div>
<div class="row w-100">
<div class="col-6">
<h2>Context Variables</h2>
<ng-container
*rxLet="
valueProvider.incremental$;
let state;
let c = $complete;
let s = $suspense;
let e = $error;
nextTrg: triggerProvider.next$;
completeTrg: triggerProvider.complete$;
errorTrg: triggerProvider.error$;
suspenseTrg: triggerProvider.suspense$;
"
>

<ng-container *ngIf="c; then: complete"></ng-container>
<ng-container *ngIf="s">
<ng-template [ngTemplateOutlet]="suspense"
[ngTemplateOutletContext]="{
$implicit: state
}"></ng-template>
</ng-container>
<ng-container *ngIf="e">
<ng-template [ngTemplateOutlet]="error"
[ngTemplateOutletContext]="{
$implicit: state,
$error: e
}"></ng-template>
</ng-container>

<div>{{ state }}</div>

</ng-container>
</div>
<div class="col-6">
<h2>Template Bindings</h2>
<ng-container
*rxLet="
valueProvider.incremental$;
let state;
rxError: error;
rxSuspense: suspense;
rxComplete: complete;
nextTrg: triggerProvider.next$;
completeTrg: triggerProvider.complete$;
errorTrg: triggerProvider.error$;
suspenseTrg: triggerProvider.suspense$;
"
>

<div>{{ state }}</div>

</ng-container>
</div>
<ng-template #complete>
<div>
<mat-icon class="complete-icon">thumb_up</mat-icon>
<h2>Completed!</h2>
</div>
</ng-template>
<ng-template #error let-value let-error="$error">
<div>
<mat-icon class="error-icon">thumb_down</mat-icon>
<h2>Error value: {{ error }}</h2>
<strong>Last valid value: {{ value }}</strong>
</div>
</ng-template>
<ng-template #suspense let-value>
<mat-progress-spinner
[diameter]="80"
[color]="'primary'"
[mode]="'indeterminate'"
></mat-progress-spinner>
<strong>Last valid value: {{ value }}</strong>
</ng-template>
</div>
</rxa-visualizer>
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { RxStrategyNames } from '@rx-angular/cdk/render-strategies';
import { ReplaySubject } from 'rxjs';

@Component({
selector: 'template-triggers',
templateUrl: 'template-triggers.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TemplateTriggersComponent implements OnInit {
strategy$ = new ReplaySubject<RxStrategyNames<any>>(1);

heroes$;

constructor() {}

ngOnInit() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { RouterModule } from '@angular/router';
import { LetModule } from '@rx-angular/template/let';
import { StrategySelectModule } from '../../../../shared/debug-helper/strategy-select/index';
import { TriggerProviderModule } from '../../../../shared/debug-helper/trigger-provider/trigger-provider.module';
import { ValueProvidersModule } from '../../../../shared/debug-helper/value-provider/index';
import { VisualizerModule } from '../../../../shared/debug-helper/visualizer/index';

import { TemplateTriggersComponent } from './template-triggers.component';
import { ROUTES } from './template-triggers.routes';

@NgModule({
imports: [
RouterModule.forChild(ROUTES),
VisualizerModule,
StrategySelectModule,
ValueProvidersModule,
TriggerProviderModule,
MatProgressSpinnerModule,
MatIconModule,
CommonModule,
LetModule,
],
exports: [],
declarations: [TemplateTriggersComponent],
providers: [],
})
export class TemplateTriggersModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Routes } from '@angular/router';
import { TemplateTriggersComponent } from './template-triggers.component';

export const ROUTES: Routes = [
{
path: '',
component: TemplateTriggersComponent,
},
];
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,58 @@ import { Subject } from 'rxjs';
@Component({
selector: 'rxa-trigger-provider',
exportAs: 'rxaTriggerProvider',
template: `
<button mat-raised-button (click)="suspense$.next(undefined)">
Suspense <mat-icon></mat-icon>
<rxa-zone-patched-icon class="mat-icon" [zoneState]="getZoneState()"></rxa-zone-patched-icon>
</button>
<button mat-raised-button [unpatch]="unpatched" (click)="error$.next(error)">
Error
<rxa-zone-patched-icon class="mat-icon" [zoneState]="getZoneState()"></rxa-zone-patched-icon>
</button>
<button mat-raised-button [unpatch]="unpatched" (click)="complete$.next(undefined)">
Complete
<rxa-zone-patched-icon class="mat-icon" [zoneState]="getZoneState()"></rxa-zone-patched-icon>
</button>
template: ` <button mat-raised-button (click)="next$.next(undefined)">
Next
<rxa-zone-patched-icon
class="mat-icon"
[zoneState]="getZoneState()"
></rxa-zone-patched-icon>
</button>
<button mat-raised-button (click)="suspense$.next(undefined)">
Suspense
<rxa-zone-patched-icon
class="mat-icon"
[zoneState]="getZoneState()"
></rxa-zone-patched-icon>
</button>
<button
mat-raised-button
[unpatch]="unpatched"
(click)="error$.next(error)"
>
Error
<rxa-zone-patched-icon
class="mat-icon"
[zoneState]="getZoneState()"
></rxa-zone-patched-icon>
</button>
<button
mat-raised-button
[unpatch]="unpatched"
(click)="complete$.next(undefined)"
>
Complete
<rxa-zone-patched-icon
class="mat-icon"
[zoneState]="getZoneState()"
></rxa-zone-patched-icon>
</button>
<ng-content></ng-content>`,
changeDetection: ChangeDetectionStrategy.OnPush
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TriggerProviderComponent {

suspense$ = new Subject<void>();
error$ = new Subject<any>();
complete$ = new Subject<any>();
next$ = new Subject<any>();

@Input()
unpatched;

@Input()
error = 'Custom error value';

constructor() {
}
constructor() {}

getZoneState() {
return this.unpatched?.length === 0 ? 'patched' : 'unpatched';
Expand Down
62 changes: 61 additions & 1 deletion libs/cdk/notifications/src/lib/create-template-notifier.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,67 @@
import { of } from 'rxjs';
import { BehaviorSubject, NEVER, of, throwError } from 'rxjs';
import { createTemplateNotifier } from './create-template-notifier';
import { RxNotification, RxNotificationKind } from './model';

describe(createTemplateNotifier.name, () => {
it('should start with suspense when there is no value', () => {
const templateNotifier = createTemplateNotifier();
let n: RxNotification<any>;
templateNotifier.next(NEVER);
templateNotifier.values$.subscribe({
next: (notification) => {
n = notification;
},
});

expect(n.kind).toBe(RxNotificationKind.Suspense);
});

it('should skip suspense when observable has value', () => {
const templateNotifier = createTemplateNotifier();
const spy = jest.fn();
let n: RxNotification<any>;
templateNotifier.next(new BehaviorSubject<number>(1));
templateNotifier.values$.subscribe({
next: (notification) => {
n = notification;
spy();
},
});

expect(spy).toHaveBeenCalledTimes(1);
expect(n.kind).toBe(RxNotificationKind.Next);
});

it('should treat undefined as suspense', () => {
const templateNotifier = createTemplateNotifier();
let n: RxNotification<any>;
templateNotifier.values$.subscribe({
next: (notification) => {
n = notification;
},
});
templateNotifier.next(of(null));
templateNotifier.next(undefined);
expect(n.kind).toBe(RxNotificationKind.Suspense);
expect(n.value).toBe(undefined);
});

it('should handle errors', () => {
const spy = { next: jest.fn(), error: jest.fn() };
const templateNotifier = createTemplateNotifier();
templateNotifier.next(throwError(() => new Error('')));
let n: RxNotification<any>;
templateNotifier.values$.subscribe({
next: (notification) => {
n = notification;
},
error: spy.error,
});

expect(n.kind).toBe(RxNotificationKind.Error);
expect(spy.error).not.toBeCalled();
});

it('should handle `null` values', () => {
const spy = { next: jest.fn(), error: jest.fn() };
const templateNotifier = createTemplateNotifier();
Expand Down
Loading