react-elmish 1.4.4
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/main.yml +40 -0
- package/.github/workflows/publish.yml +42 -0
- package/README.md +661 -0
- package/dist/Cmd.d.ts +92 -0
- package/dist/Cmd.js +230 -0
- package/dist/ElmComponent.d.ts +69 -0
- package/dist/ElmComponent.js +227 -0
- package/dist/ElmUtilities.d.ts +17 -0
- package/dist/ElmUtilities.js +26 -0
- package/dist/Hooks.d.ts +5 -0
- package/dist/Hooks.js +124 -0
- package/dist/Init.d.ts +20 -0
- package/dist/Init.js +23 -0
- package/dist/Testing/index.d.ts +19 -0
- package/dist/Testing/index.js +124 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +46 -0
- package/package.json +44 -0
package/README.md
ADDED
|
@@ -0,0 +1,661 @@
|
|
|
1
|
+
# react-elmish
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+
|
|
6
|
+
This library brings the elmish pattern to react.
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
`npm install react-elmish`
|
|
11
|
+
|
|
12
|
+
## Basic Usage
|
|
13
|
+
|
|
14
|
+
An elmish component basically consists of the following parts:
|
|
15
|
+
|
|
16
|
+
* The **Model** holding the state of the component.
|
|
17
|
+
* The **Props** for the component.
|
|
18
|
+
* The **Init** function to create the initial model based on the props.
|
|
19
|
+
* The **Messages** to dispatch which modify the model.
|
|
20
|
+
* The **Update** function to modify the model based on a specific message.
|
|
21
|
+
* The **View** which renders the component based on the current model.
|
|
22
|
+
|
|
23
|
+
### App.ts
|
|
24
|
+
|
|
25
|
+
First import everything from `react-elmish` and declare the **Message** discriminated union type:
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import * as Elm from "react-elmish";
|
|
29
|
+
|
|
30
|
+
export type Message =
|
|
31
|
+
| { name: "Increment" }
|
|
32
|
+
| { name: "Decrement" }
|
|
33
|
+
;
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
You can also create some convenience functions to dispatch a message:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
export const Msg = {
|
|
40
|
+
increment: (): Message => ({ name: "Increment" }),
|
|
41
|
+
decrement: (): Message => ({ name: "Decrement" }),
|
|
42
|
+
};
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Now we can create a `cmd` object for our messages type:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
const cmd = Elm.createCmd<Message>();
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Next, declare the model:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
export type Model = Readonly<{
|
|
55
|
+
value: number,
|
|
56
|
+
}>;
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The props are optional:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
export type Props = {
|
|
63
|
+
initialValue: number,
|
|
64
|
+
};
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
To create the initial model we need an **init** function:
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
export const init = (props: Props): [Model, Elm.Cmd<Message>] => {
|
|
71
|
+
const model: Model = {
|
|
72
|
+
value: props.initialValue,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
return [model, cmd.none];
|
|
76
|
+
};
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
To update the model based on a message we need an **update** function:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
export const update = (model: Model, msg: Msg, props: Props): Elm.UpdateReturnType<Model, Message> => {
|
|
83
|
+
switch (msg.name) {
|
|
84
|
+
case "Increment":
|
|
85
|
+
return [{ value: model.value + 1 }];
|
|
86
|
+
|
|
87
|
+
case "Decrement":
|
|
88
|
+
return [{ value: model.value - 1 }];
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
> **Note:** If you are using **typescript** and **typescript-eslint** you should enable the [switch-exhaustive-check](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md) rule.
|
|
94
|
+
|
|
95
|
+
### App.tsx
|
|
96
|
+
|
|
97
|
+
To put all this together and to render our component, we need a React component.
|
|
98
|
+
|
|
99
|
+
This can be a **class component**:
|
|
100
|
+
|
|
101
|
+
```tsx
|
|
102
|
+
// Import everything from the App.ts
|
|
103
|
+
import * as Shared from "../App";
|
|
104
|
+
// Import the ElmComponent which extends the React.Component
|
|
105
|
+
import { ElmComponent } from "react-elmish";
|
|
106
|
+
// Don't forget to import react
|
|
107
|
+
import React from "react";
|
|
108
|
+
|
|
109
|
+
// Create an elmish class component
|
|
110
|
+
class App extends ElmComponent<Shared.Model, Shared.Message, Shared.Props> {
|
|
111
|
+
// Construct the component with the props and init function
|
|
112
|
+
constructor(props: Shared.Props) {
|
|
113
|
+
super(props, Shared.init, "App");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Assign our update function to the component
|
|
117
|
+
update = Shared.update;
|
|
118
|
+
|
|
119
|
+
render(): React.ReactNode {
|
|
120
|
+
// Access the model
|
|
121
|
+
const { value } = this.model;
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<div>
|
|
125
|
+
{/* Display our current value */}
|
|
126
|
+
<p>{value}</p>
|
|
127
|
+
|
|
128
|
+
{/* Dispatch messages */}
|
|
129
|
+
<button onClick={() => this.dispatch(Shared.Msg.increment())}>Increment</button>
|
|
130
|
+
<button onClick={() => this.dispatch(Shared.Msg.decrement())}>Decrement</button>
|
|
131
|
+
</div>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
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
|
+
You can use these components like any other React component.
|
|
162
|
+
|
|
163
|
+
> **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
|
+
>
|
|
165
|
+
> 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
|
+
|
|
167
|
+
## More on messages
|
|
168
|
+
|
|
169
|
+
### Message arguments
|
|
170
|
+
|
|
171
|
+
Messages can also have arguments. You can modify the example above and pass an optional step value to the **Increment** message:
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
export type Message =
|
|
175
|
+
| { name: "Increment", step?: number }
|
|
176
|
+
...
|
|
177
|
+
|
|
178
|
+
export const Msg = {
|
|
179
|
+
increment: (step?: number): Message => ({ name: "Increment", step }),
|
|
180
|
+
...
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Then use this argument in the **update** function:
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
...
|
|
188
|
+
case "Increment":
|
|
189
|
+
return [{ value: model.value + (msg.step ?? 1)}]
|
|
190
|
+
...
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
In the **render** method you can add another button to increment the value by 10:
|
|
194
|
+
|
|
195
|
+
```tsx
|
|
196
|
+
...
|
|
197
|
+
<button onClick={() => this.dispatch(Shared.Msg.increment(10))}>Increment by 10</button>
|
|
198
|
+
...
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Symbols instead of strings
|
|
202
|
+
|
|
203
|
+
You can also use **Symbols** for the message type instead of strings:
|
|
204
|
+
|
|
205
|
+
1. Declare a Symbol for the message:
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
const ResetMsg = Symbol("reset");
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
1. Use this Symbol as message name:
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
export type Message =
|
|
215
|
+
...
|
|
216
|
+
| { name: typeof ResetMsg }
|
|
217
|
+
...
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
1. Create the convenient function
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
export const Msg = {
|
|
224
|
+
...
|
|
225
|
+
reset: (): Message => ({ name: ResetMsg }),
|
|
226
|
+
...
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
1. Handle the new message in the **update** function:
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
...
|
|
234
|
+
case ResetMsg:
|
|
235
|
+
return [{ value: 0 }];
|
|
236
|
+
...
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## Setup
|
|
240
|
+
|
|
241
|
+
**react-elmish** works without a setup. But if you want to use logging or some middleware, you can setup **react-elmish** at the start of your program.
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
import * as Elm from "react-elmish";
|
|
245
|
+
|
|
246
|
+
const myLogger = {
|
|
247
|
+
debug(...args: unknown []) {
|
|
248
|
+
console.debug(...args);
|
|
249
|
+
},
|
|
250
|
+
info(...args: unknown []) {
|
|
251
|
+
console.info(...args);
|
|
252
|
+
},
|
|
253
|
+
error(...args: unknown []) {
|
|
254
|
+
console.error(...args);
|
|
255
|
+
},
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
Elm.init({
|
|
259
|
+
logger: myLogger,
|
|
260
|
+
errorMiddleware: error => Toast.error(error.message),
|
|
261
|
+
dispatchMiddleware: msg => console.log(msg),
|
|
262
|
+
});
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
The error middleware function is called by the `handleError` function (see [Error handling](#error-handling)).
|
|
266
|
+
|
|
267
|
+
The dispatch middleware function is called whenever a Message is dispatched.
|
|
268
|
+
|
|
269
|
+
## Error handling
|
|
270
|
+
|
|
271
|
+
You can handle errors easily with the following pattern.
|
|
272
|
+
|
|
273
|
+
1. Add an error message:
|
|
274
|
+
|
|
275
|
+
```ts
|
|
276
|
+
export type Message =
|
|
277
|
+
| ...
|
|
278
|
+
| { name: "Error", error: Error }
|
|
279
|
+
;
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
1. Optionally add the convenient function to the **Msg** object:
|
|
283
|
+
|
|
284
|
+
```ts
|
|
285
|
+
export const Msg = {
|
|
286
|
+
...
|
|
287
|
+
error: (error: Error): Message => ({ name: "Error", error }),
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
1. Handle the error message in the **update** function:
|
|
292
|
+
|
|
293
|
+
```ts
|
|
294
|
+
...
|
|
295
|
+
case "Error":
|
|
296
|
+
return Elm.handleError(msg.error);
|
|
297
|
+
...
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
The **handleError** function then calls your error handling middleware.
|
|
301
|
+
|
|
302
|
+
## Dispatch commands in the update function
|
|
303
|
+
|
|
304
|
+
In addition to modifying the model, you can dispatch new commands in the **update** function.
|
|
305
|
+
|
|
306
|
+
To do so, you can call one of the functions in the `cmd` object:
|
|
307
|
+
|
|
308
|
+
| Function | Description |
|
|
309
|
+
|---|---|
|
|
310
|
+
| `cmd.none` | Does nothing. Equivalent to omit the second value. |
|
|
311
|
+
| `cmd.ofMsg` | Dispatches a new message. |
|
|
312
|
+
| `cmd.batch` | Aggregates an array of messages. |
|
|
313
|
+
| `cmd.ofFunc.either` | Calls a synchronous function and maps the result into a message. |
|
|
314
|
+
| `cmd.ofFunc.attempt` | Like `either` but ignores the success case. |
|
|
315
|
+
| `cmd.ofFunc.perform` | Like `either` but ignores the error case. |
|
|
316
|
+
| `cmd.ofPromise.either` | Calls an async function and maps the result into a message. |
|
|
317
|
+
| `cmd.ofPromise.attempt` | Like `either` but ignores the success case. |
|
|
318
|
+
| `cmd.ofPromise.perform` | Like `either` but ignores the error case. |
|
|
319
|
+
|
|
320
|
+
### Dispatch a message
|
|
321
|
+
|
|
322
|
+
Let's assume you have a message to display the description of the last called message:
|
|
323
|
+
|
|
324
|
+
```ts
|
|
325
|
+
export type Message =
|
|
326
|
+
...
|
|
327
|
+
| { name: "PrintLastMessage", message: string }
|
|
328
|
+
...
|
|
329
|
+
|
|
330
|
+
export const Msg = {
|
|
331
|
+
...
|
|
332
|
+
printLastMessage: (message: string): Message => ({ name: "PrintLastMessage", message }),
|
|
333
|
+
...
|
|
334
|
+
}
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
In the **update** function you can dispatch that message like this:
|
|
338
|
+
|
|
339
|
+
```ts
|
|
340
|
+
case "Increment":
|
|
341
|
+
return [{ value: model.value + 1 }, cmd.ofMsg(Msg.printLastMessage("Incremented by one"))];
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
This new message will immediately be dispatched after returning from the **update** function.
|
|
345
|
+
|
|
346
|
+
### Call an async function
|
|
347
|
+
|
|
348
|
+
This way you can also call functions and async operations. For an async function like:
|
|
349
|
+
|
|
350
|
+
```ts
|
|
351
|
+
const loadSettings = async (arg1: string, arg2: number): Promise<Settings> => {
|
|
352
|
+
const settings = await Storage.loadSettings();
|
|
353
|
+
return settings;
|
|
354
|
+
}
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
you can define the following messages:
|
|
358
|
+
|
|
359
|
+
```ts
|
|
360
|
+
export type Messages =
|
|
361
|
+
...
|
|
362
|
+
| { name: "LoadSettings" },
|
|
363
|
+
| { name: "SettingsLoaded", settings: Settings }
|
|
364
|
+
| { name: "Error", error: Error }
|
|
365
|
+
...
|
|
366
|
+
|
|
367
|
+
export const Msg = {
|
|
368
|
+
...
|
|
369
|
+
loadSettings: (): Message => ({ name: "LoadSettings" }),
|
|
370
|
+
settingsLoaded: (settings: Settings): Message => ({ name: "SettingsLoaded", settings }),
|
|
371
|
+
error: (error: Error): Message => ({ name: "Error", error }),
|
|
372
|
+
...
|
|
373
|
+
};
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
and handle the messages in the **update** function:
|
|
377
|
+
|
|
378
|
+
```ts
|
|
379
|
+
...
|
|
380
|
+
case "LoadSettings":
|
|
381
|
+
// Create a command out of the async function with the provided arguments
|
|
382
|
+
// If loadSettings resolves it dispatches "SettingsLoaded"
|
|
383
|
+
// If it fails it dispatches "Error"
|
|
384
|
+
// The return type of loadSettings must fit Msg.settingsLoaded
|
|
385
|
+
return [{}, cmd.ofPromise.either(loadSettings, Msg.settingsLoaded, Msg.error, "firstArg", 123)];
|
|
386
|
+
|
|
387
|
+
case "SettingsLoaded":
|
|
388
|
+
return [{ settings: msg.settings }];
|
|
389
|
+
|
|
390
|
+
case "Error":
|
|
391
|
+
return Elm.handleError(msg.error);
|
|
392
|
+
...
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
## React life cycle management
|
|
396
|
+
|
|
397
|
+
If you want to use `componentDidMount` or `componentWillUnmount` in a class component, don't forget to call the base class implementation of it as the **ElmComponent** is using them internally.
|
|
398
|
+
|
|
399
|
+
```ts
|
|
400
|
+
class App extends ElmComponent<Shared.Model, Shared.Message, Shared.Props> {
|
|
401
|
+
...
|
|
402
|
+
componentDidMount() {
|
|
403
|
+
super.componentDidMount();
|
|
404
|
+
|
|
405
|
+
// your code
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
componentWillUnmount() {
|
|
409
|
+
super.componentWillUnmount();
|
|
410
|
+
|
|
411
|
+
// your code
|
|
412
|
+
}
|
|
413
|
+
...
|
|
414
|
+
}
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
In a functional component you can use the **useEffect** hook as normal.
|
|
418
|
+
|
|
419
|
+
## Composition
|
|
420
|
+
|
|
421
|
+
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
|
+
|
|
423
|
+
Let's say you want to load some settings, you can write a module like this:
|
|
424
|
+
|
|
425
|
+
```ts LoadSettings.ts
|
|
426
|
+
import * as Elm from "react-elmish";
|
|
427
|
+
|
|
428
|
+
export type Settings = {
|
|
429
|
+
// ...
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
// We use a MsgSource to differentiate between the messages
|
|
433
|
+
type MessageSource = Elm.MsgSource<"LoadSettings">;
|
|
434
|
+
|
|
435
|
+
// Add that MessageSource to all the messages
|
|
436
|
+
export type Message =
|
|
437
|
+
| { name: "LoadSettings" } & MessageSource
|
|
438
|
+
| { name: "SettingsLoaded", settings: Settings } & MessageSource
|
|
439
|
+
| { name: "Error", error: Error } & MessageSource
|
|
440
|
+
|
|
441
|
+
// Do the same for the convenient functions
|
|
442
|
+
const MsgSource: MessageSource = { source: "LoadSettings" };
|
|
443
|
+
|
|
444
|
+
export const Msg = {
|
|
445
|
+
loadSettings: (): Message => ({ name: "LoadSettings", ...MsgSource }),
|
|
446
|
+
settingsLoaded: (settings: Settings): Message => ({ name: "SettingsLoaded", settings, ...MsgSource }),
|
|
447
|
+
error: (error: Error): Message => ({ name: "Error", error, ...MsgSource }),
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
const cmd = Elm.createCmd<Message>();
|
|
451
|
+
|
|
452
|
+
export type Model = Readonly<{
|
|
453
|
+
settings: Nullable<Settings>,
|
|
454
|
+
}>;
|
|
455
|
+
|
|
456
|
+
export const init = (): Model => ({
|
|
457
|
+
settings: null,
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
export const update = (_model: Model, msg: Message): Elm.UpdateReturnType<Model, Message> => {
|
|
461
|
+
switch (msg.name) {
|
|
462
|
+
case "LoadSettings":
|
|
463
|
+
return [{}, cmd.ofPromise.either(loadSettings, Msg.settingsLoaded, Msg.error)];
|
|
464
|
+
|
|
465
|
+
case "SettingsLoaded":
|
|
466
|
+
return [{ settings: msg.settings }];
|
|
467
|
+
|
|
468
|
+
case "Error":
|
|
469
|
+
return Elm.handleError(msg.error);
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
const loadSettings = async (): Promise<Settings> => {
|
|
474
|
+
// Call some service (e.g. database or backend)
|
|
475
|
+
return Promise.resolve({});
|
|
476
|
+
};
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
> **Note**: This module has no **View**.
|
|
480
|
+
|
|
481
|
+
In other components where we want to use this **LoadSettings** module, we also need a message source:
|
|
482
|
+
|
|
483
|
+
```ts Composition.ts
|
|
484
|
+
import * as Elm from "react-elmish";
|
|
485
|
+
// Import the LoadSettings module
|
|
486
|
+
import * as LoadSettings from "./LoadSettings";
|
|
487
|
+
|
|
488
|
+
// Create a message source for this module
|
|
489
|
+
type MessageSource = Elm.MsgSource<"Composition">;
|
|
490
|
+
|
|
491
|
+
// Here we define our local messages
|
|
492
|
+
// We don't need to export them
|
|
493
|
+
type CompositionMessage =
|
|
494
|
+
| { name: "MyMessage" } & MessageSource
|
|
495
|
+
;
|
|
496
|
+
|
|
497
|
+
// Combine the local messages and the ones from LoadSettings
|
|
498
|
+
export type Message =
|
|
499
|
+
| CompositionMessage
|
|
500
|
+
| LoadSettings.Message
|
|
501
|
+
;
|
|
502
|
+
|
|
503
|
+
const MsgSource: MessageSource = { source: "Composition" };
|
|
504
|
+
|
|
505
|
+
export const Msg = {
|
|
506
|
+
myMessage: (): Message => ({ name: "MyMessage", ...MsgSource }),
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
const cmd = Elm.createCmd<Message>();
|
|
510
|
+
|
|
511
|
+
// Include the LoadSettings Model
|
|
512
|
+
export type Model = Readonly<{
|
|
513
|
+
// ...
|
|
514
|
+
}> & LoadSettings.Model;
|
|
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
|
+
};
|
|
522
|
+
|
|
523
|
+
// Return the model and dispatch the LoadSettings message
|
|
524
|
+
return [model, cmd.ofMsg(LoadSettings.Msg.loadSettings())];
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
// In our update function, we first distinguish between the sources of the messages
|
|
528
|
+
export const update = (model: Model, msg: Message): Elm.UpdateReturnType<Model, Message> => {
|
|
529
|
+
switch (msg.source) {
|
|
530
|
+
case "Composition":
|
|
531
|
+
// Then call the update function for the local messages
|
|
532
|
+
return updateComposition(model, msg);
|
|
533
|
+
|
|
534
|
+
case "LoadSettings":
|
|
535
|
+
// Or call the update function for the LoadSettings messages
|
|
536
|
+
return LoadSettings.update(model, msg);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// For the msg parameter we use the local CompositionMessage type
|
|
541
|
+
const updateComposition = (model: Model, msg: CompositionMessage): Elm.UpdateReturnType<Model, Message> => {
|
|
542
|
+
switch (msg.name) {
|
|
543
|
+
case "MyMessage":
|
|
544
|
+
return [{}];
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
## Call back parent components
|
|
550
|
+
|
|
551
|
+
Since each component has its own model and messages, communication with parent components is done via callback functions.
|
|
552
|
+
|
|
553
|
+
To inform the parent component about some action, let's say to close a dialog form, you do the following:
|
|
554
|
+
|
|
555
|
+
1. Create a message
|
|
556
|
+
|
|
557
|
+
```ts Dialog.ts
|
|
558
|
+
export type Message =
|
|
559
|
+
...
|
|
560
|
+
| { name: "Close" }
|
|
561
|
+
...
|
|
562
|
+
|
|
563
|
+
export const Msg = {
|
|
564
|
+
...
|
|
565
|
+
close: (): Message => ({ name: "Close" }),
|
|
566
|
+
...
|
|
567
|
+
}
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
1. Define a callback function property in the **Props**:
|
|
571
|
+
|
|
572
|
+
```ts Dialog.ts
|
|
573
|
+
export type Props = {
|
|
574
|
+
onClose: () => void,
|
|
575
|
+
};
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
1. Handle the message and call the callback function:
|
|
579
|
+
|
|
580
|
+
```ts Dialog.ts
|
|
581
|
+
...
|
|
582
|
+
case "Close":
|
|
583
|
+
props.onClose();
|
|
584
|
+
return [{}];
|
|
585
|
+
...
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
1. In the **render** method of the parent component pass the callback as prop
|
|
589
|
+
|
|
590
|
+
```tsx Parent.tsx
|
|
591
|
+
...
|
|
592
|
+
<Dialog onClose={() => this.dispatch(Msg.closeDialog())}>
|
|
593
|
+
...
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
## Testing
|
|
597
|
+
|
|
598
|
+
To test your **update** function you can use some helper functions in `react-elmish/dist/Testing`:
|
|
599
|
+
|
|
600
|
+
| Function | Description |
|
|
601
|
+
| --- | --- |
|
|
602
|
+
| `getOfMsgParams` | Extracts the messages out of a command |
|
|
603
|
+
| `execCmd` | Executes the provided command and returns an array of all messages. |
|
|
604
|
+
|
|
605
|
+
### Testing the model and simple message commands
|
|
606
|
+
|
|
607
|
+
```ts
|
|
608
|
+
import * as Testing from "react-elmish/dist/Testing";
|
|
609
|
+
|
|
610
|
+
...
|
|
611
|
+
it("returns the correct model and cmd", () => {
|
|
612
|
+
// arrange
|
|
613
|
+
const model = // create model for test
|
|
614
|
+
const props = // create props for test
|
|
615
|
+
const msg = Shared.Msg.test();
|
|
616
|
+
|
|
617
|
+
const expectedValue = // what you expect in the model
|
|
618
|
+
const expectedCmds = [
|
|
619
|
+
Shared.Msg.expectedMsg1("arg"),
|
|
620
|
+
Shared.Msg.expectedMsg2(),
|
|
621
|
+
];
|
|
622
|
+
|
|
623
|
+
// act
|
|
624
|
+
const [newModel, cmd] = Shared.update(model, msg, props);
|
|
625
|
+
|
|
626
|
+
// assert
|
|
627
|
+
expect(newModel.value).toEqual(expectedValue);
|
|
628
|
+
expect(Testing.getOfMsgParams(cmd)).toEqual(expectedCmds);
|
|
629
|
+
});
|
|
630
|
+
...
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
### Testing all (async) messages
|
|
634
|
+
|
|
635
|
+
With `execCmd` you can execute all commands in a test scenario. All functions are called and awaited. The function returns all new messages (success or error messages).
|
|
636
|
+
|
|
637
|
+
It also resolves for `attempt` functions if the called functions succeed. And it rejects for `perform` functions if the called functions fail.
|
|
638
|
+
|
|
639
|
+
```ts
|
|
640
|
+
import * as Testing from "react-elmish/dist/Testing";
|
|
641
|
+
|
|
642
|
+
...
|
|
643
|
+
it("returns the correct cmd", () => {
|
|
644
|
+
// arrange
|
|
645
|
+
const model = { /* create model */ };
|
|
646
|
+
const props = { /* create props */ };
|
|
647
|
+
const msg = Shared.Msg.asyncTest();
|
|
648
|
+
|
|
649
|
+
// mock function which is called when the "AsyncTest" message is handled
|
|
650
|
+
const functionMock = jest.fn();
|
|
651
|
+
|
|
652
|
+
// act
|
|
653
|
+
const [, cmd] = Shared.update(model, msg, props);
|
|
654
|
+
const messages = await Testing.execCmd(cmd);
|
|
655
|
+
|
|
656
|
+
// assert
|
|
657
|
+
expect(functionMock).toBeCalled();
|
|
658
|
+
expect(messages).toEqual([Shared.Msg.asyncTestSuccess()])
|
|
659
|
+
});
|
|
660
|
+
...
|
|
661
|
+
```
|