-
Notifications
You must be signed in to change notification settings - Fork 349
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 for React 19 new APIs #846
base: main
Are you sure you want to change the base?
Changes from all commits
4d9eeca
39189e0
b869bd3
e47a0ca
81942e4
b823e5c
c4a7fdd
fd7faa4
97dd3d3
e56f547
06b1da6
9031415
52c8144
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
(* Provides a way to easily construct a set of key/value pairs representing form fields and their values, which can then be easily sent using the XMLHttpRequest.send() method. It uses the same format a form would use if the encoding type were set to "multipart/form-data". *) | ||
|
||
module Iterator = struct | ||
type 'a iterator | ||
type 'a t = 'a iterator | ||
type 'a value = { done_ : bool option; [@mel.as "done"] value : 'a option } | ||
|
||
external next : 'a t -> 'a value = "next" [@@mel.send] | ||
external toArray : 'a t -> 'a array = "Array.from" | ||
external toArrayWithMapper : 'a t -> f:('a -> 'b) -> 'b array = "Array.from" | ||
end | ||
|
||
type t | ||
type file | ||
type blob | ||
type entryValue | ||
|
||
let classify : entryValue -> [> `String of string | `File of file ] = | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we could just use |
||
fun t -> | ||
if Js.typeof t = "string" then `String (Obj.magic t) | ||
else `File (Obj.magic t) | ||
|
||
external make : unit -> t = "FormData" [@@mel.new] | ||
external append : string -> string -> unit = "append" [@@mel.send.pipe: t] | ||
external delete : string -> unit = "delete" [@@mel.send.pipe: t] | ||
external get : string -> entryValue option = "get" [@@mel.send.pipe: t] | ||
external getAll : string -> entryValue array = "getAll" [@@mel.send.pipe: t] | ||
external set : string -> string -> unit = "set" [@@mel.send.pipe: t] | ||
external has : string -> bool = "has" [@@mel.send.pipe: t] | ||
external keys : t -> string Iterator.t = "keys" [@@mel.send] | ||
external values : t -> entryValue Iterator.t = "values" [@@mel.send] | ||
|
||
external appendObject : string -> < .. > Js.t -> ?filename:string -> unit | ||
= "append" | ||
[@@mel.send.pipe: t] | ||
|
||
external appendBlob : string -> blob -> ?filename:string -> unit = "append" | ||
[@@mel.send.pipe: t] | ||
|
||
external appendFile : string -> file -> ?filename:string -> unit = "append" | ||
[@@mel.send.pipe: t] | ||
|
||
external setObject : string -> < .. > Js.t -> ?filename:string -> unit = "set" | ||
[@@mel.send.pipe: t] | ||
|
||
external setBlob : string -> blob -> ?filename:string -> unit = "set" | ||
[@@mel.send.pipe: t] | ||
|
||
external setFile : string -> file -> ?filename:string -> unit = "set" | ||
[@@mel.send.pipe: t] | ||
|
||
external entries : t -> (string * entryValue) Iterator.t = "entries" | ||
[@@mel.send] |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -469,6 +469,7 @@ external displayName: component('props) => option(string) = "displayName"; | |||||
|
||||||
/* This is used as return values */ | ||||||
type callback('input, 'output) = 'input => 'output; | ||||||
type callbackAsync('input, 'output) = 'input => Js.Promise.t('output); | ||||||
|
||||||
/* | ||||||
* Yeah, we know this api isn't great. tl;dr: useReducer instead. | ||||||
|
@@ -886,6 +887,31 @@ external useDebugValue: ('value, ~format: 'value => string=?, unit) => unit = | |||||
|
||||||
module Experimental = { | ||||||
/* This module is used to bind to APIs for future versions of React. There is no guarantee of backwards compatibility or stability. */ | ||||||
[@mel.module "react"] external usePromise: Js.Promise.t('a) => 'a = "use"; | ||||||
[@mel.module "react"] external useContext: Context.t('a) => 'a = "use"; | ||||||
[@mel.module "react"] external use: 'a => 'b = "use"; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should we default the name There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the use case for |
||||||
|
||||||
[@mel.module "react"] external use: Js.Promise.t('a) => 'a = "use"; | ||||||
[@mel.module "react"] | ||||||
external useTransitionAsync: | ||||||
unit => (bool, callbackAsync(callbackAsync(unit, unit), unit)) = | ||||||
"useTransition"; | ||||||
|
||||||
/* https://es.react.dev/reference/react/useOptimistic */ | ||||||
[@mel.module "react"] | ||||||
external useOptimistic: | ||||||
('state, ('state, 'optimisticValue) => 'state) => | ||||||
('state, 'optimisticValue => unit) = | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
if it'll call the
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nevermind, definitely typed says it returns |
||||||
"useOptimistic"; | ||||||
|
||||||
type formStatus = { | ||||||
pending: bool, | ||||||
data: FormData.t, | ||||||
[@mel.as "method"] | ||||||
method_: [ | `get | `post], | ||||||
action: Js.Nullable.t(unit => unit), | ||||||
}; | ||||||
|
||||||
/* https://react.dev/reference/react-dom/hooks/useFormStatus#use-form-status */ | ||||||
[@mel.module "react"] | ||||||
external useFormStatus: unit => formStatus = "useFormStatus"; | ||||||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -648,7 +648,7 @@ type domProps = { | |
[@mel.optional] | ||
acceptCharset: option(string), | ||
[@mel.optional] | ||
action: option(string), /* uri */ | ||
action: option(FormData.t => Js.Promise.t(unit)), /* Since action is taken by "form" as string and React 19 accepts a callback we keep a 'action_' field to avoid a breaking change. */ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what do you mean by "keep a 'action_'" field? isn't the field still named There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe this is now a breaking change because we don't accept a string for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I couldn't find a way to keep both (you can't have 2 mel.as with the same name), so it's either we don't support action like this (and users could cloneElement it) or we break with action. Not fan of either There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. as I understand There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we thought we'd be able to support this by e.g. adding it'd be kinda nice if ppxlib allowed to make the original type abstract, which would remove the unsoundness. |
||
[@mel.optional] | ||
allowFullScreen: option(bool), | ||
[@mel.optional] | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
open Jest; | ||
open Jest.Expect; | ||
open ReactDOMTestUtils; | ||
open Belt; | ||
|
||
/* https://react.dev/blog/2022/03/08/react-18-upgrade-guide#configuring-your-testing-environment */ | ||
[%%mel.raw "globalThis.IS_REACT_ACT_ENVIRONMENT = true"]; | ||
|
||
type message = { | ||
text: string, | ||
sending: bool, | ||
key: int, | ||
}; | ||
|
||
[@mel.send.pipe: Dom.element] external reset: unit = "reset"; | ||
|
||
let (let.await) = (p, f) => Js.Promise.then_(f, p); | ||
|
||
module Thread = { | ||
[@react.component] | ||
let make = (~messages, ~sendMessage) => { | ||
let formRef = React.useRef(Js.Nullable.null); | ||
let (optimisticMessages, addOptimisticMessage) = | ||
React.Experimental.useOptimistic(messages, (state, newMessage) => | ||
[ | ||
{text: newMessage, sending: true, key: List.length(state) + 1}, | ||
...state, | ||
] | ||
); | ||
|
||
let formAction = formData => { | ||
let formMessage = FormData.get("message", formData); | ||
switch (formMessage) { | ||
| Some(entry) => | ||
switch (FormData.classify(entry)) { | ||
| `String(text) => | ||
addOptimisticMessage(text); | ||
switch (Js.Nullable.toOption(formRef.current)) { | ||
| Some(form) => reset(form) | ||
| None => () | ||
}; | ||
let.await _ = sendMessage(formData); | ||
Js.Promise.resolve(); | ||
| _ => Js.Promise.resolve() | ||
} | ||
| None => Js.Promise.resolve() | ||
}; | ||
}; | ||
<> | ||
{{ | ||
optimisticMessages->Belt.List.map(message => | ||
<div key={Int.toString(message.key)}> | ||
{React.string(message.text)} | ||
{message.sending | ||
? React.null | ||
: <small> {React.string("(Enviando...)")} </small>} | ||
</div> | ||
); | ||
} | ||
->Belt.List.toArray | ||
->React.array} | ||
<form action=formAction ref={ReactDOM.Ref.domRef(formRef)}> | ||
<input type_="text" name="message" placeholder="Hola!" /> | ||
<button type_="submit"> {React.string("Enviar")} </button> | ||
</form> | ||
</>; | ||
}; | ||
}; | ||
|
||
module App = { | ||
let deliverMessage = message => { | ||
Js.Promise.resolve(message); | ||
}; | ||
|
||
[@react.component] | ||
let make = () => { | ||
let (messages, setMessages) = | ||
React.useState(() => [{text: "¡Hola!", sending: false, key: 1}]); | ||
|
||
let sendMessage = formData => { | ||
let formMessage = FormData.get("message", formData); | ||
switch (formMessage) { | ||
| Some(message) => | ||
let.await entry = deliverMessage(message); | ||
switch (FormData.classify(entry)) { | ||
| `String(text) => | ||
let _ = | ||
setMessages(messages => | ||
[{text, sending: true, key: 1}, ...messages] | ||
); | ||
Js.Promise.resolve(); | ||
| _ => Js.Promise.resolve() | ||
}; | ||
| None => Js.Promise.resolve() | ||
}; | ||
}; | ||
|
||
<Thread messages sendMessage />; | ||
}; | ||
}; | ||
|
||
describe("Form with useOptimistic", () => { | ||
let container = ref(None); | ||
|
||
beforeEach(prepareContainer(container)); | ||
afterEach(cleanupContainer(container)); | ||
|
||
test("should render the form", () => { | ||
let container = getContainer(container); | ||
let root = ReactDOM.Client.createRoot(container); | ||
|
||
act(() => ReactDOM.Client.render(root, <App />)); | ||
|
||
expect( | ||
container | ||
->DOM.findBySelectorAndTextContent("button", "0") | ||
->Option.isSome, | ||
) | ||
->toBe(true); | ||
|
||
let button = container->DOM.findBySelector("button"); | ||
|
||
act(() => { | ||
switch (button) { | ||
| Some(button) => Simulate.click(button) | ||
| None => () | ||
} | ||
}); | ||
|
||
expect( | ||
container | ||
->DOM.findBySelectorAndTextContent("button", "0") | ||
->Option.isSome, | ||
) | ||
->toBe(false); | ||
|
||
expect( | ||
container | ||
->DOM.findBySelectorAndTextContent("button", "1") | ||
->Option.isSome, | ||
) | ||
->toBe(true); | ||
}); | ||
}); |
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 think we added iterator stuff in melange, can you reuse that?
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.
otherwise we should add what we need
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.
Totally, added it here to make it pass (that's why it's a draft pr) until I can use melange.dom for this