Closed halcwb closed 3 years ago
Hi @halcwb! We don't plan to add support for specific React libraries here, though some community members have created binding for React/JS libraries that you can find here: https://fable.io/community/
It's not difficult to use external React components directly from JS. You can find the documentation here: https://github.com/fable-compiler/fable-react/blob/master/docs/using-third-party-react-components.md
@alfonsogarciacaro Thanks for the quick reply, I will definitely have a look at that.
This is still quite a bit more complicated than I assumed. The problem that I run into is that the fable-react binding doesn't contain the complete typescript definition for react. So, for the number format library I need to use the React.InputHTMLAttributes<HTMLInputElement>
interface as this is the required props type:
I am not sure how to approach this problem. Do I need to go manually into the rabbit hole of adding all the needed supertypes? Because InputHTMLAttributes<T>
extends HTMLAttributes<T>
, etc...
P.S. the goal is to contribute a React/Js library.
You can download Fable.Browser.Dom
and open Browser.Types
to gain access to elements like HTMLInputElement
.
@Shmew Thanks, but that namespace was already opened. I also added the Fable.React library. The problem is that the Fable.React library doesn't seem to match the react typescript definitions at all. When I manually start adding all the required react interfaces from the react type definition file, I need to keep adding interfaces as each interface extends other interfaces, etc...
I assumed that the Fable.React library would contain all react types.
@alfonsogarciacaro , @Shmew , @Zaid-Ajaj .
It took me some time, and I have things almost working using the following setup (full code in this repos):
import React from 'react';
import NumberFormat from 'react-number-format';
function merge(o1, o2) {
const o = { ...o1, ...o2 };
console.log("merged", o);
// although inputRef is a member of the object, it cannot be removed
// because of inputRef is not defined error
// return { inputRef, ..o };
return o;
}
export { merge, NumberFormat as default };
I also have a short utility function (merge
) to enable merging two objects. This is necessary because the following code has to be implemented:
function NumberFormatCustom(props) {
const { inputRef, onChange, ...other } = props;
return (
<NumberFormat
{...other}
getInputRef={inputRef}
onValueChange={(values) => {
onChange({
target: {
name: props.name,
value: values.value,
},
});
}}
thousandSeparator
isNumericString
prefix="$"
/>
);
}
So the NumberFormat
is an react type (not element) that has to be set as an inputComponent
of the inputProps
.
<TextField
label="react-number-format"
value={values.numberformat}
onChange={handleChange}
name="numberformat"
id="formatted-numberformat-input"
InputProps={{
inputComponent: NumberFormatCustom,
}}
/>
module NumberInput2 =
open System
open System.ComponentModel
open Elmish
open Thoth.Elmish
open Feliz
open Feliz.UseElmish
open Feliz.MaterialUI
open Fable.MaterialUI.Icons
open Fable.Core
open Fable.Core.JsInterop
open Browser
let private merge (o1: obj) (o2: obj) : obj = import "merge" "./../number-format.js"
[<Erase>]
type NumberFormat =
static member inline numberformat =
fun props ->
let props =
{|
onValueChange =
fun values ->
printfn "getting value: %A" values?value
{| target = {| value = values?value |} |}
decimalSeparator = ","
thousandSeparator = "."
|}
|> merge props
ofImport "default" "./../number-format.js" props []
|> ReactElementType.ofFunction
type State =
{
Error: bool
UserInput: string
}
type Msg =
| ChangeValue of string
| EndOfInput
type Props =
{| min: float option
max: float option
step: float
label: string
adorn: string
dispatch: string -> unit |}
let private init () =
{
//Debouncer = Debouncer.create ()
Error = false
UserInput = ""
//Number = None
},
Cmd.none
let private update (props: Props) msg state =
let tryParse (s : string) =
match Double.TryParse(s) with
| true, f -> f |> Some
| false, _ -> None
let isErr (s: string) =
if s.Trim() = "" then
true
else
match s |> tryParse, props.min, props.max with
| None, _, _ -> false
| Some _, None, None -> true
| Some v, Some min, None -> v >= min
| Some v, None, Some max -> v <= max
| Some v, Some min, Some max -> v >= min && v <= max
|> not
match msg with
| ChangeValue s ->
{ state with
Error = s |> isErr
UserInput = s
},
Cmd.none
| EndOfInput ->
let state =
{ state with
Error = state.UserInput |> isErr
}
state,
Cmd.ofSub (fun _ -> state.UserInput |> props.dispatch)
let useStyles err =
Styles.makeStyles (fun styles theme ->
{|
field =
styles.create [
style.minWidth (theme.spacing 14)
style.marginTop (theme.spacing 1)
]
input =
styles.create
[
if not err then
style.color (theme.palette.primary.main)
else
style.color (theme.palette.error.main)
]
label =
styles.create [
style.fontSize (theme.typography.fontSize - 15.)
style.paddingRight (theme.spacing 2)
]
|})
let defaultProps: Props =
{|
min = None
max = None
step = 1.
label = ""
adorn = ""
dispatch = (fun (s: string) -> ())
|}
let private comp =
React.functionComponent
("numericinput",
(fun (props: Props) ->
let state, dispatch =
React.useElmish (init, update props, [||])
let classes = (useStyles state.Error) ()
Mui.textField [
prop.className classes.field
textField.error state.Error
textField.label
(Mui.typography [
typography.variant.body2
typography.children [ props.label ]
])
textField.value state.UserInput
textField.onChange (ChangeValue >> dispatch)
// dirty fix to disable number field typ in FF
let isFF =
navigator.userAgent.ToLower().Contains("firefox")
if not isFF then textField.type' "text"
textField.size.small
textField.InputProps [
// uses the react-number-format lib
input.inputComponent (NumberFormat.numberformat)
input.inputProps [
prop.step props.step
match props.min with
| Some m -> prop.min m
| None -> prop.min 0.
match props.max with
| Some m -> prop.max m
| None -> ()
]
// sets the color of the input value
prop.className classes.input
// adds a unit (adornment) to a value
input.endAdornment
(Mui.inputAdornment [
inputAdornment.position.end'
inputAdornment.children
[
Mui.typography [
typography.color.textSecondary
typography.variant.body2
typography.children [ props.adorn ]
]
]
])
]
]))
let render label adorn dispatch =
comp
({| defaultProps with
label = label
adorn = adorn
dispatch = dispatch
|})
let renderWithProps props = comp (props)
It sort of works but for 2 problems:
I cannot remove the inputRef prop using my javascript merge function. When I log the merged object, the property is there and react complains about it. But if I want to exclude the property using the spread syntax (commented in the code) then I get an error.
When I type into the textfield, every time it loses focus. I suspect because for each render a new component is created and react doesn't recognize it as the same component.
Any suggestions would be very much appreciated.
Hey @halcwb, I took a look at your repro and got it working:
open System
open Elmish
open Feliz
open Feliz.UseElmish
open Feliz.MaterialUI
open Fable.Core
open Fable.Core.JsInterop
open Browser
type NumberFormatValues =
{ formattedValue: string
value: string
floatValue: float }
[<Erase>]
type numberFormat =
static member inline customInput (elem: ReactElement) = Interop.mkAttr "customInput" elem
static member inline format (fmt: string) = Interop.mkAttr "format" fmt
static member inline onValueChange (handler: NumberFormatValues -> unit) = Interop.mkAttr "onValueChange" handler
static member inline decimalSeparator (value: char) = Interop.mkAttr "decimalSeparator" value
static member inline decimalSeparator (value: string) = Interop.mkAttr "decimalSeparator" value
static member inline thousandSeparator (value: char) = Interop.mkAttr "thousandSeparator" value
static member inline thousandSeparator (value: string) = Interop.mkAttr "thousandSeparator" value
type React with
static member inline numberformat (props: IReactProperty list) =
Interop.reactElement (importDefault "react-number-format") (createObj !!props)
type State =
{ Error: bool
UserInput: string }
type Msg =
| ChangeValue of string
| EndOfInput
type Props =
{| min: float option
max: float option
step: float
label: string
adorn: string
dispatch: string -> unit |}
let private init () =
{ Error = false
UserInput = "" }
, Cmd.none
let private update (props: Props) msg state =
let tryParse (s : string) =
match Double.TryParse(s) with
| true, f -> f |> Some
| false, _ -> None
let isErr (s: string) =
if s.Trim() = "" then true
else
match s |> tryParse, props.min, props.max with
| None, _, _ -> false
| Some _, None, None -> true
| Some v, Some min, None -> v >= min
| Some v, None, Some max -> v <= max
| Some v, Some min, Some max -> v >= min && v <= max
|> not
match msg with
| ChangeValue s -> { state with Error = s |> isErr; UserInput = s }, Cmd.none
| EndOfInput ->
let state = { state with Error = state.UserInput |> isErr }
state,
Cmd.ofSub (fun _ -> state.UserInput |> props.dispatch)
let private useStyles err =
Styles.makeStyles (fun styles theme ->
{| field = styles.create [
style.minWidth (theme.spacing 14)
style.marginTop (theme.spacing 1)
]
input = styles.create [
if not err then style.color (theme.palette.primary.main)
else style.color (theme.palette.error.main)
]
label = styles.create [
style.fontSize (theme.typography.fontSize - 15.)
style.paddingRight (theme.spacing 2)
]
|})
let private defaultProps: Props =
{| min = None
max = None
step = 1.
label = ""
adorn = ""
dispatch = (fun (s: string) -> ()) |}
let private comp = React.functionComponent("numericinput", fun (props: Props) ->
let state, dispatch = React.useElmish (init, update props, [||])
let classes = (useStyles state.Error) ()
React.numberformat [
prop.className classes.field
textField.error state.Error
textField.label (
Mui.typography [
typography.variant.body2
typography.children [ props.label ]
]
)
textField.value state.UserInput
textField.onChange (ChangeValue >> dispatch)
let isFF =
navigator.userAgent.ToLower().Contains("firefox")
if not isFF then textField.type' "text"
textField.size.small
textField.InputProps [
input.inputProps [
prop.step props.step
match props.min with
| Some m -> prop.min m
| None -> prop.min 0.
match props.max with
| Some m -> prop.max m
| None -> ()
]
prop.className classes.input
input.endAdornment (
Mui.inputAdornment [
inputAdornment.position.end'
inputAdornment.children [
Mui.typography [
typography.color.textSecondary
typography.variant.body2
typography.children [ props.adorn ]
]
]
]
)
]
numberFormat.customInput (import "TextField" "@material-ui/core")
numberFormat.decimalSeparator ','
numberFormat.thousandSeparator '.'
])
let render label adorn dispatch =
comp
{| defaultProps with
label = label
adorn = adorn
dispatch = dispatch |}
let renderWithProps props = comp (props)
@Shmew Thanks so much, works like a charm. Took me days but couldn't figure this out.
So, the whole trick is to pass a material-ui textfield as custom input?
@Shmew Thanks so much, works like a charm. Took me days but couldn't figure this out.
You're welcome!
So, the whole trick is to pass a material-ui textfield as custom input?
Yeah the library passes the props down to the component type you provide. As far as implementing this type of behavior with Fable.React
(I'm assuming that's something you want to do given the repo this issue is in), I'm not sure how you would make that work. I don't think you can extend a type to add an interface, but if you can that's a solution.
I want to use this library: https://github.com/s-yadav/react-number-format. Is this something that will be implemented in the near future?