Skip to content

Commit dfe2834

Browse files
manfredsteyerrainerhahnekamp
authored andcommitted
feat: add httpMutation (#221) [backport v19]
Add a second mutation version, which is optimized for HTTP-based mutation, i.e., whenever someone wants to send a request to the server. It is modelled after Angular's `httpResource` and is also using the `HttpClient` under the hood.
1 parent 10aa8cb commit dfe2834

File tree

10 files changed

+987
-26
lines changed

10 files changed

+987
-26
lines changed
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
.counter {
2+
font-size: 3rem;
3+
font-weight: bold;
4+
text-align: center;
5+
margin: 1rem 0;
6+
padding: 1rem;
7+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
8+
color: white;
9+
border-radius: 12px;
10+
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
11+
}
12+
13+
.session-info {
14+
background-color: #f8f9fa;
15+
padding: 0.75rem;
16+
border-radius: 6px;
17+
margin: 1rem 0;
18+
border-left: 4px solid #007acc;
19+
font-family: monospace;
20+
font-size: 0.9rem;
21+
}
22+
23+
.section {
24+
margin: 2rem 0;
25+
padding: 1.5rem;
26+
border: 1px solid #e0e0e0;
27+
border-radius: 8px;
28+
background-color: #fafafa;
29+
}
30+
31+
.section h2 {
32+
margin-top: 0;
33+
color: #333;
34+
border-bottom: 2px solid #007acc;
35+
padding-bottom: 0.5rem;
36+
}
37+
38+
ul {
39+
list-style-type: none;
40+
padding: 0;
41+
margin: 1rem 0;
42+
}
43+
44+
li {
45+
padding: 0.5rem;
46+
margin: 0.25rem 0;
47+
background-color: white;
48+
border-radius: 4px;
49+
border-left: 3px solid #007acc;
50+
font-family: monospace;
51+
font-size: 0.9rem;
52+
}
53+
54+
button {
55+
padding: 0.75rem 1.5rem;
56+
font-size: 1rem;
57+
border: none;
58+
border-radius: 6px;
59+
cursor: pointer;
60+
transition: all 0.2s ease;
61+
margin: 0.5rem 0;
62+
}
63+
64+
button:not(:disabled) {
65+
background-color: #007acc;
66+
color: white;
67+
}
68+
69+
button:not(:disabled):hover {
70+
background-color: #005a9e;
71+
transform: translateY(-1px);
72+
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
73+
}
74+
75+
button:disabled {
76+
background-color: #ffa500;
77+
color: white;
78+
cursor: not-allowed;
79+
opacity: 0.8;
80+
}
81+
82+
.api-info {
83+
margin-top: 1rem;
84+
padding: 1rem;
85+
background-color: #e8f4f8;
86+
border-radius: 6px;
87+
border: 1px solid #b3d9e6;
88+
}
89+
90+
.api-info code {
91+
background-color: #fff;
92+
padding: 0.25rem 0.5rem;
93+
border-radius: 3px;
94+
font-family: monospace;
95+
font-size: 0.85rem;
96+
border: 1px solid #ddd;
97+
display: inline-block;
98+
margin-top: 0.5rem;
99+
word-break: break-all;
100+
}
Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,35 @@
1-
<h1>withMutations</h1>
1+
<h1>withMutations + HTTP</h1>
22

33
<div class="counter">{{ counter() }}</div>
44

5-
<ul>
6-
<li>isPending: {{ isPending() }}</li>
7-
<li>Status: {{ status() }}</li>
8-
<li>Error: {{ error() | json }}</li>
9-
</ul>
5+
<div class="section">
6+
<h2>Local Increment</h2>
7+
<ul>
8+
<li>isPending: {{ isPending() }}</li>
9+
<li>Status: {{ status() }}</li>
10+
<li>Error: {{ error() | json }}</li>
11+
</ul>
1012

11-
<div>
12-
<button (click)="increment()">Increment</button>
13+
<div>
14+
<button (click)="increment()" [disabled]="isPending()">
15+
@if (isPending()) { Incrementing... } @else { Increment }
16+
</button>
17+
</div>
18+
</div>
19+
20+
<div class="section">
21+
<h2>Sending to Server</h2>
22+
23+
<ul>
24+
<li>isPending: {{ saveIsPending() }}</li>
25+
<li>Status: {{ saveStatus() }}</li>
26+
<li>Error: {{ saveError() | json }}</li>
27+
<li>Last Response: {{ lastResponse() | json }}</li>
28+
</ul>
29+
30+
<div>
31+
<button (click)="saveToServer()" [disabled]="saveIsPending()">
32+
@if (saveIsPending()) { Sending to Server... } @else { Sending to Server }
33+
</button>
34+
</div>
1335
</div>

apps/demo/src/app/counter-mutation/counter-mutation.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,16 @@ export class CounterMutation {
1616
protected isPending = this.store.incrementIsPending;
1717
protected status = this.store.incrementStatus;
1818

19+
protected saveError = this.store.saveToServerError;
20+
protected saveIsPending = this.store.saveToServerIsPending;
21+
protected saveStatus = this.store.saveToServerStatus;
22+
protected lastResponse = this.store.lastResponse;
23+
1924
increment() {
2025
this.store.increment({ value: 1 });
2126
}
27+
28+
saveToServer() {
29+
this.store.saveToServer();
30+
}
2231
}

apps/demo/src/app/counter-mutation/counter.store.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
concatOp,
3+
httpMutation,
34
rxMutation,
45
withMutations,
56
} from '@angular-architects/ngrx-toolkit';
@@ -10,9 +11,17 @@ export type Params = {
1011
value: number;
1112
};
1213

14+
// httpbin.org echos the request in the json property
15+
export type CounterResponse = {
16+
json: { counter: number };
17+
};
18+
1319
export const CounterStore = signalStore(
1420
{ providedIn: 'root' },
15-
withState({ counter: 0 }),
21+
withState({
22+
counter: 0,
23+
lastResponse: undefined as unknown | undefined,
24+
}),
1625
withMutations((store) => ({
1726
increment: rxMutation({
1827
operation: (params: Params) => {
@@ -27,6 +36,21 @@ export const CounterStore = signalStore(
2736
console.error('Error occurred:', error);
2837
},
2938
}),
39+
saveToServer: httpMutation<void, CounterResponse>({
40+
request: () => ({
41+
url: `https://httpbin.org/post`,
42+
method: 'POST',
43+
body: { counter: store.counter() },
44+
headers: { 'Content-Type': 'application/json' },
45+
}),
46+
onSuccess: (response) => {
47+
console.log('Counter sent to server:', response);
48+
patchState(store, { lastResponse: response.json });
49+
},
50+
onError: (error) => {
51+
console.error('Failed to send counter:', error);
52+
},
53+
}),
3054
})),
3155
);
3256

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
.counter {
2+
font-size: 3rem;
3+
font-weight: bold;
4+
text-align: center;
5+
margin: 1rem 0;
6+
padding: 1rem;
7+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
8+
color: white;
9+
border-radius: 12px;
10+
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
11+
}
12+
13+
.session-info {
14+
background-color: #f8f9fa;
15+
padding: 0.75rem;
16+
border-radius: 6px;
17+
margin: 1rem 0;
18+
border-left: 4px solid #007acc;
19+
font-family: monospace;
20+
font-size: 0.9rem;
21+
}
22+
23+
.section {
24+
margin: 2rem 0;
25+
padding: 1.5rem;
26+
border: 1px solid #e0e0e0;
27+
border-radius: 8px;
28+
background-color: #fafafa;
29+
}
30+
31+
.section h2 {
32+
margin-top: 0;
33+
color: #333;
34+
border-bottom: 2px solid #007acc;
35+
padding-bottom: 0.5rem;
36+
}
37+
38+
ul {
39+
list-style-type: none;
40+
padding: 0;
41+
margin: 1rem 0;
42+
}
43+
44+
li {
45+
padding: 0.5rem;
46+
margin: 0.25rem 0;
47+
background-color: white;
48+
border-radius: 4px;
49+
border-left: 3px solid #007acc;
50+
font-family: monospace;
51+
font-size: 0.9rem;
52+
}
53+
54+
button {
55+
padding: 0.75rem 1.5rem;
56+
font-size: 1rem;
57+
border: none;
58+
border-radius: 6px;
59+
cursor: pointer;
60+
transition: all 0.2s ease;
61+
margin: 0.5rem 0.5rem 0.5rem 0;
62+
}
63+
64+
button:not(:disabled) {
65+
background-color: #007acc;
66+
color: white;
67+
}
68+
69+
button:not(:disabled):hover {
70+
background-color: #005a9e;
71+
transform: translateY(-1px);
72+
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
73+
}
74+
75+
button:disabled {
76+
background-color: #ffa500;
77+
color: white;
78+
cursor: not-allowed;
79+
opacity: 0.8;
80+
}
81+
82+
.api-info {
83+
margin-top: 1rem;
84+
padding: 1rem;
85+
background-color: #e8f4f8;
86+
border-radius: 6px;
87+
border: 1px solid #b3d9e6;
88+
}
89+
90+
.api-info code {
91+
background-color: #fff;
92+
padding: 0.25rem 0.5rem;
93+
border-radius: 3px;
94+
font-family: monospace;
95+
font-size: 0.85rem;
96+
border: 1px solid #ddd;
97+
display: inline-block;
98+
margin-top: 0.5rem;
99+
word-break: break-all;
100+
}
Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,40 @@
1-
<h1>rxMutation (without Store)</h1>
1+
<h1>rxMutation + httpMutation (without Store)</h1>
22

33
<div class="counter">{{ counter() }}</div>
44

5-
<ul>
6-
<li>isPending: {{ isPending() }}</li>
7-
<li>Status: {{ status() }}</li>
8-
<li>Error: {{ error() | json }}</li>
9-
<li>Value: {{ value() | json }}</li>
10-
<li>hasValue: {{ hasValue() | json }}</li>
11-
</ul>
5+
<div class="section">
6+
<h2>Local Increment (rxMutation)</h2>
7+
<ul>
8+
<li>isPending: {{ isPending() }}</li>
9+
<li>Status: {{ status() }}</li>
10+
<li>Error: {{ error() | json }}</li>
11+
<li>Value: {{ value() | json }}</li>
12+
<li>hasValue: {{ hasValue() }}</li>
13+
</ul>
1214

13-
<div>
14-
<button (click)="incrementCounter()" [disabled]="isPending()">
15-
Increment by 1
16-
</button>
17-
<button (click)="incrementBy13()" [disabled]="isPending()">
18-
Increment by 13
19-
</button>
15+
<div>
16+
<button (click)="incrementCounter()" [disabled]="isPending()">
17+
@if (isPending()) { Incrementing... } @else { Increment by 1 }
18+
</button>
19+
<button (click)="incrementBy13()" [disabled]="isPending()">
20+
@if (isPending()) { Incrementing... } @else { Increment by 13 }
21+
</button>
22+
</div>
23+
</div>
24+
25+
<div class="section">
26+
<h2>Send to Server (httpMutation)</h2>
27+
28+
<ul>
29+
<li>isPending: {{ saveIsPending() }}</li>
30+
<li>Status: {{ saveStatus() }}</li>
31+
<li>Error: {{ saveError() | json }}</li>
32+
<li>lastResponse: {{ lastResponse() | json }}</li>
33+
</ul>
34+
35+
<div>
36+
<button (click)="saveCounterToServer()" [disabled]="saveIsPending()">
37+
@if (saveIsPending()) { Sending to Server... } @else { Send to Server }
38+
</button>
39+
</div>
2040
</div>

0 commit comments

Comments
 (0)