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
11 changes: 10 additions & 1 deletion apps/playground/src/app/flight/flight-edit/flight-edit.store.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { withHypermediaResource, withHypermediaAction, withWritableStateCopy, withExperimentalDeepWritableStateCopy, withExperimentalDeepWritableStateDelegate } from "@angular-architects/ngrx-hateoas";
import { withHypermediaResource, withHypermediaAction, withWritableStateCopy, withExperimentalDeepWritableStateCopy, withExperimentalDeepWritableStateDelegate, withDeepWritableStateProjection } from "@angular-architects/ngrx-hateoas";
import { signalStore, withMethods } from "@ngrx/signals";
import { Aircraft, Flight, initialFlight } from "../flight.entities";

Expand Down Expand Up @@ -54,3 +54,12 @@ export const FlightEditViewStore3 = signalStore(
withHypermediaAction('updateFlightConnection', store => store.flightEditVm.flight.connection, 'update'),
);

// Option 4: Reading a resource from server, creating a deep writable state projection (suited for reactive forms),
// and sending back parts of the main state to the server
export const FlightEditViewStore4 = signalStore(
{ providedIn: 'root' },
withHypermediaResource('flightEditVm', initialFlightEditVm),
withDeepWritableStateProjection(store => ({ flightFormModel: { from: store.flightEditVm.flight.connection.from, to: store.flightEditVm.flight.connection.to } })),
withHypermediaAction('updateFlightConnection', store => store.flightEditVm.flight.connection, 'update'),
);

Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
---
sidebar_position: 4
---

# withDeepWritableStateProjection

`withDeepWritableStateProjection` creates a writable view composed from arbitrary parts of the store state. Every property returned by the selector is a deep writable signal. Nested object nodes are signals as well, not just containers for their child signals.

This makes it possible to create a form-shaped view without adding a synchronized copy of the selected state.

## Usage

```ts
import { signalStore, withState } from '@ngrx/signals';
import { withDeepWritableStateProjection } from '@angular-architects/ngrx-hateoas';

const FlightStore = signalStore(
withState({
search: {
from: null as string | null,
to: null as string | null,
},
settings: {
travelClass: 'economy',
language: 'en',
},
}),
withDeepWritableStateProjection(store => ({
searchForm: {
route: {
from: store.search.from,
to: store.search.to,
},
travelClass: store.settings.travelClass,
},
})),
);
```

The projection and all of its nodes can be read as signals:

```ts
store.searchForm();
// {
// route: { from: null, to: null },
// travelClass: 'economy'
// }

store.searchForm.route();
// { from: null, to: null }

store.searchForm.route.from();
// null
```

Every node can also be written:

```ts
store.searchForm.route.from.set('Graz');

store.searchForm.route.set({
from: 'Graz',
to: 'Hamburg',
});

store.searchForm.update(form => ({
...form,
travelClass: 'business',
}));
```

Writing a structure node distributes its value to all selected state properties below that node. Writes spanning multiple state roots are applied with a single `patchState` operation. State properties that are not part of the projection remain unchanged.

Changes made to the original state are immediately reflected by the projection. The projection therefore does not introduce an independent state copy and does not need explicit synchronization or reset behavior.

## API

```ts
function withDeepWritableStateProjection<
Input extends SignalStoreFeatureResult,
Selection extends DeepWritableStateProjectionSelection,
>(
stateMapFn: (store: MappedStoreState<Input>) => Selection,
): SignalStoreFeature
```

The selector returns an object whose leaves are state signals. Objects may be nested to create the desired projection shape:

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

For every top-level property, the feature adds a `DeepWritableSignal` containing the recursively unwrapped values:

```ts
type ProjectedValue<Node> =
Node extends Signal<infer Value>
? Value
: Node extends DeepWritableStateProjectionSelection
? { [Key in keyof Node]: ProjectedValue<Node[Key]> }
: never;

type DeepWritableStateProjection<Selection> = {
[Key in keyof Selection]: DeepWritableSignal<ProjectedValue<Selection[Key]>>;
};
```

The generated writable signals support `set`, `update`, and `asReadonly` at the projection root, at every structure node, and at every selected leaf.
3 changes: 2 additions & 1 deletion doc/sidebars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ const sidebars: SidebarsConfig = {
items: [
'guide/state_mutation_features/withWritableStateCopy',
'guide/state_mutation_features/withDeepWritableStateCopy',
'guide/state_mutation_features/withDeepWritableStateDelegate'
'guide/state_mutation_features/withDeepWritableStateDelegate',
'guide/state_mutation_features/withDeepWritableStateProjection'
],
},
{
Expand Down
2 changes: 1 addition & 1 deletion libs/ngrx-hateoas/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@angular-architects/ngrx-hateoas",
"version": "21.1.0",
"version": "22.0.0-beta.0",
"peerDependencies": {
"@angular/common": "^21.0.0",
"@angular/core": "^21.0.0",
Expand Down
Loading
Loading