Skip to content

Commit 1634bba

Browse files
authored
Merge pull request rx-angular#1374 from rx-angular/template-triggers
feat(template): implement rxLet template triggers
2 parents 234c6ee + a1be93b commit 1634bba

File tree

16 files changed

+962
-414
lines changed

16 files changed

+962
-414
lines changed

apps/demos/src/app/features/template/rx-let/rx-let.menu.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ export const MENU_ITEMS = [
2727
label: 'Template Bindings',
2828
link: 'template-bindings',
2929
},
30+
{
31+
label: 'Template Triggers',
32+
link: 'template-triggers',
33+
},
3034
{
3135
label: 'Preloading Techniques',
3236
link: 'preloading-images',

apps/demos/src/app/features/template/rx-let/rx-let.routes.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,13 @@ export const ROUTES: Routes = [
4343
(m) => m.LetTemplateBindingModule
4444
),
4545
},
46+
{
47+
path: 'template-triggers',
48+
loadChildren: () =>
49+
import('./template-triggers/template-triggers.module').then(
50+
(m) => m.TemplateTriggersModule
51+
),
52+
},
4653
{
4754
path: 'ng-if-hack',
4855
loadChildren: () =>
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
<rxa-visualizer>
2+
<div visualizerHeader>
3+
<h2>rxLet Template Triggers</h2>
4+
<div class="d-flex">
5+
<rxa-strategy-select
6+
(strategyChange)="strategy$.next($event)"
7+
></rxa-strategy-select>
8+
<div class="ml-2">
9+
<div class="mb-2">
10+
<strong>Stream</strong>
11+
</div>
12+
<rxa-value-provider
13+
#valueProvider
14+
[buttons]="true"
15+
></rxa-value-provider>
16+
</div>
17+
<div class="ml-2">
18+
<div class="mb-2"><strong>Trigger</strong></div>
19+
<rxa-trigger-provider #triggerProvider></rxa-trigger-provider>
20+
</div>
21+
</div>
22+
</div>
23+
<div class="row w-100">
24+
<div class="col-6">
25+
<h2>Context Variables</h2>
26+
<ng-container
27+
*rxLet="
28+
valueProvider.incremental$;
29+
let state;
30+
let c = $complete;
31+
let s = $suspense;
32+
let e = $error;
33+
nextTrg: triggerProvider.next$;
34+
completeTrg: triggerProvider.complete$;
35+
errorTrg: triggerProvider.error$;
36+
suspenseTrg: triggerProvider.suspense$;
37+
"
38+
>
39+
40+
<ng-container *ngIf="c; then: complete"></ng-container>
41+
<ng-container *ngIf="s">
42+
<ng-template [ngTemplateOutlet]="suspense"
43+
[ngTemplateOutletContext]="{
44+
$implicit: state
45+
}"></ng-template>
46+
</ng-container>
47+
<ng-container *ngIf="e">
48+
<ng-template [ngTemplateOutlet]="error"
49+
[ngTemplateOutletContext]="{
50+
$implicit: state,
51+
$error: e
52+
}"></ng-template>
53+
</ng-container>
54+
55+
<div>{{ state }}</div>
56+
57+
</ng-container>
58+
</div>
59+
<div class="col-6">
60+
<h2>Template Bindings</h2>
61+
<ng-container
62+
*rxLet="
63+
valueProvider.incremental$;
64+
let state;
65+
rxError: error;
66+
rxSuspense: suspense;
67+
rxComplete: complete;
68+
nextTrg: triggerProvider.next$;
69+
completeTrg: triggerProvider.complete$;
70+
errorTrg: triggerProvider.error$;
71+
suspenseTrg: triggerProvider.suspense$;
72+
"
73+
>
74+
75+
<div>{{ state }}</div>
76+
77+
</ng-container>
78+
</div>
79+
<ng-template #complete>
80+
<div>
81+
<mat-icon class="complete-icon">thumb_up</mat-icon>
82+
<h2>Completed!</h2>
83+
</div>
84+
</ng-template>
85+
<ng-template #error let-value let-error="$error">
86+
<div>
87+
<mat-icon class="error-icon">thumb_down</mat-icon>
88+
<h2>Error value: {{ error }}</h2>
89+
<strong>Last valid value: {{ value }}</strong>
90+
</div>
91+
</ng-template>
92+
<ng-template #suspense let-value>
93+
<mat-progress-spinner
94+
[diameter]="80"
95+
[color]="'primary'"
96+
[mode]="'indeterminate'"
97+
></mat-progress-spinner>
98+
<strong>Last valid value: {{ value }}</strong>
99+
</ng-template>
100+
</div>
101+
</rxa-visualizer>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
2+
import { RxStrategyNames } from '@rx-angular/cdk/render-strategies';
3+
import { ReplaySubject } from 'rxjs';
4+
5+
@Component({
6+
selector: 'template-triggers',
7+
templateUrl: 'template-triggers.component.html',
8+
changeDetection: ChangeDetectionStrategy.OnPush,
9+
})
10+
export class TemplateTriggersComponent implements OnInit {
11+
strategy$ = new ReplaySubject<RxStrategyNames<any>>(1);
12+
13+
heroes$;
14+
15+
constructor() {}
16+
17+
ngOnInit() {}
18+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { CommonModule } from '@angular/common';
2+
import { NgModule } from '@angular/core';
3+
import { MatIconModule } from '@angular/material/icon';
4+
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
5+
import { RouterModule } from '@angular/router';
6+
import { LetModule } from '@rx-angular/template/let';
7+
import { StrategySelectModule } from '../../../../shared/debug-helper/strategy-select/index';
8+
import { TriggerProviderModule } from '../../../../shared/debug-helper/trigger-provider/trigger-provider.module';
9+
import { ValueProvidersModule } from '../../../../shared/debug-helper/value-provider/index';
10+
import { VisualizerModule } from '../../../../shared/debug-helper/visualizer/index';
11+
12+
import { TemplateTriggersComponent } from './template-triggers.component';
13+
import { ROUTES } from './template-triggers.routes';
14+
15+
@NgModule({
16+
imports: [
17+
RouterModule.forChild(ROUTES),
18+
VisualizerModule,
19+
StrategySelectModule,
20+
ValueProvidersModule,
21+
TriggerProviderModule,
22+
MatProgressSpinnerModule,
23+
MatIconModule,
24+
CommonModule,
25+
LetModule,
26+
],
27+
exports: [],
28+
declarations: [TemplateTriggersComponent],
29+
providers: [],
30+
})
31+
export class TemplateTriggersModule {}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { Routes } from '@angular/router';
2+
import { TemplateTriggersComponent } from './template-triggers.component';
3+
4+
export const ROUTES: Routes = [
5+
{
6+
path: '',
7+
component: TemplateTriggersComponent,
8+
},
9+
];

apps/demos/src/app/shared/debug-helper/trigger-provider/trigger-provider.component.ts

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,36 +4,58 @@ import { Subject } from 'rxjs';
44
@Component({
55
selector: 'rxa-trigger-provider',
66
exportAs: 'rxaTriggerProvider',
7-
template: `
8-
<button mat-raised-button (click)="suspense$.next(undefined)">
9-
Suspense <mat-icon></mat-icon>
10-
<rxa-zone-patched-icon class="mat-icon" [zoneState]="getZoneState()"></rxa-zone-patched-icon>
11-
</button>
12-
<button mat-raised-button [unpatch]="unpatched" (click)="error$.next(error)">
13-
Error
14-
<rxa-zone-patched-icon class="mat-icon" [zoneState]="getZoneState()"></rxa-zone-patched-icon>
15-
</button>
16-
<button mat-raised-button [unpatch]="unpatched" (click)="complete$.next(undefined)">
17-
Complete
18-
<rxa-zone-patched-icon class="mat-icon" [zoneState]="getZoneState()"></rxa-zone-patched-icon>
19-
</button>
7+
template: ` <button mat-raised-button (click)="next$.next(undefined)">
8+
Next
9+
<rxa-zone-patched-icon
10+
class="mat-icon"
11+
[zoneState]="getZoneState()"
12+
></rxa-zone-patched-icon>
13+
</button>
14+
<button mat-raised-button (click)="suspense$.next(undefined)">
15+
Suspense
16+
<rxa-zone-patched-icon
17+
class="mat-icon"
18+
[zoneState]="getZoneState()"
19+
></rxa-zone-patched-icon>
20+
</button>
21+
<button
22+
mat-raised-button
23+
[unpatch]="unpatched"
24+
(click)="error$.next(error)"
25+
>
26+
Error
27+
<rxa-zone-patched-icon
28+
class="mat-icon"
29+
[zoneState]="getZoneState()"
30+
></rxa-zone-patched-icon>
31+
</button>
32+
<button
33+
mat-raised-button
34+
[unpatch]="unpatched"
35+
(click)="complete$.next(undefined)"
36+
>
37+
Complete
38+
<rxa-zone-patched-icon
39+
class="mat-icon"
40+
[zoneState]="getZoneState()"
41+
></rxa-zone-patched-icon>
42+
</button>
2043
<ng-content></ng-content>`,
21-
changeDetection: ChangeDetectionStrategy.OnPush
44+
changeDetection: ChangeDetectionStrategy.OnPush,
2245
})
2346
export class TriggerProviderComponent {
24-
2547
suspense$ = new Subject<void>();
2648
error$ = new Subject<any>();
2749
complete$ = new Subject<any>();
50+
next$ = new Subject<any>();
2851

2952
@Input()
3053
unpatched;
3154

3255
@Input()
3356
error = 'Custom error value';
3457

35-
constructor() {
36-
}
58+
constructor() {}
3759

3860
getZoneState() {
3961
return this.unpatched?.length === 0 ? 'patched' : 'unpatched';

libs/cdk/notifications/src/lib/create-template-notifier.spec.ts

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,67 @@
1-
import { of } from 'rxjs';
1+
import { BehaviorSubject, NEVER, of, throwError } from 'rxjs';
22
import { createTemplateNotifier } from './create-template-notifier';
3+
import { RxNotification, RxNotificationKind } from './model';
34

45
describe(createTemplateNotifier.name, () => {
6+
it('should start with suspense when there is no value', () => {
7+
const templateNotifier = createTemplateNotifier();
8+
let n: RxNotification<any>;
9+
templateNotifier.next(NEVER);
10+
templateNotifier.values$.subscribe({
11+
next: (notification) => {
12+
n = notification;
13+
},
14+
});
15+
16+
expect(n.kind).toBe(RxNotificationKind.Suspense);
17+
});
18+
19+
it('should skip suspense when observable has value', () => {
20+
const templateNotifier = createTemplateNotifier();
21+
const spy = jest.fn();
22+
let n: RxNotification<any>;
23+
templateNotifier.next(new BehaviorSubject<number>(1));
24+
templateNotifier.values$.subscribe({
25+
next: (notification) => {
26+
n = notification;
27+
spy();
28+
},
29+
});
30+
31+
expect(spy).toHaveBeenCalledTimes(1);
32+
expect(n.kind).toBe(RxNotificationKind.Next);
33+
});
34+
35+
it('should treat undefined as suspense', () => {
36+
const templateNotifier = createTemplateNotifier();
37+
let n: RxNotification<any>;
38+
templateNotifier.values$.subscribe({
39+
next: (notification) => {
40+
n = notification;
41+
},
42+
});
43+
templateNotifier.next(of(null));
44+
templateNotifier.next(undefined);
45+
expect(n.kind).toBe(RxNotificationKind.Suspense);
46+
expect(n.value).toBe(undefined);
47+
});
48+
49+
it('should handle errors', () => {
50+
const spy = { next: jest.fn(), error: jest.fn() };
51+
const templateNotifier = createTemplateNotifier();
52+
templateNotifier.next(throwError(() => new Error('')));
53+
let n: RxNotification<any>;
54+
templateNotifier.values$.subscribe({
55+
next: (notification) => {
56+
n = notification;
57+
},
58+
error: spy.error,
59+
});
60+
61+
expect(n.kind).toBe(RxNotificationKind.Error);
62+
expect(spy.error).not.toBeCalled();
63+
});
64+
565
it('should handle `null` values', () => {
666
const spy = { next: jest.fn(), error: jest.fn() };
767
const templateNotifier = createTemplateNotifier();

0 commit comments

Comments
 (0)