This is a Hypertext Application Language (HAL) client to be used in Angular projects.
Using npm:
$ npm install @granito/ngx-hal-client --save
$ git clone https://github.com/granito-source/ngx-hal-client.git
$ cd ngx-hal-client
$ npm install
$ npm run build
$ npm tests
For illustrative purposes, let's assume that your Angular application needs to work with the following HAL API.
The root entry point is /api/v1
. When one executes GET
on this URI,
the returned object looks like
{
"apiVersion": "1.7.21",
"_links": {
"self": {
"href": "/api/v1"
},
"messages": {
"href": "/api/v1/messages"
}
}
}
In other words, it exposes the implementation version and declares one
link to work with a collection of messages. When one executes GET
on
/api/v1/messages
the API returns a collection page like this
{
"start": 0,
"_links": {
"self": {
"href": "/api/v1/messages?start=0"
},
"next": {
"href": "/api/v1/messages?start=2"
}
},
"_embedded": {
"selected" : {
"id": 0,
"text": "Then what do they call it?",
"_links": {
"self": {
"href": "/api/v1/messages/0"
}
}
},
"items": [
{
"id": 0,
"text": "Then what do they call it?",
"_links": {
"self": {
"href": "/api/v1/messages/0"
},
"next": {
"href": "/api/v1/messages/1"
},
}
},
{
"id": 1,
"text": "They call it Royale with Cheese.",
"_links": {
"self": {
"href": "/api/v1/messages/1"
},
"prev": {
"href": "/api/v1/messages/0"
}
}
}
]
}
}
HAL Client uses Angular HTTP Client. You have to configure HTTP Client
using dependency injection before HAL Client can be used. The modern
way of doing it using providers
in app.config.ts
is shown below.
import { provideHttpClient, withFetch } from '@angular/common/http';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(withFetch())
]
};
For other ways please refer to Setting up HttpClient in the Angular documentation.
Then you need to have some resources defined, e.g. the root resource for
your API hierarchy. If your root resource does not have any properties,
you can just use Resource
. Otherwise, like in our sample API, extend
Resource
class as needed.
import { Resource } from '@granito/ngx-hal-client';
export class ApiRoot extends Resource {
readonly apiVersion!: string;
}
and
import { Resource } from '@granito/ngx-hal-client';
export class Message extends Resource {
readonly id!: number;
readonly text!: string;
withText(text: string): Message {
return this.clone({ text });
}
}
Collections are represented by Collection
resource.
HAL Client defines Accessor
object to give access to HAL resources.
In order to access the root entry point of the API, you need to get
its accessor first using HalClientService
and then read the API root
resource. Normally you would keep the root entry point of the API as
your application's state, e.g. in a ReplaySubject
.
import { Injectable } from '@angular/core';
import { HalClientService } from '@granito/ngx-hal-client';
import { Observable, ReplaySubject } from 'rxjs';
import { ApiRoot } from './api-root';
@Injectable({ providedIn: 'root' })
export class ApiRootService {
private readonly apiRoot$ = new ReplaySubject<ApiRoot>(1);
get apiRoot(): Observable<ApiRoot> {
return this.apiRoot$.asObservable();
}
constructor(client: HalClientService) {
client.root('/api/v1').read(ApiRoot).subscribe(
api => this.apiRoot$.next(api));
}
}
Once you have at least one resource instance, e.g. the API root from the example above, you can follow links available in the resource to get accessors for the linked resources.
@Injectable({ providedIn: 'root' })
export class MessageService {
private messages$: Observable<Accessor | undefined>;
constructor(apiRootService: ApiRootService) {
this.messages$ = apiRootService.apiRoot.pipe(
follow('messages')
);
}
}
In the example above, messages$
observable will emit either the
accessor to the collection of messages or undefined if the API root
does not have messages
link.
Accessor
and Resource
objects allow you to execute CRUD operations
using HAL API. Some operations are defined on both objects and work
identically, some are defined only on Resource
, and some are available
in both but have a bit different syntax and semantic.
Now that you have an accessor for the collection of messages, you can read the collection and access its elements.
readFirst(): Observable<Message | undefined> {
return this.messages$.pipe(
take(1),
readCollection(Message),
map(collection => collection?.values[0])
);
}
The example above shows how to access the first message in the collection if it exists.
Messages are resources, and they may have their own links. You can read resources referenced by these links as shown below.
private current$: Observable<Message>;
...
readNext(): Observable<Message> {
return this.current$.pipe(
take(1),
follow('next'),
read(Message)
);
}
The example shows how to access the next message in the thread if it exists.
The library provides refresh()
RxJS operator to execute a read
operation on the resource using its self
link. For example, here
is how you can refresh the API root in ApiRootService
.
refresh(): void {
this.apiRoot$.pipe(
take(1),
refresh()
).subscribe(api => this.apiRoot$.next(api));
}
To create a new message, you can use create()
operator.
post(message: { text: string; }): Observable<Accessor | undefined> {
return this.messages$.pipe(
take(1),
create(message)
);
}
Two things are worth mentioning here. First, the object passed to
create()
operator does not have to be a Resource
. Second, if the
POST
operation returns the URI for the newly created resource
in the Location
header, then the observable will emit an accessor
for this resource. You can use it to read the resource immediately after
it is created, e.g. by using read()
operator.
Resource
instances can be updated in the API by using update()
operator.
private current$: Observable<Message>;
...
edit(text: string): Observable<Message> {
return this.current$.pipe(
take(1),
update(message => message.withText(text)),
read(Message)
);
}
On successful completion the observable will emit an accessor for the resource, which can be used to obtain a fresh copy of it from the API.
And finally, resources and collections can be deleted using del()
operator.
private current$: Observable<Message>;
...
deleteCurrent(): Observable<void> {
return this.current$.pipe(
take(1),
del()
);
}
Resource
offers two methods to access embedded HAL resources. In order
to access embedded arrays, you can use getArray()
method.
array(messages: Collection<Message>): Message[] | undefined {
return messages.getArray(Message, 'items');
}
To get a single embedded object, you can use get()
method.
one(messages: Collection<Message>): Message | undefined {
return messages.get(Message, 'selected');
}
Note, Collection
is a subclass of Resource
, so both methods can be
used just fine. Also, it is worth mentioning, that using get()
on an
embedded array will return the first element, and using getArray()
on
a single embedded resource will return an array containing the resource.
Generated using TypeDoc