Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/deploy-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: 20.x
node-version: 22.x

# Restore dependencies
- name: Restore dependencies for lib
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ngrx-hateoas.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: 20.x
node-version: 22.x

# Restore dependencies
- name: Restore dependencies
Expand Down
2 changes: 1 addition & 1 deletion apps/playground/src/app/core/core.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ export const CORE_ROUTES: Routes = [{
}, {
path: 'home',
component: HomeComponent,
canActivate: [ () => whenTrue(inject(HomeStore).homeVmState.initiallyLoaded) ]
canActivate: [ () => whenTrue(inject(HomeStore).homeVmState.isLoaded) ]
}];
2 changes: 1 addition & 1 deletion apps/playground/src/app/flight/flight.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,5 @@ export const FLIHGT_ROUTES: Routes = [{
}, {
path: "create",
component: FlightCreateComponent,
canActivate: [() => whenTrue(inject(FlightCreateStore).flightCreateVmState.initiallyLoaded)]
canActivate: [() => whenTrue(inject(FlightCreateStore).flightCreateVmState.isLoaded)]
}];
343 changes: 218 additions & 125 deletions doc/docs/guide/01-getting-started.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion doc/docs/guide/02-concept.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ You don't need to follow the exact same layout as in the example to embed metada

## What is HATEOAS?

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.
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.

## Hypermedia at the Backend
To create hypermedia responses you can use community libraries for the different technologies:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,11 @@ To use a custom hypermedia JSON format with **ngrx-hateoas** you have to impleme
export interface MetadataProvider {
isMetadataKey(keyName: string): boolean;
linkLookup(resource: unknown, linkName: string): ResourceLink | undefined;
getAllLinks(resource: unknown): ResourceLink[];
actionLookup(resource: unknown, actionName: string): ResourceAction | undefined;
getAllActions(resource: unknown): ResourceAction[];
socketLookup(resource: unknown, socketName: string): ResourceSocket | undefined;
getAllSockets(resource: unknown): ResourceSocket[];
}
```

Expand All @@ -94,7 +97,7 @@ Lets imagine our hypermedia JSON looks like the following:
"_metadata": {
"_link_aLinkToAResource": { "href": "/api/..." },
"_action_anActionOnSubobject": { "href": "/api/...", "verb": "PUT" },
"_sockets_aSocketForSubobject": { "href": "/api/...", "event": "newMessageForSubobject" }
"_socket_aSocketForSubobject": { "href": "/api/...", "event": "newMessageForSubobject" }
}
},
"_metadata": {
Expand All @@ -115,15 +118,39 @@ const customMetadataProvider: MetadataProvider = {
linkLookup(resource: unknown, linkName: string): ResourceLink | undefined {
return resource['_metadata']?.['_link_' + linkName];
},
getAllLinks(resource: unknown): ResourceLink[]; {
const linksMetadata = resource['_metadata'];
if(!linksMetadata) return [];
return Object.keys(linksMetadata)
.filter(key => key.startsWith('_link_'))
.map(key => linksMetadata[key]);
},
actionLookup(resource: unknown, actionName: string): ResourceAction | undefined {
const actionMetadata = resource['_metadata']?.['_action_' + actionName];
// ResourceAction has the two keys href and method,
// therefore we have to bring the metadata into the correct format
if(actionMetadata) return { href: actionMetadata.href, method: actionMetadata.verb };
else return undefined;
},
getAllActions(resource: unknown): ResourceAction[] {
const actionsMetadata = resource['_metadata'];
if(!actionsMetadata) return [];
return Object.keys(actionsMetadata)
.filter(key => key.startsWith('_action_'))
.map(key => {
const actionMetadata = actionsMetadata[key];
return { href: actionMetadata.href, method: actionMetadata.verb };
});
},
socketLookup(resource: unknown, socketName: string): ResourceSocket | undefined {
return resource['_metadata']?.['_socket_' + socketName];
},
getAllSockets(resource: unknown): ResourceSocket[] {
const socketsMetadata = resource['_metadata'];
if(!socketsMetadata) return [];
return Object.keys(socketsMetadata)
.filter(key => key.startsWith('_socket_'))
.map(key => socketsMetadata[key]);
}
}
```
67 changes: 67 additions & 0 deletions doc/docs/guide/04-loading_features/01-withHypermediaResource.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
---
sidebar_position: 1
---

# withHypermediaResource
Creates a resource in the store.

## API
```ts
function withHypermediaResource<ResourceName extends string, TResource>(
resourceName: ResourceName, initialValue: TResource): SignalStoreFeature;
```

* **resourceName**: The name of how the resource will be declared in the store.
* **initialValue**: The initial value of the resource before it is loaded from a URL.

## State
With this feature the following state properties are added to the interface of the store:

### Resource State
```ts
<resourceName>: DeepSignal<TResource>
```
A deep signal containing the data of the resource.

### Resource Meta State
```ts
<resourceName>State: DeepSignal<
{
url: string,
isLoading: boolean,
isLoaded: boolean
}>
```
A deep signal containing meta information about the resource.

* **url**: The URL from which the resource was last loaded.
* **isLoading**: Whether the resource is currently being loaded.
* **isLoaded**: Whether the resource currently in the state was loaded from the server.

## Methods

With this feature the following methods are added to the interface of the store:

### Load the Resource from an URL
```ts
load<resourceName>FromUrl(url: string | null, fromCache?: boolean): Promise<void>
```
Loads the resource from the provided URL.

* **url**: The URL from which the resource should be loaded. If `null` is provided the resource is reset to its initial value.
* **fromCache**: Whether to load the resource even if the the current state is loaded from the same URL. If not provided, defaults to `false`.

### Load the Resource from a Link
```ts
load<resourceName>FromLink(linkRoot: unknown, linkName: string): Promise<void>
```
Loads the resource from the provided URL.

* **linkRoot**: An object containing the link to use.
* **linkName**: The name of the link to use to load the resource.

### Reload the Resource
```ts
reload<resourceName>(): Promise<void>
```
Reloads the resource from the last used URL.
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
---
sidebar_position: 2
---

# withInitialHypermediaResource
Creates a resource in the store which gets automatically loaded when an instance of the store is created.

## API
```ts
function withInitialHypermediaResource<ResourceName extends string, TResource>(
resourceName: ResourceName, initialValue: TResource, url: string | (() => string)): SignalStoreFeature;
```

* **resourceName**: The name of how the resource will be declared in the store.
* **initialValue**: The initial value of the resource before it is loaded from a URL.
* **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.

## State
With this feature the following state properties are added to the interface of the store:

### Resource State
```ts
<resourceName>: DeepSignal<TResource>
```
A deep signal containing the data of the resource.

### Resource Meta State
```ts
<resourceName>State: DeepSignal<
{
url: string,
isLoading: boolean,
isLoaded: boolean
}>
```
A deep signal containing meta information about the resource.

* **url**: The URL from which the resource was last loaded.
* **isLoading**: Whether the resource is currently being loaded.
* **isLoaded**: Whether the resource currently in the state was loaded from the server.

## Methods

With this feature the following methods are added to the interface of the store:

### Load the Resource from an URL
```ts
load<resourceName>FromUrl(url: string | null, fromCache?: boolean): Promise<void>
```
Loads the resource from the provided URL.

* **url**: The URL from which the resource should be loaded. If `null` is provided the resource is reset to its initial value.
* **fromCache**: Whether to load the resource even if the the current state is loaded from the same URL. If not provided, defaults to `false`.

### Load the Resource from a Link
```ts
load<resourceName>FromLink(linkRoot: unknown, linkName: string): Promise<void>
```
Loads the resource from the provided URL.

* **linkRoot**: An object containing the link to use.
* **linkName**: The name of the link to use to load the resource.

### Reload the Resource
```ts
reload<resourceName>(): Promise<void>
```
Reloads the resource from the last used URL.
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
---
sidebar_position: 3
---

# withLinkedHypermediaResource
Creates a resource in the store which loads depending on a signal pointing to a different resource. Each time the linked resource changes the resource will be reloaded automatically.

## API
```ts
function withLinkedHypermediaResource<ResourceName extends string, TResource, Input extends SignalStoreFeatureResult>(
resourceName: ResourceName, initialValue: TResource, linkRootFn: ResourceLinkRootFn<Input>, linkMetaName: string
): SignalStoreFeature;
```

* **resourceName**: The name of how the resource will be declared in the store.
* **initialValue**: The initial value of the resource before it is loaded from a URL.
* **linkRootFn**: A function which receives the store instance and returns a signal to an object containing the link to use to load the resource.
* **linkMetaName**: The name of the link to use to load the resource.

## State
With this feature the following state properties are added to the interface of the store:

### Resource State
```ts
<resourceName>: DeepSignal<TResource>
```
A deep signal containing the data of the resource.

### Resource Meta State
```ts
<resourceName>State: DeepSignal<
{
url: string,
isLoading: boolean,
isLoaded: boolean,
isAvailable: boolean
}>
```
A deep signal containing meta information about the resource.

* **url**: The URL from which the resource was last loaded.
* **isLoading**: Whether the resource is currently being loaded.
* **isLoaded**: Whether the resource currently in the state was loaded from the server.
* **isAvailable**: Whether the resource is available to be loaded (means whether the link exists on the linked resource).

## Methods

With this feature the following methods are added to the interface of the store:

### Reload the Resource
```ts
reload<resourceName>(): Promise<void>
```
Reloads the resource from the last used URL.
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
sidebar_position: 2
---

# withWritableStateCopy
Creates a writable copy of selected state signals. The copy is linked to the original signals. If the state original signals change, the writable copy will reflect those changes. Writing to the writable copy will not update the original state signals. Furthermore the wiritable copy is also a deep signal so that you get fine grained reactivity when working with your copy.

## API
```ts
function withWritableStateCopy<Input extends SignalStoreFeatureResult, StateSelection extends ObjectWithSignalsForStateCopy>(
stateMapFn: (store: StateSignals<Input['state']>) => StateSelection
): SignalStoreFeature;
```

* **stateMapFn**: A selector function that receives the store's state signals and returns an object describing which signals (and nested signal objects) should be exposed as a writable copy. The object can also contain nested objects, which will be recursively mapped. The result of the function shall comply with the following type definition:

```ts
type ObjectWithSignalsForStateCopy = {
[key: string]: Signal<unknown> | ObjectWithSignalsForStateCopy;
}
```

## Props
This feature adds a set of properties to the interface of the store that are created with the following type definition:

```ts
type WritableStateCopy<State extends ObjectWithSignalsForStateCopy> = {
[Key in keyof State]: State[Key] extends ObjectWithSignalsForStateCopy ?
WritableStateCopy<State[Key]>
: State[Key] extends Signal<infer InnerType> ?
WritableDeepSignal<InnerType> : never;
};
```
In short this means that each signal you put to the `ObjectWithSignalsForStateCopy` will be mapped to a `WritableDeepSignal<InnerType>`, where `InnerType` is the type contained in the original signal. Nested objects will be recursively mapped to the same shape.

Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
sidebar_position: 2
---

# withExperimentalDeepWritableStateCopy
Creates a writable copy of selected state signals. The copy is linked to the original signals. If the original state signals change, the writable copy will reflect those changes. Writing to the writable copy will not update the original state signals. Furthermore the wiritable copy is also a deep writable signal so that you get fine grained reactivity when working with your copy and additionally you can write deeply nested properties.

:::warning
This feature is experimental and might change or be removed in future versions.
:::

## API
```ts
function withExperimentalDeepWritableStateCopy<Input extends SignalStoreFeatureResult, StateSelection extends ObjectWithSignalsForStateCopy>(
stateMapFn: (store: StateSignals<Input['state']>) => StateSelection
): SignalStoreFeature;
```

* **stateMapFn**: A selector function that receives the store's state signals and returns an object describing which signals (and nested signal objects) should be exposed as a deep writable copy. The object can also contain nested objects, which will be recursively mapped. The result of the function shall comply with the following type definition:

```ts
type ObjectWithSignalsForDeepStateCopy = {
[key: string]: Signal<unknown> | ObjectWithSignalsForDeepStateCopy;
}
```

## Props
This feature adds a set of properties to the interface of the store that are created with the following type definition:

```ts
type DeepWritableStateCopy<State extends ObjectWithSignalsForDeepStateCopy> = {
[Key in keyof State]: State[Key] extends ObjectWithSignalsForDeepStateCopy ?
DeepWritableStateCopy<State[Key]>
: State[Key] extends Signal<infer InnerType> ?
DeepWritableSignal<InnerType> : never;
};
```
In short this means that each signal you put to the `ObjectWithSignalsForDeepStateCopy` will be mapped to a `DeepWritableSignal<InnerType>`, where `InnerType` is the type contained in the original signal. Nested objects will be recursively mapped to the same shape.

Loading