react-elmish 2.0.1 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/{publish.yml → release.yml} +19 -16
- package/.releaserc.json +3 -0
- package/README.md +276 -125
- package/dist/ElmComponent.d.ts +2 -1
- package/dist/ElmComponent.js +1 -5
- package/dist/ElmUtilities.d.ts +17 -0
- package/dist/ElmUtilities.js +11 -2
- package/dist/Testing/index.d.ts +3 -1
- package/dist/Testing/index.js +10 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/legacy/useElmish.d.ts +15 -0
- package/dist/legacy/useElmish.js +132 -0
- package/dist/legacy/useElmishMap.d.ts +14 -0
- package/dist/legacy/useElmishMap.js +134 -0
- package/dist/useElmish.d.ts +20 -0
- package/dist/useElmish.js +148 -0
- package/package.json +10 -8
- package/.github/workflows/main.yml +0 -41
- package/dist/Hooks.d.ts +0 -5
- package/dist/Hooks.js +0 -121
package/README.md
CHANGED
|
@@ -25,66 +25,82 @@ An elmish component basically consists of the following parts:
|
|
|
25
25
|
First import everything from `react-elmish` and declare the **Message** discriminated union type:
|
|
26
26
|
|
|
27
27
|
```ts
|
|
28
|
-
import
|
|
28
|
+
import { Cmd, createCmd, UpdateReturnType, UpdateMap } from "react-elmish";
|
|
29
29
|
|
|
30
30
|
export type Message =
|
|
31
|
-
| { name: "
|
|
32
|
-
| { name: "
|
|
33
|
-
;
|
|
31
|
+
| { name: "increment" }
|
|
32
|
+
| { name: "decrement" };
|
|
34
33
|
```
|
|
35
34
|
|
|
36
|
-
You can also create some convenience functions to
|
|
35
|
+
You can also create some convenience functions to create message objects:
|
|
37
36
|
|
|
38
37
|
```ts
|
|
39
38
|
export const Msg = {
|
|
40
|
-
increment: (): Message => ({ name: "
|
|
41
|
-
decrement: (): Message => ({ name: "
|
|
39
|
+
increment: (): Message => ({ name: "increment" }),
|
|
40
|
+
decrement: (): Message => ({ name: "decrement" }),
|
|
42
41
|
};
|
|
43
42
|
```
|
|
44
43
|
|
|
45
|
-
Now we can create a `cmd` object for our messages type:
|
|
46
|
-
|
|
47
|
-
```ts
|
|
48
|
-
const cmd = Elm.createCmd<Message>();
|
|
49
|
-
```
|
|
50
|
-
|
|
51
44
|
Next, declare the model:
|
|
52
45
|
|
|
53
46
|
```ts
|
|
54
|
-
export
|
|
47
|
+
export interface Model {
|
|
55
48
|
value: number,
|
|
56
|
-
}
|
|
49
|
+
}
|
|
57
50
|
```
|
|
58
51
|
|
|
59
52
|
The props are optional:
|
|
60
53
|
|
|
61
54
|
```ts
|
|
62
|
-
export
|
|
55
|
+
export interface Props {
|
|
63
56
|
initialValue: number,
|
|
64
|
-
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Now we create a `cmd` object for our messages type:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
const cmd = createCmd<Message>();
|
|
65
64
|
```
|
|
66
65
|
|
|
67
66
|
To create the initial model we need an **init** function:
|
|
68
67
|
|
|
69
68
|
```ts
|
|
70
|
-
export
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
69
|
+
export function init (props: Props): [Model, Cmd<Message>] {
|
|
70
|
+
return [
|
|
71
|
+
{
|
|
72
|
+
value: props.initialValue,
|
|
73
|
+
},
|
|
74
|
+
cmd.none
|
|
75
|
+
];
|
|
76
|
+
};
|
|
77
|
+
```
|
|
74
78
|
|
|
75
|
-
|
|
79
|
+
To update the model based on a message we need an `UpdateMap` object:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
export const update: UpdateMap<Props, Model, Message> = {
|
|
83
|
+
increment (msg, model, props) {
|
|
84
|
+
return [{ value: model.value + 1 }];
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
decrement (msg, model, props) {
|
|
88
|
+
return [{ value: model.value - 1 }];
|
|
89
|
+
},
|
|
76
90
|
};
|
|
77
91
|
```
|
|
78
92
|
|
|
79
|
-
|
|
93
|
+
**Note:** When using an `UpdateMap` it is recommended to use camelCase for message names ("increment" instead of "Increment").
|
|
94
|
+
|
|
95
|
+
Alternatively we can use an **update** function:
|
|
80
96
|
|
|
81
97
|
```ts
|
|
82
|
-
export const update = (model: Model, msg: Msg, props: Props):
|
|
98
|
+
export const update = (model: Model, msg: Msg, props: Props): UpdateReturnType<Model, Message> => {
|
|
83
99
|
switch (msg.name) {
|
|
84
|
-
case "
|
|
100
|
+
case "increment":
|
|
85
101
|
return [{ value: model.value + 1 }];
|
|
86
102
|
|
|
87
|
-
case "
|
|
103
|
+
case "decrement":
|
|
88
104
|
return [{ value: model.value - 1 }];
|
|
89
105
|
}
|
|
90
106
|
};
|
|
@@ -96,25 +112,49 @@ export const update = (model: Model, msg: Msg, props: Props): Elm.UpdateReturnTy
|
|
|
96
112
|
|
|
97
113
|
To put all this together and to render our component, we need a React component.
|
|
98
114
|
|
|
99
|
-
|
|
115
|
+
As a **function component**:
|
|
116
|
+
|
|
117
|
+
```tsx
|
|
118
|
+
// Import everything from the App.ts
|
|
119
|
+
import { init, update, Msg, Props } from "../App";
|
|
120
|
+
// Import the useElmish hook
|
|
121
|
+
import { useElmish } from "react-elmish";
|
|
122
|
+
|
|
123
|
+
function App (props: Props): JSX.Element {
|
|
124
|
+
// Call the useElmish hook, it returns the current model and the dispatch function
|
|
125
|
+
const [model, dispatch] = useElmish({ props, init, update, name: "App" });
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<div>
|
|
129
|
+
{/* Display our current value */}
|
|
130
|
+
<p>{model.value}</p>
|
|
131
|
+
|
|
132
|
+
{/* dispatch messages */}
|
|
133
|
+
<button onClick={() => dispatch(Msg.increment())}>Increment</button>
|
|
134
|
+
<button onClick={() => dispatch(Msg.decrement())}>Decrement</button>
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
As a **class component**:
|
|
100
141
|
|
|
101
142
|
```tsx
|
|
102
143
|
// Import everything from the App.ts
|
|
103
|
-
import
|
|
144
|
+
import { Model, Message, Props, init, update, Msg } as Shared from "../App";
|
|
104
145
|
// Import the ElmComponent which extends the React.Component
|
|
105
146
|
import { ElmComponent } from "react-elmish";
|
|
106
|
-
// Don't forget to import react
|
|
107
147
|
import React from "react";
|
|
108
148
|
|
|
109
149
|
// Create an elmish class component
|
|
110
|
-
class App extends ElmComponent<
|
|
150
|
+
class App extends ElmComponent<Model, Message, Props> {
|
|
111
151
|
// Construct the component with the props and init function
|
|
112
|
-
constructor(props:
|
|
113
|
-
super(props,
|
|
152
|
+
constructor(props: Props) {
|
|
153
|
+
super(props, init, "App");
|
|
114
154
|
}
|
|
115
155
|
|
|
116
156
|
// Assign our update function to the component
|
|
117
|
-
update =
|
|
157
|
+
update = update;
|
|
118
158
|
|
|
119
159
|
render(): React.ReactNode {
|
|
120
160
|
// Access the model
|
|
@@ -126,38 +166,13 @@ class App extends ElmComponent<Shared.Model, Shared.Message, Shared.Props> {
|
|
|
126
166
|
<p>{value}</p>
|
|
127
167
|
|
|
128
168
|
{/* Dispatch messages */}
|
|
129
|
-
<button onClick={() => this.dispatch(
|
|
130
|
-
<button onClick={() => this.dispatch(
|
|
169
|
+
<button onClick={() => this.dispatch(Msg.increment())}>Increment</button>
|
|
170
|
+
<button onClick={() => this.dispatch(Msg.decrement())}>Decrement</button>
|
|
131
171
|
</div>
|
|
132
172
|
);
|
|
133
173
|
}
|
|
134
174
|
```
|
|
135
175
|
|
|
136
|
-
Or it can be a **functional component**:
|
|
137
|
-
|
|
138
|
-
```tsx
|
|
139
|
-
// Import everything from the App.ts
|
|
140
|
-
import * as Shared from "../App";
|
|
141
|
-
// Import the useElmish hook
|
|
142
|
-
import { useElmish } from "react-elmish";
|
|
143
|
-
|
|
144
|
-
const App = (props: Shared.Props) => {
|
|
145
|
-
// Call the useElmish hook, it returns the current model and the dispatch function
|
|
146
|
-
const [model, dispatch] = useElmish(props, Shared.init, Shared.update, "App");
|
|
147
|
-
|
|
148
|
-
return (
|
|
149
|
-
<div>
|
|
150
|
-
{/* Display our current value */}
|
|
151
|
-
<p>{model.value}</p>
|
|
152
|
-
|
|
153
|
-
{/* dispatch messages */}
|
|
154
|
-
<button onClick={() => dispatch(Shared.Msg.increment())}>Increment</button>
|
|
155
|
-
<button onClick={() => dispatch(Shared.Msg.decrement())}>Decrement</button>
|
|
156
|
-
</div>
|
|
157
|
-
);
|
|
158
|
-
};
|
|
159
|
-
```
|
|
160
|
-
|
|
161
176
|
You can use these components like any other React component.
|
|
162
177
|
|
|
163
178
|
> **Note**: It is recommended to separate business logic and the view into separate modules. Here we put the `Messages`, `Model`, `Props`, `init`, and `update` functions into **App.ts**. The elmish React Component resides in a **Components** subfolder and is named **App.tsx**.
|
|
@@ -172,11 +187,11 @@ Messages can also have arguments. You can modify the example above and pass an o
|
|
|
172
187
|
|
|
173
188
|
```ts
|
|
174
189
|
export type Message =
|
|
175
|
-
| { name: "
|
|
190
|
+
| { name: "increment", step?: number }
|
|
176
191
|
...
|
|
177
192
|
|
|
178
193
|
export const Msg = {
|
|
179
|
-
increment: (step?: number): Message => ({ name: "
|
|
194
|
+
increment: (step?: number): Message => ({ name: "increment", step }),
|
|
180
195
|
...
|
|
181
196
|
}
|
|
182
197
|
```
|
|
@@ -185,7 +200,7 @@ Then use this argument in the **update** function:
|
|
|
185
200
|
|
|
186
201
|
```ts
|
|
187
202
|
...
|
|
188
|
-
case "
|
|
203
|
+
case "increment":
|
|
189
204
|
return [{ value: model.value + (msg.step ?? 1)}]
|
|
190
205
|
...
|
|
191
206
|
```
|
|
@@ -273,10 +288,11 @@ You can handle errors easily with the following pattern.
|
|
|
273
288
|
1. Add an error message:
|
|
274
289
|
|
|
275
290
|
```ts
|
|
291
|
+
import { ErrorMessage, errorMsg, handleError } from "react-elmish";
|
|
292
|
+
|
|
276
293
|
export type Message =
|
|
277
294
|
| ...
|
|
278
|
-
|
|
|
279
|
-
;
|
|
295
|
+
| ErrorMessage;
|
|
280
296
|
```
|
|
281
297
|
|
|
282
298
|
1. Optionally add the convenient function to the **Msg** object:
|
|
@@ -284,7 +300,7 @@ You can handle errors easily with the following pattern.
|
|
|
284
300
|
```ts
|
|
285
301
|
export const Msg = {
|
|
286
302
|
...
|
|
287
|
-
|
|
303
|
+
...errorMsg,
|
|
288
304
|
}
|
|
289
305
|
```
|
|
290
306
|
|
|
@@ -292,16 +308,16 @@ You can handle errors easily with the following pattern.
|
|
|
292
308
|
|
|
293
309
|
```ts
|
|
294
310
|
...
|
|
295
|
-
case "
|
|
296
|
-
return
|
|
311
|
+
case "error":
|
|
312
|
+
return handleError(msg.error);
|
|
297
313
|
...
|
|
298
314
|
```
|
|
299
315
|
|
|
300
316
|
The **handleError** function then calls your error handling middleware.
|
|
301
317
|
|
|
302
|
-
## Dispatch commands in the update function
|
|
318
|
+
## Dispatch commands in the update map or update function
|
|
303
319
|
|
|
304
|
-
In addition to modifying the model, you can dispatch new commands
|
|
320
|
+
In addition to modifying the model, you can dispatch new commands here.
|
|
305
321
|
|
|
306
322
|
To do so, you can call one of the functions in the `cmd` object:
|
|
307
323
|
|
|
@@ -324,12 +340,12 @@ Let's assume you have a message to display the description of the last called me
|
|
|
324
340
|
```ts
|
|
325
341
|
export type Message =
|
|
326
342
|
...
|
|
327
|
-
| { name: "
|
|
343
|
+
| { name: "printLastMessage", message: string }
|
|
328
344
|
...
|
|
329
345
|
|
|
330
346
|
export const Msg = {
|
|
331
347
|
...
|
|
332
|
-
printLastMessage: (message: string): Message => ({ name: "
|
|
348
|
+
printLastMessage: (message: string): Message => ({ name: "printLastMessage", message }),
|
|
333
349
|
...
|
|
334
350
|
}
|
|
335
351
|
```
|
|
@@ -337,11 +353,11 @@ export const Msg = {
|
|
|
337
353
|
In the **update** function you can dispatch that message like this:
|
|
338
354
|
|
|
339
355
|
```ts
|
|
340
|
-
case "
|
|
356
|
+
case "increment":
|
|
341
357
|
return [{ value: model.value + 1 }, cmd.ofMsg(Msg.printLastMessage("Incremented by one"))];
|
|
342
358
|
```
|
|
343
359
|
|
|
344
|
-
This new message will immediately be dispatched after returning from the **update
|
|
360
|
+
This new message will immediately be dispatched after returning from the **update**.
|
|
345
361
|
|
|
346
362
|
### Call an async function
|
|
347
363
|
|
|
@@ -359,16 +375,16 @@ you can define the following messages:
|
|
|
359
375
|
```ts
|
|
360
376
|
export type Messages =
|
|
361
377
|
...
|
|
362
|
-
| { name: "
|
|
363
|
-
| { name: "
|
|
364
|
-
|
|
|
378
|
+
| { name: "loadSettings" },
|
|
379
|
+
| { name: "settingsLoaded", settings: Settings }
|
|
380
|
+
| ErrorMessage
|
|
365
381
|
...
|
|
366
382
|
|
|
367
383
|
export const Msg = {
|
|
368
384
|
...
|
|
369
|
-
loadSettings: (): Message => ({ name: "
|
|
370
|
-
settingsLoaded: (settings: Settings): Message => ({ name: "
|
|
371
|
-
|
|
385
|
+
loadSettings: (): Message => ({ name: "loadSettings" }),
|
|
386
|
+
settingsLoaded: (settings: Settings): Message => ({ name: "settingsLoaded", settings }),
|
|
387
|
+
...errorMsg,
|
|
372
388
|
...
|
|
373
389
|
};
|
|
374
390
|
```
|
|
@@ -377,18 +393,18 @@ and handle the messages in the **update** function:
|
|
|
377
393
|
|
|
378
394
|
```ts
|
|
379
395
|
...
|
|
380
|
-
case "
|
|
396
|
+
case "loadSettings":
|
|
381
397
|
// Create a command out of the async function with the provided arguments
|
|
382
398
|
// If loadSettings resolves it dispatches "SettingsLoaded"
|
|
383
399
|
// If it fails it dispatches "Error"
|
|
384
400
|
// The return type of loadSettings must fit Msg.settingsLoaded
|
|
385
401
|
return [{}, cmd.ofPromise.either(loadSettings, Msg.settingsLoaded, Msg.error, "firstArg", 123)];
|
|
386
402
|
|
|
387
|
-
case "
|
|
403
|
+
case "settingsLoaded":
|
|
388
404
|
return [{ settings: msg.settings }];
|
|
389
405
|
|
|
390
|
-
case "
|
|
391
|
-
return
|
|
406
|
+
case "error":
|
|
407
|
+
return handleError(msg.error);
|
|
392
408
|
...
|
|
393
409
|
```
|
|
394
410
|
|
|
@@ -420,34 +436,141 @@ In a functional component you can use the **useEffect** hook as normal.
|
|
|
420
436
|
|
|
421
437
|
If you have some business logic that you want to reuse in other components, you can do this by using different sources for messages.
|
|
422
438
|
|
|
439
|
+
### With an `UpdateMap`
|
|
440
|
+
|
|
423
441
|
Let's say you want to load some settings, you can write a module like this:
|
|
424
442
|
|
|
425
443
|
```ts LoadSettings.ts
|
|
426
|
-
import
|
|
444
|
+
import { createCmd, Cmd, ErrorMessage, UpdateMap, handleError } from "react-elmish";
|
|
445
|
+
|
|
446
|
+
export interface Settings {
|
|
447
|
+
// ...
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
export type Message =
|
|
451
|
+
| { name: "loadSettings" }
|
|
452
|
+
| { name: "settingsLoaded", settings: Settings }
|
|
453
|
+
| ErrorMessage;
|
|
454
|
+
|
|
455
|
+
export const Msg = {
|
|
456
|
+
loadSettings: (): Message => ({ name: "loadSettings" }),
|
|
457
|
+
settingsLoaded: (settings: Settings): Message => ({ name: "settingsLoaded", settings }),
|
|
458
|
+
error: (error: Error): Message => ({ name: "error", error }),
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
const cmd = createCmd<Message>();
|
|
462
|
+
|
|
463
|
+
export interface Model {
|
|
464
|
+
settings: Settings | null,
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
export function init (): Model {
|
|
468
|
+
return {
|
|
469
|
+
settings: null
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
export const update: UpdateMap<Props, Model, Message> = {
|
|
474
|
+
loadSettings () {
|
|
475
|
+
return [{}, cmd.ofPromise.either(loadSettings, Msg.settingsLoaded, Msg.error)];
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
settingsLoaded ({ settings }) {
|
|
479
|
+
return [{ settings }];
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
error ({ error }) {
|
|
483
|
+
return handleError(error);
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
async function loadSettings (): Promise<Settings> {
|
|
488
|
+
// Call some service (e.g. database or backend)
|
|
489
|
+
return {};
|
|
490
|
+
}
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
> **Note**: This module has no **View**.
|
|
494
|
+
|
|
495
|
+
Now let's integrate the **LoadSettings** module in our component:
|
|
496
|
+
|
|
497
|
+
```ts Composition.ts
|
|
498
|
+
// Import the LoadSettings module
|
|
499
|
+
import * as LoadSettings from "./LoadSettings";
|
|
500
|
+
import { createCmd, Cmd, } from "react-elmish";
|
|
501
|
+
|
|
502
|
+
// Here we define our local messages
|
|
503
|
+
type Message =
|
|
504
|
+
| { name: "myMessage" }
|
|
505
|
+
| LoadSettings.Message;
|
|
506
|
+
|
|
507
|
+
// And spread the Msg of LoadSettings object
|
|
508
|
+
export const Msg = {
|
|
509
|
+
myMessage: (): Message => ({ name: "myMessage" }),
|
|
510
|
+
...LoadSettings.Msg,
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
const cmd = Elm.createCmd<Message>();
|
|
514
|
+
|
|
515
|
+
interface Props {}
|
|
516
|
+
|
|
517
|
+
// Extend the LoadSettings model
|
|
518
|
+
interface Model extends LoadSettings.Model {
|
|
519
|
+
// ...
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
export const init = (): [Model, Cmd<Message>] => {
|
|
523
|
+
// Return the model and dispatch the LoadSettings message
|
|
524
|
+
return [
|
|
525
|
+
{
|
|
526
|
+
// Spread the initial model from LoadSettings
|
|
527
|
+
...LoadSettings.init(),
|
|
528
|
+
// ...
|
|
529
|
+
},
|
|
530
|
+
cmd.ofMsg(Msg.loadSettings())
|
|
531
|
+
];
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
// Spread the UpdateMap of LoadSettings into our update map
|
|
535
|
+
const update: UpdateMap<Props, Model, Message> = {
|
|
536
|
+
myMessage () {
|
|
537
|
+
return [{}];
|
|
538
|
+
},
|
|
539
|
+
|
|
540
|
+
...LoadSettings.update,
|
|
541
|
+
};
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
## With an update function
|
|
545
|
+
|
|
546
|
+
Let's say you want to load some settings, you can write a module like this:
|
|
547
|
+
|
|
548
|
+
```ts LoadSettings.ts
|
|
549
|
+
import { MsgSource, ErrorMessage, createCmd, UpdateReturnType, handleError } from "react-elmish";
|
|
427
550
|
|
|
428
551
|
export type Settings = {
|
|
429
552
|
// ...
|
|
430
553
|
};
|
|
431
554
|
|
|
432
555
|
// We use a MsgSource to differentiate between the messages
|
|
433
|
-
type MessageSource =
|
|
556
|
+
type MessageSource = MsgSource<"LoadSettings">;
|
|
434
557
|
|
|
435
558
|
// Add that MessageSource to all the messages
|
|
436
559
|
export type Message =
|
|
437
|
-
| { name: "
|
|
438
|
-
| { name: "
|
|
439
|
-
|
|
|
560
|
+
| { name: "loadSettings" } & MessageSource
|
|
561
|
+
| { name: "settingsLoaded", settings: Settings } & MessageSource
|
|
562
|
+
| ErrorMessage & MessageSource
|
|
440
563
|
|
|
441
564
|
// Do the same for the convenient functions
|
|
442
565
|
const MsgSource: MessageSource = { source: "LoadSettings" };
|
|
443
566
|
|
|
444
567
|
export const Msg = {
|
|
445
|
-
loadSettings: (): Message => ({ name: "
|
|
446
|
-
settingsLoaded: (settings: Settings): Message => ({ name: "
|
|
447
|
-
error: (error: Error): Message => ({ name: "
|
|
568
|
+
loadSettings: (): Message => ({ name: "loadSettings", ...MsgSource }),
|
|
569
|
+
settingsLoaded: (settings: Settings): Message => ({ name: "settingsLoaded", settings, ...MsgSource }),
|
|
570
|
+
error: (error: Error): Message => ({ name: "error", error, ...MsgSource }),
|
|
448
571
|
};
|
|
449
572
|
|
|
450
|
-
const cmd =
|
|
573
|
+
const cmd = createCmd<Message>();
|
|
451
574
|
|
|
452
575
|
export type Model = Readonly<{
|
|
453
576
|
settings: Settings | null,
|
|
@@ -457,23 +580,23 @@ export const init = (): Model => ({
|
|
|
457
580
|
settings: null,
|
|
458
581
|
});
|
|
459
582
|
|
|
460
|
-
export const update = (_model: Model, msg: Message):
|
|
583
|
+
export const update = (_model: Model, msg: Message): UpdateReturnType<Model, Message> => {
|
|
461
584
|
switch (msg.name) {
|
|
462
|
-
case "
|
|
585
|
+
case "loadSettings":
|
|
463
586
|
return [{}, cmd.ofPromise.either(loadSettings, Msg.settingsLoaded, Msg.error)];
|
|
464
587
|
|
|
465
|
-
case "
|
|
588
|
+
case "settingsLoaded":
|
|
466
589
|
return [{ settings: msg.settings }];
|
|
467
590
|
|
|
468
|
-
case "
|
|
469
|
-
return
|
|
591
|
+
case "error":
|
|
592
|
+
return handleError(msg.error);
|
|
470
593
|
}
|
|
471
594
|
};
|
|
472
595
|
|
|
473
|
-
|
|
596
|
+
async function loadSettings (): Promise<Settings> {
|
|
474
597
|
// Call some service (e.g. database or backend)
|
|
475
|
-
return
|
|
476
|
-
}
|
|
598
|
+
return {};
|
|
599
|
+
}
|
|
477
600
|
```
|
|
478
601
|
|
|
479
602
|
> **Note**: This module has no **View**.
|
|
@@ -481,51 +604,51 @@ const loadSettings = async (): Promise<Settings> => {
|
|
|
481
604
|
In other components where we want to use this **LoadSettings** module, we also need a message source:
|
|
482
605
|
|
|
483
606
|
```ts Composition.ts
|
|
484
|
-
import
|
|
607
|
+
import { createCmd, MsgSource, UpdateReturnType, Cmd } from "react-elmish";
|
|
485
608
|
// Import the LoadSettings module
|
|
486
609
|
import * as LoadSettings from "./LoadSettings";
|
|
487
610
|
|
|
488
611
|
// Create a message source for this module
|
|
489
|
-
type MessageSource =
|
|
612
|
+
type MessageSource = MsgSource<"Composition">;
|
|
490
613
|
|
|
491
614
|
// Here we define our local messages
|
|
492
615
|
// We don't need to export them
|
|
493
616
|
type CompositionMessage =
|
|
494
|
-
| { name: "
|
|
495
|
-
;
|
|
617
|
+
| { name: "myMessage" } & MessageSource;
|
|
496
618
|
|
|
497
619
|
// Combine the local messages and the ones from LoadSettings
|
|
498
620
|
export type Message =
|
|
499
621
|
| CompositionMessage
|
|
500
|
-
| LoadSettings.Message
|
|
501
|
-
;
|
|
622
|
+
| LoadSettings.Message;
|
|
502
623
|
|
|
503
624
|
const MsgSource: MessageSource = { source: "Composition" };
|
|
504
625
|
|
|
505
626
|
export const Msg = {
|
|
506
|
-
myMessage: (): Message => ({ name: "
|
|
627
|
+
myMessage: (): Message => ({ name: "myMessage", ...MsgSource }),
|
|
628
|
+
...LoadSettings.Msg,
|
|
507
629
|
};
|
|
508
630
|
|
|
509
|
-
const cmd =
|
|
631
|
+
const cmd = createCmd<Message>();
|
|
510
632
|
|
|
511
633
|
// Include the LoadSettings Model
|
|
512
|
-
export
|
|
634
|
+
export interface Model extends LoadSettings.Model {
|
|
513
635
|
// ...
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
export const init = (): [Model, Elm.Cmd<Message>] => {
|
|
517
|
-
const model: Model = {
|
|
518
|
-
// Spread the initial model from LoadSettings
|
|
519
|
-
...LoadSettings.init(),
|
|
520
|
-
// ...
|
|
521
|
-
};
|
|
636
|
+
}
|
|
522
637
|
|
|
638
|
+
export const init = (): [Model, Cmd<Message>] => {
|
|
523
639
|
// Return the model and dispatch the LoadSettings message
|
|
524
|
-
return [
|
|
640
|
+
return [
|
|
641
|
+
{
|
|
642
|
+
// Spread the initial model from LoadSettings
|
|
643
|
+
...LoadSettings.init(),
|
|
644
|
+
// ...
|
|
645
|
+
},
|
|
646
|
+
cmd.ofMsg(Msg.loadSettings())
|
|
647
|
+
];
|
|
525
648
|
};
|
|
526
649
|
|
|
527
650
|
// In our update function, we first distinguish between the sources of the messages
|
|
528
|
-
export
|
|
651
|
+
export function update (model: Model, msg: Message): UpdateReturnType<Model, Message> {
|
|
529
652
|
switch (msg.source) {
|
|
530
653
|
case "Composition":
|
|
531
654
|
// Then call the update function for the local messages
|
|
@@ -540,7 +663,7 @@ export const update = (model: Model, msg: Message): Elm.UpdateReturnType<Model,
|
|
|
540
663
|
// For the msg parameter we use the local CompositionMessage type
|
|
541
664
|
const updateComposition = (model: Model, msg: CompositionMessage): Elm.UpdateReturnType<Model, Message> => {
|
|
542
665
|
switch (msg.name) {
|
|
543
|
-
case "
|
|
666
|
+
case "myMessage":
|
|
544
667
|
return [{}];
|
|
545
668
|
}
|
|
546
669
|
}
|
|
@@ -557,12 +680,12 @@ To inform the parent component about some action, let's say to close a dialog fo
|
|
|
557
680
|
```ts Dialog.ts
|
|
558
681
|
export type Message =
|
|
559
682
|
...
|
|
560
|
-
| { name: "
|
|
683
|
+
| { name: "close" }
|
|
561
684
|
...
|
|
562
685
|
|
|
563
686
|
export const Msg = {
|
|
564
687
|
...
|
|
565
|
-
close: (): Message => ({ name: "
|
|
688
|
+
close: (): Message => ({ name: "close" }),
|
|
566
689
|
...
|
|
567
690
|
}
|
|
568
691
|
```
|
|
@@ -579,8 +702,9 @@ To inform the parent component about some action, let's say to close a dialog fo
|
|
|
579
702
|
|
|
580
703
|
```ts Dialog.ts
|
|
581
704
|
...
|
|
582
|
-
case "
|
|
705
|
+
case "close":
|
|
583
706
|
props.onClose();
|
|
707
|
+
|
|
584
708
|
return [{}];
|
|
585
709
|
...
|
|
586
710
|
```
|
|
@@ -601,6 +725,7 @@ To test your **update** function you can use some helper functions in `react-elm
|
|
|
601
725
|
| --- | --- |
|
|
602
726
|
| `getOfMsgParams` | Extracts the messages out of a command |
|
|
603
727
|
| `execCmd` | Executes the provided command and returns an array of all messages. |
|
|
728
|
+
| `getUpdateFn` | returns an `update` function for your update map object. |
|
|
604
729
|
|
|
605
730
|
### Testing the model and simple message commands
|
|
606
731
|
|
|
@@ -660,7 +785,20 @@ it("returns the correct cmd", () => {
|
|
|
660
785
|
...
|
|
661
786
|
```
|
|
662
787
|
|
|
663
|
-
|
|
788
|
+
### Testing with an UpdateMap
|
|
789
|
+
|
|
790
|
+
To test your update map, you can get an `update` function by calling `getUpdateFn`:
|
|
791
|
+
|
|
792
|
+
```ts
|
|
793
|
+
import { getUpdateFn } from "react-elmish/dist/Testing";
|
|
794
|
+
|
|
795
|
+
const update = getUpdateFn(updateMap);
|
|
796
|
+
|
|
797
|
+
// in your test call update as usual
|
|
798
|
+
const [model, cmd] = update(msg, model, props);
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
## Migration from v1.x to v2.x
|
|
664
802
|
|
|
665
803
|
* Use `Logger` and `Message` instead of `ILogger` and `IMessage`.
|
|
666
804
|
* The global declaration of the `Nullable` type was removed, because it is unexpected for this library to declare such a type. You can declare this type for yourself if needed:
|
|
@@ -670,3 +808,16 @@ it("returns the correct cmd", () => {
|
|
|
670
808
|
type Nullable<T> = T | null;
|
|
671
809
|
}
|
|
672
810
|
```
|
|
811
|
+
|
|
812
|
+
## Migration from v2.x to v3.x
|
|
813
|
+
|
|
814
|
+
The signature of `useElmish` has changed. It takes an options object now. Thus there is no need for the `useElmishMap` function. Use the new `useElmish` hook with an `UpdateMap` instead.
|
|
815
|
+
|
|
816
|
+
To use the old `useElmish` and `useElmishMap` functions, import them from the legacy namespace:
|
|
817
|
+
|
|
818
|
+
```ts
|
|
819
|
+
import { useElmish } from "react-elmish/dist/legacy/useElmish";
|
|
820
|
+
import { useElmishMap } from "react-elmish/dist/legacy/useElmishMap";
|
|
821
|
+
```
|
|
822
|
+
|
|
823
|
+
**Notice**: These functions are marked as deprecated and will be removed in a later release.
|
package/dist/ElmComponent.d.ts
CHANGED
|
@@ -61,8 +61,9 @@ export declare abstract class ElmComponent<TModel, TMsg extends Message, TProps>
|
|
|
61
61
|
* @abstract
|
|
62
62
|
* @memberof ElmComponent
|
|
63
63
|
*/
|
|
64
|
-
abstract update:
|
|
64
|
+
abstract update: UpdateFunction<TProps, TModel, TMsg>;
|
|
65
65
|
}
|
|
66
|
+
export declare type UpdateFunction<TProps, TModel, TMsg> = (model: TModel, msg: TMsg, props: TProps) => UpdateReturnType<TModel, TMsg>;
|
|
66
67
|
/**
|
|
67
68
|
* Type for the return value of the update function.
|
|
68
69
|
*/
|