I'm currently designing an API to make it easier to interact with React from F# and the language combination is providing several interesting opportunities for static checking. However, as expected there are some rough ends and I have doubts about how to define the properties in a React DOM element. Currently we have:
Option 1: Dynamic properties
module R = Fable.ReactHelper
R.input [
"type" ==> "text"
"placeholder" ==> "Your name"
"value" ==> x.state.author
"onChange" ==> x.handleAuthorChange
] []
This is the most flexible option as we can pass any property in a plain JS object. Thanks to inlining functions we don't need to use createObj here and we also save some braces. If you think the ==> is too verbose, shortening is also trivial: let inline (=>) a b = a ==> b
The problem is that we don't have any static checking or auto completion for the properties. So I'm considering using a lambda instead. But now we have to decide where to get the signature from. At first I took the one from the IntrinsicElements defined in the React definition file:
Option 2: Mutating HTMLElement properties
R.input (fun p ->
p.``type`` <- "text"
p.placeholder <- "Your name"
p.value <- unbox x.state.author
//p.onchange <- unbox x.handleAuthorChange
p?onChange <- x.handleAuthorChange
) []
Now we have static checking and auto completion for the properties which is very nice. Note however that some frictions with the type definitions start to show up and need to resolved with unbox. The biggest problem with this approach is that, though the TypeScript definition uses the standard DOM elements, some attributes have a different spelling in React. In the example above, setting onchange won't work, so we have to set onChange dynamically. This almost breaks all the benefits of static checking :(
Our next option then is to use React.HTMLAttributes instead to be sure we're using the correct spelling:
Option 3: Mutating React.HTMLAttributes properties
R.input (fun p ->
p.``type`` <- Some "text"
p.placeholder <- Some "Your name"
p.value <- unbox x.state.author
p.onChange <- unbox x.handleAuthorChange
) []
Ok, all the properties have the spelling as React expects it but we now face other problems: first the frictions with the signature multiply because all properties are defined as optional and other Typescript custom elements are used like erased union types or custom-defined functions; and second, in the previous option we had the specific attributes for input tags but here every HTML element share the same attributes. However, this may still be the best option.
There'd be a fourth option but I'm not considering it very much:
Option 4: Using a custom HTMLAttributes record
R.input (fun p ->
{ p with
``type`` = Some "text"
placeholder = Some "Your name"
value = unbox x.state.author
onChange = unbox x.handleAuthorChange
}) []
This one is likely the most idiomatic in F# but the indentation rules get more complicated ({ p with cannot be put in the first line) and we don't have auto completion here to discover the properties of p, as far as I know. Most importantly, adopting this style would require creating a custom record for HTMLAttributes as the ones from TypeScript are parsed as interfaces (there are good reasons for that), so this would add an extra step in the maintenance of the Fable.Import.React file.
Here are the options to make the decision and I would love to hear your say on this. I know it's possible to add several options to the API, but this would probably make it more confusing for newcomers and I'd prefer to make it simple at first. Thanks a lot for your help in advance!
Option 5.
Extend option 1. to add typed attributes. I was doing similar API for FunScript back in the days.
https://github.com/FractalProject/Fractal.Sample/blob/master/client/App.fsx#L65-L74
https://github.com/FractalProject/Fractal/blob/master/src/Fractal.DOM.fs
Option 6.
Go super crazy and create computation expression with custom keywords.
For newcomers I think Option 4 looks to be the best, also no magic strings and it keeps thing immutable.
Thanks for the suggestion, @Krzysztof-Cieslak! At first I was afraid that this would require more maintenance work but after some consideration I realised this was the best option. I built on your code and wrote a script to generate (partially) the helper module:
https://github.com/fsprojects/Fable/blob/master/samples/browser/react/public/Fable.ReactHelper.fs
The render code is much prettier now with full static checking :)
https://github.com/fsprojects/Fable/blob/master/samples/browser/react/public/components.fs#L104-L124
There's still work to on the React Helper, but I'm closing the issue for now. Thank you all for your help!
Most helpful comment
Option 5.
Extend option 1. to add typed attributes. I was doing similar API for FunScript back in the days.
https://github.com/FractalProject/Fractal.Sample/blob/master/client/App.fsx#L65-L74
https://github.com/FractalProject/Fractal/blob/master/src/Fractal.DOM.fs
Option 6.
Go super crazy and create computation expression with custom keywords.