1.0.4 • Published 12 months ago

@mr-choi/ngx-load-item v1.0.4

Weekly downloads
-
License
MIT
Repository
gitlab
Last release
12 months ago

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.

pipeline status coverage report Latest Release

Table of contents

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 the viewProviders (i.e. not in the providers). Otherwise, the LoadItemDirective 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 from async pipes, which will be ignored without throwing errors.

  1. By calling the reload() method of the directive. The directive itself can be obtained in the template by using the instance context variable:
    <div *ngxLoadItem="let item of source$; instance as inst">
      ...
      <button (click)="inst.reload(other$)">Reload</button>
    </div>
    By clicking the 'Reload' button in the example above, the item will be reloaded from the other$ observable. The parameter of inst.reload() is optional. When omitted, the last provided source$ 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 or LoadItemPipe 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 the LoadItemPipe 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 the LoadingState 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.namedescriptiondefault
1.TResult type
2.EError typeany

LoadingState has the following properties:

nametypedetails
resT \| undefinedThe result after loading succeeded. undefined if result is not loaded (yet).
errE \| undefinedThe error if loading failed. undefined if loading did not fail (yet).
progressnumberLoading progress as percentage: 0 if not loaded at all and 100 if fully loaded.
loadingbooleantrue if source observable did not emit anything yet.
successbooleantrue if source observable emitted a value.
failedbooleantrue after source observable errors (even if loading succeeded before).
abortedbooleantrue if source observable completed without emitting any value.

Note: among the four booleans loading, success, failed, and aborted, there should always be exactly one of them true while the others are false.

Creating LoadingState instances

The LoadingState has static methods for creating instances:

  • LoadingState.createLoading(progress) for creating loading states with provided progress. The progress parameter is optional and defaults to 0.
  • LoadingState.createSuccess(res) for creating success states with provided result.
  • LoadingState.createFailed(err) for creating failed states with provided error.
  • LoadingState.createAborted() for creating aborted 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 from async pipes, which will be ignored without throwing errors.

  1. By calling the reload() method of the directive. The directive itself can be obtained in the template by using the instance context variable:
    <div *ngxStreamItem="let item of source$; instance as inst">
      ...
      <button (click)="inst.reload(other$)">Reload</button>
    </div>
    By clicking the 'Reload' button in the example above, the item will be reloaded from the other$ observable. The parameter of inst.reload() is optional. When omitted, the last provided source$ 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 or StreamItemPipe 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 the StreamItemPipe 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 the StreamingState 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.namedescriptiondefault
1.TResult type
2.EError typeany

StreamingState has the following properties:

nametypedetails
resT[]Array of loaded values, items are in the same order as they have been emitted by the observable.
errE \| undefinedThe error if loading failed. undefined if loading did not fail (yet).
progressnumberLoading progress as percentage: 0 if not loaded at all and 100 if fully loaded.
loadingbooleantrue if source observable did not complete yet.
successbooleantrue if source observable completes.
failedbooleantrue after source observable errors (even if some results have been loaded).
abortedbooleantrue if source observable completed without emitting any value.

Note: among the three booleans loading, success, failed, there should always be exactly one of them true while the others are false.

Creating StreamingState instances

The StreamingState has static methods for creating instances:

  • StreamingState.createLoading(res, progress) for creating loading states with provided result array and progress. The res and progress parameters are optional and defaults to an empty array and 0 respectively.
  • StreamingState.createSuccess(res) for creating success states with provided result array, which is an empty array by default
  • StreamingState.createFailed(res, err) for creating failed states with provided result array and error. If they are not provided, the array will be empty and the error undefined.

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 using providers). Otherwise, the LoadItemDirective 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 the LoadItemModule. 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 the toLoadingState 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 the LoadItemService does not have progress tracker, the original implementation of combineLatest 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 the toStreamingState 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 of rxjs where the sources are separate parameters, the merge method of the StreamItemService requires to pass the sources as a list, like in the example above. 2. Like the last parameter of the merge function of rxjs, the last item in the sources list can be a number indicating the maximum concurrent subscriptions: streamItemService.merge([source1$, source2$, source3$, 2]); 3. if the StreamItemService does not have progress tracker, the original implementation of merge 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);

1.0.4

12 months ago

1.0.2

1 year ago

1.0.3

1 year ago

1.0.1

1 year ago

1.0.0

1 year ago

1.0.0-beta.2

1 year ago

1.0.0-beta.1

1 year ago

1.0.0-beta.0

1 year ago