Skip to content

Commit 371bfb0

Browse files
committed
feat: add callBase function to update function options
1 parent 5994826 commit 371bfb0

File tree

7 files changed

+151
-107
lines changed

7 files changed

+151
-107
lines changed

README.md

Lines changed: 103 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@ This library brings the elmish pattern to react.
2020
- [Setup](#setup)
2121
- [Error handling](#error-handling)
2222
- [React life cycle management](#react-life-cycle-management)
23+
- [Deferring model updates and messages](#deferring-model-updates-and-messages)
24+
- [Call back parent components](#call-back-parent-components)
2325
- [Composition](#composition)
2426
- [With an `UpdateMap`](#with-an-updatemap)
2527
- [With an update function](#with-an-update-function)
26-
- [Deferring model updates and messages](#deferring-model-updates-and-messages)
27-
- [Call back parent components](#call-back-parent-components)
2828
- [Testing](#testing)
2929
- [Testing the init function](#testing-the-init-function)
3030
- [Testing the update handler](#testing-the-update-handler)
@@ -570,6 +570,97 @@ class App extends ElmComponent<Shared.Model, Shared.Message, Shared.Props> {
570570
571571
In a functional component you can use the **useEffect** hook as normal.
572572
573+
## Deferring model updates and messages
574+
575+
Sometimes you want to always dispatch a message or update the model in all cases. You can use the `defer` function from the `options` parameter to do this. The `options` parameter is the fourth parameter of the `update` function.
576+
577+
Without the `defer` function, you would have to return the model and the command in all cases:
578+
579+
```ts
580+
const update: UpdateMap<Props, Model, Message> = {
581+
deferSomething (_msg, model) {
582+
if (model.someCondition) {
583+
return [{ alwaysUpdate: "someValue", extra: "extra" }, cmd.ofMsg(Msg.alwaysExecute())];
584+
}
585+
586+
return [{ alwaysUpdate: "someValue" }, cmd.ofMsg(Msg.doSomethingElse()), cmd.ofMsg(Msg.alwaysExecute())];
587+
},
588+
589+
...LoadSettings.update,
590+
};
591+
```
592+
593+
Here we always want to update the model with the `alwaysUpdate` property and always dispatch the `alwaysExecute` message.
594+
595+
With the `defer` function, you can do this:
596+
597+
```ts
598+
const update: UpdateMap<Props, Model, Message> = {
599+
deferSomething (_msg, model, _props, { defer }) {
600+
defer({ alwaysUpdate: "someValue" }, cmd.ofMsg(Msg.alwaysExecute()));
601+
602+
if (model.someCondition) {
603+
return [{ extra: "extra" }];
604+
}
605+
606+
return [{}, cmd.ofMsg(Msg.doSomethingElse())];
607+
},
608+
609+
...LoadSettings.update,
610+
};
611+
```
612+
613+
The `defer` function can be called multiple times. Model updates and commands are then aggregated. Model updates by the return value overwrite the deferred model updates, while deferred messages are dispatched after the returned messages.
614+
615+
## Call back parent components
616+
617+
Since each component has its own model and messages, communication with parent components is done via callback functions.
618+
619+
To inform the parent component about some action, let's say to close a dialog form, you do the following:
620+
621+
1. Create a message
622+
623+
```ts Dialog.ts
624+
export type Message =
625+
...
626+
| { name: "close" }
627+
...
628+
629+
export const Msg = {
630+
...
631+
close: (): Message => ({ name: "close" }),
632+
...
633+
}
634+
```
635+
636+
1. Define a callback function property in the **Props**:
637+
638+
```ts Dialog.ts
639+
export type Props = {
640+
onClose: () => void,
641+
};
642+
```
643+
644+
1. Handle the message and call the callback function:
645+
646+
```ts Dialog.ts
647+
{
648+
// ...
649+
close () {
650+
return [{}, cmd.ofError(props.onClose, Msg.error)];
651+
}
652+
// ...
653+
};
654+
```
655+
656+
1. In the **render** method of the parent component pass the callback as prop
657+
658+
```tsx Parent.tsx
659+
...
660+
<Dialog onClose={() => this.dispatch(Msg.closeDialog())}>
661+
...
662+
```
663+
573664
## Composition
574665

575666
If you have some business logic that you want to reuse in other components, you can do this by using different sources for messages.
@@ -672,6 +763,16 @@ const update: UpdateMap<Props, Model, Message> = {
672763
},
673764

674765
...LoadSettings.update,
766+
767+
// You can overwrite the LoadSettings messages handlers here
768+
769+
settingsLoaded (_msg, _model, _props, { defer, callBase }) {
770+
// Use defer and callBase to execute the original handler function:
771+
defer(...callBase(LoadSettings.settingsLoaded));
772+
773+
// Do additional stuff
774+
return [{ /* ... */ }];
775+
}
675776
};
676777
```
677778
@@ -799,97 +900,6 @@ const updateComposition = (model: Model, msg: CompositionMessage): Elm.UpdateRet
799900
}
800901
```
801902
802-
## Deferring model updates and messages
803-
804-
Sometimes you want to always dispatch a message or update the model in all cases. You can use the `defer` function from the `options` parameter to do this. The `options` parameter is the fourth parameter of the `update` function.
805-
806-
Without the `defer` function, you would have to return the model and the command in all cases:
807-
808-
```ts
809-
const update: UpdateMap<Props, Model, Message> = {
810-
deferSomething (_msg, model) {
811-
if (model.someCondition) {
812-
return [{ alwaysUpdate: "someValue", extra: "extra" }, cmd.ofMsg(Msg.alwaysExecute())];
813-
}
814-
815-
return [{ alwaysUpdate: "someValue" }, cmd.ofMsg(Msg.doSomethingElse()), cmd.ofMsg(Msg.alwaysExecute())];
816-
},
817-
818-
...LoadSettings.update,
819-
};
820-
```
821-
822-
Here we always want to update the model with the `alwaysUpdate` property and always dispatch the `alwaysExecute` message.
823-
824-
With the `defer` function, you can do this:
825-
826-
```ts
827-
const update: UpdateMap<Props, Model, Message> = {
828-
deferSomething (_msg, model, _props, { defer }) {
829-
defer({ alwaysUpdate: "someValue" }, cmd.ofMsg(Msg.alwaysExecute()));
830-
831-
if (model.someCondition) {
832-
return [{ extra: "extra" }];
833-
}
834-
835-
return [{}, cmd.ofMsg(Msg.doSomethingElse())];
836-
},
837-
838-
...LoadSettings.update,
839-
};
840-
```
841-
842-
The `defer` function can be called multiple times. Model updates and commands are then aggregated. Model updates by the return value overwrite the deferred model updates, while deferred messages are dispatched after the returned messages.
843-
844-
## Call back parent components
845-
846-
Since each component has its own model and messages, communication with parent components is done via callback functions.
847-
848-
To inform the parent component about some action, let's say to close a dialog form, you do the following:
849-
850-
1. Create a message
851-
852-
```ts Dialog.ts
853-
export type Message =
854-
...
855-
| { name: "close" }
856-
...
857-
858-
export const Msg = {
859-
...
860-
close: (): Message => ({ name: "close" }),
861-
...
862-
}
863-
```
864-
865-
1. Define a callback function property in the **Props**:
866-
867-
```ts Dialog.ts
868-
export type Props = {
869-
onClose: () => void,
870-
};
871-
```
872-
873-
1. Handle the message and call the callback function:
874-
875-
```ts Dialog.ts
876-
{
877-
// ...
878-
close () {
879-
return [{}, cmd.ofError(props.onClose, Msg.error)];
880-
}
881-
// ...
882-
};
883-
```
884-
885-
1. In the **render** method of the parent component pass the callback as prop
886-
887-
```tsx Parent.tsx
888-
...
889-
<Dialog onClose={() => this.dispatch(Msg.closeDialog())}>
890-
...
891-
```
892-
893903
## Testing
894904
895905
To test your **update** handler you can use some helper functions in `react-elmish/dist/Testing`:

src/ElmComponent.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React from "react";
22
import { execCmd, logMessage, modelHasChanged } from "./Common";
33
import { Services } from "./Init";
44
import type { Cmd, InitFunction, Message, Nullable, UpdateFunction } from "./Types";
5+
import { createCallBase } from "./createCallBase";
56
import { createDefer } from "./createDefer";
67
import { getFakeOptionsOnce } from "./fakeOptions";
78

@@ -99,11 +100,14 @@ abstract class ElmComponent<TModel, TMessage extends Message, TProps> extends Re
99100
let modified = false;
100101

101102
do {
102-
logMessage(this.componentName, nextMsg);
103+
const currentMessage = nextMsg;
104+
105+
logMessage(this.componentName, currentMessage);
103106

104107
const [defer, getDeferred] = createDefer<TModel, TMessage>();
108+
const callBase = createCallBase(currentMessage, this.currentModel, this.props, { defer });
105109

106-
const [model, ...commands] = this.update(this.currentModel, nextMsg, this.props, { defer });
110+
const [model, ...commands] = this.update(this.currentModel, currentMessage, this.props, { defer, callBase });
107111

108112
const [deferredModel, deferredCommands] = getDeferred();
109113

src/Testing/getUpdateFn.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Message, Nullable, UpdateFunctionOptions, UpdateMap, UpdateReturnType } from "../Types";
2+
import { createCallBase } from "../createCallBase";
23
import { createDefer } from "../createDefer";
34
import { callUpdateMap } from "../useElmish";
45
import { execCmd } from "./execCmd";
@@ -18,9 +19,11 @@ function getUpdateFn<TProps, TModel, TMessage extends Message>(
1819
): (msg: TMessage, model: TModel, props: TProps) => UpdateReturnType<TModel, TMessage> {
1920
return function updateFn(msg, model, props): UpdateReturnType<TModel, TMessage> {
2021
const [defer, getDeferred] = createDefer<TModel, TMessage>();
22+
const callBase = createCallBase(msg, model, props, { defer });
2123

22-
const options: UpdateFunctionOptions<TModel, TMessage> = {
24+
const options: UpdateFunctionOptions<TProps, TModel, TMessage> = {
2325
defer,
26+
callBase,
2427
};
2528

2629
const [updatedModel, ...commands] = callUpdateMap(updateMap, msg, model, props, options);
@@ -46,9 +49,11 @@ function getUpdateAndExecCmdFn<TProps, TModel, TMessage extends Message>(
4649
): (msg: TMessage, model: TModel, props: TProps) => Promise<[Partial<TModel>, Nullable<TMessage>[]]> {
4750
return async function updateAndExecCmdFn(msg, model, props): Promise<[Partial<TModel>, Nullable<TMessage>[]]> {
4851
const [defer, getDeferred] = createDefer<TModel, TMessage>();
52+
const callBase = createCallBase(msg, model, props, { defer });
4953

50-
const options: UpdateFunctionOptions<TModel, TMessage> = {
54+
const options: UpdateFunctionOptions<TProps, TModel, TMessage> = {
5155
defer,
56+
callBase,
5257
};
5358

5459
const [updatedModel, ...commands] = callUpdateMap(updateMap, msg, model, props, options);

src/Types.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,16 +37,23 @@ type InitFunction<TProps, TModel, TMessage> = (props: TProps) => InitResult<TMod
3737
type UpdateReturnType<TModel, TMessage> = [Partial<TModel>, ...(Cmd<TMessage> | undefined)[]];
3838

3939
type DeferFunction<TModel, TMessage> = (model: Partial<TModel>, ...commands: (Cmd<TMessage> | undefined)[]) => void;
40+
type UpdateMapFunction<TProps, TModel, TMessage> = (
41+
msg: TMessage,
42+
model: TModel,
43+
props: TProps,
44+
options: UpdateFunctionOptions<TProps, TModel, TMessage>,
45+
) => UpdateReturnType<TModel, TMessage>;
4046

41-
interface UpdateFunctionOptions<TModel, TMessage> {
47+
interface UpdateFunctionOptions<TProps, TModel, TMessage> {
4248
defer: DeferFunction<TModel, TMessage>;
49+
callBase: (fn: UpdateMapFunction<TProps, TModel, TMessage>) => UpdateReturnType<TModel, TMessage>;
4350
}
4451

4552
type UpdateFunction<TProps, TModel, TMessage> = (
4653
model: TModel,
4754
msg: TMessage,
4855
props: TProps,
49-
options: UpdateFunctionOptions<TModel, TMessage>,
56+
options: UpdateFunctionOptions<TProps, TModel, TMessage>,
5057
) => UpdateReturnType<TModel, TMessage>;
5158

5259
/**
@@ -58,7 +65,7 @@ type UpdateMap<TProps, TModel, TMessage extends Message> = {
5865
msg: TMessage & { name: TMessageName },
5966
model: TModel,
6067
props: TProps,
61-
options: UpdateFunctionOptions<TModel, TMessage>,
68+
options: UpdateFunctionOptions<TProps, TModel, TMessage>,
6269
) => UpdateReturnType<TModel, TMessage>;
6370
};
6471

@@ -76,5 +83,6 @@ export type {
7683
UpdateFunction,
7784
UpdateFunctionOptions,
7885
UpdateMap,
86+
UpdateMapFunction,
7987
UpdateReturnType,
8088
};

src/createCallBase.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type { UpdateFunctionOptions, UpdateMapFunction, UpdateReturnType } from "./Types";
2+
3+
function createCallBase<TProps, TModel, TMessage>(
4+
msg: TMessage,
5+
model: TModel,
6+
props: TProps,
7+
options: Omit<UpdateFunctionOptions<TProps, TModel, TMessage>, "callBase">,
8+
): (fn: UpdateMapFunction<TProps, TModel, TMessage>) => UpdateReturnType<TModel, TMessage> {
9+
const callBase = (fn: UpdateMapFunction<TProps, TModel, TMessage>): UpdateReturnType<TModel, TMessage> =>
10+
fn(msg, model, props, { ...options, callBase });
11+
12+
return callBase;
13+
}
14+
15+
export { createCallBase };

src/useElmish.spec.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ interface Props {
1616
model: Model,
1717
msg: Message,
1818
props: Props,
19-
options: UpdateFunctionOptions<Model, Message>,
19+
options: UpdateFunctionOptions<Props, Model, Message>,
2020
) => UpdateReturnType<Model, Message>;
2121
subscription?: (model: Model) => SubscriptionResult<Message>;
2222
}
@@ -35,7 +35,7 @@ function defaultUpdate(
3535
_model: Model,
3636
msg: Message,
3737
_props: Props,
38-
{ defer }: UpdateFunctionOptions<Model, Message>,
38+
{ defer }: UpdateFunctionOptions<Props, Model, Message>,
3939
): UpdateReturnType<Model, Message> {
4040
switch (msg.name) {
4141
case "Test":

0 commit comments

Comments
 (0)