@mr-choi/ngx-load-item v1.0.4
NgxLoadItem documentation
The Angular library to handle loading in applications.
Tired of writing boilerplate code every time when you load data from an HTTP request into a component? This library will help you out!
- ✅ Simple structural directives and pipes that takes care of loading single and multiple items:
- ✅ Only an observable is needed to make it work!
- ✅ Support for reloading, errors, and aborting.
- ✅ Support for progress tracking:
- ✅ As simple as using
rxjsoperators - ✅ Additional operators with progress tracking included.
- ✅ As simple as using
Table of contents
- Installation
- Quickstart examples
- API reference
Installation
npm install @mr-choi/ngx-load-itemQuickstart examples
Loading a single item
Very often, you want to display data from an HTTP request and show a loading indication if the request is still pending:
// component.ts
@Component({
templateUrl: './component.html',
standalone: true,
imports: [CommonModule, LoadItemModule]
})
export class Component {
source$ = of("It works!").pipe(delay(3000)); // Represents e.g. an HTTP request
}<!-- component.html -->
<div *ngxLoadItem="let item of source$">
<span class="loading" *ngIf="item.loading">Loading...</span>
<span class="success" *ngIf="item.success">{{ item.res }}</span>
</div>The above component will initially display 'Loading...'. After 3 seconds, the text will change into 'It works!'.
Loading multiple items
In some situations, you want to load multiple items of the same type, for example when you search for results:
// component.ts
import {delay} from "rxjs";
@Component({
templateUrl: './component.html',
standalone: true,
imports: [CommonModule, StreamItemModule]
})
export class Component {
source$ = of("Result 3").pipe(
delay(1000),
startWith("Result 2"),
delay(1000),
startWith("Result 1"),
delay(1000)
);
}<!-- component.html -->
<div *ngxStreamItem="let item of source$">
<span class="loading" *ngIf="item.loading">Busy loading results...</span>
<span class="success" *ngIf="item.success">Done loading results</span>
<h4>Results:</h4>
<ol>
<li *ngFor="let result of item.res">{{ result }}</li>
</ol>
</div>The above component initially shows it is busy loading results. Every second, a new result will appear in the numbered list. After the third and final result appear, the top message will change into "Done loading results".
Loading from multiple sources
If you assemble the item from multiple sources, you can use a progress tracker to display the progress of the assembly:
// component.ts
@Component({
templateUrl: './component.html',
standalone: true,
imports: [CommonModule, LoadItemModule],
viewProviders: [ProgressTracker]
})
export class Component {
source1$ = of("Done").pipe(delay(1000));
source2$ = of("!!!").pipe(delay(2000));
sourceCombined$ = this.loadItemService.combineLatest(
[this.source1$, this.source2$],
this.progressTracker
).pipe(
map(([source1, source2]) => source1 + source2),
delay(1000)
);
constructor(
private loadItemService: LoadItemService,
private progressTracker: ProgressTracker
) { }
}<!-- component.html -->
<div *ngxLoadItem="let item of sourceCombined$">
<span class="loading" *ngIf="item.loading">Loading... ({{item.progress}}%)</span>
<span class="success" *ngIf="item.success">{{ item.res }}</span>
</div>The component will initially show a progress of 0%. Every second later, the progress will increase by 50%. Eventually, the component will display 'Done!!!' after showing 100% for 1 second.
Important: you must provide the
ProgressTrackerin theviewProviders(i.e. not in theproviders). Otherwise, theLoadItemDirectiveis unable to detect the progress tracker, and you will not see the progress.
API reference
LoadItemModule
The LoadItemModule includes the LoadItemDirective, the LoadItemPipe and provides the LoadItemService. The LoadItemModule is used for loading single items from an observable. For loading multiple items from an observable, the StreamItemModule should be used.
We recommend to import this module in your root application module/component so that you can make use of the library in the entire application.
LoadItemDirective
selector:
[ngxLoadItem][ngxLoadItemOf]
The LoadItemDirective enables to check and display the loading state of an observable for which a single item is expected:
<div *ngxLoadItem="let item of source$">
<span *ngIf="item.loading">
Loading with progress (i.e. the observable did not emit anything yet): {{item.progress}}%.
</span>
<span *ngIf="item.success">
Successfully loaded result (i.e. the observable emitted a value): {{item.res}}.
</span>
<span *ngIf="item.failed">
Loading failed with error (i.e. the observable errors): {{item.err}}.
</span>
<span *ngIf="item.aborted">
Loading has been aborted (i.e. the observable completed without emitting anything).
</span>
</div>Here, source$ is the input observable and item is the resulting LoadingState.
Reloading
The directive supports reloading in two ways:
1. By reassigning the value of source$ in the component. You can also use an async pipe like below:
<div *ngxLoadItem="let item of sources$ | async">
...
</div> where sources$ is a higher order observable.
Note: the directive is aware of possible
nullvalues fromasyncpipes, which will be ignored without throwing errors.
- By calling the
reload()method of the directive. The directive itself can be obtained in the template by using theinstancecontext variable:
By clicking the 'Reload' button in the example above, the item will be reloaded from the<div *ngxLoadItem="let item of source$; instance as inst"> ... <button (click)="inst.reload(other$)">Reload</button> </div>other$observable. The parameter ofinst.reload()is optional. When omitted, the last providedsource$observable will be loaded again.
Retaining results or errors
By default, the loaded result in item.res and error in item.err will not be retained when the item is reloaded. This behaviour, however, can be changed by using the [ngxLoadItemRetain] or [ngxLoadItemRetainError] input respectively:
<div *ngxLoadItem="let item of source$; retain true; retainError true; instance as inst">
<span *ngIf="item.loading">loading...</span>
<span>Last loaded result: {{item.res}}</span>
<span>Last error: {{item.err}}</span>
<button (click)="inst.reload()">Reload</button>
</div>In the above example, the previous result will still be displayed even after the 'Reload' button has been clicked and the loading has not been completed. If an error occurred, the last error will retain until reloading succeeded.
LoadItemPipe
Name:
loadItem
Like the LoadItemDirective, the LoadItemPipe converts an observable into a loading state:
<child-component [item]="source$ | loadItem"></child-component>Here, the [item]-input of the child component will be the resulting LoadingState.
Note: Whether you should use the
LoadItemDirectiveorLoadItemPipedepends on the use case:
- If the loading progress, error messages, and results have to be located very specific for the component, the
LoadItemDirectiveshould be used. Using theLoadItemPipemultiple times in a component should be avoided.- If you wish to create a generic component for how the loading process of items should be displayed, then the
LoadItemPipewill be more convenient to pass theLoadingStateas input of that generic component, like the example above.
LoadingState
/* Overview: */
class LoadingState<T, E = any> {
res?: T;
err?: E;
readonly progress: number;
readonly loading: boolean;
readonly success: boolean;
readonly failed: boolean;
readonly aborted: boolean;
static createLoading<T, E = any>(progress: number = 0): LoadingState<T, E>;
static createSuccess<T, E = any>(res: T): LoadingState<T, E>;
static createFailed<T, E = any>(err: E): LoadingState<T, E>;
static createAborted<T, E = any>(): LoadingState<T, E>;
}A LoadingState instance represents the loading status of an observable that is expected to emit a single value. It also holds the result after loading succeeds or an error if loading fails.
LoadingState has two generic type parameters:
| pos. | name | description | default |
|---|---|---|---|
| 1. | T | Result type | |
| 2. | E | Error type | any |
LoadingState has the following properties:
| name | type | details |
|---|---|---|
res | T \| undefined | The result after loading succeeded. undefined if result is not loaded (yet). |
err | E \| undefined | The error if loading failed. undefined if loading did not fail (yet). |
progress | number | Loading progress as percentage: 0 if not loaded at all and 100 if fully loaded. |
loading | boolean | true if source observable did not emit anything yet. |
success | boolean | true if source observable emitted a value. |
failed | boolean | true after source observable errors (even if loading succeeded before). |
aborted | boolean | true if source observable completed without emitting any value. |
Note: among the four booleans
loading,success,failed, andaborted, there should always be exactly one of themtruewhile the others arefalse.
Creating LoadingState instances
The LoadingState has static methods for creating instances:
LoadingState.createLoading(progress)for creatingloadingstates with provided progress. Theprogressparameter is optional and defaults to 0.LoadingState.createSuccess(res)for creatingsuccessstates with provided result.LoadingState.createFailed(err)for creatingfailedstates with provided error.LoadingState.createAborted()for creatingabortedstates.
StreamItemModule
The StreamItemModule includes the StreamItemDirective, the StreamItemPipe and provides the StreamItemService. While the LoadItemModule is used for loading single items from an observable, the StreamItemModule is used for loading multiple items from an observable.
We recommend to import this module in your root application module/component so that you can make use of the library in the entire application.
StreamItemDirective
selector:
[ngxStreamItem][ngxStreamItemOf]
The StreamItemDirective enables to check and display the loading state of an observable for which multipe items are expected:
<div *ngxStreamItem="let item of source$">
<span *ngIf="item.loading">
Loading with progress (i.e. the observable is not completed yet): {{item.progress}}%.
</span>
<span *ngIf="item.success">
Successfully loaded all results (i.e. the observable emitted a value).
</span>
<span *ngIf="item.failed">
Loading failed with error (i.e. the observable errors): {{item.err}}.
</span>
<ol>
<!-- A list of all results (i.e. all emitted values in order). -->
<!-- The list will be updated every time the source emits a value -->
<li *ngFor="let result of item.res">{{ result }}</li>
</ol>
</div>Here, source$ is the input observable and item is the resulting StreamingState.
Reloading
The directive supports reloading in two ways:
1. By reassigning the value of source$ in the component. You can also use an async pipe like below:
<div *ngxStreamItem="let item of sources$ | async">
...
</div> where sources$ is a higher order observable.
Note: the directive is aware of possible
nullvalues fromasyncpipes, which will be ignored without throwing errors.
- By calling the
reload()method of the directive. The directive itself can be obtained in the template by using theinstancecontext variable:
By clicking the 'Reload' button in the example above, the item will be reloaded from the<div *ngxStreamItem="let item of source$; instance as inst"> ... <button (click)="inst.reload(other$)">Reload</button> </div>other$observable. The parameter ofinst.reload()is optional. When omitted, the last providedsource$observable will be loaded again.
Retaining errors
By default, the error in item.err will not be retained when you retry loading. This behavior, however, can be changed by using the [ngxStreamItemRetainError] input of the directive:
<div *ngxStreamItem="let item of source$; retainError true; instance as inst">
<span *ngIf="item.loading">loading...</span>
<span>Last error: {{item.err}}</span>
<button (click)="inst.reload()">Reload</button>
</div>Here, the last error will still be displayed when you click the 'Reload' button, until loading has done successfully.
StreamItemPipe
Name:
streamItem
Like the StreamItemDirective, the StreamItemPipe converts an observable into a streaming state:
<child-component [item]="source$ | streamItem"></child-component>Here, the [item]-input of the child component will be the resulting StreamingState.
Note: Whether you should use the
StreamItemDirectiveorStreamItemPipedepends on the use case:
- If the loading progress, error messages, and results have to be located very specific for the component, the
StreamItemDirectiveshould be used. Using theStreamItemPipemultiple times in a component should be avoided.- If you wish to create a generic component for how the loading process of items should be displayed, then the
StreamItemPipewill be more convenient to pass theStreamingStateas input of that generic component, like the example above.
StreamingState
/* Overview: */
class StreamingState<T, E = any> {
readonly res?: T[];
err?: E;
readonly progress: number;
readonly loading: boolean;
readonly success: boolean;
readonly failed: boolean;
static createLoading<T, E = any>(res: T[] = [], progress: number = 0): LoadingState<T, E>;
static createSuccess<T, E = any>(res: T[] = []): LoadingState<T, E>;
static createFailed<T, E = any>(res: T[] = [], err?: E): LoadingState<T, E>;
}A StreamingState instance represents the loading status of an observable that is expected to emit multiple values. It also holds the resulting values during loading and an error if loading fails.
StreamingState has two generic type parameters:
| pos. | name | description | default |
|---|---|---|---|
| 1. | T | Result type | |
| 2. | E | Error type | any |
StreamingState has the following properties:
| name | type | details |
|---|---|---|
res | T[] | Array of loaded values, items are in the same order as they have been emitted by the observable. |
err | E \| undefined | The error if loading failed. undefined if loading did not fail (yet). |
progress | number | Loading progress as percentage: 0 if not loaded at all and 100 if fully loaded. |
loading | boolean | true if source observable did not complete yet. |
success | boolean | true if source observable completes. |
failed | boolean | true after source observable errors (even if some results have been loaded). |
aborted | boolean | true if source observable completed without emitting any value. |
Note: among the three booleans
loading,success,failed, there should always be exactly one of themtruewhile the others arefalse.
Creating StreamingState instances
The StreamingState has static methods for creating instances:
StreamingState.createLoading(res, progress)for creatingloadingstates with provided result array and progress. Theresandprogressparameters are optional and defaults to an empty array and 0 respectively.StreamingState.createSuccess(res)for creatingsuccessstates with provided result array, which is an empty array by defaultStreamingState.createFailed(res, err)for creatingfailedstates with provided result array and error. If they are not provided, the array will be empty and the errorundefined.
ProgressTracker
In some situations, you do not only want to show that an item is loading, but also the progress of the loading process. For that purpose, progress trackers can be used.
Usage with LoadItemDirective
To use progress trackers, you provide and inject it into your component that needs to load the item:
/* my.component.ts */
@Component({
/* ... */
viewProviders: [ProgressTracker]
})
export class MyComponent {
constructor(private progressTracker: ProgressTracker) { }
source$ = of("Done!").pipe(
delay(1000), // Slow processing stuff...
this.progressTracker.track(),
delay(1000), // More processing stuff...
this.progressTracker.track(),
delay(1000) // And more...
);
}The LoadItemDirective in the template will then automatically inject the progress tracker you have provided and used in the component:
<!-- my.component.html -->
<div *ngxLoadItem="let item of source$">
<!-- no need to specify progress tracker, will be injected automatically. -->
<span *ngIf="item.loading">{{item.progress}}%</span>
<span *ngIf="item.success">{{item.res}}</span>
</div>
<!-- Result: 0% -> 50% -> 100% -> Done! -->Important: you have to provide the progress tracker using
viewProviders(i.e. NOT usingproviders). Otherwise, theLoadItemDirectivewill not find the progress tracker and no progress will de displayed.
Alternatively, you can manually create a progress tracker in the component:
/* my.component.ts */
@Component({
/* ... */
})
export class MyComponent {
progressTracker = new ProgressTracker();
source$ = of("Done!").pipe(
delay(1000), // Slow processing stuff...
this.progressTracker.track(),
delay(1000), // More processing stuff...
this.progressTracker.track(),
delay(1000) // And more...
);
}and then pass it to the [ngxLoadItemUseTracker] input of the directive:
<!-- my.component.html -->
<div *ngxLoadItem="let item of source$ useTracker progressTracker">
...
</div>Notes: 1. You need a different progress tracker instance for each item you want to load. That is why
ProgressTrackeris not root injected or provided in theLoadItemModule. 2. Because dependency injection is considered best practice, we recommend to only do manual creation of progress trackers if dependency injection is not possible. For example, when you need different progress trackers to load multiple items on a single component.
Usage with LoadItemPipe
Very similar to using progress trackers in the LoadItemDirective, there are two methods to use progress trackers in the LoadItemPipe:
1. Provide the progress tracker in the viewProviders of the component that uses the pipe. The pipe will then automatically inject and use that progress tracker.
2. Manually create the progress tracker and pass it as additional parameter to the pipe:
<child-component [item]="source$ | loadItem: progressTracker"></child-component>Usage with StreamItemDirective
The usage of progress trackers in the StreamItemDirective is more or less the same as using progress trackers in the LoadItemDirective. Like the LoadItemDirective, the StreamItemDirective as a [ngxStreamItemUseTracker] input:
<div *ngxStreamItem="let item of source$ useTracker progressTracker">
...
</div>Usage with StreamItemPipe
The usage is more or less the same as for the LoadItemPipe. Like the LoadItemPipe, the StreamItemPipe accepts a parameter for progress trackers:
<child-component [item]="source$ | streamItem: progressTracker"></child-component>Weights and progress calculation
In some cases, you may want to give some parts of the loading process more weight, for example HTTP requests that usually takes more time compared to others. Weights can easily be specified as parameter in the trackers:
source$ = of("Done!").pipe(
delay(3000),
this.progressTracker.track(3), // The weight of this step is 3.
delay(1000),
this.progressTracker.track(), // Weight defaults to 1 if it is not provided.
);When providing the above source$ to a LoadItemDirective or LoadItemPipe, the displayed progress will be 75% after 3 seconds.
The progress is calculated with the following formula:
\text{progress} = \frac{\text{sum of weights of completed steps}}{\text{sum of weights of all steps}} \times 100\%In the above example after 3 seconds, the sum of weights of completed and all steps are 3 and 4 respectively, so the formula confirms that the progress is 75%.
LoadItemService
The LoadItemService is a service in the LoadItemModule used by the LoadItemDirective and LoadItemPipe. The service also has methods that may come in handy when building your application.
toLoadingState
The toLoadingState method returns an rxjs operator to convert an observable into a LoadingState. This method is also used by the LoadItemDirective and LoadItemPipe for that purpose.
To use it, simply inject the LoadItemService:
export class myService {
constructor(private loadItemService: LoadItemService) { }
item$ = of("Example").pipe(
delay(1000),
this.loadItemService.toLoadingState()
);
}If you wish to load with a ProgressTracker, there are two options:
1. Provide the ProgressTracker and the LoadItemService together:
@Component({
/** ... **/
providers: [ProgressTracker, LoadItemService]
})
export class ExampleComponent {}Explanation: you cannot inject a service provided in component into a service provided in a module. That is why you need to provide a new
LoadItemServiceinto the component, so that it can inject the progress tracker. 2. Pass the progress tracker as parameter to thetoLoadingStatemethod:export class myService { constructor(private loadItemService: LoadItemService) { }
private progressTracker = new ProgressTracker();
item$ = of("Example").pipe(
delay(1000),
this.loadItemService.toLoadingState(this.progressTracker)
);}
#### ``combineLatest``
The `LoadItemService` has an own implementation of `combineLatest` of `rxjs`. Next to the original implementation, it also adds a progress tracker to every provided source. This is useful if you assemble the item to be loaded from different sources.
By default, all trackers have weight 1, but it is possible to configure individual sources to use a different weight by replacing `$source` with `[$source, weight]`. For example:
```typescript
loadItemService.combineLatest([source1$, [source2$, 2], source3$]);Here, source2$ will contribute 50% to the progress, whereas source1$ and source3$ contribute only 25%.
Notes: 1. Not all method overloads of the original
combineLatestare supported: only the array and record of sources are expected. 2. if theLoadItemServicedoes not have progress tracker, the original implementation ofcombineLatestwill be used and provided weights are ignored. 3. It is also possible to pass the progress tracker as second argument:loadItemService.combineLatest(sources$, progressTracker);
StreamItemService
The StreamItemService is a service in the StreamItemModule used by the StreamItemDirective and StreamItemPipe. Like the LoadItemService, the StreamItemService also has methods that may come in handy when building your application.
toStreamingState
The toStreamingState method returns an rxjs operator to convert an observable into a StreamingState. This method is also used by the StreamItemDirective and StreamItemPipe for that purpose.
To use it, simply inject the StreamItemService:
export class myService {
constructor(private streamItemService: StreamItemService) {
}
item$ = of("Example").pipe(
delay(1000),
this.streamItemService.toStreamingState()
);
}If you wish to load with a ProgressTracker, there are two options:
1. Provide the ProgressTracker and the StreamItemService together:
@Component({
/** ... **/
providers: [ProgressTracker, StreamItemService]
})
export class ExampleComponent {}Explanation: you cannot inject a service provided in component into a service provided in a module. That is why you need to provide a new
StreamItemServiceinto the component, so that it can inject the progress tracker. 2. Pass the progress tracker as parameter to thetoStreamingStatemethod:export class myService { constructor(private streamItemService: StreamItemService) { }
private progressTracker = new ProgressTracker();
item$ = of("Example").pipe(
delay(1000),
this.streamItemService.toStreamingState(this.progressTracker)
);}
#### `merge`
The `StreamItemService` has an own implementation of `merge` of `rxjs`. Next to the original implementation, it also adds a progress tracker to every provided source. This is useful if you use multiple sources for loading multiple items.
By default, all trackers have weight 1, but it is possible to configure individual sources to use a different weight by replacing `$source` with `[$source, weight]`. For example:
```typescript
streamItemService.merge([source1$, [source2$, 2], source3$]);Here, source2$ will contribute 50% to the progress, whereas source1$ and source3$ contribute only 25%.
Notes: 1. Unlike the
mergefunction ofrxjswhere the sources are separate parameters, themergemethod of theStreamItemServicerequires to pass the sources as a list, like in the example above. 2. Like the last parameter of themergefunction ofrxjs, the last item in the sources list can be a number indicating the maximum concurrent subscriptions:streamItemService.merge([source1$, source2$, source3$, 2]);3. if theStreamItemServicedoes not have progress tracker, the original implementation ofmergewill be used and provided weights are ignored. 4. It is also possible to pass the progress tracker as second argument:streamItemService.merge(sources$, progressTracker);
2 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago