From 805645728dc2618557f33621b313322ce0724849 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Krzywizna?= Date: Thu, 23 May 2024 08:42:18 +0200 Subject: [PATCH] Added wrapping object for every type-declared record used as a props object with `[]` --- Feliz.CompilerPlugins/AstUtils.fs | 7 +- Feliz.CompilerPlugins/ReactComponent.fs | 92 +++++++++++++++++-------- 2 files changed, 69 insertions(+), 30 deletions(-) diff --git a/Feliz.CompilerPlugins/AstUtils.fs b/Feliz.CompilerPlugins/AstUtils.fs index 7d08f669..98ecf089 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 @@ -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)) \ No newline at end of file diff --git a/Feliz.CompilerPlugins/ReactComponent.fs b/Feliz.CompilerPlugins/ReactComponent.fs index 4ad4675b..920d8169 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,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 + // JS createElement(Component, { props = { Value: 1 } }) + + // anonymous record // F# Component { Value = 1 } // JSX // 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 @@ -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] @@ -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 []" decl.Name + let errorMessage = sprintf "Expecting a function declaration for %s when using []" 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 []. Instead it returns %A" decl.Name decl.Body.Type + let errorMessage = sprintf "Expected function %s to return a ReactElement when using []. Instead it returns %A" decl.Name decl.Body.Type compiler.LogWarning(errorMessage, ?range=decl.Body.Range) decl else @@ -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 = @@ -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) =