Search code examples
f#websharper

Websharper / Web.Control constructor issue


While wanting to customize the Google DataVisualization, I wanted to parameterize the constructor of a control, but for some reason it doesn't seem to work when I pass the inputDataTableas a parameter.

Here is the default data I use :

module Example = 
  let headers = [|"Country"; "Population (mil)"; "Area (km2)" |]
  let inputDataTable = [|
    [|box "CN"; box 1234000; box 9640821|]
    [|box "IN"; box 1133000; box 3287263|]
    [|box "US"; box 304000; box 9629091|]
    [|box "ID"; box 232000; box 1904569|]
    [|box "BR"; box 187000; box 8514877|]
  |]

The code to generate the chart on the webpage.

let GeoChart headers inputDataTable =
    Div []
    |>! OnAfterRender (fun container ->
          let dataFormatter = DefaultOptions.NumberFormatter ""
          let options = DefaultOptions.GeoChart
          let visualization = new GeoChart(container.Dom)
          let data = Data.GeoMapData headers inputDataTable
          dataFormatter.format(data, 1)
          visualization.draw(data, options)
        )

The "expected" control.

type GoogleGeoChartViewer(headers, inputDataTable) =
  inherit Web.Control()

  [<JavaScript>]
  override this.Body = 
    Google.Charts.GeoChart headers inputDataTable :> _

And a test using the basic template.

let HomePage =
  let headers = Google.Example.headers 
  let inputDataTable = Google.Example.inputDataTable
  Skin.WithTemplate "HomePage" <| fun ctx ->
      [
          Div [Text "HOME"]
          Div [new Controls.EntryPoint()]
          Div [new Controls.GoogleGeoChartViewer(headers, inputDataTable)]
          Links ctx
      ]

When I use this code however : the geo chart does not get displayed.

But if I remove the inputDataTable from the list of parameters and use :

let GeoChart headers =
    Div []
    |>! OnAfterRender (fun container ->
          let dataFormatter = DefaultOptions.NumberFormatter ""
          let options = DefaultOptions.GeoChart
          let visualization = new GeoChart(container.Dom)
          let inputDataTable = Example.inputDataTable
          let data = Data.GeoMapData headers inputDataTable
          dataFormatter.format(data, 1)
          visualization.draw(data, options)
        )

It works...

The headers array does not pose any issue however.

Would anyone have any idea what is going on ?

Thank you for your insights.


Solution

  • What happens is that values passed to the constructor of a Web.Control are server-side values that get serialized to JSON during the server-side construction of the page. However, there is no serializer for the type obj, so the serialization of your inputDataTable (of type obj[][]) fails. In contrast, in the version that works, inputDataTable is constructed on the client, so no serialization needs to take place.

    If inputDataTable does indeed need to come from the server-side, you need to use a serializable type. I don't know exactly how you fill your data object in the GeoMapData function, but the solution is probably to define inputDataTable as follows:

    let inputDataTable = [|
        ("CN", 1234000, 9640821)
        ("IN", 1133000, 3287263)
        ("US", 304000, 9629091)
        ("ID", 232000, 1904569)
        ("BR", 187000, 8514877)
      |]
    

    and then on the client-side, do something like:

    let data = new Base.DataTable()
    data.addRows(inputDataTable) |> ignore
    

    The trick is that WebSharper represents tuples using JavaScript arrays on the client-side. And the method addRows, which normally takes obj[][], also has an overload that takes 'T[]. This allows you to write the correctly-typed F# code above, and it compiles into the exact same JavaScript code as if you were passing an obj[][]. This trick is used in this example (line 155).