How to pass arguments from html page to compiled js app using Elmish with Fable

293 views Asked by At

In elm you can pass a flag to an elm application like so:

Html / JS

  <div id="elm"></div>
  <script>
var app = Elm.Main.init({
  node: document.getElementById('elm'),
  flags: Date.now()
});
  </script>

The elm app then gets the parameters on init:

init : Int -> ( Model, Cmd Msg )
init currentTime =
  ...

I've been through the fable documentation and to me it's not clear how I can achieve the same thing.

I see there is an option Program.runWith to send parameters on the init function, but I can't find documentation and I can't see from the compiled javascript how I should call the main function from the html file.

With fable, I want to be able to do something like this in the html file, but not sure what "Program.Run.." would be:

<body>
    <div id="elmish-app" class="elmish-app"></div>
    <script src="bundle.js"></script>
    <script>
        Program.Run({time: Date.now()});
    </script>
</body>

Thanks

2

There are 2 answers

0
SteelCityRKP On

Just building off of the earlier answer from Maxime Mangel, I've took the hacky approach, but adapated it a little bit to make use of local storage. I'm sharing this is an alternative for anyone else that needs to find a way to pass in Args to a Fable-Elmish App that are set/collected prior to the start.

In the App.fs (last file run/compiled):

let initialArgs = {
    Sender = Browser.Dom.window.localStorage.getItem("Sender")
    HubConnectionURI = Browser.Dom.window.localStorage.getItem("ServiceURI")}

let startApplication args = 
    Program.mkSimple init update view
    |> Program.withReactSynchronous "elmish-app"
    |> Program.withSubscription Subscription.hubConnection
    |> Program.withConsoleTrace
    |> Program.runWith args

do startApplication initialArgs

This pulls from the Local Storage Values set in Browser prior to the App Initialization, so then you can have these set in the JS however, you'd like.

within index.html:

<body>
    <div id="elmish-app" class="elmish-app"></div>
    <script>
      window.localStorage.setItem('Sender', 'Chairman Meow')
      window.localStorage.setItem('ServiceURI', 'http://localhost:7071')
    </script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/3.1.3/signalr.min.js"></script>
    <script src="bundle.js"></script>
</body>

You could of-course adapt this however you wanted to pull in the values/variables that are set in local storage. I needed these 'dynamic' initial arguments for parameterizing a Subscription that I want to start on the Program Run and am using this for a demo I'm working on for using SignalR in Fable-Elmish, and I will likely adapt this to prompt for these on loading the page.

However, the point here is that you can do whatever you want to get the values, as long as they're set in the Local Storage prior to the start of the App! I find this a relatively 'cleaner hack', but am still a beginner with Fable-Elmish, and there is likely a better approach.

[Just want to highlight for further clarity that these will be a part of the Initial Model]

3
Maxime Mangel On

when creating your Elmish Program you can use |> Program.runWith in order to pass it an initial argument.

type Model =
    { Value : string }

let init (initial : string) = { Value = initial }, Cmd.none

Program.mkProgram init update view
|> Program.withReact "elmish-app"
|> Program.runWith "My initial value"

Note that initial in init is a string because it receives the argument of Program.runWith.

In order to expose the API, in your browser the hacky way is to do something like that:

open Fable.Core.JsInterop

let private startApplication arg =
    Program.mkProgram init update view
    |> Program.withReact "elmish-app"
    |> Program.runWith arg

Fable.Import.Browser.window?startApplication <- startApplication

So you are setting register a startApplication function in the global scope window.

A cleaner solution is to use configure webpack in order to make it generate a library and register it for you in the window scope.

In your webpack.config.js your output node should look like that:

    output: {
        path: resolve('./output'),
        filename: '[name].js',
        libraryTarget: 'var',
        library: 'Program'
    },

Then in your code use an interface to expose a JavaScript friendly API:

type MyApp =
    abstract Run : string -> unit

let api =
    { new MyApp with
        member __.Run arg = startApplication arg
    }

And now you can call it from JavaScript using Program.api.Run("initial value")