Skip to content
This repository was archived by the owner on Jun 15, 2023. It is now read-only.

More generalized props type for ref and type variables #637

Merged
merged 2 commits into from
Sep 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 58 additions & 26 deletions cli/reactjs_jsx_v4.ml
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ let recordFromProps ~loc ~removeKey callArguments =
let makePropsTypeParamsTvar namedTypeList =
namedTypeList
|> List.filter_map (fun (_isOptional, label, _, _interiorType) ->
if label = "key" || label = "ref" then None
if label = "key" then None
else Some (Typ.var @@ safeTypeFromValue (Labelled label)))

let stripOption coreType =
Expand All @@ -242,13 +242,36 @@ let stripOption coreType =
List.nth_opt coreTypes 0 [@doesNotRaise]
| _ -> Some coreType

(* make type params for make sig arguments and for external *)
(* let make: React.componentLike<props<string, option<string>>, React.element> *)
(* external make: React.componentLike<props< .. >, React.element> = "default" *)
let makePropsTypeParams ?(stripExplicitOption = false) namedTypeList =
let stripJsNullable coreType =
match coreType with
| {
ptyp_desc =
Ptyp_constr ({txt = Ldot (Ldot (Lident "Js", "Nullable"), "t")}, coreTypes);
} ->
List.nth_opt coreTypes 0 [@doesNotRaise]
| _ -> Some coreType

(* Make type params of the props type *)
(* (Sig) let make: React.componentLike<props<string>, React.element> *)
(* (Str) let make = ({x, _}: props<'x>) => body *)
(* (Str) external make: React.componentLike<props< .. >, React.element> = "default" *)
let makePropsTypeParams ?(stripExplicitOption = false)
?(stripExplicitJsNullableOfRef = false) namedTypeList =
namedTypeList
|> List.filter_map (fun (isOptional, label, _, interiorType) ->
if label = "key" || label = "ref" then None
if label = "key" then None
(* TODO: Worth thinking how about "ref_" or "_ref" usages *)
else if label = "ref" then
(*
If ref has a type annotation then use it, else `ReactDOM.Ref.currentDomRef.
For example, if JSX ppx is used for React Native, type would be different.
*)
match interiorType with
| {ptyp_desc = Ptyp_var "ref"} -> Some (refType Location.none)
| _ ->
(* Strip explicit Js.Nullable.t in case of forwardRef *)
if stripExplicitJsNullableOfRef then stripJsNullable interiorType
else Some interiorType
(* Strip the explicit option type in implementation *)
(* let make = (~x: option<string>=?) => ... *)
else if isOptional && stripExplicitOption then stripOption interiorType
Expand All @@ -259,10 +282,6 @@ let makeLabelDecls ~loc namedTypeList =
|> List.map (fun (isOptional, label, _, interiorType) ->
if label = "key" then
Type.field ~loc ~attrs:optionalAttr {txt = label; loc} interiorType
else if label = "ref" then
Type.field ~loc
~attrs:(if isOptional then optionalAttr else [])
{txt = label; loc} interiorType
else if isOptional then
Type.field ~loc ~attrs:optionalAttr {txt = label; loc}
(Typ.var @@ safeTypeFromValue @@ Labelled label)
Expand Down Expand Up @@ -614,9 +633,22 @@ let rec recursivelyTransformNamedArgsForMake mapper expr args newtypes coreType
| Pexp_fun
( Nolabel,
_,
{ppat_desc = Ppat_var _ | Ppat_constraint ({ppat_desc = Ppat_var _}, _)},
({
ppat_desc =
Ppat_var {txt} | Ppat_constraint ({ppat_desc = Ppat_var {txt}}, _);
} as pattern),
_expression ) ->
(args, newtypes, coreType)
if txt = "ref" then
let type_ =
match pattern with
| {ppat_desc = Ppat_constraint (_, type_)} -> Some type_
| _ -> None
in
(* The ref arguement of forwardRef should be optional *)
( (Optional "ref", None, pattern, txt, pattern.ppat_loc, type_) :: args,
newtypes,
coreType )
else (args, newtypes, coreType)
| Pexp_fun (Nolabel, _, pattern, _expression) ->
Location.raise_errorf ~loc:pattern.ppat_loc
"React: react.component refs only support plain arguments and type \
Expand Down Expand Up @@ -943,10 +975,7 @@ let transformStructureItem ~config mapper item =
let vbMatchList = List.map vbMatch namedArgWithDefaultValueList in
(* type props = { ... } *)
let propsRecordType =
makePropsRecordType "props" emptyLoc
((if hasForwardRef then [(true, "ref", [], refType Location.none)]
else [])
@ namedTypeList)
makePropsRecordType "props" emptyLoc namedTypeList
in
let innerExpression =
Exp.apply
Expand Down Expand Up @@ -1013,12 +1042,12 @@ let transformStructureItem ~config mapper item =
| Pexp_fun
(arg_label, _default, ({ppat_loc; ppat_desc} as pattern), expr)
-> (
let pattern = stripConstraint pattern in
let patternWithoutConstraint = stripConstraint pattern in
if isLabelled arg_label || isOptional arg_label then
returnedExpression
(( {loc = ppat_loc; txt = Lident (getLabel arg_label)},
{
pattern with
patternWithoutConstraint with
ppat_attributes =
(if isOptional arg_label then optionalAttr else [])
@ pattern.ppat_attributes;
Expand All @@ -1029,7 +1058,8 @@ let transformStructureItem ~config mapper item =
(* Special case of nolabel arg "ref" in forwardRef fn *)
(* let make = React.forwardRef(ref => body) *)
match ppat_desc with
| Ppat_var {txt} ->
| Ppat_var {txt}
| Ppat_constraint ({ppat_desc = Ppat_var {txt}}, _) ->
returnedExpression patternsWithLabel
(( {loc = ppat_loc; txt = Lident txt},
{
Expand All @@ -1046,27 +1076,29 @@ let transformStructureItem ~config mapper item =
let patternsWithLabel, patternsWithNolabel, expression =
returnedExpression [] [] expression
in
let pattern =
match patternsWithLabel with
| [] -> Pat.any ()
| _ -> Pat.record (List.rev patternsWithLabel) Open
in
(* add pattern matching for optional prop value *)
let expression =
if List.length vbMatchList = 0 then expression
else Exp.let_ Nonrecursive vbMatchList expression
in
(* (ref) => expr *)
let expression =
List.fold_left
(fun expr (_, pattern) -> Exp.fun_ Nolabel None pattern expr)
expression patternsWithNolabel
in
let recordPattern =
match patternsWithLabel with
| [] -> Pat.any ()
| _ -> Pat.record (List.rev patternsWithLabel) Open
in
let expression =
Exp.fun_ Nolabel None
(Pat.constraint_ pattern
(Pat.constraint_ recordPattern
(Typ.constr ~loc:emptyLoc
{txt = Lident "props"; loc = emptyLoc}
(makePropsTypeParams ~stripExplicitOption:true namedTypeList)))
(makePropsTypeParams ~stripExplicitOption:true
~stripExplicitJsNullableOfRef:hasForwardRef namedTypeList)))
expression
in
(* let make = ({id, name, ...}: props<'id, 'name, ...>) => { ... } *)
Expand Down
36 changes: 36 additions & 0 deletions tests/ppx/react/expected/externalWithRef.res.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
@@jsxConfig({version: 3})

module V3 = {
@obj
external makeProps: (
~x: string,
~ref: ReactDOM.Ref.currentDomRef=?,
~key: string=?,
unit,
) => {"x": string, "ref": option<ReactDOM.Ref.currentDomRef>} = ""
@module("componentForwardRef")
external make: React.componentLike<
{"x": string, "ref": option<ReactDOM.Ref.currentDomRef>},
React.element,
> = "component"
}

@@jsxConfig({version: 4, mode: "classic"})

module V4C = {
type props<'x, 'ref> = {x: 'x, ref?: 'ref}

@module("componentForwardRef")
external make: React.componentLike<props<string, ReactDOM.Ref.currentDomRef>, React.element> =
"component"
}

@@jsxConfig({version: 4, mode: "automatic"})

module V4C = {
type props<'x, 'ref> = {x: 'x, ref?: 'ref}

@module("componentForwardRef")
external make: React.componentLike<props<string, ReactDOM.Ref.currentDomRef>, React.element> =
"component"
}
32 changes: 32 additions & 0 deletions tests/ppx/react/expected/externalWithTypeVariables.res.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
@@jsxConfig({version: 3})

module V3 = {
@obj
external makeProps: (
~x: t<'a>,
~children: React.element,
~key: string=?,
unit,
) => {"x": t<'a>, "children": React.element} = ""
@module("c")
external make: React.componentLike<{"x": t<'a>, "children": React.element}, React.element> =
"component"
}

@@jsxConfig({version: 4, mode: "classic"})

module V4C = {
type props<'x, 'children> = {x: 'x, children: 'children}

@module("c")
external make: React.componentLike<props<t<'a>, React.element>, React.element> = "component"
}

@@jsxConfig({version: 4, mode: "automatic"})

module V4C = {
type props<'x, 'children> = {x: 'x, children: 'children}

@module("c")
external make: React.componentLike<props<t<'a>, React.element>, React.element> = "component"
}
20 changes: 13 additions & 7 deletions tests/ppx/react/expected/forwardRef.res.txt
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,17 @@ module V3 = {

module V4C = {
module FancyInput = {
type props<'className, 'children> = {
ref?: ReactDOM.Ref.currentDomRef,
type props<'className, 'children, 'ref> = {
className?: 'className,
children: 'children,
ref?: 'ref,
}

@react.component
let make = ({?className, children, _}: props<'className, 'children>, ref) =>
let make = (
{?className, children, _}: props<'className, 'children, ReactRef.currentDomRef>,
ref: Js.Nullable.t<ReactRef.currentDomRef>,
) =>
ReactDOMRe.createDOMElementVariadic(
"div",
[
Expand All @@ -82,7 +85,7 @@ module V4C = {
~props=ReactDOMRe.domProps(
~type_="text",
~className?,
~ref=?{Js.Nullable.toOption(ref)->Belt.Option.map(ReactDOM.Ref.domRef)},
~ref=?{Js.Nullable.toOption(ref)->Belt.Option.map(React.Ref.domRef)},
(),
),
[],
Expand Down Expand Up @@ -123,14 +126,17 @@ module V4C = {

module V4A = {
module FancyInput = {
type props<'className, 'children> = {
ref?: ReactDOM.Ref.currentDomRef,
type props<'className, 'children, 'ref> = {
className?: 'className,
children: 'children,
ref?: 'ref,
}

@react.component
let make = ({?className, children, _}: props<'className, 'children>, ref) =>
let make = (
{?className, children, _}: props<'className, 'children, ReactDOM.Ref.currentDomRef>,
ref,
) =>
ReactDOM.jsxs(
"div",
{
Expand Down
13 changes: 13 additions & 0 deletions tests/ppx/react/expected/interfaceWithRef.res.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
type props<'x, 'ref> = {x: 'x, ref?: 'ref}
@react.component
let make = (
{x, _}: props<string, ReactDOM.Ref.currentDomRef>,
ref: Js.Nullable.t<ReactDOM.Ref.currentDomRef>,
) => {
let _ = ref->Js.Nullable.toOption->Belt.Option.map(ReactDOM.Ref.domRef)
React.string(x)
}
let make = React.forwardRef({
let \"InterfaceWithRef" = (props: props<_>, ref) => make(props, ref)
\"InterfaceWithRef"
})
2 changes: 2 additions & 0 deletions tests/ppx/react/expected/interfaceWithRef.resi.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
type props<'x, 'ref> = {x: 'x, ref?: 'ref}
let make: React.componentLike<props<string, ReactDOM.Ref.currentDomRef>, React.element>
29 changes: 29 additions & 0 deletions tests/ppx/react/externalWithRef.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
@@jsxConfig({version: 3})

module V3 = {
@module("componentForwardRef") @react.component
external make: (
~x: string,
~ref: ReactDOM.Ref.currentDomRef=?,
) => React.element = "component"
}

@@jsxConfig({version: 4, mode: "classic"})

module V4C = {
@module("componentForwardRef") @react.component
external make: (
~x: string,
~ref: ReactDOM.Ref.currentDomRef=?,
) => React.element = "component"
}

@@jsxConfig({version: 4, mode: "automatic"})

module V4C = {
@module("componentForwardRef") @react.component
external make: (
~x: string,
~ref: ReactDOM.Ref.currentDomRef=?,
) => React.element = "component"
}
29 changes: 29 additions & 0 deletions tests/ppx/react/externalWithTypeVariables.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
@@jsxConfig({version: 3})

module V3 = {
@module("c") @react.component
external make: (
~x: t<'a>,
~children: React.element,
) => React.element = "component"
}

@@jsxConfig({version: 4, mode: "classic"})

module V4C = {
@module("c") @react.component
external make: (
~x: t<'a>,
~children: React.element,
) => React.element = "component"
}

@@jsxConfig({version: 4, mode: "automatic"})

module V4C = {
@module("c") @react.component
external make: (
~x: t<'a>,
~children: React.element,
) => React.element = "component"
}
4 changes: 2 additions & 2 deletions tests/ppx/react/forwardRef.res
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,12 @@ module V3 = {
module V4C = {
module FancyInput = {
@react.component
let make = React.forwardRef((~className=?, ~children, ref) =>
let make = React.forwardRef((~className=?, ~children, ref: Js.Nullable.t<ReactRef.currentDomRef>) =>
<div>
<input
type_="text"
?className
ref=?{Js.Nullable.toOption(ref)->Belt.Option.map(ReactDOM.Ref.domRef)}
ref=?{Js.Nullable.toOption(ref)->Belt.Option.map(React.Ref.domRef)}
/>
children
</div>
Expand Down
5 changes: 5 additions & 0 deletions tests/ppx/react/interfaceWithRef.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@react.component
let make = React.forwardRef((~x: string, ref: Js.Nullable.t<ReactDOM.Ref.currentDomRef>) => {
let _ = ref->Js.Nullable.toOption->Belt.Option.map(ReactDOM.Ref.domRef)
React.string(x)
})
2 changes: 2 additions & 0 deletions tests/ppx/react/interfaceWithRef.resi
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@react.component
let make: (~x: string, ~ref: ReactDOM.Ref.currentDomRef=?) => React.element