Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support jsx transform with fragment #835

Merged
merged 5 commits into from
Jun 4, 2024
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
5 changes: 5 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# Unreleased

* Support JSX transform with fragments (@tatchi in
[#835](https://github.com/reasonml/reason-react/pull/835))

# 0.14.0

* Wrap the `React` library, exposing just a single top-level module
Expand Down
56 changes: 30 additions & 26 deletions ppx/reason_react_ppx.ml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ module Builder = struct
let value_binding ~loc ~attrs ~pat ~expr =
let vb = Ast_builder.Default.value_binding ~loc ~pat ~expr in
{ vb with pvb_attributes = attrs }

let unit =
pexp_construct ~loc:Location.none
{ txt = Lident "()"; loc = Location.none }
None
end

(* [merlinHide] tells merlin to not look at a node, or at any of its
Expand Down Expand Up @@ -44,6 +49,14 @@ let optional str = Optional str
module Binding = struct
(* Binding is the interface that the ppx uses to interact with the bindings.
Here we define the same APIs as the bindings but it generates Parsetree *)
module ReactDOM = struct
let domProps ~applyLoc ~loc props =
Builder.pexp_apply ~loc:applyLoc
(Builder.pexp_ident ~loc:applyLoc ~attrs:merlinHideAttrs
{ loc; txt = Ldot (Lident "ReactDOM", "domProps") })
props
end

module React = struct
let null ~loc =
Builder.pexp_ident ~loc { loc; txt = Ldot (Lident "React", "null") }
Expand All @@ -59,23 +72,19 @@ module Binding = struct
( { loc; txt = Ldot (Lident "React", "componentLike") },
[ props; return ] )

let jsxFragment ~loc =
Builder.pexp_ident ~loc
{ loc; txt = Ldot (Lident "React", "jsxFragment") }
end

module ReactDOM = struct
let createElement ~loc ~attrs element children =
let jsxFragment ~loc ~attrs children =
let fragment =
Builder.pexp_ident ~loc
{ loc; txt = Ldot (Lident "React", "jsxFragment") }
in
Builder.pexp_apply ~loc ~attrs
(Builder.pexp_ident ~loc
{ loc; txt = Ldot (Lident "ReactDOM", "createElement") })
[ (nolabel, element); (nolabel, children) ]

let domProps ~applyLoc ~loc props =
Builder.pexp_apply ~loc:applyLoc
(Builder.pexp_ident ~loc:applyLoc ~attrs:merlinHideAttrs
{ loc; txt = Ldot (Lident "ReactDOM", "domProps") })
props
(Builder.pexp_ident ~loc { loc; txt = Ldot (Lident "React", "jsx") })
[
(nolabel, fragment);
( nolabel,
ReactDOM.domProps ~applyLoc:loc ~loc
[ (labelled "children", children); (nolabel, Builder.unit) ] );
]
end
end

Expand Down Expand Up @@ -497,11 +506,7 @@ let makeExternalDecl fnName loc namedArgListWithKeyAndRef namedTypeList =

(* TODO: some line number might still be wrong *)
let jsxMapper =
let unit =
Builder.pexp_construct ~loc:Location.none
{ txt = Lident "()"; loc = Location.none }
None
and key_var_txt = "Key" in
let key_var_txt = "Key" in
let transformUppercaseCall3 ~caller ~ctxt modulePath mapper parentExpLoc attrs
callArguments =
let children, argsWithLabels = extractChildren callArguments in
Expand Down Expand Up @@ -577,7 +582,7 @@ let jsxMapper =
{ txt = Lident key_var_txt; loc = parentExpLoc } );
component;
props;
(nolabel, unit);
(nolabel, Builder.unit);
])
in

Expand Down Expand Up @@ -628,7 +633,7 @@ let jsxMapper =
(label, Builder.pexp_ident ~loc { txt = Lident key_var_txt; loc });
component;
props;
(nolabel, unit);
(nolabel, Builder.unit);
])
| None -> Builder.pexp_apply ~loc ~attrs jsxExpr [ component; props ]
in
Expand Down Expand Up @@ -1410,10 +1415,9 @@ let jsxMapper =
let childrenExpr =
transformChildrenIfList ~loc ~ctxt ~mapper:self listItems
in
let fragment = Binding.React.jsxFragment ~loc in
(* throw away the [@JSX] attribute and keep the others, if any *)
Binding.ReactDOM.createElement ~loc ~attrs:nonJSXAttributes
fragment childrenExpr)
Binding.React.jsxFragment ~loc ~attrs:nonJSXAttributes
(Binding.React.array ~loc childrenExpr))
(* Delegate to the default mapper, a deep identity traversal *)
| e -> super#expression ctxt e
[@@raises Invalid_argument]
Expand Down
18 changes: 11 additions & 7 deletions ppx/test/component.t/run.t
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,17 @@ We need to output ML syntax here, otherwise refmt could not parse it.
""[@@mel.obj ]
let make =
((fun ?(name= "") ->
ReactDOM.createElement React.jsxFragment
[|(ReactDOM.jsx "div"
(((ReactDOM.domProps)[@merlin.hide ])
~children:(React.string ("First " ^ name)) ()));(
React.jsx Hello.make
(Hello.makeProps ~children:(React.string ("2nd " ^ name))
~one:"1" ()))|])
React.jsx React.jsxFragment
(((ReactDOM.domProps)[@merlin.hide ])
~children:(React.array
[|(ReactDOM.jsx "div"
(((ReactDOM.domProps)[@merlin.hide ])
~children:(React.string ("First " ^ name))
()));(React.jsx Hello.make
(Hello.makeProps
~children:(React.string
("2nd " ^ name))
~one:"1" ()))|]) ()))
[@warning "-16"])
let make =
let Output$Upper_case_with_fragment_as_root
Expand Down
1 change: 1 addition & 0 deletions ppx/test/fragment.t/input.re
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
let fragment = foo => [@bla] <> foo </>;

let just_one_child = foo => <> bar </>;
let poly_children_fragment = (foo, bar) => <> foo bar </>;
let nested_fragment = (foo, bar, baz) => <> foo <> bar baz </> </>;

Expand Down
54 changes: 43 additions & 11 deletions ppx/test/fragment.t/run.t
Original file line number Diff line number Diff line change
@@ -1,20 +1,52 @@
$ ../ppx.sh --output re input.re
let fragment = foo =>
[@bla] ReactDOM.createElement(React.jsxFragment, [|foo|]);
[@bla]
React.jsx(
React.jsxFragment,
([@merlin.hide] ReactDOM.domProps)(~children=React.array([|foo|]), ()),
);
let just_one_child = foo =>
React.jsx(
React.jsxFragment,
([@merlin.hide] ReactDOM.domProps)(~children=React.array([|bar|]), ()),
);
let poly_children_fragment = (foo, bar) =>
ReactDOM.createElement(React.jsxFragment, [|foo, bar|]);
React.jsx(
React.jsxFragment,
([@merlin.hide] ReactDOM.domProps)(
~children=React.array([|foo, bar|]),
(),
),
);
let nested_fragment = (foo, bar, baz) =>
ReactDOM.createElement(
React.jsx(
React.jsxFragment,
[|foo, ReactDOM.createElement(React.jsxFragment, [|bar, baz|])|],
([@merlin.hide] ReactDOM.domProps)(
~children=
React.array([|
foo,
React.jsx(
React.jsxFragment,
([@merlin.hide] ReactDOM.domProps)(
~children=React.array([|bar, baz|]),
(),
),
),
|]),
(),
),
);
let nested_fragment_with_lower = foo =>
ReactDOM.createElement(
React.jsx(
React.jsxFragment,
[|
ReactDOM.jsx(
"div",
([@merlin.hide] ReactDOM.domProps)(~children=foo, ()),
),
|],
([@merlin.hide] ReactDOM.domProps)(
~children=
React.array([|
ReactDOM.jsx(
"div",
([@merlin.hide] ReactDOM.domProps)(~children=foo, ()),
),
|]),
(),
),
);
Loading