Coming from a JS background I was spoiled with such a rich ecosystem of packages via npm. Pre-built components that work out the box with minimal effort so that I could focus on business logic.
Similarly the F# community have built incredible tools like Fable and Feliz that leverage the JS ecosystem by letting us build F# wrappers around React/JS components with full type safety!
The SAFE docs have recently been updated to show the steps needed to wrap react-d3-speedometer. In this series I'd like to flesh out the Feliz example and show how easy it can be to wrap and publish a React component for F#.
-
Create a new SAFE app with
dotnet new SAFE
- depending on which SAFE template you're using you may or may not need to install Feliz. I'm using the latest version 3.0.0-beta004 which supports .NET 5 and Fable 3. -
Run
npm install react-d3-speedometer
at the root directory as specified in the SAFE docs -
In the Client directory create a new file
Feliz.ReactSpeedometer.fs
-
In
Feliz.ReactSpeedometer.fs
add the following:
module Feliz.ReactSpeedometer
open Feliz
open Fable.Core.JsInterop
let reactSpeedometer: obj = importDefault "react-d3-speedometer"
type ReactSpeedometer =
static member inline Value (number: int) = "value" ==> number
static member inline MinValue (number: int) = "minValue" ==> number
static member inline MaxValue (number: int) = "maxValue" ==> number
static member inline Segments (number: int) = "segments" ==> number
static member inline create props = Interop.reactApi.createElement (reactSpeedometer, createObj !!props)
- If we head over to the
Index.fs
ensureFeliz.ReactSpeedometer
is open and update the view function:
let view model dispatch =
ReactSpeedometer.create [
ReactSpeedometer.Value 10
ReactSpeedometer.MaxValue 100
ReactSpeedometer.MinValue 0
ReactSpeedometer.Segments 4
]
That's the bare minimum we need to port over this component with type safety since we can never pass anything other than an int
to the props we've defined.
But what if the props being passed to a component are more complicated? Luckily react-d3-speedometer has one such property - "customSegmentLabels".
Which from the prop list takes an Array<CustomSegmentLabel>
and from the code is defined as:
enum CustomSegmentLabelPosition {
Outside = "OUTSIDE",
Inside = "INSIDE",
}
type CustomSegmentLabel = {
text?: string
position?: CustomSegmentLabelPosition
fontSize?: string
color?: string
}
- Add the F# translation of the above to
Feliz.ReactSpeedometer.fs
.
type CustomSegmentLabelPosition =
| Outside
| Inside
with static member toJSValue = function
| Outside -> "OUTSIDE"
| Inside -> "INSIDE"
type CustomSegmentLabel =
{ Text: string
Position: CustomSegmentLabelPosition
FontSize: string
Color: string }
- Now we need to add the prop within the
ReactSpeedometer
type.
type ReactSpeedometer =
....
static member inline CustomSegmentLabels (customSegmentLabels: CustomSegmentLabel []) = "customSegmentLabels" ==> customSegmentLabels
- Add our newest prop to the component in the view.
let view model dispatch =
ReactSpeedometer.create [
ReactSpeedometer.Value 10
ReactSpeedometer.MaxValue 100
ReactSpeedometer.MinValue 0
ReactSpeedometer.Segments 4
// new code to add below
ReactSpeedometer.CustomSegmentLabels [|
{ Text = "Hi"
Position = Inside
FontSize = "20px"
Color = "crimson" }
{ Text = "From"
Position = Outside
FontSize = "20px"
Color = "blue" }
{ Text = "Feliz"
Position = Inside
FontSize = "20px"
Color = "green" }
{ Text = "Speedometer"
Position = Outside
FontSize = "20px"
Color = "purple" }
|]
]
Annnd something is broken 😢, our awesome custom labels aren't showing?
This is because a F# record doesn't directly translate to a JS object. To overcome this we can add a member to CustomSegmentLabel
which will map our F# record into an anonymous record with lower cased fields.
- Update the
CustomSegmentLabel
type to include the static member.
type CustomSegmentLabel =
{ Text: string
Position: CustomSegmentLabelPosition
FontSize: string
Color: string }
// new code to add below
with static member toJSObj customLabel =
{| text = customLabel.Text
position = customLabel.Position |> CustomSegmentLabelPosition.toJSValue
fontSize = customLabel.FontSize
color = customLabel.Color |}
You could alternatively define CustomSegmentLabel
as an anonymous record with lower cased field names but I'd like to have the standard F# syntax when using a component and leave the JS Interop behind the scenes. So as eventual library authors it's on us to make the component a natural feel for F#.
- Finally, call the static member in the prop
static member inline CustomSegmentLabels (customSegmentLabels: CustomSegmentLabel []) =
let jsCustomSegmentLabels = customSegmentLabels |> Array.map CustomSegmentLabel.toJSObj
"customSegmentLabels" ==> jsCustomSegmentLabels
Now we've done it!
I hope you enjoyed this post and join me next time when we will publish this wrapper to Nuget. In the mean time try mapping some of the remaining props yourself, you can use the react-d3-speedometer storybook to help see the what props are left and how they're implemented.