Skip to content

Commit

Permalink
feat: add callBase function to update function options
Browse files Browse the repository at this point in the history
  • Loading branch information
atheck committed Jun 26, 2024
1 parent 5994826 commit 371bfb0
Show file tree
Hide file tree
Showing 7 changed files with 151 additions and 107 deletions.
196 changes: 103 additions & 93 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ This library brings the elmish pattern to react.
- [Setup](#setup)
- [Error handling](#error-handling)
- [React life cycle management](#react-life-cycle-management)
- [Deferring model updates and messages](#deferring-model-updates-and-messages)
- [Call back parent components](#call-back-parent-components)
- [Composition](#composition)
- [With an `UpdateMap`](#with-an-updatemap)
- [With an update function](#with-an-update-function)
- [Deferring model updates and messages](#deferring-model-updates-and-messages)
- [Call back parent components](#call-back-parent-components)
- [Testing](#testing)
- [Testing the init function](#testing-the-init-function)
- [Testing the update handler](#testing-the-update-handler)
Expand Down Expand Up @@ -570,6 +570,97 @@ class App extends ElmComponent<Shared.Model, Shared.Message, Shared.Props> {
In a functional component you can use the **useEffect** hook as normal.
## Deferring model updates and messages
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.
Without the `defer` function, you would have to return the model and the command in all cases:
```ts
const update: UpdateMap<Props, Model, Message> = {
deferSomething (_msg, model) {
if (model.someCondition) {
return [{ alwaysUpdate: "someValue", extra: "extra" }, cmd.ofMsg(Msg.alwaysExecute())];
}

return [{ alwaysUpdate: "someValue" }, cmd.ofMsg(Msg.doSomethingElse()), cmd.ofMsg(Msg.alwaysExecute())];
},

...LoadSettings.update,
};
```
Here we always want to update the model with the `alwaysUpdate` property and always dispatch the `alwaysExecute` message.
With the `defer` function, you can do this:
```ts
const update: UpdateMap<Props, Model, Message> = {
deferSomething (_msg, model, _props, { defer }) {
defer({ alwaysUpdate: "someValue" }, cmd.ofMsg(Msg.alwaysExecute()));

if (model.someCondition) {
return [{ extra: "extra" }];
}

return [{}, cmd.ofMsg(Msg.doSomethingElse())];
},

...LoadSettings.update,
};
```
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.
## Call back parent components
Since each component has its own model and messages, communication with parent components is done via callback functions.
To inform the parent component about some action, let's say to close a dialog form, you do the following:
1. Create a message
```ts Dialog.ts
export type Message =
...
| { name: "close" }
...

export const Msg = {
...
close: (): Message => ({ name: "close" }),
...
}
```
1. Define a callback function property in the **Props**:
```ts Dialog.ts
export type Props = {
onClose: () => void,
};
```
1. Handle the message and call the callback function:
```ts Dialog.ts
{
// ...
close () {
return [{}, cmd.ofError(props.onClose, Msg.error)];
}
// ...
};
```
1. In the **render** method of the parent component pass the callback as prop
```tsx Parent.tsx
...
<Dialog onClose={() => this.dispatch(Msg.closeDialog())}>
...
```

## Composition

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

...LoadSettings.update,

// You can overwrite the LoadSettings messages handlers here

settingsLoaded (_msg, _model, _props, { defer, callBase }) {
// Use defer and callBase to execute the original handler function:
defer(...callBase(LoadSettings.settingsLoaded));

// Do additional stuff
return [{ /* ... */ }];
}
};
```
Expand Down Expand Up @@ -799,97 +900,6 @@ const updateComposition = (model: Model, msg: CompositionMessage): Elm.UpdateRet
}
```
## Deferring model updates and messages
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.
Without the `defer` function, you would have to return the model and the command in all cases:
```ts
const update: UpdateMap<Props, Model, Message> = {
deferSomething (_msg, model) {
if (model.someCondition) {
return [{ alwaysUpdate: "someValue", extra: "extra" }, cmd.ofMsg(Msg.alwaysExecute())];
}

return [{ alwaysUpdate: "someValue" }, cmd.ofMsg(Msg.doSomethingElse()), cmd.ofMsg(Msg.alwaysExecute())];
},

...LoadSettings.update,
};
```
Here we always want to update the model with the `alwaysUpdate` property and always dispatch the `alwaysExecute` message.
With the `defer` function, you can do this:
```ts
const update: UpdateMap<Props, Model, Message> = {
deferSomething (_msg, model, _props, { defer }) {
defer({ alwaysUpdate: "someValue" }, cmd.ofMsg(Msg.alwaysExecute()));

if (model.someCondition) {
return [{ extra: "extra" }];
}

return [{}, cmd.ofMsg(Msg.doSomethingElse())];
},

...LoadSettings.update,
};
```
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.
## Call back parent components
Since each component has its own model and messages, communication with parent components is done via callback functions.
To inform the parent component about some action, let's say to close a dialog form, you do the following:
1. Create a message
```ts Dialog.ts
export type Message =
...
| { name: "close" }
...

export const Msg = {
...
close: (): Message => ({ name: "close" }),
...
}
```
1. Define a callback function property in the **Props**:
```ts Dialog.ts
export type Props = {
onClose: () => void,
};
```
1. Handle the message and call the callback function:
```ts Dialog.ts
{
// ...
close () {
return [{}, cmd.ofError(props.onClose, Msg.error)];
}
// ...
};
```
1. In the **render** method of the parent component pass the callback as prop
```tsx Parent.tsx
...
<Dialog onClose={() => this.dispatch(Msg.closeDialog())}>
...
```

## Testing
To test your **update** handler you can use some helper functions in `react-elmish/dist/Testing`:
Expand Down
8 changes: 6 additions & 2 deletions src/ElmComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from "react";
import { execCmd, logMessage, modelHasChanged } from "./Common";
import { Services } from "./Init";
import type { Cmd, InitFunction, Message, Nullable, UpdateFunction } from "./Types";
import { createCallBase } from "./createCallBase";
import { createDefer } from "./createDefer";
import { getFakeOptionsOnce } from "./fakeOptions";

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

do {
logMessage(this.componentName, nextMsg);
const currentMessage = nextMsg;

logMessage(this.componentName, currentMessage);

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

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

const [deferredModel, deferredCommands] = getDeferred();

Expand Down
9 changes: 7 additions & 2 deletions src/Testing/getUpdateFn.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Message, Nullable, UpdateFunctionOptions, UpdateMap, UpdateReturnType } from "../Types";
import { createCallBase } from "../createCallBase";
import { createDefer } from "../createDefer";
import { callUpdateMap } from "../useElmish";
import { execCmd } from "./execCmd";
Expand All @@ -18,9 +19,11 @@ function getUpdateFn<TProps, TModel, TMessage extends Message>(
): (msg: TMessage, model: TModel, props: TProps) => UpdateReturnType<TModel, TMessage> {
return function updateFn(msg, model, props): UpdateReturnType<TModel, TMessage> {
const [defer, getDeferred] = createDefer<TModel, TMessage>();
const callBase = createCallBase(msg, model, props, { defer });

const options: UpdateFunctionOptions<TModel, TMessage> = {
const options: UpdateFunctionOptions<TProps, TModel, TMessage> = {
defer,
callBase,
};

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

const options: UpdateFunctionOptions<TModel, TMessage> = {
const options: UpdateFunctionOptions<TProps, TModel, TMessage> = {
defer,
callBase,
};

const [updatedModel, ...commands] = callUpdateMap(updateMap, msg, model, props, options);
Expand Down
14 changes: 11 additions & 3 deletions src/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,23 @@ type InitFunction<TProps, TModel, TMessage> = (props: TProps) => InitResult<TMod
type UpdateReturnType<TModel, TMessage> = [Partial<TModel>, ...(Cmd<TMessage> | undefined)[]];

type DeferFunction<TModel, TMessage> = (model: Partial<TModel>, ...commands: (Cmd<TMessage> | undefined)[]) => void;
type UpdateMapFunction<TProps, TModel, TMessage> = (
msg: TMessage,
model: TModel,
props: TProps,
options: UpdateFunctionOptions<TProps, TModel, TMessage>,
) => UpdateReturnType<TModel, TMessage>;

interface UpdateFunctionOptions<TModel, TMessage> {
interface UpdateFunctionOptions<TProps, TModel, TMessage> {
defer: DeferFunction<TModel, TMessage>;
callBase: (fn: UpdateMapFunction<TProps, TModel, TMessage>) => UpdateReturnType<TModel, TMessage>;
}

type UpdateFunction<TProps, TModel, TMessage> = (
model: TModel,
msg: TMessage,
props: TProps,
options: UpdateFunctionOptions<TModel, TMessage>,
options: UpdateFunctionOptions<TProps, TModel, TMessage>,
) => UpdateReturnType<TModel, TMessage>;

/**
Expand All @@ -58,7 +65,7 @@ type UpdateMap<TProps, TModel, TMessage extends Message> = {
msg: TMessage & { name: TMessageName },
model: TModel,
props: TProps,
options: UpdateFunctionOptions<TModel, TMessage>,
options: UpdateFunctionOptions<TProps, TModel, TMessage>,
) => UpdateReturnType<TModel, TMessage>;
};

Expand All @@ -76,5 +83,6 @@ export type {
UpdateFunction,
UpdateFunctionOptions,
UpdateMap,
UpdateMapFunction,
UpdateReturnType,
};
15 changes: 15 additions & 0 deletions src/createCallBase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { UpdateFunctionOptions, UpdateMapFunction, UpdateReturnType } from "./Types";

function createCallBase<TProps, TModel, TMessage>(
msg: TMessage,
model: TModel,
props: TProps,
options: Omit<UpdateFunctionOptions<TProps, TModel, TMessage>, "callBase">,
): (fn: UpdateMapFunction<TProps, TModel, TMessage>) => UpdateReturnType<TModel, TMessage> {
const callBase = (fn: UpdateMapFunction<TProps, TModel, TMessage>): UpdateReturnType<TModel, TMessage> =>
fn(msg, model, props, { ...options, callBase });

return callBase;
}

export { createCallBase };
4 changes: 2 additions & 2 deletions src/useElmish.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ interface Props {
model: Model,
msg: Message,
props: Props,
options: UpdateFunctionOptions<Model, Message>,
options: UpdateFunctionOptions<Props, Model, Message>,
) => UpdateReturnType<Model, Message>;
subscription?: (model: Model) => SubscriptionResult<Message>;
}
Expand All @@ -35,7 +35,7 @@ function defaultUpdate(
_model: Model,
msg: Message,
_props: Props,
{ defer }: UpdateFunctionOptions<Model, Message>,
{ defer }: UpdateFunctionOptions<Props, Model, Message>,
): UpdateReturnType<Model, Message> {
switch (msg.name) {
case "Test":
Expand Down
Loading

0 comments on commit 371bfb0

Please sign in to comment.