-
Notifications
You must be signed in to change notification settings - Fork 19
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
Inline props as object #66
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@davesnx This is super cool. 😍
Re: the refs, there is some context about these decisions in reasonml/reason-react#421 (comment). Essentially forwardRef
is unsafe in ReasonReact so I tried to fix that by introducing a new type for refs for dom elements (Dom.Ref.t
).
After your changes, I just realized that we could keep the ref as non-optional, and actually it makes sense to do so, because in some cases (like when using useRef
) we get a {current: elementOrNull}
JS object, so converting back and forth was not trivial.
I updated the tests as well, and checked that the ref examples were working correctly.
Another thing, I checked the bundle size of the example (built with profile=prod
) and we went from 200KB to 117KB for the file App.bc.js
. 🎉 This is all application code (not react or prism libs), but I think it's a huge improvement.
Giving the same flexibility as the underneath layer could enable features that aren't possible on reason-react or rescript/react, for example adding missing attributes, allowing destructuring props in bindings, allowing "data"-attributes or type-safety on certain attributes.
This is SO exciting.
Thanks for working on this! 🤝
lib/dom.mli
Outdated
|
||
module DangerouslySetInnerHTML : sig | ||
(* Need to check the representation in JS of this type, can we have an Obj with a field type-safe in jsoo? *) | ||
type t = { __html: string } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can leave t
abstract and use js.builder
, e.g.
val make: __html:string -> t
[@@js.builder]
@@ -0,0 +1,584 @@ | |||
type attributeType = String | Int | Float | Bool | Style | Ref | InnerHtml |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you think it would make sense to move this whole module to dom.mli
? It seems all of it is part of Dom
🤔
dom.mli
is processed by gen_js_api, but it should be easy to move the html.ml
contents to it. The way the gen_js_api works allows to stop the preprocessor from processing any code with [@@@js.stop]
and start it with [@@@js.start]
. It also allows to "send" some code to the resulting ml
file with [@@@js.implem]
. With these three primitives, we can do everything we need, e.g. if we call the submodule Prop
, so we would end up with Dom.Prop
:
(* stop gen_js_api preprocessor so that types below are not processed *)
[@@@js.stop]
module Prop: sig
(* can hide here the types or values that don't need to be public. *)
type attrType = String | Int | Float | Bool | Style | Ref | InnerHtml
type eventType = ...
...
val propTypeList: prop list
end
(* Start preprocessing again *)
[@@@js.start]
(* This should go in resulting `.ml` file created by gen_js_api *)
[@@@js.implem
module Prop = struct
type attrType = String | Int | Float | Bool | Style | Ref | InnerHtml
type eventType = ...
...
let propTypeList = ...
end ]
(* Now the rest of the file can continue being processed *)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I moved out of dom.mli since those get exposed on purpose to the user, while the html.ml/i shouldn't be. If makes more sense to keep it inside Dom, we can make a module called... Private__ or somekind to express it better. WDYT?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah right, I didn't realize the html.ml
file was in ppx
folder. 👍
Maybe just inline this inside this module in ppx.ml
then? So it's clear that is something internal to the ppx.
About the modlue naming, I would use module Dom
for consistency with the other one, and with React.js docs. https://reactjs.org/docs/dom-elements.html, as HTML has different attributes than the props passed to these elements.
But these are all nits :)
ppx/ppx.ml
Outdated
(label, mapper.expr mapper expression)) ) | ||
(* Filtering out the props that don't have a label, not sure how's possible *) | ||
let propsWithName = | ||
List.filter (fun (name, _) -> getLabel name != "") nonEmptyProps |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this needed? Swallowing silently is suspicious 😬
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably the last prop as unit? I'm not sure actually. Will look into it and not merge this until it's "discovered/fixed".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Made it more explicit, was the unit
This is a quick analysis of bundle size impact of this PR using webpack-bundle-analyzer. For each case, I ran:
This branch (84e7869): Master branch (1ff2683): |
ppx/ppx.ml
Outdated
let makePropField (arg_label, value) = | ||
let valueLoc = value.pexp_loc in | ||
let argLabelLoc = unionLocation loc value.pexp_loc in |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I append both locations to ensure the argLabel is part of the error.
Before
File "example/src/Events.re", line 49, characters 6-13:
49 | <button type_="submit" xxxx=0> {"submit dis" |> React.string} </button>
^^^^^^^
Error: prop 'xxxx' isn't a valid React prop
Passing the expression loc
49 | <button type_="submit" xxxx=0> {"submit dis" |> React.string} </button>
^
Error: prop 'xxxx' isn't a valid React prop
After (with unioning the locations)
File "example/src/Events.re", lines 1-49, characters 6-35:
-1 | <button type_="submit" xxxx=0> {"submit dis" |> React.string} </button>
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Error: prop 'xxxx' isn't a valid React prop
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hope it's clear. We could get fancier: with the length of the argLabel and minus the expr loc minus 1 (the =
) maybe can point to the argLabel. Let me know if you think is worth it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is great! Another idea (for the future ™️ ) could be to print after the error a link to a dedicated page in https://ml-in-barcelona.github.io/jsoo-react/ that explains native elements, and lists the whole list of tags that are allowed (the list could be shared code with the ppx ✨ ).
One nice thing is that because all these checks happen at compile time we don't need to care about the errors or urls making bundle size larger.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I append both locations to ensure the argLabel is part of the error.
Would it be easier to pick the location from the parent ast node? So we avoid having to manipulate locations this way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's way smarter!
Pushed a commit with this change, and appended a small refactor afterwards.
Bringing this in. Thanks again @davesnx ! |
Fixes #43.
This PR changes the interface for
React.Dom.domProps
. Before these changes,domProps
was a function with ~400 optional labelled arguments, after these changes we treat it as an object.There are a few reasons for this change, but the most important is bundle size. jsoo compiles the optional arguments into zeros generating per each
domProps
call near 400 zeros, jsoo treated labeled arguments as positions into arrays. There are a few issues opened about this, I only found this one though: ocsigen/js_of_ocaml#801Another of the reasons is about correctness,
props
in React are an object and after those changes, we treat it as an Object as well. Giving the same flexibility as the underneath layer could enable features that aren't possible on reason-react or rescript/react, for example adding missing attributes, allowing destructuring props in bindings, allowing "data"-attributes or type-safety on certain attributes.Todos
ref
andkey
suppressContentEditableWarning
dangerouslySetInnerHTML
baseSuite:refs:forwardRef
(maybe it's related with my doubt? probably no)dangerouslySetInnerHTML
type/format.Doubts
I have a doubt about the
ref
prop. Right now the ref is typed withReact.Dom.Ref.t option
butReact.Dom.domRef
doesn't seem to return an optional. But I believe the bindings are not the same as reason-react since they use an anonymous type from the useRef.Future
As mentioned before, this change allows us a few niceties on the dom attributes API, from the top of my head:
?preload is now string (* "none", "metadata" or "auto" (and "" as a synonym for "auto") *)
into'None | 'Metadata | 'Auto)
max (* should be int or Js.Date.t *)
into Js.DatePS: I have the editor configured to format using ocamlformat, I believe we should run it across all files (I separate the formatting into one commit, in order to not affect the review)