This project was generated with Angular CLI version 1.5.5 . To run this locally, see the CLI documentation section.
Every snippet is articulated around:
- the context: a brief description of the problem statement
- concepts: key points with code illustrations
- demo: link to demo of concepts
- further: if applicable, what would deserve more analysis
All snippets can be tried on the demo installation
You need to retrieve data with a paging API where the number of pages required all data is not known in advance. You know that you finished when:
- number of data items retrieved is strictly smaller than the page size
- or when you get an empty result set
The service that makes use of the paging API returns an Observable
.
The result type (the generic type parameter) would typically be an
array of items.
The caller of the service will subscribe to the returned Observable
to display or process the incoming data.
Iterating through pages will be implemented with the Angular
HttpClient
.
It will start from the initial page (position zero) and move up until
the last.
From a single page, we:
- get a result
- get to know the next start at position for reading the next page
- also find out whether the page traversal is complete.
This is encapsulated in:
class PageContext {
startAt = 0;
result: any = null;
completed = false;
}
The signature of the method to read a single page is expressed as:
processPage(ctx: PageContext): Observable<PageContext>
As a simplication, let's assume that the return type is a
PageContext
and not an Observable
. The algorithm to traverse
all pages would be something like:
while(! ctx.completed) {
ctx = processPage(ctx);
}
... but the result type is an Observable. So the algorithm above needs to be written with Rx Operators.
To apply a basic transformation, one can use the map operator:
i.e. your tranformation is purely doing basic synchronous operations.
For example, if function t returns a type T, the return type of the
expression below will be Observable<T>
:
processPage(ctx).map( ctx => t(ctx) );
Here after reading a page, depending the status we either return what we got: a simple transformation, or, we need to read another page: an async call returning an observable. Because of the second condition we need to use the mergeMap operator. Using map would make the return type be:
// No this is not what we want :(
Observable<Observable<PageContext>>
So the simple while loop becomes:
private iteratePages(initialCtx: PageContext): Observable<PageContext> {
return this.processPage(initialCtx)
.mergeMap(ctx => {
if (ctx.completed) {
return Observable.of(ctx);
} else {
return this.iteratePages(ctx);
}
});
}
The demo to illustrate the implementation principle computes factorial. See the factorial.service.ts implementation:
- A delay of 500ms is introduced to mimick the latency of retrieving one page of items
- The code is almost identical to the snippets above.
It works! But one has to wait for the full result: i.e. the implementation above yields the results until the very last iteration.
As in the section RxJs Recursive Observable we need to read data by iterating through pages. This time we want to issue results as soon as we get them from a page.
We used an Observable in the previous section but nevertheless ended up with a blocking implementation. Whereas writing a loop is easy, emitting intermediary results was a strugle, until I understood the ...
The RxJs expand operator will keep calling the function passed as an argument until it
returns Observable.empty()
.
Let's take a close look at the ReactiveFactorialServiceImpl
class, in factorial.service.ts:
factorial(n: number): Observable<number> {
return Observable.of(new PageContext(n))
.expand( ctx => {
return (ctx.completed) ? Observable.empty() : this.singleOp.processPage(ctx);
})
.map(ctx => ctx.result);
}
From the perspective of an imperative style developer, the expand operator will:
- emit the results down the transformation chain after every call
- will reinject the non empty result as an input in the next call to the function in expand
Try now the demo and see how intermediary results are delivered at every step of the calculation 😄
A beneficial side effect of displaying results as they arrive is that the user may want to interrupt the process:
- maybe the user found what he needed in the first results
- or she/he sees that this is not what she/he was after
- browses to another page, ...
The RxJs Don't unsuscribe | Medium.com article is very pertinent in that respect. It proposes the use of the takeUntil operator. A very good illustration can be found in the Aligator.io/angular/takeuntil-rxjs-unsubscribe/ example.
You want to the user to save content on her/his local device. This content is typically provided by an API.
The downloaded content is saved in the browser's local storage and made
available to the download to the user by creating a so called
HTML 5 blob URL with the method createObjectURL
.:
const xml = '<hello></hello>';
const cnt = new Blob([xml], {type: 'text/xml'});
const url = window.URL.createObjectURL(cnt);
The locator from the variable url
is then included in an anchor element <a href="">
to allow the user to save the file.
Angular will prevent a raw URL generated with the createObjectURL
method from working if used as is by adding the prefix unsafe:
.
To avoid this, one has to inject the DomSanitizer
utility as
shown in the blob-save.component.ts component.
It will generate a SafeUrl
object which can then be used in
the HTML view: see blob-save.component.html.
The storage space needs to be freed up with the method window.URL.revokeObjectURL
to avoid memory leaks.
This call is made by the ngOnDestroy
method in
blob-save.component.ts.
Check the demo.
The user experience from the demo displays the progress and the option to save on the same page. This would need to be included in a pop-up for a real app.
Because your workstation cannot reach the real implementations, you need to substitue calls to real APIs by calls to a fake API layer that will mimic the real one.
A mock layer is supposed to be simple and does not intend to replace the real thing. It is more to allow to carry on some work despite not having access to the real backend.
Using Angular's Modules, one will encapsulate backend calls and will configure the Dependency Injection to use either the module with real implementations or the mock layer.
Please access the very impressive demo :).
What you see is a very simple string returned by a Mock implementation.
The switch between the real or mocks is done in the app.module
.
This will configure the backend layer module called mocking to use the real implementation:
imports: [
BrowserModule,
AppRoutingModule,
MockingModule.forRoot()
],
Changing forRoot()
to forMocks()
will run the backend layer with mocks.
Run ng serve
for a dev server. Navigate to http://localhost:4200/
. The app will automatically reload if you change any of the source files.
Run ng generate component component-name
to generate a new component. You can also use ng generate directive|pipe|service|class|guard|interface|enum|module
.
Run ng build
to build the project. The build artifacts will be stored in the dist/
directory. Use the -prod
flag for a production build.
Run ng test
to execute the unit tests via Karma.
Run ng e2e
to execute the end-to-end tests via Protractor.
To get more help on the Angular CLI use ng help
or go check out the Angular CLI README.