@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
rxjs
operators - ✅ 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-item
Quickstart 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
ProgressTracker
in theviewProviders
(i.e. not in theproviders
). Otherwise, theLoadItemDirective
is 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
null
values fromasync
pipes, 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 theinstance
context 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
LoadItemDirective
orLoadItemPipe
depends on the use case:
- If the loading progress, error messages, and results have to be located very specific for the component, the
LoadItemDirective
should be used. Using theLoadItemPipe
multiple 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
LoadItemPipe
will be more convenient to pass theLoadingState
as 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 themtrue
while the others arefalse
.
Creating LoadingState
instances
The LoadingState
has static methods for creating instances:
LoadingState.createLoading(progress)
for creatingloading
states with provided progress. Theprogress
parameter is optional and defaults to 0.LoadingState.createSuccess(res)
for creatingsuccess
states with provided result.LoadingState.createFailed(err)
for creatingfailed
states with provided error.LoadingState.createAborted()
for creatingaborted
states.
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
null
values fromasync
pipes, 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 theinstance
context 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
StreamItemDirective
orStreamItemPipe
depends on the use case:
- If the loading progress, error messages, and results have to be located very specific for the component, the
StreamItemDirective
should be used. Using theStreamItemPipe
multiple 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
StreamItemPipe
will be more convenient to pass theStreamingState
as 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 themtrue
while the others arefalse
.
Creating StreamingState
instances
The StreamingState
has static methods for creating instances:
StreamingState.createLoading(res, progress)
for creatingloading
states with provided result array and progress. Theres
andprogress
parameters are optional and defaults to an empty array and 0 respectively.StreamingState.createSuccess(res)
for creatingsuccess
states with provided result array, which is an empty array by defaultStreamingState.createFailed(res, err)
for creatingfailed
states 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, theLoadItemDirective
will 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
ProgressTracker
is 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
LoadItemService
into the component, so that it can inject the progress tracker. 2. Pass the progress tracker as parameter to thetoLoadingState
method: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
combineLatest
are supported: only the array and record of sources are expected. 2. if theLoadItemService
does not have progress tracker, the original implementation ofcombineLatest
will 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
StreamItemService
into the component, so that it can inject the progress tracker. 2. Pass the progress tracker as parameter to thetoStreamingState
method: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
merge
function ofrxjs
where the sources are separate parameters, themerge
method of theStreamItemService
requires to pass the sources as a list, like in the example above. 2. Like the last parameter of themerge
function 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 theStreamItemService
does not have progress tracker, the original implementation ofmerge
will be used and provided weights are ignored. 4. It is also possible to pass the progress tracker as second argument:streamItemService.merge(sources$, progressTracker);
12 months ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago