Skip to content

Commit 4bad8b7

Browse files
Added documentation for loading features with small api improvements
1 parent b710c2f commit 4bad8b7

12 files changed

Lines changed: 260 additions & 38 deletions

File tree

apps/playground/src/app/core/core.routes.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,5 @@ export const CORE_ROUTES: Routes = [{
1111
}, {
1212
path: 'home',
1313
component: HomeComponent,
14-
canActivate: [ () => whenTrue(inject(HomeStore).homeVmState.initiallyLoaded) ]
14+
canActivate: [ () => whenTrue(inject(HomeStore).homeVmState.isLoaded) ]
1515
}];

apps/playground/src/app/flight/flight.routes.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,5 @@ export const FLIHGT_ROUTES: Routes = [{
2828
}, {
2929
path: "create",
3030
component: FlightCreateComponent,
31-
canActivate: [() => whenTrue(inject(FlightCreateStore).flightCreateVmState.initiallyLoaded)]
31+
canActivate: [() => whenTrue(inject(FlightCreateStore).flightCreateVmState.isLoaded)]
3232
}];

doc/docs/guide/01-getting-started.md

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ export const initialFlight: Flight = {
114114
```
115115

116116
## 1. Load a Resource from the Backend into the Store
117-
To load a resource from the backend into the NgRx Signal Store you can use the `withHypermediaResource` feature from **ngrx-hateoas**. This feature adds state, metastate and methods to the store to work with this resource. The following snippet shows how to set up a signal store for the flight resource:
117+
To load a resource from the backend into the NgRx Signal Store you can use the `withHypermediaResource(<ModelName>, <InitialState>)` feature from **ngrx-hateoas**. This feature adds state, metastate and methods to the store to work with this resource. The following snippet shows how to set up a signal store for the flight resource:
118118

119119
```ts
120120
import { signalStore } from '@ngrx/signals';
@@ -126,21 +126,21 @@ export const FlightEditStore = signalStore(
126126
);
127127
```
128128

129-
The store can be injected in your Angular component, a router activator or a router resolver. The `withHypermediaResource` add a method called `load<ModelName>FromUrl` which load the resource from the backend into the store. In our case the model name is `flightModel`. Also a property called `flightModel` is added to the store which holds the current state of the resource in form a NgRx deep signal. The following code snippet shows how to inject the store, load data from the backend and access the singals holding the data;
129+
The store can be injected in your Angular component, a router activator or a router resolver. The `withHypermediaResource` adds a method called `load<ModelName>FromUrl` which loads the resource from the backend into the store. In our case the model name is `flightModel`. Also a property named with the provided `<ModelName>`, in our case `flightModel` is added to the store which holds the current state of the resource in form a NgRx deep signal. The following code snippet shows how to inject the store, load data from the backend and access the singals holding the data;
130130

131131
```ts
132132
// Inject the store using Angular's inject function
133133
store = inject(FlightEditStore);
134134

135135
// Load the flight resource from the backend into the store via an async call
136-
await this.store.loadFlightModelFromUrl('/api/flights/123');
136+
await store.loadFlightModelFromUrl('/api/flights/123');
137137

138138
// Access signals holding data from the store
139-
const idSignal = this.store.flightModel.id;
140-
const connectionSignal = this.store.flightModel.connection;
139+
const idSignal = store.flightModel.id;
140+
const connectionSignal = store.flightModel.connection;
141141

142142
```
143-
The `withHypermediaResource` feature also adds a property called `<modelName>State` to the store which holds metastate information about the resource. This can be used to e.g. show loading spinners in the UI while the resource is loaded from the backend. The following code snippet shows how to access some of the metastate signals:
143+
The `withHypermediaResource` feature also adds a property called `<ModelName>State` to the store which holds metastate information about the resource. This can be used to e.g. show loading spinners in the UI while the resource is loaded from the backend. The following code snippet shows how to access some of the metastate signals:
144144

145145
```ts
146146
// A signal toggling between true and false to indicate if the
@@ -194,8 +194,12 @@ export const FlightEditStore = signalStore(
194194

195195
You can now set a new state of the connection object by using the `writableFlightConnection` signal on the store like this: `store.writableFlightConnection.set(<new value>)`.
196196

197+
:::info
198+
The store properties created by the `withWritableStateCopy` feature are writable signals and are deep signals at the same time. Therefore you can get fine grained signals from the state copy the same way as you get them from the store state itself.
199+
:::
200+
197201
### Option 3: Use a Deep Writable State Copy
198-
In case you want want to bind your state to a template driven form you can use the `withExperimentalDeepWritableStateCopy` feature from **ngrx-hateoas**. While `withWritableStateCopy` creates a writable signal for the root object only, the `withExperimentalDeepWritableStateCopy` feature creates a deep writable signal which allows you to set each key of the object individually. This is especiall helpfull in cases where you want to bind your data to a form with the help of `ngModel`. The following code snippet shows how to set up the store with this feature:
202+
While `withWritableStateCopy` creates a writable signal for the root object only, the `withExperimentalDeepWritableStateCopy` feature creates a deep writable signal which allows you to set each key of the object individually. This is especiall helpfull in cases where you want to bind your data to a form with the help of `ngModel`. The following code snippet shows how to set up the store with this feature:
199203

200204
```ts
201205
import { signalStore, withMethods, patchState } from "@ngrx/signals";
@@ -211,14 +215,14 @@ export const FlightEditStore = signalStore(
211215
);
212216
```
213217

214-
You can now set a new state of the connection object by using the `writableFlightConnection` signal on the store like this: `store.writableFlightConnection.from.set('<new value>')`.
218+
You can now set a new state of individual prperties of the connection object by using the `writableFlightConnection` signal on the store like this: `store.writableFlightConnection.from.set('<new value>')`.
215219

216220
:::info
217221
This feature is currently experimental and might change in future releases.
218222
:::
219223

220224
### Option 4: Use a Deep Writable State Delegate
221-
If you want to have a writalbe signal in the interface of your store which directly modifies the state in the store you can use the `withExperimentalDeepWritableStateDelegate` feature from **ngrx-hateoas**. This feature creates a writable signal graph which directly modifies the state in the store when you call `set` on it. Because it is a deep writable it works well with template driven forms as well as signal firms. The following code snippet shows how to set up the store with this feature:
225+
If you want to have a writalbe signal in the interface of your store which directly modifies the state in the store you can use the `withExperimentalDeepWritableStateDelegate` feature from **ngrx-hateoas**. This feature creates a writable signal graph which directly modifies the state in the store when you call `set` on it. Because it is a deep writable it works well with template driven forms as well as signal forms. The following code snippet shows how to set up the store with this feature:
222226

223227
```ts
224228
import { signalStore, withMethods, patchState } from "@ngrx/signals";
@@ -263,7 +267,7 @@ To send changed state back to the server you can use the `withHypermediaAction`
263267
The `_actions` key holds the metadata for the actions available for this object. In this case there is an action called `update` which uses the HTTP verb `PUT` to send the updated connection object back to the server to the specified URL in the `href` property.
264268

265269
:::info
266-
If your metadata have different names or structure you can customize this via the `HateoasConfig` when you provide the **ngrx-hateoas** services in your application. For this have a look into the [Metadata Provider](./configuration/01-metadata-provider.md) section.
270+
If your metadata has different names or a different structure you can customize this via the `HateoasConfig` when you provide the **ngrx-hateoas** services in your application. For this have a look into the [Metadata Provider](./configuration/01-metadata-provider.md) section.
267271
:::
268272

269273
The following code snippet shows how to set up the store with an action that sends back the changed connection object to the server using the metadata provided in the resource:
@@ -282,7 +286,7 @@ export const FlightEditStore = signalStore(
282286
withHypermediaAction('updateFlightConnection', store => store.writableFlightConnection, 'update')
283287
);
284288
```
285-
Through the `withHypermediaAction` feature a method called `updateFlightConnection` is added to the store which can be called to send the connection object pointed to by the lambda in the second argument back to the server. The following code snippet shows how to call this method:
289+
Through the `withHypermediaAction(<MethodName>, <LambdaToAffectedState>, <ActionName>)` feature a method called `updateFlightConnection` is added to the store which can be called to send the connection object pointed to by the lambda in the second argument back to the server. The following code snippet shows how to call this method:
286290

287291
```ts
288292
await this.store.updateFlightConnection();
@@ -357,7 +361,7 @@ export const FlightEditStore = signalStore(
357361
```
358362

359363
The store, as shown in the previous code sample is able to do the following things:
360-
* read the JSON from backend into the store and provide it as state by providing a url
364+
* read the JSON from backend into the store and provide it as reactive deep signal
361365
* rectively read a linked json object triggered by changes from the main object
362366
* provide writable copies of parts of the state
363367
* provide metastate about the requests to the backend (e.g. such as if a request is currently running)

doc/docs/guide/02-concept.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ You don't need to follow the exact same layout as in the example to embed metada
8686

8787
## What is HATEOAS?
8888

89-
HATEOAS (Hypermedia as the Engine of Application State) is an architectural paradigm that allows clients to dynamically navigate resources through hyperlinks provided in responses. Possible state changes are provided via actions. Hyperlinks and actions are metadata sent next to the actual payload. Instead of hardcoding API endpoints, clients rely on these links to discover available actions and related resources. For example, if a user is not allowed to navigate to a linked resource or execute an action, the server would not send this metainformation to the client within its response. Finally, the client has the full state of the resource available, the actual payload, and information to related data and possible actions (or state changes). This approach decouples the client from the server, keeps domain logic away from the client and enables more flexible and evolvable APIs.
89+
HATEOAS (Hypermedia as the Engine of Application State) is an architectural paradigm that allows clients to dynamically navigate resources through hyperlinks provided in responses. Possible state changes are provided via actions. Hyperlinks and actions are metadata sent next to the actual payload. Instead of hardcoding API endpoints, clients rely on these links to discover available actions and related resources. For example, if a user is not allowed to navigate to a linked resource or execute an action, the server would not send this metainformation to the client within its response. Finally, the client has the full state of the resource available, the actual payload and information to related data and possible actions (or state changes). This approach decouples the client from the server, keeps domain logic away from the client and enables more flexible and evolvable APIs.
9090

9191
## Hypermedia at the Backend
9292
To create hypermedia responses you can use community libraries for the different technologies:

doc/docs/guide/configuration/01-metadata-provider.md

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,11 @@ To use a custom hypermedia JSON format with **ngrx-hateoas** you have to impleme
6767
export interface MetadataProvider {
6868
isMetadataKey(keyName: string): boolean;
6969
linkLookup(resource: unknown, linkName: string): ResourceLink | undefined;
70+
getAllLinks(resource: unknown): ResourceLink[];
7071
actionLookup(resource: unknown, actionName: string): ResourceAction | undefined;
72+
getAllActions(resource: unknown): ResourceAction[];
7173
socketLookup(resource: unknown, socketName: string): ResourceSocket | undefined;
74+
getAllSockets(resource: unknown): ResourceSocket[];
7275
}
7376
```
7477

@@ -94,7 +97,7 @@ Lets imagine our hypermedia JSON looks like the following:
9497
"_metadata": {
9598
"_link_aLinkToAResource": { "href": "/api/..." },
9699
"_action_anActionOnSubobject": { "href": "/api/...", "verb": "PUT" },
97-
"_sockets_aSocketForSubobject": { "href": "/api/...", "event": "newMessageForSubobject" }
100+
"_socket_aSocketForSubobject": { "href": "/api/...", "event": "newMessageForSubobject" }
98101
}
99102
},
100103
"_metadata": {
@@ -115,15 +118,39 @@ const customMetadataProvider: MetadataProvider = {
115118
linkLookup(resource: unknown, linkName: string): ResourceLink | undefined {
116119
return resource['_metadata']?.['_link_' + linkName];
117120
},
121+
getAllLinks(resource: unknown): ResourceLink[]; {
122+
const linksMetadata = resource['_metadata'];
123+
if(!linksMetadata) return [];
124+
return Object.keys(linksMetadata)
125+
.filter(key => key.startsWith('_link_'))
126+
.map(key => linksMetadata[key]);
127+
},
118128
actionLookup(resource: unknown, actionName: string): ResourceAction | undefined {
119129
const actionMetadata = resource['_metadata']?.['_action_' + actionName];
120130
// ResourceAction has the two keys href and method,
121131
// therefore we have to bring the metadata into the correct format
122132
if(actionMetadata) return { href: actionMetadata.href, method: actionMetadata.verb };
123133
else return undefined;
124134
},
135+
getAllActions(resource: unknown): ResourceAction[] {
136+
const actionsMetadata = resource['_metadata'];
137+
if(!actionsMetadata) return [];
138+
return Object.keys(actionsMetadata)
139+
.filter(key => key.startsWith('_action_'))
140+
.map(key => {
141+
const actionMetadata = actionsMetadata[key];
142+
return { href: actionMetadata.href, method: actionMetadata.verb };
143+
});
144+
},
125145
socketLookup(resource: unknown, socketName: string): ResourceSocket | undefined {
126146
return resource['_metadata']?.['_socket_' + socketName];
147+
},
148+
getAllSockets(resource: unknown): ResourceSocket[] {
149+
const socketsMetadata = resource['_metadata'];
150+
if(!socketsMetadata) return [];
151+
return Object.keys(socketsMetadata)
152+
.filter(key => key.startsWith('_socket_'))
153+
.map(key => socketsMetadata[key]);
127154
}
128155
}
129156
```
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
---
2+
sidebar_position: 1
3+
---
4+
5+
# withHypermediaResource
6+
Creates a resource in the store.
7+
8+
## API
9+
```ts
10+
function withHypermediaResource<ResourceName extends string, TResource>(
11+
resourceName: ResourceName, initialValue: TResource): SignalStoreFeature;
12+
```
13+
14+
* **resourceName**: The name of how the resource will be declared in the store.
15+
* **initialValue**: The initial value of the resource before it is loaded from a URL.
16+
17+
## State
18+
With this feature the following state properties are added to the interface of the store:
19+
20+
### Resource State
21+
```ts
22+
<resourceName>: DeepSignal<TResource>
23+
```
24+
A deep signal containing the data of the resource.
25+
26+
### Resource Meta State
27+
```ts
28+
<resourceName>State: DeepSignal<
29+
{
30+
url: string,
31+
isLoading: boolean,
32+
isLoaded: boolean
33+
}>
34+
```
35+
A deep signal containing meta information about the resource.
36+
37+
* **url**: The URL from which the resource was last loaded.
38+
* **isLoading**: Whether the resource is currently being loaded.
39+
* **isLoaded**: Whether the resource currently in the state was loaded from the server.
40+
41+
## Methods
42+
43+
With this feature the following methods are added to the interface of the store:
44+
45+
### Load the Resource from an URL
46+
```ts
47+
load<resourceName>FromUrl(url: string | null, fromCache?: boolean): Promise<void>
48+
```
49+
Loads the resource from the provided URL.
50+
51+
* **url**: The URL from which the resource should be loaded. If `null` is provided the resource is reset to its initial value.
52+
* **fromCache**: Whether to load the resource even if the the current state is loaded from the same URL. If not provided, defaults to `false`.
53+
54+
### Load the Resource from a Link
55+
```ts
56+
load<resourceName>FromLink(linkRoot: unknown, linkName: string) => Promise<void>
57+
```
58+
Loads the resource from the provided URL.
59+
60+
* **linkRoot**: An object containing the link to use.
61+
* **linkName**: The name of the link to use to load the resource.
62+
63+
### Reload the Resource
64+
```ts
65+
reload<resourceName>(): Promise<void>
66+
```
67+
Reloads the resource from the last used URL.
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
---
2+
sidebar_position: 2
3+
---
4+
5+
# withInitialHypermediaResource
6+
Creates a resource in the store which gets automatically loaded when an instance of the store is created.
7+
8+
## API
9+
```ts
10+
function withInitialHypermediaResource<ResourceName extends string, TResource>(
11+
resourceName: ResourceName, initialValue: TResource, url: string | (() => string)): SignalStoreFeature;
12+
```
13+
14+
* **resourceName**: The name of how the resource will be declared in the store.
15+
* **initialValue**: The initial value of the resource before it is loaded from a URL.
16+
* **url**: The URL from which the resource will be loaded. Can also be a function returning a string. If a function is provided, it will be executed at the time the store is created.
17+
18+
## State
19+
With this feature the following state properties are added to the interface of the store:
20+
21+
### Resource State
22+
```ts
23+
<resourceName>: DeepSignal<TResource>
24+
```
25+
A deep signal containing the data of the resource.
26+
27+
### Resource Meta State
28+
```ts
29+
<resourceName>State: DeepSignal<
30+
{
31+
url: string,
32+
isLoading: boolean,
33+
isLoaded: boolean
34+
}>
35+
```
36+
A deep signal containing meta information about the resource.
37+
38+
* **url**: The URL from which the resource was last loaded.
39+
* **isLoading**: Whether the resource is currently being loaded.
40+
* **isLoaded**: Whether the resource currently in the state was loaded from the server.
41+
42+
## Methods
43+
44+
With this feature the following methods are added to the interface of the store:
45+
46+
### Load the Resource from an URL
47+
```ts
48+
load<resourceName>FromUrl(url: string | null, fromCache?: boolean): Promise<void>
49+
```
50+
Loads the resource from the provided URL.
51+
52+
* **url**: The URL from which the resource should be loaded. If `null` is provided the resource is reset to its initial value.
53+
* **fromCache**: Whether to load the resource even if the the current state is loaded from the same URL. If not provided, defaults to `false`.
54+
55+
### Load the Resource from a Link
56+
```ts
57+
load<resourceName>FromLink(linkRoot: unknown, linkName: string) => Promise<void>
58+
```
59+
Loads the resource from the provided URL.
60+
61+
* **linkRoot**: An object containing the link to use.
62+
* **linkName**: The name of the link to use to load the resource.
63+
64+
### Reload the Resource
65+
```ts
66+
reload<resourceName>(): Promise<void>
67+
```
68+
Reloads the resource from the last used URL.

0 commit comments

Comments
 (0)