Skip to content

Commit 8056457

Browse files
Added wrapping object for every type-declared record used as a props object with [<ReactComponentAttribute>]
1 parent e2da7db commit 8056457

File tree

2 files changed

+69
-30
lines changed

2 files changed

+69
-30
lines changed

Feliz.CompilerPlugins/AstUtils.fs

+6-1
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,11 @@ let makeImport (selector: string) (path: string) =
7474
Path = path.Trim()
7575
Kind = Fable.UserImport(false) }, Fable.Any, None)
7676

77+
let isDeclaredRecord (compiler: PluginHelper) (fableType: Fable.Type) =
78+
match fableType with
79+
| Fable.Type.DeclaredType (entity, genericArgs) -> compiler.GetEntity(entity).IsFSharpRecord
80+
| _ -> false
81+
7782
let isRecord (compiler: PluginHelper) (fableType: Fable.Type) =
7883
match fableType with
7984
| Fable.Type.AnonymousRecordType _ -> true
@@ -163,4 +168,4 @@ let capitalize (input: string) =
163168
let camelCase (input: string) =
164169
if String.IsNullOrWhiteSpace input
165170
then ""
166-
else input.First().ToString().ToLower() + String.Join("", input.Skip(1))
171+
else input.First().ToString().ToLower() + String.Join("", input.Skip(1))

Feliz.CompilerPlugins/ReactComponent.fs

+63-29
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,30 @@ module internal ReactComponentHelpers =
4949
{ decl with MemberRef = info; Args = []; Body = body }
5050

5151
| _ -> { decl with Body = injectReactImport decl.Body }
52+
53+
let rewriteArgs (decl: MemberDecl) =
54+
// rewrite all other arguments into getters of a single props object
55+
// TODO: transform any callback into into useCallback(callback) to stabilize reference
56+
let propsArg = AstUtils.makeIdent (sprintf "%sInputProps" (AstUtils.camelCase decl.Name))
57+
let propBindings =
58+
([], decl.Args) ||> List.fold (fun bindings arg ->
59+
let getterKey = if arg.DisplayName = "key" then "$key" else arg.DisplayName
60+
let getterKind = ExprGet(AstUtils.makeStrConst getterKey)
61+
let getter = Get(IdentExpr propsArg, getterKind, Any, None)
62+
(arg, getter)::bindings)
63+
|> List.rev
64+
65+
let body =
66+
match decl.Body with
67+
// If the body is surrounded by a memo call we put the bindings within the call
68+
// because Fable will later move the surrounding function into memo
69+
| Call(ReactMemo reactMemo, ({ Args = arg::restArgs } as callInfo), t, r) ->
70+
let arg = propBindings |> List.fold (fun body (k,v) -> Let(k, v, body)) arg
71+
Call(reactMemo, { callInfo with Args = arg::restArgs }, t, r)
72+
| _ ->
73+
propBindings |> List.fold (fun body (k,v) -> Let(k, v, body)) decl.Body
74+
75+
{ decl with Args = [propsArg]; Body = body }
5276

5377
open ReactComponentHelpers
5478

@@ -74,16 +98,39 @@ type ReactComponentAttribute(?exportDefault: bool, ?import: string, ?from:string
7498
callee
7599

76100
if List.length membArgs = info.Args.Length && info.Args.Length = 1 && AstUtils.isRecord compiler info.Args[0].Type then
101+
102+
// declared record
103+
// https://github.com/Zaid-Ajaj/Feliz/issues/603
104+
// F# Component { Value = 1 }
105+
// JSX <Component props={ { Value={1} } } />
106+
// JS createElement(Component, { props = { Value: 1 } })
107+
108+
// anonymous record
77109
// F# Component { Value = 1 }
78110
// JSX <Component Value={1} />
79111
// JS createElement(Component, { Value: 1 })
112+
113+
let isDeclaredRecord = AstUtils.isDeclaredRecord compiler info.Args[0].Type
114+
80115
if AstUtils.recordHasField "Key" compiler info.Args[0].Type then
81116
// When the key property is upper-case (which is common in record fields)
82117
// then we should rewrite it
83-
let modifiedRecord = AstUtils.emitJs "(($value) => { $value.key = $value.Key; return $value; })($0)" [info.Args[0]]
118+
let modifiedRecord =
119+
if isDeclaredRecord then
120+
AstUtils.objExpr [
121+
"key", AstUtils.emitJs "$0.Key" [info.Args[0]];
122+
membArgs[0].Name.Value, info.Args[0]
123+
]
124+
else
125+
AstUtils.emitJs "(($value) => { $value.key = $value.Key; return $value; })($0)" [info.Args[0]]
84126
AstUtils.createElement reactElType [reactComponent; modifiedRecord]
85127
else
86-
AstUtils.createElement reactElType [reactComponent; info.Args[0]]
128+
let value =
129+
if isDeclaredRecord then
130+
AstUtils.objExpr [ membArgs[0].Name.Value, info.Args[0] ]
131+
else
132+
info.Args[0]
133+
AstUtils.createElement reactElType [reactComponent; value]
87134
elif info.Args.Length = 1 && info.Args[0].Type = Type.Unit then
88135
// F# Component()
89136
// JSX <Component />
@@ -93,7 +140,8 @@ type ReactComponentAttribute(?exportDefault: bool, ?import: string, ?from:string
93140
let mutable keyBinding = None
94141

95142
let propsObj =
96-
List.zip (List.take info.Args.Length membArgs) info.Args
143+
info.Args
144+
|> List.zip (List.take info.Args.Length membArgs)
97145
|> List.collect (fun (arg, expr) ->
98146
match arg.Name, expr with
99147
| Some "key", IdentExpr _ -> ["key", expr; "$key", expr]
@@ -127,12 +175,12 @@ type ReactComponentAttribute(?exportDefault: bool, ?import: string, ?from:string
127175
let info = compiler.GetMember(decl.MemberRef)
128176
if info.IsValue || info.IsGetter || info.IsSetter then
129177
// Invalid attribute usage
130-
let errorMessage = sprintf "Expecting a function declaration for %s when using [<ReactComponent>]" decl.Name
178+
let errorMessage = sprintf "Expecting a function declaration for %s when using [<PortalLibs.ClientPlugin.ReactComponent>]" decl.Name
131179
compiler.LogWarning(errorMessage, ?range=decl.Body.Range)
132180
decl
133181
else if not (AstUtils.isReactElement decl.Body.Type) then
134182
// output of a React function component must be a ReactElement
135-
let errorMessage = sprintf "Expected function %s to return a ReactElement when using [<ReactComponent>]. Instead it returns %A" decl.Name decl.Body.Type
183+
let errorMessage = sprintf "Expected function %s to return a ReactElement when using [<PortalLibs.ClientPlugin.ReactComponent>]. Instead it returns %A" decl.Name decl.Body.Type
136184
compiler.LogWarning(errorMessage, ?range=decl.Body.Range)
137185
decl
138186
else
@@ -144,8 +192,13 @@ type ReactComponentAttribute(?exportDefault: bool, ?import: string, ?from:string
144192
| Some true -> { decl with Tags = "export-default"::decl.Tags }
145193
| Some false | None -> decl
146194

147-
// do not rewrite components accepting records as input
148-
if decl.Args.Length = 1 && AstUtils.isRecord compiler decl.Args[0].Type then
195+
// do not rewrite components accepting anonymous records as input
196+
if decl.Args.Length = 1 && AstUtils.isAnonymousRecord decl.Args.[0].Type then
197+
decl
198+
|> applyImportOrMemo import from memo
199+
// put record into a single props object to stabilize prototype chain
200+
// https://github.com/Zaid-Ajaj/Feliz/issues/603
201+
elif decl.Args.Length = 1 && AstUtils.isDeclaredRecord compiler decl.Args[0].Type then
149202
// check whether the record type is defined in this file
150203
// trigger warning if that is case
151204
let definedInThisFile =
@@ -186,34 +239,15 @@ type ReactComponentAttribute(?exportDefault: bool, ?import: string, ?from:string
186239
()
187240

188241
decl
242+
|> rewriteArgs
189243
|> applyImportOrMemo import from memo
190244
else if decl.Args.Length = 1 && decl.Args[0].Type = Type.Unit then
191245
// remove arguments from functions requiring unit as input
192246
{ decl with Args = [ ] }
193247
|> applyImportOrMemo import from memo
194248
else
195-
// rewrite all other arguments into getters of a single props object
196-
// TODO: transform any callback into into useCallback(callback) to stabilize reference
197-
let propsArg = AstUtils.makeIdent (sprintf "%sInputProps" (AstUtils.camelCase decl.Name))
198-
let propBindings =
199-
([], decl.Args) ||> List.fold (fun bindings arg ->
200-
let getterKey = if arg.DisplayName = "key" then "$key" else arg.DisplayName
201-
let getterKind = ExprGet(AstUtils.makeStrConst getterKey)
202-
let getter = Get(IdentExpr propsArg, getterKind, Any, None)
203-
(arg, getter)::bindings)
204-
|> List.rev
205-
206-
let body =
207-
match decl.Body with
208-
// If the body is surrounded by a memo call we put the bindings within the call
209-
// because Fable will later move the surrounding function into memo
210-
| Call(ReactMemo reactMemo, ({ Args = arg::restArgs } as callInfo), t, r) ->
211-
let arg = propBindings |> List.fold (fun body (k,v) -> Let(k, v, body)) arg
212-
Call(reactMemo, { callInfo with Args = arg::restArgs }, t, r)
213-
| _ ->
214-
propBindings |> List.fold (fun body (k,v) -> Let(k, v, body)) decl.Body
215-
216-
{ decl with Args = [propsArg]; Body = body }
249+
decl
250+
|> rewriteArgs
217251
|> applyImportOrMemo import from memo
218252

219253
type ReactMemoComponentAttribute(?exportDefault: bool) =

0 commit comments

Comments
 (0)