Skip to content

Commit

Permalink
LetDirective template binding - tests, docs and demo (#185)
Browse files Browse the repository at this point in the history
This PR adds ng-template bindings to be used with LetDirective. With this, we can create and bind templates for a different state of an observable:

- next - when values are emitted
- complete - on observable completion
- error - on observable error
- 🔥 suspense 🔥 - render something before the first value is emitted from an observable

Co-authored-by: kajetan.swiatek <kajetan.swiatek@aptitudesoftware.com>
Co-authored-by: Michael Hladky <michael@hladky.at>
  • Loading branch information
3 people committed Jul 28, 2020
1 parent 84b0c68 commit 0064d96
Show file tree
Hide file tree
Showing 17 changed files with 816 additions and 96 deletions.
Expand Up @@ -23,6 +23,7 @@ <h3 matSubheader>Tutorials</h3>
>Viewport Prio</a
>
<a mat-list-item [routerLink]="['tree-prio']">ComponentTree Prio</a>
<a mat-list-item [routerLink]="['template-binding']">Template Binding</a>
</mat-nav-list>
</mat-sidenav>
<mat-sidenav-content>
Expand Down
1 change: 0 additions & 1 deletion apps/template-demo/src/app/app.module.ts
Expand Up @@ -15,7 +15,6 @@ import { RouterModule } from '@angular/router';
import { AppComponent } from './app-component/app.component';
import { ROUTES } from './app.routes';
import { ViewportPrioModule } from '@rx-angular/template';
import { ComparisonUnpatchModule } from './examples/unpatch/comparison-unpatch.module';
import { SharedModule } from './examples/shared/shared.module';

export const materialModules = [
Expand Down
7 changes: 7 additions & 0 deletions apps/template-demo/src/app/app.routes.ts
Expand Up @@ -40,5 +40,12 @@ export const ROUTES: Routes = [
import('./examples/component-tree-prio/component-tree-prio.module').then(
m => m.ComponentTreePrioModule
)
},
{
path: 'template-binding',
loadChildren: () =>
import('./examples/template-binding/template-binding.module').then(
m => m.TemplateBindingModule
)
}
];
@@ -0,0 +1,157 @@
import { Component } from '@angular/core';
import { BehaviorSubject, interval, Subject, throwError } from 'rxjs';
import { fromPromise } from 'rxjs/internal-compatibility';
import {
map,
share,
switchMap,
takeUntil,
withLatestFrom
} from 'rxjs/operators';

@Component({
selector: 'let-template-binding-http-example',
template: `
<mat-card class="card">
<mat-card-header>
<h1>HTTP Request</h1>
</mat-card-header>
<mat-card-content>
<div
*rxLet="
heroes$;
let hero;
strategy: visibleStrategy;
complete: complete;
error: error;
suspense: suspense
"
>
random Star Wars character fetched!
<h2>{{ hero }}</h2>
</div>
</mat-card-content>
<mat-card-actions>
<button
mat-button
[unpatch]
*ngIf="!(heroes$ | push)"
(click)="startFetch()"
>
START
</button>
<button
mat-button
[unpatch]
*ngIf="heroes$ | push"
(click)="completeFetch()"
>
COMPLETE
</button>
<button
mat-button
[unpatch]
*ngIf="heroes$ | push"
(click)="errorFetch()"
>
ERROR
</button>
</mat-card-actions>
</mat-card>
<ng-template #complete>
<div>
<mat-icon class="complete-icon">thumb_up</mat-icon>
<h2>Completed!</h2>
</div>
</ng-template>
<ng-template #error>
<div>
<mat-icon class="error-icon">thumb_down</mat-icon>
<h2>Something went wrong...</h2>
</div>
</ng-template>
<ng-template #suspense>
<mat-progress-spinner
[diameter]="80"
[color]="'primary'"
[mode]="'indeterminate'"
></mat-progress-spinner>
</ng-template>
`,
styles: [
`
mat-card-content {
min-height: 10rem;
display: flex;
justify-content: center;
align-items: center;
}
mat-icon {
font-size: 5rem;
height: initial;
width: initial;
}
.card {
margin: 2rem;
text-align: center;
width: 20rem;
}
.complete-icon {
color: forestgreen;
}
.error-icon {
color: darkred;
}
`
]
})
export class LetTemplateBindingHttpExampleComponent {
visibleStrategy = 'local';
start$ = new Subject();
complete$ = new Subject();
error$ = new BehaviorSubject<Error>(undefined);
heroes$ = this.start$.pipe(
switchMap(() => {
return interval(1500).pipe(
withLatestFrom(this.error$),
switchMap(([_, error]) => {
if (error) {
return throwError(error);
} else {
return fromPromise(
fetch(
`https://swapi.dev/api/people/${Math.floor(Math.random() * 50) +
1}`
).then(a => a.json())
);
}
}),
map(hero => hero.name || hero.detail || 'Not found')
);
}),
share(),
takeUntil(this.complete$)
);

startFetch() {
this.start$.next();
this.start$.complete();
}

completeFetch() {
this.complete$.next();
this.complete$.complete();
}

errorFetch() {
this.error$.next(new Error('Fetch Error!'));
this.error$.complete();
}
}
@@ -0,0 +1,111 @@
import { Component } from '@angular/core';
import { Subject } from 'rxjs';
import { scan, startWith } from 'rxjs/operators';

@Component({
selector: 'let-template-binding-subject-example',
template: `
<mat-card class="card">
<mat-card-header>
<h1>Subject</h1>
</mat-card-header>
<mat-card-content>
<div
*rxLet="
signals$;
let count;
strategy: visibleStrategy;
complete: complete;
error: error;
suspense: suspense
"
>
value emitted
<h2>{{ count | json }}</h2>
</div>
</mat-card-content>
<mat-card-actions>
<button mat-button [unpatch] (click)="signals$.complete()">
COMPLETE
</button>
<button
mat-button
[matBadge]="signalsCount$ | push | toString"
[matBadgeHidden]="(signalsCount$ | push) === 0"
[unpatch]
(click)="signals$.next(random())"
>
NEXT
</button>
<button mat-button [unpatch] (click)="signals$.error(errorStub)">
ERROR
</button>
</mat-card-actions>
</mat-card>
<ng-template #complete>
<div>
<mat-icon class="complete-icon">thumb_up</mat-icon>
<h2>Completed!</h2>
</div>
</ng-template>
<ng-template #error>
<div>
<mat-icon class="error-icon">thumb_down</mat-icon>
<h2>Something went wrong...</h2>
</div>
</ng-template>
<ng-template #suspense>
<mat-progress-spinner
[diameter]="80"
[color]="'primary'"
[mode]="'indeterminate'"
></mat-progress-spinner>
</ng-template>
`,
styles: [
`
mat-card-content {
min-height: 10rem;
display: flex;
justify-content: center;
align-items: center;
}
mat-icon {
font-size: 5rem;
height: initial;
width: initial;
}
.card {
margin: 2rem;
text-align: center;
width: 20rem;
}
.complete-icon {
color: forestgreen;
}
.error-icon {
color: darkred;
}
`
]
})
export class LetTemplateBindingSubjectExampleComponent {
errorStub = new Error('Template observable error!');
visibleStrategy = 'local';
signals$ = new Subject<any>();
signalsCount$ = this.signals$.pipe(
scan(acc => acc + 1, 0),
startWith(0)
);

random() {
return Math.random();
}
}
@@ -0,0 +1,10 @@
import { Component } from '@angular/core';

@Component({
selector: 'let-template-binding',
template: `
<let-template-binding-subject-example></let-template-binding-subject-example>
<let-template-binding-http-example></let-template-binding-http-example>
`
})
export class LetTemplateBindingComponent {}
@@ -0,0 +1,10 @@
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
name: 'toString'
})
export class ToStringPipe implements PipeTransform {
transform(value: number): string {
return value + '';
}
}
@@ -0,0 +1,44 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { MatBadgeModule } from '@angular/material/badge';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { RouterModule } from '@angular/router';
import { TemplateModule, ViewportPrioModule } from '@rx-angular/template';
import { LetTemplateBindingHttpExampleComponent } from './let-template-binding/examples/let-template-binding-http-example.component';
import { LetTemplateBindingSubjectExampleComponent } from './let-template-binding/examples/let-template-binding-subject-example.component';
import { LetTemplateBindingComponent } from './let-template-binding/let-template-binding.component';
import { ToStringPipe } from './let-template-binding/to-string.pipe';

export const ROUTES = [
{
path: '',
pathMatch: 'full',
component: LetTemplateBindingComponent
}
];
const DECLARATIONS = [
LetTemplateBindingComponent,
ToStringPipe,
LetTemplateBindingSubjectExampleComponent,
LetTemplateBindingHttpExampleComponent
];

@NgModule({
declarations: [DECLARATIONS],
imports: [
CommonModule,
ViewportPrioModule,
RouterModule.forChild(ROUTES),
TemplateModule,
MatProgressSpinnerModule,
MatCardModule,
MatButtonModule,
MatBadgeModule,
MatIconModule
],
exports: [DECLARATIONS]
})
export class TemplateBindingModule {}

0 comments on commit 0064d96

Please sign in to comment.