Work in progress folks 👀, almost there 🏁 and getting pretty excited 😎
Showing content as soon as it arrives can be a nice thing to pursue. However, when this results in a page full of spinners, skeleton loaders and what not, the user experience becomes less joyful. Somewhere there exists a balance between fast load times and jankless web pages, and with AngularSuspense, developers don't have to compromise between the two.
This project was inspired by react suspense and react's error boundary.
Components have their own loader component, i.e. SuspenseComponent. SuspenseComponent is aware of its component's LoadingState and of the LoadingStates of all of the component's children.
A LoadingState is either one of the following:
- LOADING
- EMPTY
- ERROR
- SUCCESS
These are the rules AngularSuspense takes into account when displaying loaders, empty or error state:
- As long as the LoadingState of a component or the LoadingState of one of its children is still LOADING, a loader will be shown.
- When a component's LoadingState has become SUCCESS, it will only be reevaluated when its own LoadingState changes.
- When a component's LoadingState is EMPTY, the empty state will be shown.
- When a component's LoadingState is ERROR, or one of its child components' LoadingStates is either EMPTY or ERROR, an error state is shown
First install the library.
npm install @david-bulte/angular-suspense --save
In your root module (typically AppModule) you import SuspenseModule like so:
@NgModule({
imports: [
SuspenseModule.forRoot()
]
})Feature modules import SuspenseModule without forRoot():
@NgModule({
imports: [
SuspenseModule
]
})In your components, areas that are loaded asynchronously are wrapped by a SuspenseComponent (tag susp). Each SuspenseComponent is given a loading state via its [state] input attribute.
<susp [state]="loadingMoviesState$ | async">
<app-movie [movie]="movie$ | async"></app-movie>
<susp [state]="loadingActorsState$ | async">
<app-actors [actors]="actors$ | async"></app-actors>
</susp>
</susp>
Note that this also works with a route hierarchy. If there exist a loading state in the child hierarchy, display will be supsended until all child components have been loaded.
<susp [state]="loadingMoviesState$ | async">
<app-movie [movie]="movie$ | async"></app-movie>
<router-outlet></router-outlet>
</susp>
Sometimes it does not matter whether a part of the page has been loaded or not. In that case we can mark that part as not being part of the parent's loading state via the [stopPropagation] attribute:
<susp [state]="loadingMoviesState$ | async">
<app-movie [movie]="movie$ | async"></app-movie>
<susp
[stopPropagation]="true"
[state]="loadingActorsState$ | async">
<app-actors [actors]="actors$ | async"></app-actors>
</susp>
</susp>
When you don't want the error state of a child to impact its parent's loading state, you can set an error boundary with the catchError attribute:
<susp [state]="loadingMoviesState$ | async">
<app-movie [movie]="movie$ | async"></app-movie>
<susp
[catchError]="true"
[state]="loadingActorsState$ | async">
<app-actors [actors]="actors$ | async"></app-actors>
</susp>
</susp>
Sometimes you want to wait a couple of microseconds before showing the loading state. In that case one can set the global debounce attribute of via SuspenseModule.forRoot().
SuspenseModule.forRoot({ debounce: 300 })You can also set a timeout case by case by setting the SuspenseComponent's [debounce] input property.
<susp [state]="state$ | async" [debounce]="300"></susp>
When a SuspenseComponent is conditionally created, its parent component cannot know whether the page has been completely loaded or not. You can give it a hint to show a loading state until its children have been created, via the [waitFor] attribute. This contains the number of expected child SuspensComponents.
<susp [state]="loadingState$ | async" [waitFor]="1">
<div *ngIf="movie$ | async as movie; else noMovie">
<susp [state]="loadingState$ | async">
...
</susp>
</div>
<ng-template #noMovie>
<susp [state]="'success'"></susp>
</ng-template>
</susp>
Provide the susp-default-templates component on your top page.
@Component({
selector: 'demo-loading-states',
template: `
<susp-default-templates>
<ng-template suspLoading> This is my global loading state </ng-template>
<ng-template suspEmpty> This is my global empty state </ng-template>
<ng-template suspError> This is my global error state </ng-template>
</susp-default-templates>
`,
})
export class LoadingStatesComponent {}
Provide ng-templates with suspLoading, suspEmpty and suspError directives
<susp [state]="loadingMoviesState$ | async">
<div>
<app-movie [movie]="movie$ | async"></app-movie>
<router-outlet></router-outlet>
</div>
<ng-template suspLoading>
<div class="loading">loading...</div>
</ng-template>
<ng-template suspEmpty>NOTHING HERE</ng-template>
<ng-template suspError>OOPS</ng-template>
</susp>
Check the cookbook for more examples.
- better documentation
think of better prefix- logo :)
set up demo appset up github actionsunit testsintroduce timeoutsintermediate loading state templatesetup cookbook, better examples- catchError map
- SuspenseDirective
publish to npm- ...
Just some quick notes on what I have learned while working on this project.
When deploying angular applications to github pages, you have to apply a little hack to make routing work: copy the compiled index.html file and name it 404.html. Add the following to your github actions file to do just that:
- name: Copy index.html -> 404.html
uses: canastro/copy-file-action@master
with:
source: "dist/apps/demo/index.html"
target: "dist/apps/demo/404.html"
