I've created an app which downloads some data and plots it using Google Charts, to a div
with a particular Id
.
This works fine on a static page. However, when using menus to switch between multiple views, I'm struggling to redraw the chart when switching back to the appropriate view.
If I use a command to redraw the chart when the model is updated, the request fails because the particular div
has not yet been rendered.
Is there a way to trigger a command when the View
stage of Model-View-Update
has been completed? Or a better way to redraw the chart?
Example code follows (not runnable, but should be straightforward to follow). Clicking the "Get data" button works fine, but if you then switch to the other page and back I can't find a way to draw the chart again.
module Client
open Elmish
open Elmish.React
open Fable.Helpers.React
open Fable.Helpers.React.Props
open Fulma
type View = ShowChart | NoChart
type Model = { View: View; Values: float[] option }
type Msg =
| RequestValues
| ReceiveValues of float[]
| PageWithChart
| PageWithoutChart
| DrawChart
let init () : Model * Cmd<Msg> =
{ View = ShowChart; Values = None }, []
let update (msg : Msg) (currentModel : Model) : Model * Cmd<Msg> =
match msg with
| RequestValues -> currentModel, Cmd.ofMsg (ReceiveValues [| 1.0; 3.0; 2.0 |]) // normally this would be a long-running process
| ReceiveValues values -> { currentModel with Values = Some values }, Cmd.ofMsg DrawChart
| DrawChart ->
match currentModel.Values with
| Some values ->
let series = TheGamma.Series.series<_, _>.values values
let chart = TheGamma.GoogleCharts.chart.line series
TheGamma.GoogleCharts.chart.show chart "Chart1"
| None -> ()
currentModel, []
| PageWithChart ->
match currentModel.Values with
| Some _ -> { currentModel with View = ShowChart }, Cmd.ofMsg DrawChart // FAILS as the div with Id="Chart1" does not exist yet
| None -> { currentModel with View = ShowChart }, []
| PageWithoutChart -> { currentModel with View = NoChart }, []
let button txt onClick =
Button.button
[ Button.OnClick onClick ]
[ str txt ]
let view (model : Model) (dispatch : Msg -> unit) =
match model.View with
| NoChart ->
div []
[ div [] [ str "Page without chart" ]
button "Show page with chart" (fun _ -> dispatch PageWithChart) ]
| ShowChart ->
div []
[ div [] [ str "Page with chart" ]
button "Get data" (fun _ -> dispatch RequestValues)
button "Show page without chart" (fun _ -> dispatch PageWithoutChart )
div [ Id "Chart1" ] [] ]
#if DEBUG
open Elmish.Debug
open Elmish.HMR
#endif
Program.mkProgram init update view
#if DEBUG
|> Program.withConsoleTrace
|> Program.withHMR
#endif
|> Program.withReact "elmish-app"
|> Program.run
The HTML is just:
<!doctype html>
<html>
<head>
<script type="text/javascript" src="http://www.google.com/jsapi"></script>
<title>SAFE Template</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.6.1/css/bulma.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" />
<link href="https://fonts.googleapis.com/css?family=Open+Sans" rel="stylesheet">
<link rel="shortcut icon" type="image/png" href="/Images/safe_favicon.png"/>
</head>
<body>
<div id="elmish-app"></div>
<script src="./js/bundle.js"></script>
</body>
</html>
You can detect when a stateless react element has been mounted by using the Ref
attributes.
For example, this is what I use in my production app:
div [ Id "Chart1"
Ref (fun element ->
// Ref is trigger with null once for stateless element so we need to wait for the second trigger
if not (isNull element) then
// The div has been mounted check if this is the first time
if model.IsJustLoaded then
// This is the first time, we can trigger a draw
dispatch DrawChart
)
]
[ ]
Another way to go, would be to transform the div [ Id "Chart1" ] [ ]
into a stateful react component and trigger a draw when componentDidMount
has been called.
It's a bit hard to give specific suggestions without seeing your code, but here's a general idea of how you might handle it. In your shoes, I'd probably use the Promise implementation from the Fable Powerpack, and set it up like this:
script
element at the bottom of your view so that its execution doesn't block anything else from rendering, then inside the script is where you'd resolve that promise).Sorry this is so vague; if I saw your code, I might be able to make more specific suggestions. In particular, I don't know how best to handle passing that promise around: storing it in the model seems like a bad idea (storing something mutable in an immutable data model... yikes), but I haven't come up with a better idea yet. But something promise-related seems like it will be the best way to handle this situation.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With