在寓言精灵中,如何在渲染视图后触发命令?

In fable-elmish, how to trigger a command after the view has been rendered?

我创建了一个应用程序,它下载一些数据并使用 Google 图表将其绘制到具有特定 Id.

div

这在静态页面上工作正常。但是,当使用菜单在多个视图之间切换时,当切换回适当的视图时,我很难重新绘制图表。

如果我在更新模型时使用命令重绘图表,请求将失败,因为特定的 div 尚未呈现。

有没有办法在Model-View-UpdateView阶段完成后触发命令?或者重新绘制图表的更好方法?

示例代码如下(不可运行,但应该很容易理解)。单击 "Get data" 按钮可以正常工作,但如果您随后切换到另一页并返回,我将找不到重新绘制图表的方法。

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

HTML就是:

<!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>

在没有看到您的代码的情况下很难给出具体的建议,但这里有一个您可能如何处理它的大致思路。在你看来,我可能会使用 Promise implementation from the Fable Powerpack,并像这样设置它:

  • 图表绘制代码还没有绘制图表,它只是returns一个承诺,当解决时,将绘制图表。
  • 在视图函数中的 div 之后,立即使用 this hack to trigger some Javascript code that will resolve the promise, firing the draw() event. (Or, as suggested in another answer to that question,将该代码放在视图底部的 script 元素中,这样它的执行就不会阻塞渲染中的任何其他内容,然后在脚本内部是您解决该承诺的地方)。

抱歉,这太含糊了;如果我看到你的代码,我可能会提出更具体的建议。特别是,我不知道如何最好地处理传递这个承诺:将它存储在模型中似乎是一个坏主意(在不可变数据模型中存储一些可变的东西......哎呀),但我还没有想出一个更好的主意呢。但是一些 承诺相关的东西似乎是处理这种情况的最佳方式。

您可以使用 Ref 属性检测何时安装了无状态反应元素。

例如,这是我在生产应用中使用的:

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 
          )
 ]
    [ ]

另一种方法是将 div [ Id "Chart1" ] [ ] 转换为有状态的 React 组件,并在调用 componentDidMount 时触发绘制。