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

JSX V4 WIP #517

Closed
wants to merge 25 commits into from
Closed

JSX V4 WIP #517

wants to merge 25 commits into from

Conversation

mununki
Copy link
Member

@mununki mununki commented Jun 2, 2022

Hi, team!
I'd like to convey my sincere appreciation for your hard work and contributions as a big fan of ReScript.

This PR adds the syntax support for JSX spread props such as below. I'm not sure whether the contribution from others besides the core team is open or not. But I think this feature will help many jsx use cases easier and cleaner. I didn't write the test yet, but please let me know and have a guide to writing a test for this case.

module Foo = {
  let bar = {
    name: "ReScript",
    attr: "Awesome"
  }
}

let baz = {
  nameFromBaz: "ReScript",
  attrFromBaz: "Awesome"
}

@react.component
let make = () => {
  <>
    <div id="1" onClick={_ => ()} {...Foo.bar}> {`div`->React.string} </div>
    <Foo id="2" onClick={_ => ()} {...baz}> {`Foo`->React.string} </Foo>
  </>
}

It is parsed and extended to

// ./lib/rescript.exe -ppx jsx test.res
module Foo = {
  let bar = {
    name: "ReScript",
    attr: "Awesome",
  }
}

let baz = {
  nameFromBaz: "ReScript",
  attrFromBaz: "Awesome",
}
@obj external makeProps: (~key: string=?, unit) => {.} = ""

let make = () => {
  ReactDOMRe.createElement(
    ReasonReact.fragment,
    [
      ReactDOMRe.createDOMElementVariadic(
        "div",
        ~props=ReactDOMRe.domProps(
          ~id="1",
          ~onClick={_ => ()},
          ~name="ReScript",
          ~attr="Awesome",
          (),
        ),
        [{`div`->React.string}],
      ),
      React.createElement(
        Foo.make,
        Foo.makeProps(
          ~id="2",
          ~onClick={_ => ()},
          ~nameFromBaz="ReScript",
          ~attrFromBaz="Awesome",
          ~children={`Foo`->React.string},
          (),
        ),
      ),
    ],
  )
}
let make = {
  let \"Test" = (\"Props": {.}) => make()
  \"Test"
}

@mununki mununki changed the title Add parsing and jsx ppx for spread props Add parsing and jsx ppx support for spread props Jun 2, 2022
@cristianoc
Copy link
Contributor

Hey @mattdamon108 normally Foo would be defined in another file, that one has no access to in a ppx.

@cristianoc
Copy link
Contributor

In general, would you ask for feedback in the forum, as chances there are people who have thought about this already.

@mununki
Copy link
Member Author

mununki commented Jun 2, 2022

normally Foo would be defined in another file, that one has no access to in a ppx.

@cristianoc Yes, it is. The ppx has no access to it either. But I'd like to add the functionality to access the module which exists in the same file.

normally Foo would be defined in another file, that one has no access to in a ppx.

Sure thing, thanks.

@JsonKim
Copy link
Contributor

JsonKim commented Jun 2, 2022

Thank you for your work. I have something to suggest.

Is it possible to output the following when there is an input like the following?

input:

<input type=text {...register("name")} />

output:

React.cloneElement(
  ReactDOMRe.createDOMElementVariadic("input", ~props=ReactDOMRe.domProps(~type_="text", ()), []),
  register("name"),
)

What I would suggest is to use the expression as-is. This means automating the use of cloneElement. Or does anyone have a better idea?

@mununki
Copy link
Member Author

mununki commented Jun 4, 2022

10d7dec rebase and force-pushed due to pulling the recent commits from master. Build with dune 🥇👍

@cristianoc
Copy link
Contributor

@mattdamon108 to continue the discussion about implementation, this looks like a feature that needs type (hence scope) information.
Untyped parse trees do not provide type information, so if one would like to go ahead, some different route would need to be explored.

Not sure if this is something you would like to explore. Likely it will need to be explored during a longer time frame.

@cristianoc
Copy link
Contributor

Also it might be useful to focus on examples that cannot be equivalently done with a function.

type pr = {
  sizes: string,
  srcDoc: string,
}

let baz = {
  sizes: "ReScript",
  srcDoc: "Awesome",
}

@react.component
let make = () => {
  let mkDiv = (~id, ~props as {sizes, srcDoc}, ~children) =>
    <div id onClick={_ => ()} sizes srcDoc> children </div>
  <>
    {mkDiv(~id="1", ~props=baz, ~children=React.string("ab"))}
    {mkDiv(~id="2", ~props=baz, ~children=React.string("cd"))}
  </>
}

@cristianoc
Copy link
Contributor

or as

@react.component
let make = () => {
  module Div = {
    @react.component
    let make = (~id, ~props as {sizes, srcDoc}, ~children) =>
      <div id onClick={_ => ()} sizes srcDoc> children </div>
  }
  <>
    <Div id="1" props=baz> {"ab"->React.string} </Div>
    <Div id="2" props=baz> {"cd"->React.string} </Div>
  </>
}

The language offers already a lot of abbreviation mechanisms, so it might be good to explore what are examples that cannot be expressed this way.

@mununki
Copy link
Member Author

mununki commented Jun 4, 2022

@cristianoc That would be alternatives at this moment without syntax support and instead of this PR. But actually, the point is spreading by language, not by the user. For example,

@react.component
let make = () => {
  let { register } = ReactHookForm.use() // register: string => {onChange, onBlur, ref, ... }

  <input id="1" {...register("name")} />
}

I know that this PR can't solve this use case but, my ultimate intention is to solve this use case with syntax support. I think this feature will embrace more Js users and helps a lot to interop the Js modules.

I agree that this can't be resolved with an untyped parse tree. I already encountered several times during making the ppxes. I want to explore the journey to resolve this syntax support if the core team has willing to adopt this language feature in future.

Anyway, can you give a clue on how to access the expression or type after making typedtree? How to traverse the all merged typedtrees from one to another? I'm reading the reanalyze to search for a clue, but no gain so far. I'm still learning and not sure what actually the flambda and lambda IR is, but is it possible to spread in that step?

@cristianoc
Copy link
Contributor

@cristianoc That would be alternatives at this moment without syntax support and instead of this PR. But actually, the point is spreading by language, not by the user. For example,

@react.component
let make = () => {
  let { register } = ReactHookForm.use() // register: string => {onChange, onBlur, ref, ... }

  <input id="1" {...register("name")} />
}

I know that this PR can't solve this use case but, my ultimate intention is to solve this use case with syntax support. I think this feature will embrace more Js users and helps a lot to interop the Js modules.

I agree that this can't be resolved with an untyped parse tree. I already encountered several times during making the ppxes. I want to explore the journey to resolve this syntax support if the core team has willing to adopt this language feature in future.

Anyway, can you give a clue on how to access the expression or type after making typedtree? How to traverse the all merged typedtrees from one to another? I'm reading the reanalyze to search for a clue, but no gain so far. I'm still learning and not sure what actually the flambda and lambda IR is, but is it possible to spread in that step?

In this example, what's the type of register("name")?
In other words, is this untyped or typed spread?

As of possible ways forward, it seems likely that advancing this #521 would be a stepping stone towards this.

@mununki
Copy link
Member Author

mununki commented Jun 4, 2022

In my example, the type of register("name") is type t.

type t = {
  onChange: ReactEvent.Form.t => unit,
  onBlur: ReactEvent.Focus.t => unit,
  ref: ReactDOM.domRef,
  name: string,
}

let register: string => t

@cristianoc
Copy link
Contributor

OK then I think this feature will come for free with #521
So the way to process on this feature is to progress on that.
And the firs step to make that happen is to help with rescript-lang/rescript#5412

@mununki
Copy link
Member Author

mununki commented Jun 4, 2022

I got your point. If the jsx is parsed to the normal function and passes the object as arguments based on the more general structural typing (case no. 1), I agree that it is the first step to advance to make happen. But I'm still unclear about case no. 2, how to make structurally typing this multiple arguments layer?

@react.component
let make = () => {
  let { register } = ReactHookForm.use()

  // case 1. OK
  // just parse it to ReactDOMRe.createDOMElementVariadic("input", ~props=register("name"))
   <input {...register("name")} />

  // case 2. unclear
  // {"id": string, "type_": string} and {"onChange": ... , "onBlur": ... , "ref": ..., "name": string }
  <input id="1" type_="button" {...register("name")} />
}

@cristianoc
Copy link
Contributor

cristianoc commented Jun 4, 2022

That only requires record spread, which is part of the language.
{...foo, x:e}.

@mununki
Copy link
Member Author

mununki commented Jun 4, 2022

Wow, I've verified this just moment ago.

type foo = {name: string}

let foo = {
  name: "woonki",
}

let foo1 = foo

type bar = {
  name: string,
  age: int,
}

let bar = {...foo1, age: 10}
// let bar = {...foo1, age: 10}
structure_item (test.res[9,76+0]..[12,117+1])
    Pstr_type Nonrec
    [
      type_declaration "bar" (test.res[9,76+5]..[9,76+8]) (test.res[9,76+0]..[12,117+1])
        ptype_params =
          []
        ptype_cstrs =
          []
        ptype_kind =
          Ptype_record
            [
              (test.res[10,89+2]..[10,89+14])
                Immutable
                "name" (test.res[10,89+2]..[10,89+6])                core_type (test.res[10,89+8]..[10,89+14])
                  Ptyp_constr "string" (test.res[10,89+8]..[10,89+14])
                  []
              (test.res[11,105+2]..[11,105+10])
                Immutable
                "age" (test.res[11,105+2]..[11,105+5])                core_type (test.res[11,105+7]..[11,105+10])
                  Ptyp_constr "int" (test.res[11,105+7]..[11,105+10])
                  []
            ]
        ptype_private = Public
        ptype_manifest =
          None
    ]

@mununki
Copy link
Member Author

mununki commented Jun 4, 2022

I got your point again. the more general structural typing and the immutable updated record will solve cases 1, 2! I think it needs to change the jsx mapper either. Can I ask how the progress is going? I think I can contribute to making up for this PR further.

@cristianoc
Copy link
Contributor

There's no work except for the little text written in the issues.
So any help there is useful to make progress.

@mununki
Copy link
Member Author

mununki commented Jun 4, 2022

Got it with thanks! I'm gonna work on this PR and let you guys know.

@cristianoc
Copy link
Contributor

First, test the new record types' functionality.
I've already reported an issue with error messages. There might be more.

Solving the error message issue is also useful.

@mununki
Copy link
Member Author

mununki commented Jun 4, 2022

Solving the error message issue is also useful.

Checked, good to know that!

@mununki
Copy link
Member Author

mununki commented Jun 6, 2022

@cristianoc I've implemented the spread props feature with the general type checking of the structural typings. https://forum.rescript-lang.org/t/rfc-more-general-type-checking-for-structural-typings/1485/73 This is quite a visible change behind the jsx definition. This PR will change how to define the makeProps and make for jsx parsing and mapping.

// as-is
@obj external makeProps: (~key: string=?, unit) => {.} = ""
let make = () => {
  ReactDOMRe.createElement(
    ReasonReact.fragment,
    [
      React.createElement(
        Foo.make,
        Foo.makeProps(~id="2", ~onClick={_ => ()}, ~children={`Foo`->React.string}, ()),
      ),
    ],
  )
}
// to-be
@obj                   
type makeProps = {
  key: option<string>,
}
let makeProps = (props: makeProps) => props
let make = () => {
  ReactDOMRe.createElement(
    ReasonReact.fragment,
    [
      React.createElement(
        Foo.make,
        Foo.makeProps({id: "2", onClick: _ => (), children: {`Foo`->React.string}}),
      ),
    ],
  )
}

I well acknowledge that this RFC needs more tests and polishing rescript-lang/rescript#5412, but I hope this PR could be one of the good samples and tests for the RFC as well.

@mununki
Copy link
Member Author

mununki commented Jun 6, 2022

spread props

module Foo = {         
  @obj
  type makeProps<'id, 'onClick, 'onBlur, 'children> = {
    key: option<string>,
    id: 'id,
    onClick: 'onClick,
    onBlur: 'onBlur,
    children: 'children,
  }
  let makeProps = (props: makeProps) => props
  let make =
    (
      @warning("-16") ~id,
      @warning("-16") ~onClick,
      @warning("-16") ~onBlur,
      @warning("-16") ~children,
    ) => {
      ReactDOMRe.createDOMElementVariadic("div", [{`Foo`->React.string}])
    }
  }

@obj
type makeProps = {
  key: option<string>,
}
let makeProps = (props: makeProps) => props

let make = () => {
  ReactDOMRe.createElement(
    ReasonReact.fragment,
    [
      ReactDOMRe.createDOMElementVariadic(
        "div",
        ~props={...foo, id: "1", onClick: _ => ()},
        [{`div`->React.string}],
      ),
      React.createElement(
        Foo.make,
        Foo.makeProps({...foo, id: "2", onClick: _ => (), children: {`Foo`->React.string}}),
      ),
    ],
  )
}

@cristianoc
Copy link
Contributor

@mattdamon108 how about adding the corresponding pr in the compiler repo, which uses this as syntax, and shows a few compiled examples with their generated code.

One of the questions is whether makeProps is still needed. Can one do without?

@cristianoc
Copy link
Contributor

Also it might be best to start with simple examples, rather than the new spread, which is a more advanced case.

Also, this should probably be called ppx version 4.

For the purpose of gradual upgrade the two need to at least co-exist.

In addition, one should see if that is enough for a v4 project using v3 dependencies.

@mununki
Copy link
Member Author

mununki commented Jun 6, 2022

how about adding the corresponding pr in the compiler repo, which uses this as syntax, and shows a few compiled examples with their generated code.
Also it might be best to start with simple examples, rather than the new spread, which is a more advanced case.

Agree. Do you want me to make PR in the compiler repo without the new spread first? It isn't hard to remove that part. You mean the example without spread case, I got it.

One of the questions is whether makeProps is still needed. Can one do without?

I had the same question, but I think it is still needed because it has a role to check the type of props in the record, unless we are designing the make function has the argument as record without makeProps. But I'm not sure of it. I think we better keep the make fn using the labelled arguments per each prop. What do you think?

@mununki
Copy link
Member Author

mununki commented Jun 6, 2022

And plus, react.createElement API interface looks like. https://reactjs.org/docs/react-api.html#createelement 🤷‍♂️

React.createElement(
  type,
  [props],
  [...children]
)

@cristianoc
Copy link
Contributor

cristianoc commented Jun 6, 2022

Yes, the idea is to illustrate the new mechanism with simpler examples first, then the prop spread comes later (in the description), as an advanced example. The implementation can cover all at once, does not matter.

Notice the previous work on this: #235
Haven't reviewed either one in detail. But it would be good to have an understanding of what has been tried and current thoughts in a little summary.

@mununki
Copy link
Member Author

mununki commented Jun 14, 2022

I missed adding test files, then I found that some tests failed after pushing the commits. 1c185ed I'll fix it.
Several thoughts came to mind while I'm implementing the applying @react.component to fundef. Replacing @react.component from Pstr_value to Pexp_fun is not enough to generate the output. e.g. type props is generated in place of Pstr_value, not Pexp_fun. So I have to generate type props and another structure_item to shadow the make function to capitalized name, then I add @react.component attr to Pexp_fun. It seems more abuse of ppx to me.

@mununki
Copy link
Member Author

mununki commented Jun 14, 2022

I have an idea to generate the proper output for forwardRef case without replacing @react.component. 1c185ed commit is just FYI to peek. I'm willing to reset the commits and implement it again. What do you think?

@cristianoc
Copy link
Contributor

It does not need to literally change the ast. It can be just a guide on how to structure the implementation.

@cristianoc
Copy link
Contributor

The spec indicates what is the result of the transformation, it does not impose a specific way to implement it.
Declarative specification.

@mununki
Copy link
Member Author

mununki commented Jun 14, 2022

Oh I misunderstood your intention. I thought the changing ast will be more cleaner desgin, but I found it is not. 🥲

@mununki
Copy link
Member Author

mununki commented Jun 14, 2022

I'll reset the latest 2 commits, then I'll fix the forwardRef example. I think I can make it better.

@cristianoc cristianoc changed the title Add parsing and jsx ppx support for spread props JSX V4 WIP Jun 14, 2022
@cristianoc
Copy link
Contributor

I have renamed this PR.
Also, perhaps this is a good time to start a new one, as scrolling through this page takes a while.

@mununki
Copy link
Member Author

mununki commented Jun 14, 2022

OK I'll make a new PR

@mununki
Copy link
Member Author

mununki commented Jun 15, 2022

Closed. Further discussion in #547

@mununki mununki closed this Jun 15, 2022
@mununki mununki deleted the spread-props branch June 15, 2022 07:34
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants