Skip to content

Commit

Permalink
Added wrapping object for every type-declared record used as a props …
Browse files Browse the repository at this point in the history
…object with `[<ReactComponentAttribute>]`
  • Loading branch information
lukaszkrzywizna committed May 23, 2024
1 parent e2da7db commit 8056457
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 30 deletions.
7 changes: 6 additions & 1 deletion Feliz.CompilerPlugins/AstUtils.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -163,4 +168,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))
92 changes: 63 additions & 29 deletions Feliz.CompilerPlugins/ReactComponent.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -74,16 +98,39 @@ 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 <Component props={ { Value={1} } } />
// JS createElement(Component, { props = { Value: 1 } })

// anonymous record
// F# Component { Value = 1 }
// JSX <Component Value={1} />
// JS createElement(Component, { Value: 1 })

let isDeclaredRecord = AstUtils.isDeclaredRecord compiler info.Args[0].Type

if AstUtils.recordHasField "Key" compiler info.Args[0].Type then
// 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 =
if isDeclaredRecord then
AstUtils.objExpr [
"key", AstUtils.emitJs "$0.Key" [info.Args[0]];
membArgs[0].Name.Value, info.Args[0]
]
else
AstUtils.emitJs "(($value) => { $value.key = $value.Key; return $value; })($0)" [info.Args[0]]
AstUtils.createElement reactElType [reactComponent; modifiedRecord]
else
AstUtils.createElement reactElType [reactComponent; info.Args[0]]
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 <Component />
Expand All @@ -93,7 +140,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]
Expand Down Expand Up @@ -127,12 +175,12 @@ type ReactComponentAttribute(?exportDefault: bool, ?import: string, ?from:string
let info = compiler.GetMember(decl.MemberRef)
if info.IsValue || info.IsGetter || info.IsSetter then
// Invalid attribute usage
let errorMessage = sprintf "Expecting a function declaration for %s when using [<ReactComponent>]" decl.Name
let errorMessage = sprintf "Expecting a function declaration for %s when using [<PortalLibs.ClientPlugin.ReactComponent>]" decl.Name
compiler.LogWarning(errorMessage, ?range=decl.Body.Range)
decl
else if not (AstUtils.isReactElement decl.Body.Type) then
// output of a React function component must be a ReactElement
let errorMessage = sprintf "Expected function %s to return a ReactElement when using [<ReactComponent>]. Instead it returns %A" decl.Name decl.Body.Type
let errorMessage = sprintf "Expected function %s to return a ReactElement when using [<PortalLibs.ClientPlugin.ReactComponent>]. Instead it returns %A" decl.Name decl.Body.Type
compiler.LogWarning(errorMessage, ?range=decl.Body.Range)
decl
else
Expand All @@ -144,8 +192,13 @@ 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
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 =
Expand Down Expand Up @@ -186,34 +239,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) =
Expand Down

0 comments on commit 8056457

Please sign in to comment.