diff --git a/Feliz.CompilerPlugins/AstUtils.fs b/Feliz.CompilerPlugins/AstUtils.fs
index 7d08f669..d88cfd5e 100644
--- a/Feliz.CompilerPlugins/AstUtils.fs
+++ b/Feliz.CompilerPlugins/AstUtils.fs
@@ -74,6 +74,11 @@ let makeImport (selector: string) (path: string) =
Path = path.Trim()
Kind = Fable.UserImport(false) }, Fable.Any, None)
+let isDeclaredRecord (compiler: PluginHelper) (fableType: Fable.Type) =
+ match fableType with
+ | Fable.Type.DeclaredType (entity, genericArgs) -> compiler.GetEntity(entity).IsFSharpRecord
+ | _ -> false
+
let isRecord (compiler: PluginHelper) (fableType: Fable.Type) =
match fableType with
| Fable.Type.AnonymousRecordType _ -> true
@@ -101,18 +106,20 @@ let isReactElement (fableType: Fable.Type) =
| Fable.Type.DeclaredType (entity, genericArgs) -> entity.FullName.EndsWith "ReactElement"
| _ -> false
-let recordHasField name (compiler: PluginHelper) (fableType: Fable.Type) =
+let tryGetRecordField (name: string) (compiler: PluginHelper) (fableType: Fable.Type) =
+ let name = name.ToLower()
match fableType with
| Fable.Type.AnonymousRecordType (fieldNames, genericArgs, _isStruct) ->
fieldNames
- |> Array.exists (fun field -> field = name)
+ |> Array.tryFind (fun field -> field.ToLower() = name)
| Fable.Type.DeclaredType (entity, genericArgs) ->
compiler.GetEntity(entity).FSharpFields
- |> List.exists (fun field -> field.Name = name)
+ |> List.tryFind (fun field -> field.Name.ToLower() = name)
+ |> Option.map(fun field -> field.Name)
| _ ->
- false
+ None
let memberName = function
| Fable.MemberRef(_,m) -> m.CompiledName
@@ -163,4 +170,4 @@ let capitalize (input: string) =
let camelCase (input: string) =
if String.IsNullOrWhiteSpace input
then ""
- else input.First().ToString().ToLower() + String.Join("", input.Skip(1))
+ else input.First().ToString().ToLower() + String.Join("", input.Skip(1))
\ No newline at end of file
diff --git a/Feliz.CompilerPlugins/ReactComponent.fs b/Feliz.CompilerPlugins/ReactComponent.fs
index 4ad4675b..ac972927 100644
--- a/Feliz.CompilerPlugins/ReactComponent.fs
+++ b/Feliz.CompilerPlugins/ReactComponent.fs
@@ -49,6 +49,30 @@ module internal ReactComponentHelpers =
{ decl with MemberRef = info; Args = []; Body = body }
| _ -> { decl with Body = injectReactImport decl.Body }
+
+ let rewriteArgs (decl: MemberDecl) =
+ // rewrite all other arguments into getters of a single props object
+ // TODO: transform any callback into into useCallback(callback) to stabilize reference
+ let propsArg = AstUtils.makeIdent (sprintf "%sInputProps" (AstUtils.camelCase decl.Name))
+ let propBindings =
+ ([], decl.Args) ||> List.fold (fun bindings arg ->
+ let getterKey = if arg.DisplayName = "key" then "$key" else arg.DisplayName
+ let getterKind = ExprGet(AstUtils.makeStrConst getterKey)
+ let getter = Get(IdentExpr propsArg, getterKind, Any, None)
+ (arg, getter)::bindings)
+ |> List.rev
+
+ let body =
+ match decl.Body with
+ // If the body is surrounded by a memo call we put the bindings within the call
+ // because Fable will later move the surrounding function into memo
+ | Call(ReactMemo reactMemo, ({ Args = arg::restArgs } as callInfo), t, r) ->
+ let arg = propBindings |> List.fold (fun body (k,v) -> Let(k, v, body)) arg
+ Call(reactMemo, { callInfo with Args = arg::restArgs }, t, r)
+ | _ ->
+ propBindings |> List.fold (fun body (k,v) -> Let(k, v, body)) decl.Body
+
+ { decl with Args = [propsArg]; Body = body }
open ReactComponentHelpers
@@ -74,16 +98,40 @@ type ReactComponentAttribute(?exportDefault: bool, ?import: string, ?from:string
callee
if List.length membArgs = info.Args.Length && info.Args.Length = 1 && AstUtils.isRecord compiler info.Args[0].Type then
+
+ // declared record
+ // https://github.com/Zaid-Ajaj/Feliz/issues/603
// F# Component { Value = 1 }
+ // JSX
+ // JS createElement(Component, props = { Value: 1 })
+
+ // anonymous record
+ // F# Component {| Value = 1 |}
// JSX
// JS createElement(Component, { Value: 1 })
- if AstUtils.recordHasField "Key" compiler info.Args[0].Type then
+
+ let isDeclaredRecord = AstUtils.isDeclaredRecord compiler info.Args[0].Type
+
+ match AstUtils.tryGetRecordField "key" compiler info.Args[0].Type with
+ | Some keyField when isDeclaredRecord ->
// When the key property is upper-case (which is common in record fields)
// then we should rewrite it
- let modifiedRecord = AstUtils.emitJs "(($value) => { $value.key = $value.Key; return $value; })($0)" [info.Args[0]]
+ let modifiedRecord =
+ AstUtils.emitJs
+ (sprintf "(($value) => { $value.key = $value.%s.%s; return $value; })($0)" (membArgs[0].Name.Value) keyField)
+ [ AstUtils.objExpr [ membArgs[0].Name.Value, info.Args[0]] ]
AstUtils.createElement reactElType [reactComponent; modifiedRecord]
- else
- AstUtils.createElement reactElType [reactComponent; info.Args[0]]
+ | Some "Key" -> // anonymous record won't have wrapped object so 'key' (lowercase) prop is automatically recognized
+ let modifiedRecord =
+ AstUtils.emitJs "(($value) => { $value.key = $value.Key; return $value; })($0)" [info.Args[0]]
+ AstUtils.createElement reactElType [reactComponent; modifiedRecord]
+ | _ ->
+ let value =
+ if isDeclaredRecord then
+ AstUtils.objExpr [ membArgs[0].Name.Value, info.Args[0] ]
+ else
+ info.Args[0]
+ AstUtils.createElement reactElType [reactComponent; value]
elif info.Args.Length = 1 && info.Args[0].Type = Type.Unit then
// F# Component()
// JSX
@@ -93,7 +141,8 @@ type ReactComponentAttribute(?exportDefault: bool, ?import: string, ?from:string
let mutable keyBinding = None
let propsObj =
- List.zip (List.take info.Args.Length membArgs) info.Args
+ info.Args
+ |> List.zip (List.take info.Args.Length membArgs)
|> List.collect (fun (arg, expr) ->
match arg.Name, expr with
| Some "key", IdentExpr _ -> ["key", expr; "$key", expr]
@@ -144,8 +193,18 @@ type ReactComponentAttribute(?exportDefault: bool, ?import: string, ?from:string
| Some true -> { decl with Tags = "export-default"::decl.Tags }
| Some false | None -> decl
- // do not rewrite components accepting records as input
- if decl.Args.Length = 1 && AstUtils.isRecord compiler decl.Args[0].Type then
+ // do not rewrite components accepting anonymous records as input
+ if decl.Args.Length = 1 && AstUtils.isAnonymousRecord decl.Args.[0].Type then
+ if AstUtils.tryGetRecordField "key" compiler decl.Args[0].Type = Some "key" then
+ let errorMessage =
+ sprintf "The function %s expects an anonymous record with a 'key' property, which is not allowed by React. The value will be erased and it will return undefined. More info: https://reactjs.org/link/special-props" decl.Name
+ compiler.LogWarning(errorMessage, ?range=decl.Body.Range)
+
+ decl
+ |> applyImportOrMemo import from memo
+ // put record into a single props object to stabilize prototype chain
+ // https://github.com/Zaid-Ajaj/Feliz/issues/603
+ elif decl.Args.Length = 1 && AstUtils.isDeclaredRecord compiler decl.Args[0].Type then
// check whether the record type is defined in this file
// trigger warning if that is case
let definedInThisFile =
@@ -186,34 +245,15 @@ type ReactComponentAttribute(?exportDefault: bool, ?import: string, ?from:string
()
decl
+ |> rewriteArgs
|> applyImportOrMemo import from memo
else if decl.Args.Length = 1 && decl.Args[0].Type = Type.Unit then
// remove arguments from functions requiring unit as input
{ decl with Args = [ ] }
|> applyImportOrMemo import from memo
else
- // rewrite all other arguments into getters of a single props object
- // TODO: transform any callback into into useCallback(callback) to stabilize reference
- let propsArg = AstUtils.makeIdent (sprintf "%sInputProps" (AstUtils.camelCase decl.Name))
- let propBindings =
- ([], decl.Args) ||> List.fold (fun bindings arg ->
- let getterKey = if arg.DisplayName = "key" then "$key" else arg.DisplayName
- let getterKind = ExprGet(AstUtils.makeStrConst getterKey)
- let getter = Get(IdentExpr propsArg, getterKind, Any, None)
- (arg, getter)::bindings)
- |> List.rev
-
- let body =
- match decl.Body with
- // If the body is surrounded by a memo call we put the bindings within the call
- // because Fable will later move the surrounding function into memo
- | Call(ReactMemo reactMemo, ({ Args = arg::restArgs } as callInfo), t, r) ->
- let arg = propBindings |> List.fold (fun body (k,v) -> Let(k, v, body)) arg
- Call(reactMemo, { callInfo with Args = arg::restArgs }, t, r)
- | _ ->
- propBindings |> List.fold (fun body (k,v) -> Let(k, v, body)) decl.Body
-
- { decl with Args = [propsArg]; Body = body }
+ decl
+ |> rewriteArgs
|> applyImportOrMemo import from memo
type ReactMemoComponentAttribute(?exportDefault: bool) =