react-elmish 2.2.0 → 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/README.md +265 -155
- package/dist/ElmUtilities.d.ts +3 -0
- package/dist/ElmUtilities.js +11 -2
- package/dist/Testing/index.js +3 -3
- package/dist/index.d.ts +1 -2
- package/dist/index.js +1 -9
- 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 +19 -3
- package/dist/useElmish.js +33 -6
- package/package.json +1 -1
- package/dist/useElmishMap.d.ts +0 -5
- package/dist/useElmishMap.js +0 -128
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,72 +166,19 @@ 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**.
|
|
164
179
|
>
|
|
165
180
|
> You can even split the contents of the **App.ts** into two files: **Types.ts** (`Message`, `Model`, and `Props`) and **State.ts** (`init` and `update`).
|
|
166
181
|
|
|
167
|
-
## A new approach
|
|
168
|
-
|
|
169
|
-
Instead of `useElmish` you can use the `useElmishMap` hook. Then you have an `UpdateMap` instead of an `update` function:
|
|
170
|
-
|
|
171
|
-
```ts
|
|
172
|
-
const updateMap: UpdateMap<Props, Model, Message> {
|
|
173
|
-
// Now the message is the first parameter, so it is easier to omit the model parameter.
|
|
174
|
-
Increment: (msg) => [{ value: model.value + 1 }],
|
|
175
|
-
Decrement: (msg) => [{ value: model.value - 1 }],
|
|
176
|
-
}
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
Add your component looks like:
|
|
180
|
-
|
|
181
|
-
```tsx
|
|
182
|
-
import { useElmishMap } from "react-elmish";
|
|
183
|
-
|
|
184
|
-
const App = (props: Shared.Props) => {
|
|
185
|
-
const [model, dispatch] = useElmishMap(props, init, updateMap, "App");
|
|
186
|
-
|
|
187
|
-
return (
|
|
188
|
-
<div>
|
|
189
|
-
...
|
|
190
|
-
</div>
|
|
191
|
-
);
|
|
192
|
-
};
|
|
193
|
-
```
|
|
194
|
-
|
|
195
182
|
## More on messages
|
|
196
183
|
|
|
197
184
|
### Message arguments
|
|
@@ -200,11 +187,11 @@ Messages can also have arguments. You can modify the example above and pass an o
|
|
|
200
187
|
|
|
201
188
|
```ts
|
|
202
189
|
export type Message =
|
|
203
|
-
| { name: "
|
|
190
|
+
| { name: "increment", step?: number }
|
|
204
191
|
...
|
|
205
192
|
|
|
206
193
|
export const Msg = {
|
|
207
|
-
increment: (step?: number): Message => ({ name: "
|
|
194
|
+
increment: (step?: number): Message => ({ name: "increment", step }),
|
|
208
195
|
...
|
|
209
196
|
}
|
|
210
197
|
```
|
|
@@ -213,7 +200,7 @@ Then use this argument in the **update** function:
|
|
|
213
200
|
|
|
214
201
|
```ts
|
|
215
202
|
...
|
|
216
|
-
case "
|
|
203
|
+
case "increment":
|
|
217
204
|
return [{ value: model.value + (msg.step ?? 1)}]
|
|
218
205
|
...
|
|
219
206
|
```
|
|
@@ -301,10 +288,11 @@ You can handle errors easily with the following pattern.
|
|
|
301
288
|
1. Add an error message:
|
|
302
289
|
|
|
303
290
|
```ts
|
|
291
|
+
import { ErrorMessage, errorMsg, handleError } from "react-elmish";
|
|
292
|
+
|
|
304
293
|
export type Message =
|
|
305
294
|
| ...
|
|
306
|
-
|
|
|
307
|
-
;
|
|
295
|
+
| ErrorMessage;
|
|
308
296
|
```
|
|
309
297
|
|
|
310
298
|
1. Optionally add the convenient function to the **Msg** object:
|
|
@@ -312,7 +300,7 @@ You can handle errors easily with the following pattern.
|
|
|
312
300
|
```ts
|
|
313
301
|
export const Msg = {
|
|
314
302
|
...
|
|
315
|
-
|
|
303
|
+
...errorMsg,
|
|
316
304
|
}
|
|
317
305
|
```
|
|
318
306
|
|
|
@@ -320,16 +308,16 @@ You can handle errors easily with the following pattern.
|
|
|
320
308
|
|
|
321
309
|
```ts
|
|
322
310
|
...
|
|
323
|
-
case "
|
|
324
|
-
return
|
|
311
|
+
case "error":
|
|
312
|
+
return handleError(msg.error);
|
|
325
313
|
...
|
|
326
314
|
```
|
|
327
315
|
|
|
328
316
|
The **handleError** function then calls your error handling middleware.
|
|
329
317
|
|
|
330
|
-
## Dispatch commands in the update function
|
|
318
|
+
## Dispatch commands in the update map or update function
|
|
331
319
|
|
|
332
|
-
In addition to modifying the model, you can dispatch new commands
|
|
320
|
+
In addition to modifying the model, you can dispatch new commands here.
|
|
333
321
|
|
|
334
322
|
To do so, you can call one of the functions in the `cmd` object:
|
|
335
323
|
|
|
@@ -352,12 +340,12 @@ Let's assume you have a message to display the description of the last called me
|
|
|
352
340
|
```ts
|
|
353
341
|
export type Message =
|
|
354
342
|
...
|
|
355
|
-
| { name: "
|
|
343
|
+
| { name: "printLastMessage", message: string }
|
|
356
344
|
...
|
|
357
345
|
|
|
358
346
|
export const Msg = {
|
|
359
347
|
...
|
|
360
|
-
printLastMessage: (message: string): Message => ({ name: "
|
|
348
|
+
printLastMessage: (message: string): Message => ({ name: "printLastMessage", message }),
|
|
361
349
|
...
|
|
362
350
|
}
|
|
363
351
|
```
|
|
@@ -365,11 +353,11 @@ export const Msg = {
|
|
|
365
353
|
In the **update** function you can dispatch that message like this:
|
|
366
354
|
|
|
367
355
|
```ts
|
|
368
|
-
case "
|
|
356
|
+
case "increment":
|
|
369
357
|
return [{ value: model.value + 1 }, cmd.ofMsg(Msg.printLastMessage("Incremented by one"))];
|
|
370
358
|
```
|
|
371
359
|
|
|
372
|
-
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**.
|
|
373
361
|
|
|
374
362
|
### Call an async function
|
|
375
363
|
|
|
@@ -387,16 +375,16 @@ you can define the following messages:
|
|
|
387
375
|
```ts
|
|
388
376
|
export type Messages =
|
|
389
377
|
...
|
|
390
|
-
| { name: "
|
|
391
|
-
| { name: "
|
|
392
|
-
|
|
|
378
|
+
| { name: "loadSettings" },
|
|
379
|
+
| { name: "settingsLoaded", settings: Settings }
|
|
380
|
+
| ErrorMessage
|
|
393
381
|
...
|
|
394
382
|
|
|
395
383
|
export const Msg = {
|
|
396
384
|
...
|
|
397
|
-
loadSettings: (): Message => ({ name: "
|
|
398
|
-
settingsLoaded: (settings: Settings): Message => ({ name: "
|
|
399
|
-
|
|
385
|
+
loadSettings: (): Message => ({ name: "loadSettings" }),
|
|
386
|
+
settingsLoaded: (settings: Settings): Message => ({ name: "settingsLoaded", settings }),
|
|
387
|
+
...errorMsg,
|
|
400
388
|
...
|
|
401
389
|
};
|
|
402
390
|
```
|
|
@@ -405,18 +393,18 @@ and handle the messages in the **update** function:
|
|
|
405
393
|
|
|
406
394
|
```ts
|
|
407
395
|
...
|
|
408
|
-
case "
|
|
396
|
+
case "loadSettings":
|
|
409
397
|
// Create a command out of the async function with the provided arguments
|
|
410
398
|
// If loadSettings resolves it dispatches "SettingsLoaded"
|
|
411
399
|
// If it fails it dispatches "Error"
|
|
412
400
|
// The return type of loadSettings must fit Msg.settingsLoaded
|
|
413
401
|
return [{}, cmd.ofPromise.either(loadSettings, Msg.settingsLoaded, Msg.error, "firstArg", 123)];
|
|
414
402
|
|
|
415
|
-
case "
|
|
403
|
+
case "settingsLoaded":
|
|
416
404
|
return [{ settings: msg.settings }];
|
|
417
405
|
|
|
418
|
-
case "
|
|
419
|
-
return
|
|
406
|
+
case "error":
|
|
407
|
+
return handleError(msg.error);
|
|
420
408
|
...
|
|
421
409
|
```
|
|
422
410
|
|
|
@@ -448,34 +436,141 @@ In a functional component you can use the **useEffect** hook as normal.
|
|
|
448
436
|
|
|
449
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.
|
|
450
438
|
|
|
439
|
+
### With an `UpdateMap`
|
|
440
|
+
|
|
451
441
|
Let's say you want to load some settings, you can write a module like this:
|
|
452
442
|
|
|
453
443
|
```ts LoadSettings.ts
|
|
454
|
-
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";
|
|
455
550
|
|
|
456
551
|
export type Settings = {
|
|
457
552
|
// ...
|
|
458
553
|
};
|
|
459
554
|
|
|
460
555
|
// We use a MsgSource to differentiate between the messages
|
|
461
|
-
type MessageSource =
|
|
556
|
+
type MessageSource = MsgSource<"LoadSettings">;
|
|
462
557
|
|
|
463
558
|
// Add that MessageSource to all the messages
|
|
464
559
|
export type Message =
|
|
465
|
-
| { name: "
|
|
466
|
-
| { name: "
|
|
467
|
-
|
|
|
560
|
+
| { name: "loadSettings" } & MessageSource
|
|
561
|
+
| { name: "settingsLoaded", settings: Settings } & MessageSource
|
|
562
|
+
| ErrorMessage & MessageSource
|
|
468
563
|
|
|
469
564
|
// Do the same for the convenient functions
|
|
470
565
|
const MsgSource: MessageSource = { source: "LoadSettings" };
|
|
471
566
|
|
|
472
567
|
export const Msg = {
|
|
473
|
-
loadSettings: (): Message => ({ name: "
|
|
474
|
-
settingsLoaded: (settings: Settings): Message => ({ name: "
|
|
475
|
-
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 }),
|
|
476
571
|
};
|
|
477
572
|
|
|
478
|
-
const cmd =
|
|
573
|
+
const cmd = createCmd<Message>();
|
|
479
574
|
|
|
480
575
|
export type Model = Readonly<{
|
|
481
576
|
settings: Settings | null,
|
|
@@ -485,23 +580,23 @@ export const init = (): Model => ({
|
|
|
485
580
|
settings: null,
|
|
486
581
|
});
|
|
487
582
|
|
|
488
|
-
export const update = (_model: Model, msg: Message):
|
|
583
|
+
export const update = (_model: Model, msg: Message): UpdateReturnType<Model, Message> => {
|
|
489
584
|
switch (msg.name) {
|
|
490
|
-
case "
|
|
585
|
+
case "loadSettings":
|
|
491
586
|
return [{}, cmd.ofPromise.either(loadSettings, Msg.settingsLoaded, Msg.error)];
|
|
492
587
|
|
|
493
|
-
case "
|
|
588
|
+
case "settingsLoaded":
|
|
494
589
|
return [{ settings: msg.settings }];
|
|
495
590
|
|
|
496
|
-
case "
|
|
497
|
-
return
|
|
591
|
+
case "error":
|
|
592
|
+
return handleError(msg.error);
|
|
498
593
|
}
|
|
499
594
|
};
|
|
500
595
|
|
|
501
|
-
|
|
596
|
+
async function loadSettings (): Promise<Settings> {
|
|
502
597
|
// Call some service (e.g. database or backend)
|
|
503
|
-
return
|
|
504
|
-
}
|
|
598
|
+
return {};
|
|
599
|
+
}
|
|
505
600
|
```
|
|
506
601
|
|
|
507
602
|
> **Note**: This module has no **View**.
|
|
@@ -509,51 +604,51 @@ const loadSettings = async (): Promise<Settings> => {
|
|
|
509
604
|
In other components where we want to use this **LoadSettings** module, we also need a message source:
|
|
510
605
|
|
|
511
606
|
```ts Composition.ts
|
|
512
|
-
import
|
|
607
|
+
import { createCmd, MsgSource, UpdateReturnType, Cmd } from "react-elmish";
|
|
513
608
|
// Import the LoadSettings module
|
|
514
609
|
import * as LoadSettings from "./LoadSettings";
|
|
515
610
|
|
|
516
611
|
// Create a message source for this module
|
|
517
|
-
type MessageSource =
|
|
612
|
+
type MessageSource = MsgSource<"Composition">;
|
|
518
613
|
|
|
519
614
|
// Here we define our local messages
|
|
520
615
|
// We don't need to export them
|
|
521
616
|
type CompositionMessage =
|
|
522
|
-
| { name: "
|
|
523
|
-
;
|
|
617
|
+
| { name: "myMessage" } & MessageSource;
|
|
524
618
|
|
|
525
619
|
// Combine the local messages and the ones from LoadSettings
|
|
526
620
|
export type Message =
|
|
527
621
|
| CompositionMessage
|
|
528
|
-
| LoadSettings.Message
|
|
529
|
-
;
|
|
622
|
+
| LoadSettings.Message;
|
|
530
623
|
|
|
531
624
|
const MsgSource: MessageSource = { source: "Composition" };
|
|
532
625
|
|
|
533
626
|
export const Msg = {
|
|
534
|
-
myMessage: (): Message => ({ name: "
|
|
627
|
+
myMessage: (): Message => ({ name: "myMessage", ...MsgSource }),
|
|
628
|
+
...LoadSettings.Msg,
|
|
535
629
|
};
|
|
536
630
|
|
|
537
|
-
const cmd =
|
|
631
|
+
const cmd = createCmd<Message>();
|
|
538
632
|
|
|
539
633
|
// Include the LoadSettings Model
|
|
540
|
-
export
|
|
634
|
+
export interface Model extends LoadSettings.Model {
|
|
541
635
|
// ...
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
export const init = (): [Model, Elm.Cmd<Message>] => {
|
|
545
|
-
const model: Model = {
|
|
546
|
-
// Spread the initial model from LoadSettings
|
|
547
|
-
...LoadSettings.init(),
|
|
548
|
-
// ...
|
|
549
|
-
};
|
|
636
|
+
}
|
|
550
637
|
|
|
638
|
+
export const init = (): [Model, Cmd<Message>] => {
|
|
551
639
|
// Return the model and dispatch the LoadSettings message
|
|
552
|
-
return [
|
|
640
|
+
return [
|
|
641
|
+
{
|
|
642
|
+
// Spread the initial model from LoadSettings
|
|
643
|
+
...LoadSettings.init(),
|
|
644
|
+
// ...
|
|
645
|
+
},
|
|
646
|
+
cmd.ofMsg(Msg.loadSettings())
|
|
647
|
+
];
|
|
553
648
|
};
|
|
554
649
|
|
|
555
650
|
// In our update function, we first distinguish between the sources of the messages
|
|
556
|
-
export
|
|
651
|
+
export function update (model: Model, msg: Message): UpdateReturnType<Model, Message> {
|
|
557
652
|
switch (msg.source) {
|
|
558
653
|
case "Composition":
|
|
559
654
|
// Then call the update function for the local messages
|
|
@@ -568,7 +663,7 @@ export const update = (model: Model, msg: Message): Elm.UpdateReturnType<Model,
|
|
|
568
663
|
// For the msg parameter we use the local CompositionMessage type
|
|
569
664
|
const updateComposition = (model: Model, msg: CompositionMessage): Elm.UpdateReturnType<Model, Message> => {
|
|
570
665
|
switch (msg.name) {
|
|
571
|
-
case "
|
|
666
|
+
case "myMessage":
|
|
572
667
|
return [{}];
|
|
573
668
|
}
|
|
574
669
|
}
|
|
@@ -585,12 +680,12 @@ To inform the parent component about some action, let's say to close a dialog fo
|
|
|
585
680
|
```ts Dialog.ts
|
|
586
681
|
export type Message =
|
|
587
682
|
...
|
|
588
|
-
| { name: "
|
|
683
|
+
| { name: "close" }
|
|
589
684
|
...
|
|
590
685
|
|
|
591
686
|
export const Msg = {
|
|
592
687
|
...
|
|
593
|
-
close: (): Message => ({ name: "
|
|
688
|
+
close: (): Message => ({ name: "close" }),
|
|
594
689
|
...
|
|
595
690
|
}
|
|
596
691
|
```
|
|
@@ -607,8 +702,9 @@ To inform the parent component about some action, let's say to close a dialog fo
|
|
|
607
702
|
|
|
608
703
|
```ts Dialog.ts
|
|
609
704
|
...
|
|
610
|
-
case "
|
|
705
|
+
case "close":
|
|
611
706
|
props.onClose();
|
|
707
|
+
|
|
612
708
|
return [{}];
|
|
613
709
|
...
|
|
614
710
|
```
|
|
@@ -629,6 +725,7 @@ To test your **update** function you can use some helper functions in `react-elm
|
|
|
629
725
|
| --- | --- |
|
|
630
726
|
| `getOfMsgParams` | Extracts the messages out of a command |
|
|
631
727
|
| `execCmd` | Executes the provided command and returns an array of all messages. |
|
|
728
|
+
| `getUpdateFn` | returns an `update` function for your update map object. |
|
|
632
729
|
|
|
633
730
|
### Testing the model and simple message commands
|
|
634
731
|
|
|
@@ -690,10 +787,10 @@ it("returns the correct cmd", () => {
|
|
|
690
787
|
|
|
691
788
|
### Testing with an UpdateMap
|
|
692
789
|
|
|
693
|
-
To test your update map
|
|
790
|
+
To test your update map, you can get an `update` function by calling `getUpdateFn`:
|
|
694
791
|
|
|
695
792
|
```ts
|
|
696
|
-
import
|
|
793
|
+
import { getUpdateFn } from "react-elmish/dist/Testing";
|
|
697
794
|
|
|
698
795
|
const update = getUpdateFn(updateMap);
|
|
699
796
|
|
|
@@ -701,7 +798,7 @@ const update = getUpdateFn(updateMap);
|
|
|
701
798
|
const [model, cmd] = update(msg, model, props);
|
|
702
799
|
```
|
|
703
800
|
|
|
704
|
-
## Migration from v1.x to
|
|
801
|
+
## Migration from v1.x to v2.x
|
|
705
802
|
|
|
706
803
|
* Use `Logger` and `Message` instead of `ILogger` and `IMessage`.
|
|
707
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:
|
|
@@ -711,3 +808,16 @@ const [model, cmd] = update(msg, model, props);
|
|
|
711
808
|
type Nullable<T> = T | null;
|
|
712
809
|
}
|
|
713
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.
|