@@ -38,51 +38,58 @@ A demo application is available on [GitHub](https://github.com/BioPhoton/rx-angu
38
38
39
39
## Motivation
40
40
41
- Signals , or a more commonly used name actions, are a common part of state management and reactive systems in general.
41
+ Operations , or a more commonly used name actions, are a common part of state management and reactive systems in general.
42
42
Even if ` @rx-angular/state ` provides ` set ` method, sometimes you need to add behavior to your user input or incoming events.
43
43
44
44
Subjects are normally used to implement this feature. This leads, especially in bigger applications, to a messy code that is bloated with Subjects.
45
45
46
46
Let's have a look at this piece of code:
47
47
48
48
``` typescript
49
+ import { Component } from ' @angular/core' ;
50
+ import { Subject } from ' rxjs' ;
51
+ import { withLatestFrom , switchMap } from ' rxjs/operators' ;
52
+
49
53
@Component ({
54
+ selector: ' my-component' ,
50
55
template: `
51
56
<input (input)="searchInput($event?.target?.value)" /> Search for:
52
57
{{ search$ | async }}<br />
53
58
<button (click)="submitBtn()">
54
- Submit<button>
55
- <br />
56
- <ul>
57
- <li *ngFor="let item of list$ | async as list">{{ item }}</li>
58
- </ul>
59
- </button>
59
+ Submit
60
60
</button>
61
+ <br />
62
+ <ul>
63
+ <li *ngFor="let item of list$ | async">{{ item }}</li>
64
+ </ul>
61
65
` ,
62
66
})
63
- class Component {
67
+ export class MyComponent {
64
68
private _submitBtn = new Subject <void >();
65
69
private _searchInput = new Subject <string >();
66
70
67
- set submitBtn() {
68
- _submitBtn .next ();
71
+ submitBtn() {
72
+ this . _submitBtn .next ();
69
73
}
74
+
70
75
get submitBtn$() {
71
- return _searchInput .asObservable ();
76
+ return this . _submitBtn .asObservable ();
72
77
}
73
- set search(search : string ) {
74
- _searchInput .next (search );
78
+
79
+ searchInput(value : string ) {
80
+ this ._searchInput .next (value );
75
81
}
82
+
76
83
get search$() {
77
- return _searchInput .asObservable ();
84
+ return this . _searchInput .asObservable ();
78
85
}
79
86
80
87
list$ = this .submitBtn$ .pipe (
81
88
withLatestFrom (this .search$ ),
82
89
switchMap (([_ , searchString ]) => this .api .query (searchString ))
83
90
);
84
91
85
- constructor (api : API ) {}
92
+ constructor (private api : API ) {}
86
93
}
87
94
```
88
95
@@ -110,26 +117,26 @@ interface UiActions {
110
117
<input (input)="ui.searchInput($event)" /> Search for:
111
118
{{ ui.search$ | async }}<br />
112
119
<button (click)="ui.submitBtn()">
113
- Submit<button>
114
- <br />
115
- <ul>
116
- <li *ngFor="let item of list$ | async as list">{{ item }}</li>
117
- </ul>
118
- </button>
120
+ Submit
119
121
</button>
122
+ <br />
123
+ <ul>
124
+ <li *ngFor="let item of list$ | async as list">{{ item }}</li>
125
+ </ul>
120
126
` ,
121
127
providers: [RxActionFactory ],
122
128
})
123
- class Component {
124
- ui = this .factory .create ({ searchInput : (e ) => e .target . value });
129
+ export class Component {
130
+ ui = this .factory .create ({ searchInput : (e ) => e .target ? e . target . value : ' ' });
125
131
126
132
list$ = this .ui .submitBtn$ .pipe (
127
133
withLatestFrom (this .ui .search$ ),
128
134
switchMap (([_ , searchString ]) => this .api .query (searchString ))
129
135
);
130
136
131
- constructor (api : API , private factory : RxActionFactory <UiActions >) {}
137
+ constructor (private api : API , private factory : RxActionFactory <UiActions >) {}
132
138
}
139
+
133
140
```
134
141
135
142
## RxAngular Actions
@@ -155,7 +162,7 @@ yarn add @rx-angular/state
155
162
By using RxAngular Actions we can reduce the boilerplate significantly, to do so we can start by thinking about the specific section their events and event payload types:
156
163
157
164
``` typescript
158
- interface Commands {
165
+ interface Actions {
159
166
refreshUser: string | number ;
160
167
refreshList: string | number ;
161
168
refreshGenres: string | number ;
@@ -165,40 +172,35 @@ interface Commands {
165
172
Next we can use the typing to create the action object:
166
173
167
174
``` typescript
168
- commands = getActions <Commands >();
169
- ```
170
-
171
- The object can now be used to emit signals over setters:
172
-
173
- ``` typescript
174
- commands .refreshUser (value );
175
- commands .refreshList (value );
176
- commands .refreshGenres (value );
175
+ const actionsFactory = new RxActionFactory <Actions >();
176
+ const actions = actionsFactory .create ();
177
177
```
178
178
179
- The emitted signals can be received over observable properties :
179
+ The object can now be used to emit operations over setters :
180
180
181
181
``` typescript
182
- refreshUser$ = commands .refreshUser$ ;
183
- refreshList$ = commands .refreshList$ ;
184
- refreshGenres$ = commands .refreshGenres$ ;
182
+ actions .refreshUser ( value ) ;
183
+ actions .refreshList ( value ) ;
184
+ actions .refreshGenres ( value ) ;
185
185
```
186
186
187
- You can also emit multiple signals at once :
187
+ The emitted operations can be received over observable properties :
188
188
189
189
``` typescript
190
- commands ({ refreshUser: true , refreshList: true });
190
+ const refreshUser$ = actions .refreshUser$ ;
191
+ const refreshList$ = actions .refreshList$ ;
192
+ const refreshGenres$ = actions .refreshGenres$ ;
191
193
```
192
194
193
- If there is the need to make a combined signal you can also select multiple signals and get their emissions in one stream:
195
+ If there is the need to make a combined operation you can also select multiple operations and get their emissions in one stream:
194
196
195
197
``` typescript
196
- refreshUserOrList$ = commands . $ ([ ' refreshUser' , ' refreshList' ] );
198
+ const refreshUserOrList$ = concat ( actions . refreshUser$ , actions . refreshList$ );
197
199
```
198
200
199
- ### Signals in components
201
+ ### Operations in components
200
202
201
- In components/templates we can use signals to map user interaction as well as programmatic to effects or state changes.
203
+ In components/templates we can use operations to map user interaction as well as programmatic to effects or state changes.
202
204
This reduces the component class code as well as template.
203
205
204
206
In addition, we can use it as a shorthand in the template and directly connect to action dispatching in the class.
@@ -211,8 +213,8 @@ interface UiActions {
211
213
212
214
@Component ({
213
215
template: `
214
- <input (input)="ui.searchInput($event)" /> Search for:
215
- {{ ui.search $ | async }}<br />
216
+ <input (input)="ui.searchInput($event.target.value )" /> Search for:
217
+ {{ ui.searchInput $ | async }}<br />
216
218
<button (click)="ui.submitBtn()">
217
219
Submit<button>
218
220
<br />
@@ -225,10 +227,10 @@ interface UiActions {
225
227
providers: [RxState , RxActionFactory ],
226
228
})
227
229
class Component {
228
- ui = this .factory .create ({ searchInput : (e ) => e ?. target ?. value });
230
+ ui = this .factory .create < UiActions > ({ searchInput : (e ) => e });
229
231
list$ = this .state .select (' list' );
230
232
submittedSearchQuery$ = this .ui .submitBtn$ .pipe (
231
- withLatestFrom (this .ui .search $ ),
233
+ withLatestFrom (this .ui .searchInput $ ),
232
234
map (([_ , search ]) => search ),
233
235
debounceTime (1500 )
234
236
);
@@ -238,14 +240,14 @@ class Component {
238
240
private factory : RxActionFactory <UiActions >,
239
241
globalState : StateService
240
242
) {
241
- super ();
242
243
this .connect (' list' , this .globalState .refreshGenres$ );
243
244
244
245
this .state .hold (this .submittedSearchQuery$ , this .globalState .refreshGenres );
245
246
// Optional reactively:
246
247
// this.globalState.connectRefreshGenres(this.submittedSearchQuery$);
247
248
}
248
249
}
250
+
249
251
```
250
252
251
253
#### Using transforms
@@ -265,43 +267,46 @@ You can write you own transforms or use the predefined functions:
265
267
- eventValue
266
268
267
269
``` typescript
270
+ import { Component } from ' @angular/core' ;
271
+ import { RxState , RxActionFactory } from ' @rx-angular/state' ;
272
+ import { StateService } from ' ./state.service' ;
273
+ import { withLatestFrom , map , debounceTime } from ' rxjs/operators' ;
274
+
268
275
interface UiActions {
269
276
submitBtn: void ;
270
277
searchInput: string ;
271
278
}
272
279
273
280
@Component ({
281
+ selector: ' my-component' ,
274
282
template: `
275
283
<input (input)="ui.searchInput($event)" /> Search for:
276
- {{ ui.search $ | async }}<br />
284
+ {{ ui.searchInput $ | async }}<br />
277
285
<button (click)="ui.submitBtn()">
278
- Submit<button>
279
- <br />
280
- <ul>
281
- <li *ngFor="let item of list$ | async as list">{{ item }}</li>
282
- </ul>
283
- </button>
286
+ Submit
284
287
</button>
288
+ <br />
289
+ <ul>
290
+ <li *ngFor="let item of list$ | async">{{ item }}</li>
291
+ </ul>
285
292
` ,
286
293
providers: [RxState , RxActionFactory ],
287
294
})
288
- class Component {
289
- // (e) => e.target ? e.target.value : e
290
- ui = this .factory .create ({ searchInput: eventValue });
295
+ export class MyComponent {
296
+ ui = this .factory .create <UiActions >({ searchInput: eventValue });
291
297
list$ = this .state .select (' list' );
292
298
submittedSearchQuery$ = this .ui .submitBtn$ .pipe (
293
- withLatestFrom (this .ui .search $ ),
299
+ withLatestFrom (this .ui .searchInput $ ),
294
300
map (([_ , search ]) => search ),
295
301
debounceTime (1500 )
296
302
);
297
303
298
304
constructor (
299
305
private state : RxState <State >,
300
306
private factory : RxActionFactory <UiActions >,
301
- globalState : StateService
307
+ private globalState : StateService
302
308
) {
303
- super ();
304
- this .connect (' list' , this .globalState .refreshGenres$ );
309
+ this .state .connect (' list' , this .globalState .refreshGenres$ );
305
310
306
311
this .state .hold (this .submittedSearchQuery$ , this .globalState .refreshGenres );
307
312
// Optional reactively:
@@ -321,15 +326,15 @@ interface State {
321
326
genres: MovieGenreModel [];
322
327
}
323
328
324
- interface Commands {
329
+ interface Actions {
325
330
refreshGenres: string | number ;
326
331
}
327
332
328
333
@Injectable ({
329
334
providedIn: ' root' ,
330
335
})
331
336
export class StateService extends RxState <State > {
332
- private commands = new RxActionFactory <Commands >().create ();
337
+ private actions = new RxActionFactory <Actions >().create ();
333
338
334
339
genres$ = this .select (' genres' );
335
340
@@ -338,20 +343,21 @@ export class StateService extends RxState<State> {
338
343
339
344
this .connect (
340
345
' genres' ,
341
- this .commands . fetchGenres $ .pipe (exhaustMap (this .tmdb2Service .getGenres ))
346
+ this .actions . refreshGenres $ .pipe (exhaustMap (this .tmdb2Service .getGenres ))
342
347
);
343
348
}
344
349
345
- refreshGenres(genre : string ): void {
346
- this .commands . fetchGenres (genre );
350
+ refreshGenres(genre : string | number ): void {
351
+ this .actions . refreshGenres (genre );
347
352
}
348
353
349
354
// Optionally the reactive way
350
- connectRefreshGenres(genre$ : Observable <string >): void {
351
- this .connect (
352
- ' genres ' ,
353
- this .commands . fetchGenres$ . pipe ( exhaustMap ( this . tmdb2Service . getGenres ) )
355
+ connectRefreshGenres(genre$ : Observable <string | number >): void {
356
+ this .hold (
357
+ genre$ ,
358
+ genre => this .actions . refreshGenres ( genre )
354
359
);
355
360
}
356
361
}
362
+
357
363
```
0 commit comments