Skip to content

Instantly share code, notes, and snippets.

@sheridanchris
Last active September 28, 2023 11:32
Show Gist options
  • Save sheridanchris/76fe752043e9ec2961558a6a5b15ed25 to your computer and use it in GitHub Desktop.
Save sheridanchris/76fe752043e9ec2961558a6a5b15ed25 to your computer and use it in GitHub Desktop.
module Elmish
open System
open Sutil
open Sutil.CoreElements
type Article = {
Title: string
TagLine: string
Link: string
}
// This is the current state of our application.
type Model = {
Articles: Article list
SearchQuery: string option
}
// These messages will be published as a result of UI interaction or commands.
// Messages can be published by clicking a button, typing in an input field, or navigating to a page.
// They are instrumental for calculating the new state of the application and updating the UI.
type Msg =
| GetArticles
| SetSearchQuery of string
| SetArticles of Article list
let getArticles searchQuery =
async {
// ... perform asynchronous api call here ...
// (fake articles for demo)
return [
{
Title = "Oh no! OCaml is Trending!"
TagLine = "Why couldn't it be F# :("
Link = "https://twitter.com/everywhere"
}
]
}
// commands are additional instructors or side-effects that ~may~ produce one or more messages.
// this command will perform an asynchronous operation
// and when it's complete the `SetArticles` message will be published with the result.
let getArticlesCmd searchQuery =
Cmd.OfAsync.perform getArticles searchQuery SetArticles // <- the message we want to publish (with data)!
// ^^^^^^^^^^^
// this is the input parameter to the asynchronous operation
let initialState () =
let initialSearchQuery = None
{
Articles = []
SearchQuery = initialSearchQuery
},
getArticlesCmd initialSearchQuery
let optionOfString value =
if String.IsNullOrWhiteSpace value then None else Some value
let stringFromOption value = value |> Option.defaultValue ""
// This function will recieve a message and the current state of the application
// it's responsibility is to calculate the new state of the application based on these parameters
// and specify any additional instructions via a command.
let update msg model =
match msg with
| GetArticles -> model, getArticlesCmd model.SearchQuery
| SetArticles articles -> { model with Articles = articles }, Cmd.none
| SetSearchQuery query ->
let query = optionOfString query
{ model with SearchQuery = query }, Cmd.none
let articleView newsArticle =
Html.tr [
Html.td (Html.a [ Attr.href newsArticle.Link; Attr.text newsArticle.Title ])
Html.td newsArticle.TagLine
]
let view () =
let dispose model = ()
// `model` here is an observable value. you can subscribe to changes and perform side-effects to you liking.
// `dispatch` is used to publish messages to the update loop and abstracts away state & command handling.
let model, dispatch = () |> Store.makeElmish initialState update dispose
Html.main [
disposeOnUnmount [ model ]
// The bind functions will subscribe to any changes and perform an update on the DOM when it does.
Html.input [
Attr.placeholder "Search for a news article"
Attr.value (model |> Store.map (fun model -> stringFromOption model.SearchQuery), SetSearchQuery >> dispatch)
// ^^^^^^^^^^^^^^^^^^^^^^^^^^
// this will dispatch a `SetSearchQuery` message to the update loop whenever the user types.
]
Html.button [ Attr.text "Search!"; onClick (fun _ -> dispatch GetArticles) [] ]
Html.table [
Html.tr [ Html.th "Title"; Html.th "Tag line" ]
// we use Store.map because we only care about a specific value (we don't want to subscribe to ~any~ change)
Bind.each (model |> Store.map (fun model -> model.Articles), articleView)
]
]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment