-
-
Notifications
You must be signed in to change notification settings - Fork 122
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add examples for the new CmdMsg pattern allowing unit testability of Cmds #418
Conversation
Interestingly enough, this also enables code reuse between Fabulous and Elmish. |
Looks good! You might want to consider including helper functions that accept a Again though, it's so trivial that it's not really needed. Just a courtesy. |
I'm fine with it, too. (Side note: In my small sample app I ended up with a similar approach like the one used in the ElmishContacts sample, having pages with |
@aspnetde That's interesting. I'm trying to imagine when it would make sense to introduce the concept of type Msg =
| SomeMsg
type ExternalMsg =
| TriggerCmdOnOtherPage
type CmdMsg =
| TriggerCmdOnThisPage
let update msg model =
match msg with
| SomeMsg -> model, (Some TriggerCmdOnOtherPage), [ TriggerCmdOnThisPage ]
/// The other page
type Msg =
| PageATriggeredCmd
type CmdMsg =
| TriggerCmdOnThisPage
let update msg model =
match msg with
| PageATriggeredCmd -> model, None, [ TriggerCmdOnThisPage ] |
For example I had a small function I didn't want to duplicate x times across the app. Like: let private showErrorAlert errorMessage = async {
do! Application.Current.MainPage
.DisplayAlert("Error", errorMessage, "Okay") |> Async.AwaitTask
return None
} I guess that one could also be moved in some kind of helper module. But it's not hard to imagine a scenario where one would return a specific message type etcetera. Anyway: No showstopper here! |
childElements | ||
|> Seq.map (fun e -> e |> tryFindViewElement automationId) | ||
|> Seq.filter (fun e -> e.IsSome) | ||
|> Seq.map (fun e -> e.Value) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this mapping useful? You are only interested in the first element after all. So you could take the first and return the value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm doing this 2nd map here because the type returned by Seq.filter is Seq<ViewElement option>
, so Seq.tryHead will return a ViewElement option option
.
Without this map, I would need to match on the ViewElement option option
and in the case tryHead
found something, convert the ViewElement option
to ViewElement
.
I think it's faster to read and write the way I did.
Also the advantage of Seq here is that it won't evaluate the whole collection (like IEnumerable and LINQ in C#), it'll try each item one by one until it finds a matching one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it works like this then I didn't say anything. Was just curious about it because if it would work like LINQ it would be bad for performance.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could use Seq.choose here instead of Seq.map and Seq.filter, which would auto-unwrap the option for you and reduce the option nesting.
Excellent explanation in 922fa90. Might borrow from that (and the rest of this PR) for Elmish.WPF. I suggest that you explicitly state that |
Wonderful! 👍 |
/azp help |
Supported commands help: Get descriptions, examples and documentation about supported commands Example: help "command_name" list: List all pipelines for this repository using a comment. Example: "list" run: Run all pipelines or a specific pipeline for this repository using a comment. Use this command by itself to trigger all related pipelines, or specify a pipeline to run. Example: "run" or "run pipeline_name" where: Report back the Azure DevOps orgs that are related to this repository and org Example: "where" See additional documentation. |
/azp list |
CI/CD Pipelines for this repository: |
/azp run pr build |
Azure Pipelines successfully started running 1 pipeline(s). |
Closes #402 (this PR includes the
tryFindViewElement
helper function)After much discussion, we came to the conclusion that a simple pattern can allow complete unit testing when using
Cmd<'msg>
despite it not being unit testable.(See full discussion in #320 and #309)
This pattern is completely up to the developer, nothing has been changed in Fabulous to support it.
The fact is that
Cmd<'msg>
is not unit testable because it's essentially just an array of functions.Functions returning Cmds won't be fully eligible for unit testing, that include
init
andupdate
, which would otherwise be perfect for unit testing...The idea of this pattern is to remove the dependency to
Cmd<'msg>
frominit
andupdate
by using an intermediateCmdMsg
discriminated union.This union will only state intents instead of actually executing a Cmd.
Updated with Program.mkProgramWithCmdMsg
init
andupdate
now have a full data-only return type.Greatly simplifying unit testing.