diff --git a/.envrc b/.envrc new file mode 100644 index 000000000..3add3fcca --- /dev/null +++ b/.envrc @@ -0,0 +1,3 @@ +use flake +layout node +eval "$shellHook" diff --git a/.gitignore b/.gitignore index eabf4fd55..2336af8ef 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ metals.sbt node_modules/ package-lock.json target +.direnv/ diff --git a/README.md b/README.md index 94ac1ee97..030292eb1 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ Includes a router, testing utils, performance utils, more. - [Modules](doc/MODULES.md) - [VDOM](doc/VDOM.md) - [Hooks](doc/HOOKS.md) + - [Hooks via builder](doc/HOOKS_BUILDER.md) - [Refs](doc/REFS.md) - [IDE support](doc/IDE.md) - [The `Callback` class](doc/CALLBACK.md) @@ -71,7 +72,7 @@ Includes a router, testing utils, performance utils, more. * [scastie](https://github.com/scalacenter/scastie) - An interactive playground for Scala [https://scastie.scala-lang.org](https://scastie.scala-lang.org) ##### Requirements: -* React ≥ 17 +* React ≥ 18 * Scala ≥ 2.13 * Scala.JS ≥ 1.10 diff --git a/bin/ci b/bin/ci index ca5b4f98f..5ea709323 100755 --- a/bin/ci +++ b/bin/ci @@ -33,7 +33,11 @@ dryrun= # dryrun=-n # See how much memory is available -free -h +if [ "$(uname)" == "Darwin" ]; then # https://apple.stackexchange.com/a/94258 + vm_stat | perl -ne '/page size of (\d+)/ and $size=$1; /Pages\s+([^:]+)[^\d]+(\d+)/ and printf("%-16s % 16.2f Mi\n", "$1:", $2 * $size / 1048576);' +elif [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then + free -h +fi echo # Test upstream diff --git a/bin/update_react_version b/bin/update_react_version index 42c330a2f..0e0322045 100755 --- a/bin/update_react_version +++ b/bin/update_react_version @@ -1,10 +1,14 @@ #!/bin/bash -cd "$(dirname "$0")/../library" || exit 1 +cd "$(dirname "$0")/.." || exit 1 [ $# -ne 1 ] && echo "Usage: $0 " && exit 1 ver="$1" perl -pi -e 's/(? Callback { - document.title = s"You clicked ${count.value} times" - }) - - .useState("banana") - - .render((props, count, fruit) => + val Component = ScalaFnComponent[Unit]( props => + for { + count <- useState(0) + _ <- useEffect(Callback { + document.title = s"You clicked ${count.value} times" + }) + fruit <- useState("banana") + } yield <.div( <.p(s"You clicked ${count.value} times"), <.button( @@ -95,104 +89,38 @@ object Example { ), <.p(s"Your favourite fruit is a ${fruit.value}!") ) - ) -} -``` - -### Method 2 - -```scala -import japgolly.scalajs.react._ -import japgolly.scalajs.react.vdom.html_<^._ -import org.scalajs.dom.document - -object Example { - val Component = ScalaFnComponent.withHooks[Unit] - - .useState(0) - - .useEffectBy($ => Callback { - document.title = s"You clicked ${$.hook1.value} times" - }) - - .useState("banana") - - .render($ => - <.div( - <.p(s"You clicked ${$.hook1.value} times"), - <.button( - ^.onClick --> $.hook1.modState(_ + 1), - "Click me" - ), - <.p(s"Your favourite fruit is a ${$.hook2.value}!") - ) - ) + ) } ``` -### Diff between both methods - -```diff - import japgolly.scalajs.react._ - import japgolly.scalajs.react.vdom.html_<^._ - import org.scalajs.dom.document - - object Example { - val Component = ScalaFnComponent.withHooks[Unit] - - .useState(0) - -- .useEffectBy((props, count) => Callback { -- document.title = s"You clicked ${count.value} times" -+ .useEffectBy($ => Callback { -+ document.title = s"You clicked ${$.hook1.value} times" - }) - - .useState("banana") - -- .render((props, count, fruit) => -+ .render($ => - <.div( -- <.p(s"You clicked ${count.value} times"), -+ <.p(s"You clicked ${$.hook1.value} times"), - <.button( -- ^.onClick --> count.modState(_ + 1), -+ ^.onClick --> $.hook1.modState(_ + 1), - "Click me" - ), -- <.p(s"Your favourite fruit is a ${fruit.value}!") -+ <.p(s"Your favourite fruit is a ${$.hook2.value}!") - ) - ) - } -``` - # React hooks in scalajs-react | JavaScript | scalajs-react | | --------- | -------- | -| `useCallback(c)` | `.useCallback(c)` | -| `useCallback(c, [deps])` | `.useCallbackWithDeps((deps))(_ => c)` | -| `useCallback(f([deps]), [deps])` | `.useCallbackWithDeps((deps))(f)` | -| `useContext(c)` | `.useContext(c)` | -| `useDebugValue(desc)` | `.useDebugValue(desc)` | -| `useDebugValue(a, f)` | `.useDebugValue(f(a))` | -| `useEffect(e)` | `.useEffect(e)` | -| `useEffect(e, [])` | `.useEffectOnMount(e)` | -| `useEffect(e, [deps])` | `.useEffectWithDeps((deps))(_ => e)` | -| `useEffect(f([deps]), [deps])` | `.useEffectWithDeps((deps))(f)` | -| `useLayoutEffect(e)` | `.useLayoutEffect(e)` | -| `useLayoutEffect(e, [])` | `.useLayoutEffectOnMount(e)` | -| `useLayoutEffect(e, [deps])` | `.useLayoutEffectWithDeps((deps))(_ => e)` | -| `useLayoutEffect(f([deps]), [deps])` | `.useLayoutEffectWithDeps((deps))(f)` | -| `useMemo(() => a, [deps])` | `.useMemo((deps))(_ => a) | -| `useMemo(() => f([deps]), [deps])` | `.useMemo((deps))(f) | -| `useReducer(f, s)` | `.useReducer(f, s)` | -| `useReducer(f, a, i)` | `.useReducer(f, i(a))`
*(Note: `i(a)` is actually `(=> i(a))` and isn't evaluated immediately)* | -| `useRef()` | `.useRefToAnyVdom`
`.useRefToVdom[DomType]`
`.useRefToScalaComponent(component)`
`.useRefToScalaComponent[P, S, B]`
`.useRefToJsComponent(component)`
`.useRefToJsComponent[P, S]`
`.useRefToJsComponentWithMountedFacade[P, S, F]` | -| `useRef(initialValue)` | `.useRef(initialValue)` | -| `useState(initialState)`
`useState(() => initialState)` | `.useState(initialState)` | -| Custom hook
`useBlah(i)` | `.custom(useBlah(i))` +| `useCallback(c)` | `useCallback(c)` | +| `useCallback(c, [deps])` | `useCallbackWithDeps((deps))(_ => c)` | +| `useCallback(f([deps]), [deps])` | `useCallbackWithDeps((deps))(f)` | +| `useContext(c)` | `useContext(c)` | +| `useDebugValue(desc)` | `useDebugValue(desc)` | +| `useDebugValue(a, f)` | `useDebugValue(f(a))` | +| `useEffect(e)` | `useEffect(e)` | +| `useEffect(e, [])` | `useEffectOnMount(e)` | +| `useEffect(e, [deps])` | `useEffectWithDeps((deps))(_ => e)` | +| `useEffect(f([deps]), [deps])` | `useEffectWithDeps((deps))(f)` | +| `useLayoutEffect(e)` | `useLayoutEffect(e)` | +| `useLayoutEffect(e, [])` | `useLayoutEffectOnMount(e)` | +| `useLayoutEffect(e, [deps])` | `useLayoutEffectWithDeps((deps))(_ => e)` | +| `useLayoutEffect(f([deps]), [deps])` | `useLayoutEffectWithDeps((deps))(f)` | +| `useMemo(() => a, [deps])` | `useMemo((deps))(_ => a)` | +| `useMemo(() => f([deps]), [deps])` | `useMemo((deps))(f)` | +| `useReducer(f, s)` | `useReducer(f, s)` | +| `useReducer(f, a, i)` | `useReducer(f, i(a))`
*(Note: `i(a)` is actually `(=> i(a))` and isn't evaluated immediately)* | +| `useRef()` | `.useRefToAnyVdom`
`useRefToVdom[DomType]`
`useRefToScalaComponent(component)`
`useRefToScalaComponent[P, S, B]`
`useRefToJsComponent(component)`
`useRefToJsComponent[P, S]`
`useRefToJsComponentWithMountedFacade[P, S, F]` | +| `useRef(initialValue)` | `useRef(initialValue)` | +| `useState(initialState)`
`useState(() => initialState)` | `useState(initialState)` | +| `useId()` | `useId` | +| `useTransition` | `useTransition` | +| Custom hook
`useBlah(i)` | `useBlah(i)`
(`def useBlah(i: I): HookResult[O]`) | Note: The reason that `[deps]` on the JS side becomes `(deps)` on the Scala side, is that in JS you'd use an array but in Scala you'd use a tuple. @@ -204,113 +132,53 @@ So `[dep1, dep2]` becomes `(dep1, dep2)`; and `[dep1]` becomes just `dep1` which | Hook | Description | | ---- | ----------- | -| `.localLazyVal(a)` | Creates a new `lazy val` on each render. | -| `.localVal(a)` | Creates a new `val` on each render. | -| `.localVar(a)` | Creates a new `var` on each render. | -| `.useForceUpdate` | Provides a `Reusable[Callback]` then when invoked, forces a re-render of the component. | -| `.useStateSnapshot(initialState)`
*(Requires import japgolly.scalajs.react.extra._)* | Same as `.useState` except you get a `StateSnapshot` (which accepts callbacks on set updates). | -| `.useStateSnapshotWithReuse(initialState)`
*(Requires import japgolly.scalajs.react.extra._)* | Same as `.useState` except you get a `StateSnapshot` (which accepts callbacks on set updates) with state `Reusability`. | -| `.useStateWithReuse(initialState)` | Conceptually `useState` + `shouldComponentUpdate`. Same as `useState` except that updates are dropped according to `Reusability`. | +| `useForceUpdate` | Provides a `Reusable[Callback]` then when invoked, forces a re-render of the component. | +| `useStateSnapshot(initialState)`
*(Requires import japgolly.scalajs.react.extra._)* | Same as `.useState` except you get a `StateSnapshot` (which accepts callbacks on set updates). | +| `useStateSnapshotWithReuse(initialState)`
*(Requires import japgolly.scalajs.react.extra._)* | Same as `.useState` except you get a `StateSnapshot` (which accepts callbacks on set updates) with state `Reusability`. | +| `useStateWithReuse(initialState)` | Conceptually `useState` + `shouldComponentUpdate`. Same as `useState` except that updates are dropped according to `Reusability`. | # `shouldComponentUpdate` -Instead of calling `render`, you can call one of the following to get `shouldComponentUpdate` -behaviour just like classes have. - -* `renderWithReuse(f: Ctx => VdomNode)(implicit r: Reusability[Ctx])` -* `renderWithReuseBy[A: Reusability](reusableInputs: Ctx => A)(f: A => VdomNode)` -* `renderReusable(f: Ctx => Reusable[VdomNode])` - -# Hooks with dependencies - -Sometimes hooks are initialised using props and/or the output of other hooks, -(which scalajs-react refers to as "context"). -Each hook that has a return type that's not `Unit`, -becomes available in subsequent contexts. - -In order to get access to this context, append a `By` suffix to the hook method -of your choice, and change the arguments to functions that take the context. -There are two ways to do this. - -### 1. useXxxxxBy((props, hook1, hook2, ...) => arg) - -```scala -val comp = ScalaFnComponent.withHooks[Int] - .useStateBy(props => props - 1) // initialise state according to props - .useEffectBy((props, hook1) => Callback.log(s"Props: $props, State: ${hook1.value}")) -``` - -### 2. useXxxxxBy(ctxObj => arg) - -```scala -val comp = ScalaFnComponent.withHooks[Int] - .useStateBy(props => props - 1) // initialise state according to props - .useEffectBy(c => Callback.log(s"Props: ${c.props}, State: ${c.hook1.value}")) -``` - -### Hooks that return `Unit` don't appear in context - -```scala -val comp = ScalaFnComponent.withHooks[Int] - - // The result of this hook becomes "hook1" - .useStateBy(props => props - 1) - - // The result of useEffect is Unit and doesn't appear in context - .useEffectBy(c => Callback.log(s"Props: ${c.props}, State: ${c.hook1.value}")) - - // The result of this hook becomes "hook2" - .useState(123) - - .render((props, hook1, hook2) => - <.div( - <.div("State 1 = ", hook1.value), - <.div("State 2 = ", hook2.value), - ) - ) -``` - -# Hooks and PropsChildren - -In order to get access to `PropsChildren`, call `.withPropsChildren` as the first step in your DSL. -It will then become available... - -1) as argument #2 after `props` in multi-arg fns (eg. `.render((props, propsChildren, hook1, hook2, ...) => `) -2) as `.propsChildren` from context objects (eg. `.render($ => $.propsChildren)`) - -Example: +In order to avoid a rerender in the case where the render dependencies are reusable, +you can render in a new component wrapped in `React.memo`, just as you would in JS. +For example, the above component can be rewritten as: ```scala import japgolly.scalajs.react._ import japgolly.scalajs.react.vdom.html_<^._ +import org.scalajs.dom.document object Example { - final case class Props(name: String) - - val Component = ScalaFnComponent.withHooks[Props] - .withPropsChildren - .useState(0) - .render((props, propsChildren, counter) => + private val ReusableRender = React.memo( + ScalaFnComponent[(UseState[Int], UseState[String])]{ case (count, fruit) => <.div( - <.p(s"Hello ${props.name}."), - <.p(s"You clicked ${counter.value} times."), - <.button("Click me", ^.onClick --> counter.modState(_ + 1)), - <.div(propsChildren) + <.p(s"You clicked ${count.value} times"), + <.button( + ^.onClick --> count.modState(_ + 1), + "Click me" + ), + <.p(s"Your favourite fruit is a ${fruit.value}!") ) - ) + } + ) + + val Component = ScalaFnComponent[Unit]( props => + for { + count <- useState(0) + _ <- useEffect(Callback { + document.title = s"You clicked ${count.value} times" + }) + fruit <- useState("banana") + } yield ReusableRender(count, fruit) + ) } ``` # Custom hooks -A custom hook has the type `CustomHook[I, O]` where -`I` is the input type (or `Unit` if your custom hook doesn't take an input), -and `O` is the output type (or `Unit` if your custom hook doesn't return an output), +A custom hook is just a function that returns a `HookResult[O]`, where `O` is the output type (or `Unit` if your custom hook doesn't return an output). -To create a custom hook, the API is nearly identical to building a component with hooks. - -1. Start with `CustomHook[I]` instead of `ScalaFnComponent.withHooks[P]` -2. Complete your hook with `.buildReturning(ctx => O)`, or just `.build` if you don't need to return a value. +To create a custom hook, the API is nearly identical to building a component with hooks, only that you are free to return any value instead of a `VdomNode`. Example: @@ -319,12 +187,13 @@ import japgolly.scalajs.react._ import org.scalajs.dom.document object ExampleHook { - val useTitleCounter = CustomHook[Unit] - .useState(0) - .useEffectBy((_, count) => Callback { - document.title = s"You clicked ${count.value} times" - }) - .buildReturning(_.hook1) + val useTitleCounter: HookResult[UseState[Int]] = + for { + count <- useState(0) + _ <- useEffect(Callback { + document.title = s"You clicked ${count.value} times" + }) + } yield count } ``` @@ -335,322 +204,50 @@ import japgolly.scalajs.react._ import japgolly.scalajs.react.vdom.html_<^._ object Example { - val Component = ScalaFnComponent.withHooks[Unit] - .custom(ExampleHook.useTitleCounter) // <--- usage - .render((_, count) => + val Component = ScalaFnComponent[Unit]( _ => + for { + count <- useTitleCounter // <--- usage + } yield <.div( <.p(s"You clicked ${count.value} times"), <.button( ^.onClick --> count.modState(_ + 1), - "Click me"))) -} -``` - -In order to provide the hook directly via `.custom` the input type of the hook must be one of the following... -* `Unit` -* same as the `Props` type -* `PropsChildren` - -If the custom hook has any other kind of type, simply provide it to the hook directly. -Example: - -```scala - val someCustomHook: CustomHook[Int, Unit] = ??? - - final case class Props(someInt: Int) - - val Component = ScalaFnComponent.withHooks[Props] - .custom(someCustomHook(123)) // provide a constant Int arg - .customBy($ => someCustomHook($.props.someInt)) // or use a dynamic value -``` - -# Custom hook composition - -CustomHooks can be composed by calling `++`. - -The input/output type of the result will be the "natural" result according to these rules: -* `A ++ Unit` or `Unit ++ A` becomes `A` -* `A ++ A` becomes `A` if it's in the input position -* `A ++ A` becomes `A` if it's in the output position and `A <: scala.Singleton` -* otherwise `A ++ B` becomes `(A, B)` - -Examples: - -```scala -object Example1 { - val hook1: CustomHook[Int, Unit] = ??? - val hook2: CustomHook[Int, Unit] = ??? - val hooks: CustomHook[Int, Unit] = hook1 ++ hook2 -} - -object Example2 { - val hook1: CustomHook[Unit, Int] = ??? - val hook2: CustomHook[Unit, Int] = ??? - val hooks: CustomHook[Unit, (Int, Int)] = hook1 ++ hook2 -} - -object Example3 { - val hook1: CustomHook[Long, Boolean] = ??? - val hook2: CustomHook[String, Int] = ??? - val hooks: CustomHook[(Long, String), (Boolean, Int)] = hook1 ++ hook2 -} - -object Example4 { - val hook1: CustomHook[Unit, Boolean] = ??? - val hook2: CustomHook[String, Unit] = ??? - val hooks: CustomHook[String, Boolean] = hook1 ++ hook2 + "Click me" + ) + ) + ) } ``` # Using third-party JavaScript hooks -Using a third-party JavaScript hook is as simple as wrapping it in `CustomHook.unchecked`. +Using a third-party JavaScript hook is as simple as wrapping it in `HookResult.fromFunction`. ```scala -// Declare your JS facade as normal. Type should be a subtype of js.Function. - -// I is the type of the hook's inputs (use a tuple or case class for multiple args) -// O is the type of the hook's output (or Unit if none) -val jsHook = CustomHook.unchecked[I, O](i => JsHookFacade(i)) +// Declare your JS facade as normal as `useJsHookFacade`. Type should be a subtype of js.Function. +val useJsHook = HookResult.fromFunction(useJsHookFacade) ``` -Then to use it, either... - -1) simply call `.custom(jsHook)` from your component -2) or create an API extension as shown below - -# API extensions - -You can also provide your own implicit extensions to the hook API. - -Unfortunately it involves a bit of boilerplate. Copy and customise one of the following templates: - -### Template 1: Custom hooks that take input and return output - +Then you can use just like any other hook: ```scala -import japgolly.scalajs.react._ - -object MyCustomHook { - - // TODO: Replace - val hook = CustomHook[String] - .useEffectOnMountBy(name => Callback.log(s"HELLO $name")) - .buildReturning(name => name) - - object HooksApiExt { - sealed class Primary[Ctx, Step <: HooksApi.AbstractStep](api: HooksApi.Primary[Ctx, Step]) { - - // TODO: Change hook name, input args/type(s), and output type - final def useMyCustomHook(name: String)(implicit step: Step): step.Next[String] = - // TODO: Change hook name - useMyCustomHookBy(_ => name) - - // TODO: Change hook name, input args/type(s), and output type - final def useMyCustomHookBy(name: Ctx => String)(implicit step: Step): step.Next[String] = - api.customBy(ctx => hook(name(ctx))) - } - - final class Secondary[Ctx, CtxFn[_], Step <: HooksApi.SubsequentStep[Ctx, CtxFn]](api: HooksApi.Secondary[Ctx, CtxFn, Step]) extends Primary[Ctx, Step](api) { - - // TODO: Change hook name, input args/type(s), and output type - def useMyCustomHookBy(name: CtxFn[String])(implicit step: Step): step.Next[String] = - // TODO: Change hook name, squash each parameter - // useMyCustomHookBy(step.squash(arg1)(_), step.squash(arg2)(_), ...) - useMyCustomHookBy(step.squash(name)(_)) - } - } - - trait HooksApiExt { - import HooksApiExt._ - - // TODO: Change hook name so that it won't conflict with other custom hooks - implicit def hooksExtMyCustomHook1[Ctx, Step <: HooksApi.AbstractStep](api: HooksApi.Primary[Ctx, Step]): Primary[Ctx, Step] = - new Primary(api) - - // TODO: Change hook name so that it won't conflict with other custom hooks - implicit def hooksExtMyCustomHook2[Ctx, CtxFn[_], Step <: HooksApi.SubsequentStep[Ctx, CtxFn]](api: HooksApi.Secondary[Ctx, CtxFn, Step]): Secondary[Ctx, CtxFn, Step] = - new Secondary(api) - } - - object Implicits extends HooksApiExt -} + for { + ... + output <- useJsHook(input1, input2, ...) + ... + } yield ... ``` -### Template 2: Custom hooks that take input and don't return output - -```scala -import japgolly.scalajs.react._ - -object MyCustomHook { - - // TODO: Replace - val hook = CustomHook[String] - .useEffectOnMountBy(name => Callback.log(s"HELLO $name")) - .build +# Interop with builder-style - object HooksApiExt { - sealed class Primary[Ctx, Step <: HooksApi.AbstractStep](api: HooksApi.Primary[Ctx, Step]) { - - // TODO: Change hook name, input args/type(s), and output type - final def useMyCustomHook(name: String)(implicit step: Step): step.Self = - // TODO: Change hook name - useMyCustomHookBy(_ => name) - - // TODO: Change hook name, input args/type(s), and output type - final def useMyCustomHookBy(name: Ctx => String)(implicit step: Step): step.Self = - api.customBy(ctx => hook(name(ctx))) - } - - final class Secondary[Ctx, CtxFn[_], Step <: HooksApi.SubsequentStep[Ctx, CtxFn]](api: HooksApi.Secondary[Ctx, CtxFn, Step]) extends Primary[Ctx, Step](api) { - - // TODO: Change hook name, input args/type(s), and output type - def useMyCustomHookBy(name: CtxFn[String])(implicit step: Step): step.Self = - // TODO: Change hook name, squash each parameter - // useMyCustomHookBy(step.squash(arg1)(_), step.squash(arg2)(_), ...) - useMyCustomHookBy(step.squash(name)(_)) - } - } - - trait HooksApiExt { - import HooksApiExt._ - - // TODO: Change hook name so that it won't conflict with other custom hooks - implicit def hooksExtMyCustomHook1[Ctx, Step <: HooksApi.AbstractStep](api: HooksApi.Primary[Ctx, Step]): Primary[Ctx, Step] = - new Primary(api) - - // TODO: Change hook name so that it won't conflict with other custom hooks - implicit def hooksExtMyCustomHook2[Ctx, CtxFn[_], Step <: HooksApi.SubsequentStep[Ctx, CtxFn]](api: HooksApi.Secondary[Ctx, CtxFn, Step]): Secondary[Ctx, CtxFn, Step] = - new Secondary(api) - } - - object Implicits extends HooksApiExt -} -``` - -### Template 3: Custom hooks that don't take input but return output +Conversion is possible between hooks of the form `I => HookResult[O]` and builder-style [`CustomHook[I, O]`](HOOKS_BUILDER.md#custom-hooks) via: ```scala -import japgolly.scalajs.react._ - -object MyCustomHook { +val customHook1: CustomHook[I, O] = ... +val customHook2: CustomHook[I, Unit] = ... - // TODO: Replace - val hook = CustomHook[Unit] - .useEffectOnMount(Callback.log("HELLO!")) - .buildReturning(_ => 123) - - object HooksApiExt { - sealed class Primary[Ctx, Step <: HooksApi.AbstractStep](api: HooksApi.Primary[Ctx, Step]) { - - // TODO: Change hook name, and output type - final def useMyCustomHook(implicit step: Step): step.Next[Int] = - api.custom(hook) - } - } - - trait HooksApiExt { - import HooksApiExt._ - - // TODO: Change hook name so that it won't conflict with other custom hooks - implicit def hooksExtMyCustomHook[Ctx, Step <: HooksApi.AbstractStep](api: HooksApi.Primary[Ctx, Step]): Primary[Ctx, Step] = - new Primary(api) - } - - object Implicits extends HooksApiExt -} -``` +val useHook1: I => HookResult[O] = customHook1.toHookResult +val useHook2: HookResult[O] = customHook2.toHookResult -### Template 4: Custom hooks that don't take input or return output - -```scala -import japgolly.scalajs.react._ - -object MyCustomHook { - - // TODO: Replace - val hook = CustomHook[Unit] - .useEffectOnMount(Callback.log("HELLO!")) - .build - - object HooksApiExt { - sealed class Primary[Ctx, Step <: HooksApi.AbstractStep](api: HooksApi.Primary[Ctx, Step]) { - - // TODO: Change hook name - final def useMyCustomHook(implicit step: Step): step.Self = - api.custom(hook) - } - } - - trait HooksApiExt { - import HooksApiExt._ - - // TODO: Change hook name so that it won't conflict with other custom hooks - implicit def hooksExtMyCustomHook[Ctx, Step <: HooksApi.AbstractStep](api: HooksApi.Primary[Ctx, Step]): Primary[Ctx, Step] = - new Primary(api) - } - - object Implicits extends HooksApiExt -} -``` - -### Usage - -By importing `MyCustomHook.Implicits._` users will be able to use your custom hook directly from the hooks API. - -Example: - -```scala -import japgolly.scalajs.react._ -import japgolly.scalajs.react.vdom.html_<^._ -import MyCustomHook.Implicits._ - -object Example { - val Component = ScalaFnComponent.withHooks[Unit] - .useMyCustomHook // Implicitly available - .render($ => - <.div("MyCustomHook: ", $.hook1) - ) -} -``` - -# Escape hatches - -If you really, really want to work with JS-style imperative hooks, you can! -But it's important to note that the onus is on you to ensure you use hooks correctly without violating React's rules. -If you use the escape hatch, scalajs-react won't be able to check that your code will always work. - -In the hooks API, there's `.unchecked(body)` and `.uncheckedBy(ctx => body)` that you can use as an escape hatch, -and create hooks using React directly instead of using scalajs-react's hooks API. - -Example: - -```scala -package japgolly.scalajs.react.core - -import japgolly.scalajs.react._ -import japgolly.scalajs.react.facade.{React => ReactJs} -import japgolly.scalajs.react.vdom.html_<^._ -import org.scalajs.dom.document - -object Example { - val Component = ScalaFnComponent.withHooks[Unit] - .unchecked { - val count = ReactJs.useState[Int](0) - ReactJs.useEffect(() => { - document.title = s"You clicked ${count._1} times" - }) - count - } - .render { (_, countHook) => - val count = countHook._1 - val setCount = countHook._2 - <.div( - <.p(s"You clicked $count times"), - <.button( - ^.onClick --> Callback(setCount(count + 1)), - "Click me" - ), - ) - } -} +val newCustomHook: CustomHook[I, O] = CustomHook.fromHookResult(useHook1) +val newCustomHook: CustomHook[Unit, O] = CustomHook.fromHookResult(useHook2) ``` diff --git a/doc/HOOKS_BUILDER.md b/doc/HOOKS_BUILDER.md new file mode 100644 index 000000000..15f2e675d --- /dev/null +++ b/doc/HOOKS_BUILDER.md @@ -0,0 +1,649 @@ +# React Hooks with scalajs-react using a builder + +* [Design](#design) +* [Quickstart](#quickstart) +* [React hooks in scalajs-react](#react-hooks-in-scalajs-react) +* [New hooks provided by scalajs-react](#new-hooks-provided-by-scalajs-react) +* [`shouldComponentUpdate`](#shouldcomponentupdate) +* [Hooks with dependencies](#hooks-with-dependencies) +* [Hooks and PropsChildren](#hooks-and-propschildren) +* [Custom hooks](#custom-hooks) +* [Custom hook composition](#custom-hook-composition) +* [Using third-party JavaScript hooks](#using-third-party-javascript-hooks) +* [API extensions](#api-extensions) +* [Escape hatches](#escape-hatches) + + +# Design + +scalajs-react aims to provide a DSL which enforces usage of React's rules for +hooks at compile time. There's actually 2 DSL flavors implemented: + * A composition-based approach documented [here](HOOKS.md), which results in + more concise code, but doesn't protect you from breaking a couple of rules + via `fold`/`traverse`. + * A builder-like approach documented in this page, which gives you complete + safety at the cost of being more vebose. + +If you have a spare 9 hours (!), you can watch the livestreamed coding sessions +([part 1](https://www.youtube.com/watch?v=rDTr9TRFGSA), [part 2](https://www.youtube.com/watch?v=8pMXmk_YM5s)) +and see how the design gradually evolved into what it (conceptually) is today. + +# Quickstart + +Let's translate this JS component... + +```js +import React, { useState, useEffect } from 'react'; + +function Example() { + const [count, setCount] = useState(0); + + useEffect(() => { + document.title = `You clicked ${count} times`; + }); + + const [fruit, setFruit] = useState("banana"); + + return ( +
+

You clicked {count} times

+ +

Your favourite fruit is a {fruit}!

+
+ ); +} +``` + +The above JS component can be written in scalajs-react in two +very similar ways. + +### Method 1 + +```scala +import japgolly.scalajs.react._ +import japgolly.scalajs.react.vdom.html_<^._ +import org.scalajs.dom.document + +object Example { + val Component = ScalaFnComponent.withHooks[Unit] + + .useState(0) + + .useEffectBy((props, count) => Callback { + document.title = s"You clicked ${count.value} times" + }) + + .useState("banana") + + .render((props, count, fruit) => + <.div( + <.p(s"You clicked ${count.value} times"), + <.button( + ^.onClick --> count.modState(_ + 1), + "Click me" + ), + <.p(s"Your favourite fruit is a ${fruit.value}!") + ) + ) +} +``` + +### Method 2 + +```scala +import japgolly.scalajs.react._ +import japgolly.scalajs.react.vdom.html_<^._ +import org.scalajs.dom.document + +object Example { + val Component = ScalaFnComponent.withHooks[Unit] + + .useState(0) + + .useEffectBy($ => Callback { + document.title = s"You clicked ${$.hook1.value} times" + }) + + .useState("banana") + + .render($ => + <.div( + <.p(s"You clicked ${$.hook1.value} times"), + <.button( + ^.onClick --> $.hook1.modState(_ + 1), + "Click me" + ), + <.p(s"Your favourite fruit is a ${$.hook2.value}!") + ) + ) +} +``` + +### Diff between both methods + +```diff + import japgolly.scalajs.react._ + import japgolly.scalajs.react.vdom.html_<^._ + import org.scalajs.dom.document + + object Example { + val Component = ScalaFnComponent.withHooks[Unit] + + .useState(0) + +- .useEffectBy((props, count) => Callback { +- document.title = s"You clicked ${count.value} times" ++ .useEffectBy($ => Callback { ++ document.title = s"You clicked ${$.hook1.value} times" + }) + + .useState("banana") + +- .render((props, count, fruit) => ++ .render($ => + <.div( +- <.p(s"You clicked ${count.value} times"), ++ <.p(s"You clicked ${$.hook1.value} times"), + <.button( +- ^.onClick --> count.modState(_ + 1), ++ ^.onClick --> $.hook1.modState(_ + 1), + "Click me" + ), +- <.p(s"Your favourite fruit is a ${fruit.value}!") ++ <.p(s"Your favourite fruit is a ${$.hook2.value}!") + ) + ) + } +``` + +# React hooks in scalajs-react + +| JavaScript | scalajs-react | +| --------- | -------- | +| `useCallback(c)` | `.useCallback(c)` | +| `useCallback(c, [deps])` | `.useCallbackWithDeps((deps))(_ => c)` | +| `useCallback(f([deps]), [deps])` | `.useCallbackWithDeps((deps))(f)` | +| `useContext(c)` | `.useContext(c)` | +| `useDebugValue(desc)` | `.useDebugValue(desc)` | +| `useDebugValue(a, f)` | `.useDebugValue(f(a))` | +| `useEffect(e)` | `.useEffect(e)` | +| `useEffect(e, [])` | `.useEffectOnMount(e)` | +| `useEffect(e, [deps])` | `.useEffectWithDeps((deps))(_ => e)` | +| `useEffect(f([deps]), [deps])` | `.useEffectWithDeps((deps))(f)` | +| `useLayoutEffect(e)` | `.useLayoutEffect(e)` | +| `useLayoutEffect(e, [])` | `.useLayoutEffectOnMount(e)` | +| `useLayoutEffect(e, [deps])` | `.useLayoutEffectWithDeps((deps))(_ => e)` | +| `useLayoutEffect(f([deps]), [deps])` | `.useLayoutEffectWithDeps((deps))(f)` | +| `useMemo(() => a, [deps])` | `.useMemo((deps))(_ => a)` | +| `useMemo(() => f([deps]), [deps])` | `.useMemo((deps))(f)` | +| `useReducer(f, s)` | `.useReducer(f, s)` | +| `useReducer(f, a, i)` | `.useReducer(f, i(a))`
*(Note: `i(a)` is actually `(=> i(a))` and isn't evaluated immediately)* | +| `useRef()` | `.useRefToAnyVdom`
`.useRefToVdom[DomType]`
`.useRefToScalaComponent(component)`
`.useRefToScalaComponent[P, S, B]`
`.useRefToJsComponent(component)`
`.useRefToJsComponent[P, S]`
`.useRefToJsComponentWithMountedFacade[P, S, F]` | +| `useRef(initialValue)` | `.useRef(initialValue)` | +| `useState(initialState)`
`useState(() => initialState)` | `.useState(initialState)` | +| `useId()` | `.useId` | +| `useTransition` | `.useTransition` | +| Custom hook
`useBlah(i)` | `.custom(useBlah(i))`
(`val useBlah: CustomHook[I, O]`) | + +Note: The reason that `[deps]` on the JS side becomes `(deps)` on the Scala side, +is that in JS you'd use an array but in Scala you'd use a tuple. +So `[dep1, dep2]` becomes `(dep1, dep2)`; and `[dep1]` becomes just `dep1` which is the same as +`(dep1)`. + + +# New hooks provided by scalajs-react + +| Hook | Description | +| ---- | ----------- | +| `.localLazyVal(a)` | Creates a new `lazy val` on each render. | +| `.localVal(a)` | Creates a new `val` on each render. | +| `.localVar(a)` | Creates a new `var` on each render. | +| `.useForceUpdate` | Provides a `Reusable[Callback]` then when invoked, forces a re-render of the component. | +| `.useStateSnapshot(initialState)`
*(Requires import japgolly.scalajs.react.extra._)* | Same as `.useState` except you get a `StateSnapshot` (which accepts callbacks on set updates). | +| `.useStateSnapshotWithReuse(initialState)`
*(Requires import japgolly.scalajs.react.extra._)* | Same as `.useState` except you get a `StateSnapshot` (which accepts callbacks on set updates) with state `Reusability`. | +| `.useStateWithReuse(initialState)` | Conceptually `useState` + `shouldComponentUpdate`. Same as `useState` except that updates are dropped according to `Reusability`. | + +# `shouldComponentUpdate` + +Instead of calling `render`, you can call one of the following to get `shouldComponentUpdate` +behaviour just like classes have. + +* `renderWithReuse(f: Ctx => VdomNode)(implicit r: Reusability[Ctx])` +* `renderWithReuseBy[A: Reusability](reusableInputs: Ctx => A)(f: A => VdomNode)` +* `renderReusable(f: Ctx => Reusable[VdomNode])` + +# Hooks with dependencies + +Sometimes hooks are initialised using props and/or the output of other hooks, +(which scalajs-react refers to as "context"). +Each hook that has a return type that's not `Unit`, +becomes available in subsequent contexts. + +In order to get access to this context, append a `By` suffix to the hook method +of your choice, and change the arguments to functions that take the context. +There are two ways to do this. + +### 1. useXxxxxBy((props, hook1, hook2, ...) => arg) + +```scala +val comp = ScalaFnComponent.withHooks[Int] + .useStateBy(props => props - 1) // initialise state according to props + .useEffectBy((props, hook1) => Callback.log(s"Props: $props, State: ${hook1.value}")) +``` + +### 2. useXxxxxBy(ctxObj => arg) + +```scala +val comp = ScalaFnComponent.withHooks[Int] + .useStateBy(props => props - 1) // initialise state according to props + .useEffectBy(c => Callback.log(s"Props: ${c.props}, State: ${c.hook1.value}")) +``` + +### Hooks that return `Unit` don't appear in context + +```scala +val comp = ScalaFnComponent.withHooks[Int] + + // The result of this hook becomes "hook1" + .useStateBy(props => props - 1) + + // The result of useEffect is Unit and doesn't appear in context + .useEffectBy(c => Callback.log(s"Props: ${c.props}, State: ${c.hook1.value}")) + + // The result of this hook becomes "hook2" + .useState(123) + + .render((props, hook1, hook2) => + <.div( + <.div("State 1 = ", hook1.value), + <.div("State 2 = ", hook2.value), + ) + ) +``` + +# Hooks and PropsChildren + +In order to get access to `PropsChildren`, call `.withPropsChildren` as the first step in your DSL. +It will then become available... + +1) as argument #2 after `props` in multi-arg fns (eg. `.render((props, propsChildren, hook1, hook2, ...) => `) +2) as `.propsChildren` from context objects (eg. `.render($ => $.propsChildren)`) + +Example: + +```scala +import japgolly.scalajs.react._ +import japgolly.scalajs.react.vdom.html_<^._ + +object Example { + final case class Props(name: String) + + val Component = ScalaFnComponent.withHooks[Props] + .withPropsChildren + .useState(0) + .render((props, propsChildren, counter) => + <.div( + <.p(s"Hello ${props.name}."), + <.p(s"You clicked ${counter.value} times."), + <.button("Click me", ^.onClick --> counter.modState(_ + 1)), + <.div(propsChildren) + ) + ) +} +``` + +# Custom hooks + +A custom hook has the type `CustomHook[I, O]` where +`I` is the input type (or `Unit` if your custom hook doesn't take an input), +and `O` is the output type (or `Unit` if your custom hook doesn't return an output), + +To create a custom hook, the API is nearly identical to building a component with hooks. + +1. Start with `CustomHook[I]` instead of `ScalaFnComponent.withHooks[P]` +2. Complete your hook with `.buildReturning(ctx => O)`, or just `.build` if you don't need to return a value. + +Example: + +```scala +import japgolly.scalajs.react._ +import org.scalajs.dom.document + +object ExampleHook { + val useTitleCounter = CustomHook[Unit] + .useState(0) + .useEffectBy((_, count) => Callback { + document.title = s"You clicked ${count.value} times" + }) + .buildReturning(_.hook1) +} +``` + +and to use it: + +```scala +import japgolly.scalajs.react._ +import japgolly.scalajs.react.vdom.html_<^._ + +object Example { + val Component = ScalaFnComponent.withHooks[Unit] + .custom(ExampleHook.useTitleCounter) // <--- usage + .render((_, count) => + <.div( + <.p(s"You clicked ${count.value} times"), + <.button( + ^.onClick --> count.modState(_ + 1), + "Click me"))) +} +``` + +In order to provide the hook directly via `.custom` the input type of the hook must be one of the following... +* `Unit` +* same as the `Props` type +* `PropsChildren` + +If the custom hook has any other kind of type, simply provide it to the hook directly. +Example: + +```scala + val someCustomHook: CustomHook[Int, Unit] = ??? + + final case class Props(someInt: Int) + + val Component = ScalaFnComponent.withHooks[Props] + .custom(someCustomHook(123)) // provide a constant Int arg + .customBy($ => someCustomHook($.props.someInt)) // or use a dynamic value +``` + +# Custom hook composition + +CustomHooks can be composed by calling `++`. + +The input/output type of the result will be the "natural" result according to these rules: +* `A ++ Unit` or `Unit ++ A` becomes `A` +* `A ++ A` becomes `A` if it's in the input position +* `A ++ A` becomes `A` if it's in the output position and `A <: scala.Singleton` +* otherwise `A ++ B` becomes `(A, B)` + +Examples: + +```scala +object Example1 { + val hook1: CustomHook[Int, Unit] = ??? + val hook2: CustomHook[Int, Unit] = ??? + val hooks: CustomHook[Int, Unit] = hook1 ++ hook2 +} + +object Example2 { + val hook1: CustomHook[Unit, Int] = ??? + val hook2: CustomHook[Unit, Int] = ??? + val hooks: CustomHook[Unit, (Int, Int)] = hook1 ++ hook2 +} + +object Example3 { + val hook1: CustomHook[Long, Boolean] = ??? + val hook2: CustomHook[String, Int] = ??? + val hooks: CustomHook[(Long, String), (Boolean, Int)] = hook1 ++ hook2 +} + +object Example4 { + val hook1: CustomHook[Unit, Boolean] = ??? + val hook2: CustomHook[String, Unit] = ??? + val hooks: CustomHook[String, Boolean] = hook1 ++ hook2 +} +``` + +# Using third-party JavaScript hooks + +Using a third-party JavaScript hook is as simple as wrapping it in `CustomHook.unchecked`. + +```scala +// Declare your JS facade as normal. Type should be a subtype of js.Function. + +// I is the type of the hook's inputs (use a tuple or case class for multiple args) +// O is the type of the hook's output (or Unit if none) +val jsHook = CustomHook.unchecked[I, O](i => JsHookFacade(i)) +``` + +Then to use it, either... + +1) simply call `.custom(jsHook)` from your component +2) or create an API extension as shown below + +# API extensions + +You can also provide your own implicit extensions to the hook API. + +Unfortunately it involves a bit of boilerplate. Copy and customise one of the following templates: + +### Template 1: Custom hooks that take input and return output + +```scala +import japgolly.scalajs.react._ + +object MyCustomHook { + + // TODO: Replace + val hook = CustomHook[String] + .useEffectOnMountBy(name => Callback.log(s"HELLO $name")) + .buildReturning(name => name) + + object HooksApiExt { + sealed class Primary[Ctx, Step <: HooksApi.AbstractStep](api: HooksApi.Primary[Ctx, Step]) { + + // TODO: Change hook name, input args/type(s), and output type + final def useMyCustomHook(name: String)(implicit step: Step): step.Next[String] = + // TODO: Change hook name + useMyCustomHookBy(_ => name) + + // TODO: Change hook name, input args/type(s), and output type + final def useMyCustomHookBy(name: Ctx => String)(implicit step: Step): step.Next[String] = + api.customBy(ctx => hook(name(ctx))) + } + + final class Secondary[Ctx, CtxFn[_], Step <: HooksApi.SubsequentStep[Ctx, CtxFn]](api: HooksApi.Secondary[Ctx, CtxFn, Step]) extends Primary[Ctx, Step](api) { + + // TODO: Change hook name, input args/type(s), and output type + def useMyCustomHookBy(name: CtxFn[String])(implicit step: Step): step.Next[String] = + // TODO: Change hook name, squash each parameter + // useMyCustomHookBy(step.squash(arg1)(_), step.squash(arg2)(_), ...) + useMyCustomHookBy(step.squash(name)(_)) + } + } + + trait HooksApiExt { + import HooksApiExt._ + + // TODO: Change hook name so that it won't conflict with other custom hooks + implicit def hooksExtMyCustomHook1[Ctx, Step <: HooksApi.AbstractStep](api: HooksApi.Primary[Ctx, Step]): Primary[Ctx, Step] = + new Primary(api) + + // TODO: Change hook name so that it won't conflict with other custom hooks + implicit def hooksExtMyCustomHook2[Ctx, CtxFn[_], Step <: HooksApi.SubsequentStep[Ctx, CtxFn]](api: HooksApi.Secondary[Ctx, CtxFn, Step]): Secondary[Ctx, CtxFn, Step] = + new Secondary(api) + } + + object Implicits extends HooksApiExt +} +``` + +### Template 2: Custom hooks that take input and don't return output + +```scala +import japgolly.scalajs.react._ + +object MyCustomHook { + + // TODO: Replace + val hook = CustomHook[String] + .useEffectOnMountBy(name => Callback.log(s"HELLO $name")) + .build + + object HooksApiExt { + sealed class Primary[Ctx, Step <: HooksApi.AbstractStep](api: HooksApi.Primary[Ctx, Step]) { + + // TODO: Change hook name, input args/type(s), and output type + final def useMyCustomHook(name: String)(implicit step: Step): step.Self = + // TODO: Change hook name + useMyCustomHookBy(_ => name) + + // TODO: Change hook name, input args/type(s), and output type + final def useMyCustomHookBy(name: Ctx => String)(implicit step: Step): step.Self = + api.customBy(ctx => hook(name(ctx))) + } + + final class Secondary[Ctx, CtxFn[_], Step <: HooksApi.SubsequentStep[Ctx, CtxFn]](api: HooksApi.Secondary[Ctx, CtxFn, Step]) extends Primary[Ctx, Step](api) { + + // TODO: Change hook name, input args/type(s), and output type + def useMyCustomHookBy(name: CtxFn[String])(implicit step: Step): step.Self = + // TODO: Change hook name, squash each parameter + // useMyCustomHookBy(step.squash(arg1)(_), step.squash(arg2)(_), ...) + useMyCustomHookBy(step.squash(name)(_)) + } + } + + trait HooksApiExt { + import HooksApiExt._ + + // TODO: Change hook name so that it won't conflict with other custom hooks + implicit def hooksExtMyCustomHook1[Ctx, Step <: HooksApi.AbstractStep](api: HooksApi.Primary[Ctx, Step]): Primary[Ctx, Step] = + new Primary(api) + + // TODO: Change hook name so that it won't conflict with other custom hooks + implicit def hooksExtMyCustomHook2[Ctx, CtxFn[_], Step <: HooksApi.SubsequentStep[Ctx, CtxFn]](api: HooksApi.Secondary[Ctx, CtxFn, Step]): Secondary[Ctx, CtxFn, Step] = + new Secondary(api) + } + + object Implicits extends HooksApiExt +} +``` + +### Template 3: Custom hooks that don't take input but return output + +```scala +import japgolly.scalajs.react._ + +object MyCustomHook { + + // TODO: Replace + val hook = CustomHook[Unit] + .useEffectOnMount(Callback.log("HELLO!")) + .buildReturning(_ => 123) + + object HooksApiExt { + sealed class Primary[Ctx, Step <: HooksApi.AbstractStep](api: HooksApi.Primary[Ctx, Step]) { + + // TODO: Change hook name, and output type + final def useMyCustomHook(implicit step: Step): step.Next[Int] = + api.custom(hook) + } + } + + trait HooksApiExt { + import HooksApiExt._ + + // TODO: Change hook name so that it won't conflict with other custom hooks + implicit def hooksExtMyCustomHook[Ctx, Step <: HooksApi.AbstractStep](api: HooksApi.Primary[Ctx, Step]): Primary[Ctx, Step] = + new Primary(api) + } + + object Implicits extends HooksApiExt +} +``` + +### Template 4: Custom hooks that don't take input or return output + +```scala +import japgolly.scalajs.react._ + +object MyCustomHook { + + // TODO: Replace + val hook = CustomHook[Unit] + .useEffectOnMount(Callback.log("HELLO!")) + .build + + object HooksApiExt { + sealed class Primary[Ctx, Step <: HooksApi.AbstractStep](api: HooksApi.Primary[Ctx, Step]) { + + // TODO: Change hook name + final def useMyCustomHook(implicit step: Step): step.Self = + api.custom(hook) + } + } + + trait HooksApiExt { + import HooksApiExt._ + + // TODO: Change hook name so that it won't conflict with other custom hooks + implicit def hooksExtMyCustomHook[Ctx, Step <: HooksApi.AbstractStep](api: HooksApi.Primary[Ctx, Step]): Primary[Ctx, Step] = + new Primary(api) + } + + object Implicits extends HooksApiExt +} +``` + +### Usage + +By importing `MyCustomHook.Implicits._` users will be able to use your custom hook directly from the hooks API. + +Example: + +```scala +import japgolly.scalajs.react._ +import japgolly.scalajs.react.vdom.html_<^._ +import MyCustomHook.Implicits._ + +object Example { + val Component = ScalaFnComponent.withHooks[Unit] + .useMyCustomHook // Implicitly available + .render($ => + <.div("MyCustomHook: ", $.hook1) + ) +} +``` + +# Escape hatches + +If you really, really want to work with JS-style imperative hooks, you can! +But it's important to note that the onus is on you to ensure you use hooks correctly without violating React's rules. +If you use the escape hatch, scalajs-react won't be able to check that your code will always work. + +In the hooks API, there's `.unchecked(body)` and `.uncheckedBy(ctx => body)` that you can use as an escape hatch, +and create hooks using React directly instead of using scalajs-react's hooks API. + +Example: + +```scala +package japgolly.scalajs.react.core + +import japgolly.scalajs.react._ +import japgolly.scalajs.react.facade.{React => ReactJs} +import japgolly.scalajs.react.vdom.html_<^._ +import org.scalajs.dom.document + +object Example { + val Component = ScalaFnComponent.withHooks[Unit] + .unchecked { + val count = ReactJs.useState[Int](0) + ReactJs.useEffect(() => { + document.title = s"You clicked ${count._1} times" + }) + count + } + .render { (_, countHook) => + val count = countHook._1 + val setCount = countHook._2 + <.div( + <.p(s"You clicked $count times"), + <.button( + ^.onClick --> Callback(setCount(count + 1)), + "Click me" + ), + ) + } +} +``` diff --git a/doc/TESTING.md b/doc/TESTING.md index 817b8236e..b674df24e 100644 --- a/doc/TESTING.md +++ b/doc/TESTING.md @@ -1,5 +1,4 @@ -Testing -======= +# Testing This file describes testing functionality provided by React.JS and scalajs-react.
It is plenty for simple and small unit tests. @@ -10,6 +9,7 @@ For larger and/or complicated tests, **it is highly recommended to use for how to write tests for real-world scalajs-react applications. #### Contents + - [Setup](#setup) - [`ReactTestUtils`](#reacttestutils) - [`Simulate` and `Simulation`](#simulate-and-simulation) @@ -18,31 +18,28 @@ for how to write tests for real-world scalajs-react applications. - [`Test Scripts`](#test-scripts) - [Fatal React warnings](#fatal-react-warnings) -Setup -===== +# Setup 1. Install PhantomJS. 2. Add the following to SBT: - ```scala - // scalajs-react test module - libraryDependencies += "com.github.japgolly.scalajs-react" %%% "test" % "2.1.1" % Test - - // React JS itself. - // NOTE: Requires react-with-addons.js instead of just react.js - jsDependencies += + ```scala + // scalajs-react test module + libraryDependencies += "com.github.japgolly.scalajs-react" %%% "test" % "2.1.1" % Test - "org.webjars.npm" % "react-dom" % "17.0.2" % Test - / "umd/react-dom-test-utils.development.js" - minified "umd/react-dom-test-utils.production.min.js" - dependsOn "umd/react-dom.development.js" - commonJSName "ReactTestUtils" - ``` + // React JS itself. + // NOTE: Requires react-with-addons.js instead of just react.js + jsDependencies += + "org.webjars.npm" % "react-dom" % "18.3.1" % Test + / "umd/react-dom-test-utils.development.js" + minified "umd/react-dom-test-utils.production.min.js" + dependsOn "umd/react-dom.development.js" + commonJSName "ReactTestUtils" + ``` -`ReactTestUtils` -================ +# `ReactTestUtils` The main bucket of testing utilities lies in `japgolly.scalajs.react.test.ReactTestUtils`. @@ -50,37 +47,41 @@ Half of the methods delegate to React.JS's [React.addons.TestUtils](https://face (for which there is a raw facade in `japgolly.scalajs.react.test.raw.ReactAddonsTestUtils` if you're interested). The other half are new functions added specifically in scalajs-react. -* Rendering into DOM with auto-removal - * `withRendered[M, A](u: Unmounted[M], intoBody: Boolean)(f: M => A): A` - * `withRenderedIntoDocument[M, A](u: Unmounted[M])(f: M => A): A` - * `withRenderedIntoBody[M, A](u: Unmounted[M])(f: M => A): A` - * `withNewBodyElement[A](use: Element => A): A` - * `newBodyElement(): Element` - * `removeNewBodyElement(e: Element): Unit` - * `renderIntoBody[M, A](u: Unmounted[M]): M` -* Asynchronously rendering into DOM with auto-removal - * `withRenderedAsync[M, A](u: Unmounted[M], intoBody: Boolean)(f: M => Future[A]): Future[A]` - * `withRenderedIntoDocumentAsync[M, A](u: Unmounted[M])(f: M => Future[A]): Future[A]` - * `withRenderedIntoBodyAsync[M, A](u: Unmounted[M])(f: M => Future[A]): Future[A]` - * `withNewBodyElementAsync[A](use: Element => Future[A]): Future[A]` -* Mounted props modification - * `replaceProps(component, mounted)(newProps: P): mounted'` - * `modifyProps(component, mounted)(f: P => P): mounted'` -* Other - * `removeReactInternals(html: String): String` - Removes internal annotations from HTML that React inserts. + +- Rendering into DOM with auto-removal + - `withRendered[M, A](u: Unmounted[M], intoBody: Boolean)(f: M => A): A` + - `withRenderedIntoDocument[M, A](u: Unmounted[M])(f: M => A): A` + - `withRenderedIntoBody[M, A](u: Unmounted[M])(f: M => A): A` + - `withNewBodyElement[A](use: Element => A): A` + - `newBodyElement(): Element` + - `removeNewBodyElement(e: Element): Unit` + - `renderIntoBody[M, A](u: Unmounted[M]): M` +- Asynchronously rendering into DOM with auto-removal + - `withRenderedAsync[M, A](u: Unmounted[M], intoBody: Boolean)(f: M => Future[A]): Future[A]` + - `withRenderedIntoDocumentAsync[M, A](u: Unmounted[M])(f: M => Future[A]): Future[A]` + - `withRenderedIntoBodyAsync[M, A](u: Unmounted[M])(f: M => Future[A]): Future[A]` + - `withNewBodyElementAsync[A](use: Element => Future[A]): Future[A]` +- Mounted props modification + - `replaceProps(component, mounted)(newProps: P): mounted'` + - `modifyProps(component, mounted)(f: P => P): mounted'` +- Other + - `removeReactInternals(html: String): String` - Removes internal annotations from HTML that React inserts. There's only one magic implicit method this time around: Mounted components get `.outerHtmlScrubbed()` which is shorthand for `ReactTestUtils.removeReactInternals(m.getDOMNode.outerHTML)`. -`Simulate` and `Simulation` -=========================== +# `Simulate` and `Simulation` + To make event simulation easier, certain event types have dedicated, strongly-typed case classes to wrap event data. For example, JS like + ```js // JavaScript -ReactAddons.TestUtils.Simulate.change(t, {target: {value: "Hi"}}) +ReactAddons.TestUtils.Simulate.change(t, { target: { value: "Hi" } }); ``` + becomes + ```scala // Scala Simulate.change(t, SimEvent.Change(value = "Hi")) @@ -94,6 +95,7 @@ If you'd like more composability and/or purity there's also `Simulation` which represents action (without a target). It does nothing until `.run` is called and a target is provided. Example: + ```scala val a = Simulation.focus val b = Simulation.change(SimEvent.Change(value = "hi")) @@ -110,14 +112,13 @@ val s = Simulation.focusChangeBlur("hi") s run component ``` - -Testing props changes -===================== +# Testing props changes When you want to simulate a parent component re-rendering a child component with different props, you can test the child directly using `ReactTestUtils.{modify,replace}Props`. Example of code to test: + ```scala class CP { var prev = "none" @@ -131,6 +132,7 @@ val CP = ScalaComponent.builder[String]("asd") ``` Example test case: + ```scala ReactTestUtils.withRenderedIntoDocument(CP("start")) { m => assert(m.outerHtmlScrubbed(), "
none → start
") @@ -143,16 +145,15 @@ ReactTestUtils.withRenderedIntoDocument(CP("start")) { m => } ``` - -`ReactTestVar` -============== +# `ReactTestVar` A `ReactTestVar[A]` is a wrapper around a `var a: A` that: -* can produce a `StateSnapshot[A]` with or without `Reusability` -* can produce a `StateAccess[A]` -* retains history when modified -* can perform arbitrary actions when modified -* can be reset + +- can produce a `StateSnapshot[A]` with or without `Reusability` +- can produce a `StateAccess[A]` +- retains history when modified +- can perform arbitrary actions when modified +- can be reset It's useful for testing components that accept `StateSnapshot[A]`/`StateAccess[A]` instances in their props. @@ -207,17 +208,15 @@ ReactTestUtils.withRenderedIntoDocument(component(testVar.stateAccess)) { m => } ``` - -Test Scripts -============ +# Test Scripts It's possible to write test scripts like -1. *click this* -2. *verify that* -3. *press the Back button* -4. *type name* -5. *press Enter* +1. _click this_ +2. _verify that_ +3. _press the Back button_ +4. _type name_ +5. _press Enter_ In case you missed the notice at the top of the file, that functionality is provided in a sister library called [Scala Test-State](https://github.com/japgolly/test-state). @@ -225,44 +224,42 @@ In case you missed the notice at the top of the file, that functionality is prov See [this example](https://github.com/japgolly/test-state/tree/master/example-react) for how to write tests for real-world scalajs-react applications. - -Fatal React warnings -==================== +# Fatal React warnings The easiest way to make `ReactTestUtils` to turn React warnings into runtime exceptions, is via a [config option](./CONFIG.md#testwarningsreact). Alternatively, you can do any of the following... -* Wrapping a test +- Wrapping a test - ```scala - import japgolly.scalajs.react.test.ReactTestUtilsConfig - ReactTestUtilsConfig.AroundReact.fatalReactWarnings { - // test code here - } - ``` + ```scala + import japgolly.scalajs.react.test.ReactTestUtilsConfig + ReactTestUtilsConfig.AroundReact.fatalReactWarnings { + // test code here + } + ``` -* Installing for all `ReactTestUtils` usage +- Installing for all `ReactTestUtils` usage - ```scala - import japgolly.scalajs.react.test.ReactTestUtilsConfig - ReactTestUtilsConfig.aroundReact.set( - ReactTestUtilsConfig.AroundReact.fatalReactWarnings) - ``` + ```scala + import japgolly.scalajs.react.test.ReactTestUtilsConfig + ReactTestUtilsConfig.aroundReact.set( + ReactTestUtilsConfig.AroundReact.fatalReactWarnings) + ``` -* Installing outside of test code +- Installing outside of test code - ```scala - import japgolly.scalajs.react.util.ConsoleHijack - ConsoleHijack.fatalReactWarnings.install() - ``` + ```scala + import japgolly.scalajs.react.util.ConsoleHijack + ConsoleHijack.fatalReactWarnings.install() + ``` -* Wrapping non-test code +- Wrapping non-test code - ```scala - import japgolly.scalajs.react.util.ConsoleHijack - ConsoleHijack.fatalReactWarnings { - // code here - } - ``` + ```scala + import japgolly.scalajs.react.util.ConsoleHijack + ConsoleHijack.fatalReactWarnings { + // code here + } + ``` diff --git a/doc/USAGE.md b/doc/USAGE.md index 4b1f3d106..789695525 100644 --- a/doc/USAGE.md +++ b/doc/USAGE.md @@ -1,11 +1,11 @@ -Usage -===== +# Usage This will attempt to show you how to use React in Scala. It is expected that you know how React itself works. #### Contents + - [Setup](#setup) - [Creating Virtual-DOM](#creating-virtual-dom) - [Callbacks](#callbacks) @@ -14,82 +14,82 @@ It is expected that you know how React itself works. - [React Extensions](#react-extensions) - [Gotchas](#gotchas) -Setup -===== +# Setup 1. Add [Scala.js](http://www.scala-js.org) to your project. -2. Add *scalajs-react* to SBT: +2. Add _scalajs-react_ to SBT: There are a number of different modules available. On this page we'll just use the `core` module but refer to the [Modules doc](./MODULES.md) to see other module options. - ```scala - // "core" = essentials only. No bells or whistles. - libraryDependencies += "com.github.japgolly.scalajs-react" %%% "core" % "2.1.1" - ``` +```scala +// "core" = essentials only. No bells or whistles. +libraryDependencies += "com.github.japgolly.scalajs-react" %%% "core" % "2.1.1" +``` 3. Add React to your build. - How to do this depends on your Scala.JS config and build setup. + How to do this depends on your Scala.JS config and build setup. - If you're using [scalajs-bundler](https://scalacenter.github.io/scalajs-bundler/), - add the following SBT settings to get started: + If you're using [scalajs-bundler](https://scalacenter.github.io/scalajs-bundler/), + add the following SBT settings to get started: - ```scala - enablePlugins(ScalaJSPlugin) + ```scala + enablePlugins(ScalaJSPlugin) - enablePlugins(ScalaJSBundlerPlugin) + enablePlugins(ScalaJSBundlerPlugin) - libraryDependencies += "com.github.japgolly.scalajs-react" %%% "core" % "2.1.1" + libraryDependencies += "com.github.japgolly.scalajs-react" %%% "core" % "2.1.1" - Compile / npmDependencies ++= Seq( - "react" -> "17.0.2", - "react-dom" -> "17.0.2") - ``` + Compile / npmDependencies ++= Seq( + "react" -> "18.3.1", + "react-dom" -> "18.3.1") + ``` - If you're using old-school `jsDependencies`, add something akin to: + If you're using `jsDependencies`, add the following: - ```scala - // React JS itself (Note the filenames, adjust as needed, eg. to remove addons.) - jsDependencies ++= Seq( + ```scala + // Required for React 18.3.1 + dependencyOverrides += "org.webjars.npm" % "scheduler" % "0.22.0", - "org.webjars.npm" % "react" % "17.0.2" - / "umd/react.development.js" - minified "umd/react.production.min.js" - commonJSName "React", + jsDependencies ++= Seq( - "org.webjars.npm" % "react-dom" % "17.0.2" - / "umd/react-dom.development.js" - minified "umd/react-dom.production.min.js" - dependsOn "umd/react.development.js" - commonJSName "ReactDOM", + // Polyfill required for React 18.3.1 + "org.webjars.npm" % "fast-text-encoding" % "1.0.3" / "text.js" minified "text.min.js" - "org.webjars.npm" % "react-dom" % "17.0.2" - / "umd/react-dom-server.browser.development.js" - minified "umd/react-dom-server.browser.production.min.js" - dependsOn "umd/react-dom.development.js" - commonJSName "ReactDOMServer"), - ``` + "org.webjars.npm" % "react" % "18.3.1" + / "umd/react.development.js" + minified "umd/react.production.min.js" + dependsOn "text.js" // <-- Load the fast-text-encoding polyfill before loading React itself + commonJSName "React", -[See here](IDE.md) for tips on configuring your IDE. + "org.webjars.npm" % "react-dom" % "18.3.1" + / "umd/react-dom.development.js" + minified "umd/react-dom.production.min.js" + dependsOn "umd/react.development.js" + commonJSName "ReactDOM", + "org.webjars.npm" % "react-dom" % "18.3.1" + / "umd/react-dom-server.browser.development.js" + minified "umd/react-dom-server.browser.production.min.js" + dependsOn "umd/react-dom.development.js" + commonJSName "ReactDOMServer", + ), + ``` -Creating Virtual-DOM -==================== +[See here](IDE.md) for tips on configuring your IDE. -See [VDOM.md](VDOM.md). +# Creating Virtual-DOM +See [VDOM.md](VDOM.md). -Callbacks -========= +# Callbacks See [CALLBACK.md](CALLBACK.md). - -Creating Components -=================== +# Creating Components This is how to create components from Scala. (For JS components, see [INTEROP.md](INTEROP.md).) @@ -98,29 +98,30 @@ There is a component builder DSL beginning at `ScalaComponent.build`. You throw types and functions at it, call `build` and when it compiles you will have a React component. 1. The first step is to specify your component's properties type, and a component name. - ```scala - import japgolly.scalajs.react._ - import japgolly.scalajs.react.vdom.html_<^._ - object MyComponent { +```scala +import japgolly.scalajs.react._ +import japgolly.scalajs.react.vdom.html_<^._ - case class Props(/* TODO */) +object MyComponent { - val Component = - ScalaComponent.builder[Props] - | - } - ``` + case class Props(/* TODO */) + + val Component = + ScalaComponent.builder[Props] + | +} +``` -2. *(Optional)* If you want a stateful component, - call one of the methods beginning with `.initialState`. - Use your IDE to see the methods and the differences in their type signatures. +2. _(Optional)_ If you want a stateful component, + call one of the methods beginning with `.initialState`. + Use your IDE to see the methods and the differences in their type signatures. -3. *(Optional)* If you want a backend (explained below) for your component - (and you do for non-trivial components), call `.backend`. - If your backend has a `.render` function, instead of `.backend` here you can call `.renderBackend` - which will use a macro to instantiate your backend, and automatically choose the - appropriate `.render` function in the next step, bypassing it for you. +3. _(Optional)_ If you want a backend (explained below) for your component + (and you do for non-trivial components), call `.backend`. + If your backend has a `.render` function, instead of `.backend` here you can call `.renderBackend` + which will use a macro to instantiate your backend, and automatically choose the + appropriate `.render` function in the next step, bypassing it for you. 4. Choose from one of the many available `render` functions. Use your IDE to see the methods and the differences in their type signatures. @@ -128,8 +129,8 @@ You throw types and functions at it, call `build` and when it compiles you will and your backend has a `render` function, you can call `.renderBackend` here to have the builder automatically select the appropriate `render` function. -5. *(Optional)* Type in the name of one of the React lifecycle hooks (eg. `componentDidMount`) - to add that hook to your component. +5. _(Optional)_ Type in the name of one of the React lifecycle hooks (eg. `componentDidMount`) + to add that hook to your component. 6. Call `.build` and you're done.
If your props is a singleton type (eg. `Unit`) then the buider automatically @@ -137,6 +138,7 @@ You throw types and functions at it, call `build` and when it compiles you will props be specified. (See [TYPES.md](TYPES.md) for more info.) Example with props: + ```scala val Hello = ScalaComponent.builder[String] @@ -148,6 +150,7 @@ Hello("Draconus") ``` Example without props: + ```scala val NoArgs = ScalaComponent.builder[Unit] @@ -167,7 +170,7 @@ In plain React with JS, functions which can have access to the component's props are placed within the body of the component class. In scalajs-react you need another place for such functions as scalajs-react emphasises type-safety and provides different types for the component's scope at different points in the lifecycle. -Instead they should be placed in some arbitrary class you may provide, called a *backend*. +Instead they should be placed in some arbitrary class you may provide, called a _backend_. See the [online timer demo](http://japgolly.github.io/scalajs-react/#examples/timer) for an example. @@ -177,7 +180,8 @@ It will locate the `render` method, determine what the arguments need (props/sta types or the arg names when the types are ambiguous, and create the appropriate function at compile-time. If can also automate the creation of the backend, see below. -Example before: *(yuk!)* +Example before: _(yuk!)_ + ```scala type State = Vector[String] @@ -198,6 +202,7 @@ val Example = ScalaComponent.builder[Unit] ``` After: + ```scala class Backend(bs: BackendScope[Unit, State]) { def render(s: State): VdomElement = // ← Accept props, state and/or propsChildren as argument @@ -213,6 +218,7 @@ val Example = ScalaComponent.builder[Unit] ``` You can also create a backend yourself and still use `.renderBackend`: + ```scala val Example = ScalaComponent.builder[Unit] .initialState(Vector("hello", "world")) @@ -221,8 +227,7 @@ val Example = ScalaComponent.builder[Unit] .build ``` -Using Components -================ +# Using Components Once you've created a Scala React component, it mostly acts like a typical Scala case class. To use it, you create an instance. @@ -274,17 +279,17 @@ import org.scalajs.dom.document NoArgs().renderIntoDOM(document.body) ``` -React Extensions -================ +# React Extensions + +- Where `setState(State)` is applicable, you can also run: -* Where `setState(State)` is applicable, you can also run: - * `modState(State => State)` - * `modState((State, Props) => State)` - * `setStateOption(Option[State])` - * `modStateOption(State => Option[State])` - * `modStateOption((State, Props) => Option[State])` + - `modState(State => State)` + - `modState((State, Props) => State)` + - `setStateOption(Option[State])` + - `modStateOption(State => Option[State])` + - `modStateOption((State, Props) => Option[State])` -* React has a [classSet addon](https://facebook.github.io/react/docs/class-name-manipulation.html) +- React has a [classSet addon](https://facebook.github.io/react/docs/class-name-manipulation.html) for specifying multiple optional class attributes. The same mechanism is applicable with this library is as follows: ```scala @@ -305,7 +310,7 @@ React Extensions props.message) ``` -* Sometimes you want to allow a function to both get and affect a portion of a component's state. Anywhere that you can call `.setState()` you can also call `.zoomState()` to return an object that has the same `.setState()`, `.modState()` methods but only operates on a subset of the total state. +- Sometimes you want to allow a function to both get and affect a portion of a component's state. Anywhere that you can call `.setState()` you can also call `.zoomState()` to return an object that has the same `.setState()`, `.modState()` methods but only operates on a subset of the total state. ```scala def incrementCounter(s: StateAccessPure[Int]): Callback = @@ -335,7 +340,7 @@ React Extensions } ``` -* The `.getDOMNode` callback can sometimes execute when unmounted which is an increasingly annoying bug to track down. +- The `.getDOMNode` callback can sometimes execute when unmounted which is an increasingly annoying bug to track down. Since React 16 with its new burn-it-all-down error handling approach, an occurance of this can be fatal. In order to properly model the reality of the callback and ensure compile-time safety, rather than just getting back a VDOM reference, the return type is an ADT like this: @@ -362,13 +367,11 @@ React Extensions In unit tests you'll typically use `asMounted().asElement()` or `asMounted().asText()` for inspection. +# Gotchas -Gotchas -======= - -* `table(tr(...))` will appear to work fine at first then crash later. React needs `table(tbody(tr(...)))`. +- `table(tr(...))` will appear to work fine at first then crash later. React needs `table(tbody(tr(...)))`. -* React's `setState` functions are asynchronous; they don't apply invocations of `this.setState` until the end of `render` or the current callback. Calling `.state` after `.setState` will return the initial, original value, i.e. +- React's `setState` functions are asynchronous; they don't apply invocations of `this.setState` until the end of `render` or the current callback. Calling `.state` after `.setState` will return the initial, original value, i.e. ```scala val s1 = $.state @@ -383,5 +386,5 @@ Gotchas 1. Use `modState`. 2. Refactor your logic so that you only call `setState` once. -* Since `setState` and `modState` return callbacks, if you need to call them from outside of a component (e.g. by accessing the backend of a mounted component), call `.runNow()` to trigger the change; else the callback will never run. +- Since `setState` and `modState` return callbacks, if you need to call them from outside of a component (e.g. by accessing the backend of a mounted component), call `.runNow()` to trigger the change; else the callback will never run. See the [Callbacks](#callbacks) section for more detail. diff --git a/doc/changelog/2.2.0.md b/doc/changelog/2.2.0.md new file mode 100644 index 000000000..64ab2f2df --- /dev/null +++ b/doc/changelog/2.2.0.md @@ -0,0 +1,14 @@ +## 2.2.0 + +### New Stuff + +- `ScalaFnComponent` and `ScalaForwardRef` now support `.withDisplayName(name)`. Must be called first (before hooks/`render*`/`withChildren*`). + +Example: + +```scala + ScalaFnComponent.withDisplayName("MyComponent") + .withHooks[Props] + .useState(0) + .render((_, s) => s.value.toString) +``` diff --git a/doc/changelog/3.0.0-betas.md b/doc/changelog/3.0.0-betas.md new file mode 100644 index 000000000..287d006b6 --- /dev/null +++ b/doc/changelog/3.0.0-betas.md @@ -0,0 +1,102 @@ +# 3.0.0-beta*x* + +# Changes in beta1 (released as 2.2.0-beta1) + +- Upgrade to React 18. + + - List of implemented features is here: https://github.com/japgolly/scalajs-react/issues/1057 + - For the moment, using class components emits the `Until you switch to the new API, your app will behave as if it’s running React 17.` warning, even if the app was initialized the React 18 way (see [here](https://react.dev/blog/2022/03/08/react-18-upgrade-guide#updates-to-client-rendering-apis)). + +- Addition of `React.majorVersion: Int`. + +- Added a new typeclass `Renderable`, now required to invoke `render*` and `hydrate*` methods. Instances are provided for the expected types. + +- Dependencies updates: + + - Scala.js upgraded to 1.15.0 + - microlibs upgraded to 4.2.1 + - Scala 3 upgraded to 3.1.2 + +- Added test utilities: + + - `TestContainer`/`TestReactRoot` and `.{outer,inner}HTML.assert*` + - `WithDsl.*` + - `replaceProps` replacement + +- To upgrade when using `jsDependencies`, make your sbt config look like this (comments for clarity) + + ```scala + // Required for React 18.3.1 + dependencyOverrides += "org.webjars.npm" % "scheduler" % "0.22.0", + + jsDependencies ++= Seq( + // Polyfill required for React 18.3.1 + "org.webjars.npm" % "fast-text-encoding" % "1.0.6" / "text.min.js" minified "text.min.js" + + "org.webjars.npm" % "react" % "18.3.1" + / "umd/react.development.js" + minified "umd/react.production.min.js" + dependsOn "text.min.js" // <-- Load the fast-text-encoding polyfill before loading React itself + commonJSName "React", + + "org.webjars.npm" % "react-dom" % "18.3.1" + / "umd/react-dom.development.js" + minified "umd/react-dom.production.min.js" + dependsOn "umd/react.development.js" + commonJSName "ReactDOM", + + "org.webjars.npm" % "react-dom" % "18.3.1" + / "umd/react-dom-server.browser.development.js" + minified "umd/react-dom-server.browser.production.min.js" + dependsOn "umd/react-dom.development.js" + commonJSName "ReactDOMServer", + ), + ``` + +# Changes in beta2 (released as 2.2.0-beta2) + +- Fix initialization issue due to `renderWithReuse` hooks using functional component with `React.memo`. + +# Changes in beta3 + +- Add `useTransition` and `useId` hooks. +- Add `startTransition` React API. + +- `ScalaFnComponent` and `ScalaForwardRef` now support `.withDisplayName(name)`. Must be called first (before hooks/`render*`/`withChildren*`). + +Example: + +```scala + ScalaFnComponent.withDisplayName("MyComponent") + .withHooks[Props] + .useState(0) + .render((_, s) => s.value.toString) +``` + +If not specified, a unique name will be inferred from the fully qualified name of the call site. + +# Changes in beta4 + +- Add documentation. + +# Changes in beta5 + +- Improve default display names. + +# Changes in beta6 + +- Wrap router actions in `startTransition`, making the change of pages interruptible. + +# Chengas in beta7 + +- Allow creating custom `Route` instances outside the DSL model. A `Route` just wraps functions to turn a `Path` into a potential page and vice-versa. +- Update some dependencies. + +# Changes in beta8 + +- Monadic hooks + +# Changes in beta9 + +- `useDeferredValue`, `useSyncExternalStore`, `useInsertionEffects`. +- Allow calling `useCallback*` with sync functions that return a value. \ No newline at end of file diff --git a/doc/changelog/3.0.0.md b/doc/changelog/3.0.0.md new file mode 100644 index 000000000..f0c66e0a4 --- /dev/null +++ b/doc/changelog/3.0.0.md @@ -0,0 +1,50 @@ +# 2.2.0 + +TODO: Add demo/doc for new testing API + +- `TestContainer`/`TestReactRoot` and `.{outer,inner}HTML.assert*` +- `WithDsl.*` +- `replaceProps` replacement + +- `React.majorVersion` + +- Added a new typeclass `Renderable` + +- Upgrade to React 18 + +- `LegacyReactTestUtils` + +- Scala.js upgraded to 1.15.0 +- microlibs upgraded to 4.2.1 +- Scala 3 upgraded to 3.1.2 + +- To upgrade when using `jsDependencies`, make your sbt config look like this (comments for clarity) + + ```scala + // Required for React 18.0.0 + dependencyOverrides += "org.webjars.npm" % "scheduler" % "0.22.0", + + jsDependencies ++= Seq( + + // Polyfill required for React 18.0.0 + "org.webjars.npm" % "fast-text-encoding" % "1.0.3" / "text.js" minified "text.min.js" + + "org.webjars.npm" % "react" % "18.0.0" + / "umd/react.development.js" + minified "umd/react.production.min.js" + dependsOn "text.js" // <-- Load the fast-text-encoding polyfill before loading React itself + commonJSName "React", + + "org.webjars.npm" % "react-dom" % "18.0.0" + / "umd/react-dom.development.js" + minified "umd/react-dom.production.min.js" + dependsOn "umd/react.development.js" + commonJSName "ReactDOM", + + "org.webjars.npm" % "react-dom" % "18.0.0" + / "umd/react-dom-server.browser.development.js" + minified "umd/react-dom-server.browser.production.min.js" + dependsOn "umd/react-dom.development.js" + commonJSName "ReactDOMServer", + ), + ``` diff --git a/doc/changelog/next.md b/doc/changelog/next.md index b9ca2adff..a58648d72 100644 --- a/doc/changelog/next.md +++ b/doc/changelog/next.md @@ -1,7 +1,6 @@ # 2.2.0 - -* Scala.js upgraded to 1.10.0 -* microlibs upgraded to 4.1.0 -* Scala 3 upgraded to 3.1.2 -* Fixed the source map uri to point to the correct location +- Scala.js upgraded to 1.15.0 +- microlibs upgraded to 4.2.1 +- Scala 3 upgraded to 3.1.2 +- Fixed the source map uri to point to the correct location diff --git a/downstream-tests/js-cbio/src/test/scala/downstream/CBIOBundleTests.scala b/downstream-tests/js-cbio/src/test/scala/downstream/CBIOBundleTests.scala index abf845f10..70ce4b42a 100644 --- a/downstream-tests/js-cbio/src/test/scala/downstream/CBIOBundleTests.scala +++ b/downstream-tests/js-cbio/src/test/scala/downstream/CBIOBundleTests.scala @@ -2,7 +2,7 @@ package downstream import concurrent.ExecutionContext.Implicits.global import japgolly.microlibs.testutil.TestUtil._ -import japgolly.scalajs.react.test.ReactTestUtils._ +import japgolly.scalajs.react.test.ReactTestUtils2 import scala.concurrent.Future import scala.concurrent.Promise import scalajs.js @@ -22,10 +22,10 @@ object CBIOBundleTests extends TestSuite { Globals.clear() "catnip" - { - withRenderedIntoDocumentFuture(Catnip.Component("omg")) { m => + ReactTestUtils2.withRendered(Catnip.Component("omg")).future { m => delay(500).map { _ => assertEq(Globals.catnipMounts, List("omg")) - assertEq(m.showDom(), "
Hello(1) omg
") + m.outerHTML.assert("
Hello(1) omg
") } }.map { _ => assertEq(Globals.catnipMounts, List("omg")) diff --git a/downstream-tests/js-ce/src/test/scala/downstream/CatsEffectBundleTests.scala b/downstream-tests/js-ce/src/test/scala/downstream/CatsEffectBundleTests.scala index c9ce3b5c0..5a32471b5 100644 --- a/downstream-tests/js-ce/src/test/scala/downstream/CatsEffectBundleTests.scala +++ b/downstream-tests/js-ce/src/test/scala/downstream/CatsEffectBundleTests.scala @@ -1,7 +1,7 @@ package downstream import japgolly.microlibs.testutil.TestUtil._ -import japgolly.scalajs.react.test.ReactTestUtils._ +import japgolly.scalajs.react.test.ReactTestUtils2 import utest._ object CatsEffectBundleTests extends TestSuite { @@ -10,9 +10,9 @@ object CatsEffectBundleTests extends TestSuite { Globals.clear() "catnip" - { - withRenderedIntoDocument(Catnip.Component("omg")) { m => + ReactTestUtils2.withRendered(Catnip.Component("omg")) { m => assertEq(Globals.catnipMounts, List("omg")) - assertEq(m.showDom(), "
Hello omg
") + m.outerHTML.assert("
Hello omg
") } assertEq(Globals.catnipMounts, List("omg")) } diff --git a/downstream-tests/js/src/test/scala/downstream/MimaTests.scala b/downstream-tests/js/src/test/scala/downstream/MimaTests.scala index 79776ca2a..9afb8992a 100644 --- a/downstream-tests/js/src/test/scala/downstream/MimaTests.scala +++ b/downstream-tests/js/src/test/scala/downstream/MimaTests.scala @@ -7,14 +7,14 @@ object MimaTests extends TestSuite { override def tests = Tests { - "2_0_0" - { - import mima200._ + // "2_0_0" - { + // import mima200._ - "HookUseRef" - HookUseRef.test { ref => - val a = ref.value - val b = ref.map(_ + 1).unsafeGet() - assertEq(b, a + 1) - } - } + // "HookUseRef" - HookUseRef.test { ref => + // val a = ref.value + // val b = ref.map(_ + 1).unsafeGet() + // assertEq(b, a + 1) + // } + // } } } diff --git a/downstream-tests/js/src/test/scala/downstream/RuntimeTests.scala b/downstream-tests/js/src/test/scala/downstream/RuntimeTests.scala index d1642113c..2a148a7c2 100644 --- a/downstream-tests/js/src/test/scala/downstream/RuntimeTests.scala +++ b/downstream-tests/js/src/test/scala/downstream/RuntimeTests.scala @@ -5,14 +5,13 @@ import japgolly.microlibs.compiletime.CompileTimeInfo import japgolly.microlibs.testutil.TestUtil._ import japgolly.scalajs.react._ import japgolly.scalajs.react.vdom.html_<^._ -import japgolly.scalajs.react.test.ReactTestUtils._ +import japgolly.scalajs.react.test.ReactTestUtils2 import japgolly.scalajs.react.util.JsUtil import org.scalajs.dom.console import scala.scalajs.js import scala.scalajs.LinkingInfo.developmentMode import scala.util.Try import utest._ -import japgolly.scalajs.react.test.ReactTestUtils object RuntimeTests extends TestSuite { @@ -65,13 +64,13 @@ object RuntimeTests extends TestSuite { val (promise, completePromise) = JsUtil.newPromise[Unit]() val io = IO(completePromise(Try(()))()) - withRenderedIntoDocument(Carrot.Props("1", io).render) { m => - replaceProps(Carrot.Component, m)(Carrot.Props("1")) - replaceProps(Carrot.Component, m)(Carrot.Props("2")) + ReactTestUtils2.withRendered(Carrot.Props("1", io).render) { m => + m.root.render(Carrot.Props("1").render) + m.root.render(Carrot.Props("2").render) } - withRenderedIntoDocument(Pumpkin.Component("1")) { m => - replaceProps(Pumpkin.Component, m)("1") - replaceProps(Pumpkin.Component, m)("2") + ReactTestUtils2.withRendered(Pumpkin.Component("1")) { m => + m.root.render(Pumpkin.Component("1")) + m.root.render(Pumpkin.Component("2")) } assertEq(Globals.carrotMountsA, 1) @@ -91,13 +90,13 @@ object RuntimeTests extends TestSuite { "react" - { val c = ScalaFnComponent[Int](i => <.p(<.td(s"i = $i"))) - val t = Try(ReactTestUtils.withRenderedIntoBody(c(123))(_ => ())) + val t = Try(ReactTestUtils2.withRendered(c(123))(_ => ())) assertEq(t.isFailure, testWarningsReact.contains("react")) } "unlreated" - { val c = ScalaFnComponent[Int](i => <.p(s"i = $i")) - val t = Try(ReactTestUtils.withRenderedIntoBody(c(123)) { _ => + val t = Try(ReactTestUtils2.withRendered(c(123)) { _ => console.info(".") console.log(".") console.warn(".") diff --git a/downstream-tests/mima-2.0.0/src/main/scala/DomTester.scala b/downstream-tests/mima-2.0.0/src/main/scala/DomTester.scala deleted file mode 120000 index 0a97dff66..000000000 --- a/downstream-tests/mima-2.0.0/src/main/scala/DomTester.scala +++ /dev/null @@ -1 +0,0 @@ -../../../../../library/tests/src/test/scala/japgolly/scalajs/react/test/DomTester.scala \ No newline at end of file diff --git a/downstream-tests/mima-2.0.0/src/main/scala/downstream/mima200/DomTester.scala b/downstream-tests/mima-2.0.0/src/main/scala/downstream/mima200/DomTester.scala new file mode 100644 index 000000000..a9a417ca9 --- /dev/null +++ b/downstream-tests/mima-2.0.0/src/main/scala/downstream/mima200/DomTester.scala @@ -0,0 +1,48 @@ +package japgolly.scalajs.react.test + +import japgolly.microlibs.testutil.TestUtil._ +import japgolly.scalajs.react._ +import japgolly.scalajs.react.test.ReactTestUtils._ +import japgolly.scalajs.react.test._ +import org.scalajs.dom.html.{Button, Element, Input} +import sourcecode.Line + +object DomTester { + val tagRegex = "<[a-zA-Z].*?>|".r + + def getText(e: Element): String = + tagRegex.replaceAllIn(e.innerHTML, "").trim + + def assertText(e: Element, expect: String)(implicit l: Line): Unit = + assertEq(getText(e), expect) +} + +// ===================================================================================================================== + +class DomTester(root: Element) { + + def assertText(expect: String)(implicit l: Line): Unit = + DomTester.assertText(root, expect) + + def clickButton(n: Int = 1): Unit = { + val bs = root.querySelectorAll("button") + assert(n > 0 && n <= bs.length, s"${bs.length} buttons found (n=$n)") + val b = bs(n - 1).asInstanceOf[Button] + act(Simulate.click(b)) + } + + def assertInputText(expect: String)(implicit l: Line): Unit = + assertEq(getInputText().value, expect) + + def setInputText(t: String): Unit = { + val i = getInputText() + act(SimEvent.Change(t).simulate(i)) + } + + private def getInputText(): Input = { + val is = root.querySelectorAll("input[type=text]") + val len = is.length + assert(len == 1) + is(0).domCast[Input] + } +} diff --git a/downstream-tests/scalafix.sbt b/downstream-tests/scalafix.sbt index 094b38fc1..3515f51a1 100644 --- a/downstream-tests/scalafix.sbt +++ b/downstream-tests/scalafix.sbt @@ -10,7 +10,7 @@ ThisBuild / scalacOptions ++= { ThisBuild / semanticdbEnabled := true -ThisBuild / semanticdbVersion := "4.5.9" +ThisBuild / semanticdbVersion := "4.12.0" ThisBuild / scalafixScalaBinaryVersion := "2.13" diff --git a/flake.lock b/flake.lock new file mode 100644 index 000000000..055567cf7 --- /dev/null +++ b/flake.lock @@ -0,0 +1,109 @@ +{ + "nodes": { + "devshell": { + "inputs": { + "nixpkgs": [ + "typelevel-nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1728330715, + "narHash": "sha256-xRJ2nPOXb//u1jaBnDP56M7v5ldavjbtR6lfGqSvcKg=", + "owner": "numtide", + "repo": "devshell", + "rev": "dd6b80932022cea34a019e2bb32f6fa9e494dfef", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "devshell", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1733376361, + "narHash": "sha256-aLJxoTDDSqB+/3orsulE6/qdlX6MzDLIITLZqdgMpqo=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "929116e316068c7318c54eb4d827f7d9756d5e9c", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": [ + "typelevel-nix", + "flake-utils" + ], + "nixpkgs": [ + "typelevel-nix", + "nixpkgs" + ], + "typelevel-nix": "typelevel-nix" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "typelevel-nix": { + "inputs": { + "devshell": "devshell", + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + }, + "locked": { + "lastModified": 1733783241, + "narHash": "sha256-nDM0W3EhiVJDFPCInPzJ9+QUyyjMTZhlTK8JGN7vfJQ=", + "owner": "typelevel", + "repo": "typelevel-nix", + "rev": "a4a46f0b1b94e150a1dda0a8f0f013696d9299fd", + "type": "github" + }, + "original": { + "owner": "typelevel", + "repo": "typelevel-nix", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 000000000..e6885acf5 --- /dev/null +++ b/flake.nix @@ -0,0 +1,43 @@ +{ + inputs = { + typelevel-nix.url = "github:typelevel/typelevel-nix"; + nixpkgs.follows = "typelevel-nix/nixpkgs"; + flake-utils.follows = "typelevel-nix/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils, typelevel-nix }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs-x86_64 = import nixpkgs { system = "x86_64-darwin"; }; + scala-cli-overlay = final: prev: { scala-cli = pkgs-x86_64.scala-cli; }; + pkgs = import nixpkgs { + inherit system; + overlays = [ typelevel-nix.overlays.default scala-cli-overlay]; + }; + in + { + devShell = pkgs.devshell.mkShell { + imports = [ typelevel-nix.typelevelShell ]; + packages = [ + pkgs.nodePackages.typescript-language-server + pkgs.nodePackages.vscode-langservers-extracted + pkgs.nodePackages.prettier + pkgs.nodePackages.typescript + pkgs.nodePackages.graphqurl + pkgs.hasura-cli + ]; + typelevelShell = { + nodejs.enable = true; + jdk.package = pkgs.jdk11; + }; + env = [ + { + name = "NODE_OPTIONS"; + value = "--max-old-space-size=8192"; + } + ]; + }; + } + + ); +} diff --git a/library/callback/src/main/scala-2/japgolly/scalajs/react/callback/Callback.scala b/library/callback/src/main/scala-2/japgolly/scalajs/react/callback/Callback.scala index 15dab917a..30509f96d 100644 --- a/library/callback/src/main/scala-2/japgolly/scalajs/react/callback/Callback.scala +++ b/library/callback/src/main/scala-2/japgolly/scalajs/react/callback/Callback.scala @@ -1,5 +1,6 @@ package japgolly.scalajs.react.callback +import japgolly.scalajs.react.util.Trampoline import japgolly.scalajs.react.util.Util.identityFn import java.time.Duration import org.scalajs.dom.{console, window} diff --git a/library/callback/src/main/scala-2/japgolly/scalajs/react/callback/CallbackTo.scala b/library/callback/src/main/scala-2/japgolly/scalajs/react/callback/CallbackTo.scala index b607bd63a..10a63b7b2 100644 --- a/library/callback/src/main/scala-2/japgolly/scalajs/react/callback/CallbackTo.scala +++ b/library/callback/src/main/scala-2/japgolly/scalajs/react/callback/CallbackTo.scala @@ -2,8 +2,8 @@ package japgolly.scalajs.react.callback import japgolly.scalajs.react.callback.CallbackTo.MapGuard import japgolly.scalajs.react.util.Effect.Sync -import japgolly.scalajs.react.util.JsUtil import japgolly.scalajs.react.util.Util.{catchAll, identityFn} +import japgolly.scalajs.react.util.{JsUtil, Trampoline} import java.time.{Duration, Instant} import org.scalajs.dom.{Window, window} import scala.annotation.tailrec diff --git a/library/callback/src/main/scala-3/japgolly/scalajs/react/callback/Callback.scala b/library/callback/src/main/scala-3/japgolly/scalajs/react/callback/Callback.scala index 1f1a1c18a..82863d57a 100644 --- a/library/callback/src/main/scala-3/japgolly/scalajs/react/callback/Callback.scala +++ b/library/callback/src/main/scala-3/japgolly/scalajs/react/callback/Callback.scala @@ -1,5 +1,6 @@ package japgolly.scalajs.react.callback +import japgolly.scalajs.react.util.Trampoline import japgolly.scalajs.react.util.Util.identityFn import java.time.Duration import org.scalajs.dom.{console, window} diff --git a/library/callback/src/main/scala-3/japgolly/scalajs/react/callback/CallbackOption.scala b/library/callback/src/main/scala-3/japgolly/scalajs/react/callback/CallbackOption.scala index 0948c371c..5120167c3 100644 --- a/library/callback/src/main/scala-3/japgolly/scalajs/react/callback/CallbackOption.scala +++ b/library/callback/src/main/scala-3/japgolly/scalajs/react/callback/CallbackOption.scala @@ -183,7 +183,7 @@ final class CallbackOption[+A](val underlyingRepr: CallbackOption.UnderlyingRepr def asCallback: CallbackTo[Option[A]] = CallbackTo lift cbfn - inline def map[B](f: A => B)(using inline ev: MapGuard[B]): CallbackOption[ev.Out] = + inline def map[B](f: A => B)(using ev: MapGuard[B]): CallbackOption[ev.Out] = unsafeMap(f) private[react] def unsafeMap[B](f: A => B): CallbackOption[B] = @@ -192,7 +192,7 @@ final class CallbackOption[+A](val underlyingRepr: CallbackOption.UnderlyingRepr /** * Alias for `map`. */ - inline def |>[B](f: A => B)(using inline ev: MapGuard[B]): CallbackOption[ev.Out] = + inline def |>[B](f: A => B)(using ev: MapGuard[B]): CallbackOption[ev.Out] = map(f) def flatMapOption[B](f: A => Option[B]): CallbackOption[B] = diff --git a/library/callback/src/main/scala-3/japgolly/scalajs/react/callback/CallbackTo.scala b/library/callback/src/main/scala-3/japgolly/scalajs/react/callback/CallbackTo.scala index 1a80ef18c..17c4a32e2 100644 --- a/library/callback/src/main/scala-3/japgolly/scalajs/react/callback/CallbackTo.scala +++ b/library/callback/src/main/scala-3/japgolly/scalajs/react/callback/CallbackTo.scala @@ -3,6 +3,7 @@ package japgolly.scalajs.react.callback import japgolly.scalajs.react.callback.CallbackTo.MapGuard import japgolly.scalajs.react.util.Effect.Sync import japgolly.scalajs.react.util.JsUtil +import japgolly.scalajs.react.util.Trampoline import japgolly.scalajs.react.util.Util.{catchAll, identityFn} import java.time.{Duration, Instant} import org.scalajs.dom.Window @@ -306,11 +307,11 @@ final class CallbackTo[+A] /*private[react]*/ (private[CallbackTo] val trampolin inline def runNow(): A = trampoline.run - inline def map[B](f: A => B)(using inline ev: MapGuard[B]): CallbackTo[ev.Out] = + inline def map[B](f: A => B)(using ev: MapGuard[B]): CallbackTo[ev.Out] = new CallbackTo(trampoline.map(f)) /** Alias for `map`. */ - inline def |>[B](inline f: A => B)(using inline ev: MapGuard[B]): CallbackTo[ev.Out] = + inline def |>[B](inline f: A => B)(using ev: MapGuard[B]): CallbackTo[ev.Out] = map(f) inline def flatMap[B](f: A => CallbackTo[B]): CallbackTo[B] = @@ -589,7 +590,7 @@ final class CallbackTo[+A] /*private[react]*/ (private[CallbackTo] val trampolin /** Convenience-method to run additional code after this callback. */ - inline def thenRun[B](inline runNext: B)(using inline ev: MapGuard[B]): CallbackTo[ev.Out] = + inline def thenRun[B](inline runNext: B)(using ev: MapGuard[B]): CallbackTo[ev.Out] = this >> CallbackTo(runNext) /** Convenience-method to run additional code before this callback. */ diff --git a/library/coreBundleCallback/src/main/scala/japgolly/scalajs/react/internal/ReactCallbackExtensions.scala b/library/coreBundleCallback/src/main/scala/japgolly/scalajs/react/internal/ReactCallbackExtensions.scala index 40bd4e324..7a54ed01d 100644 --- a/library/coreBundleCallback/src/main/scala/japgolly/scalajs/react/internal/ReactCallbackExtensions.scala +++ b/library/coreBundleCallback/src/main/scala/japgolly/scalajs/react/internal/ReactCallbackExtensions.scala @@ -1,6 +1,7 @@ package japgolly.scalajs.react.internal import japgolly.scalajs.react.callback._ +import japgolly.scalajs.react.util.Trampoline import japgolly.scalajs.react.{ReactEventTypes, Reusability, Reusable} import scala.annotation.nowarn diff --git a/library/coreGeneric/src/main/scala-2/japgolly/scalajs/react/vdom/VdomNode.scala b/library/coreGeneric/src/main/scala-2/japgolly/scalajs/react/vdom/VdomNode.scala index c1dbdb8be..f3b0a946e 100644 --- a/library/coreGeneric/src/main/scala-2/japgolly/scalajs/react/vdom/VdomNode.scala +++ b/library/coreGeneric/src/main/scala-2/japgolly/scalajs/react/vdom/VdomNode.scala @@ -10,6 +10,7 @@ trait VdomNode extends TagMod { override def applyTo(b: VdomBuilder): Unit = b.appendChild(rawNode) + @deprecated("Use ReactDOM.createRoot and root.render instead", "2.2.0 / React v18") @inline final def renderIntoDOM(container: facade.ReactDOM.Container): facade.React.ComponentUntyped = facade.ReactDOM.render(rawNode, container) diff --git a/library/coreGeneric/src/main/scala-3/japgolly/scalajs/react/vdom/VdomNode.scala b/library/coreGeneric/src/main/scala-3/japgolly/scalajs/react/vdom/VdomNode.scala index 7938f5083..3c17b64bc 100644 --- a/library/coreGeneric/src/main/scala-3/japgolly/scalajs/react/vdom/VdomNode.scala +++ b/library/coreGeneric/src/main/scala-3/japgolly/scalajs/react/vdom/VdomNode.scala @@ -10,6 +10,7 @@ trait VdomNode extends TagMod { override def applyTo(b: VdomBuilder): Unit = b.appendChild(rawNode) + @deprecated("Use ReactDOM.createRoot and root.render instead", "2.2.0 / React v18") inline final def renderIntoDOM(container: facade.ReactDOM.Container): facade.React.ComponentUntyped = facade.ReactDOM.render(rawNode, container) diff --git a/library/coreGeneric/src/main/scala/japgolly/scalajs/react/ComponentDom.scala b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/ComponentDom.scala index 45cbc8b01..d2cd88845 100644 --- a/library/coreGeneric/src/main/scala/japgolly/scalajs/react/ComponentDom.scala +++ b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/ComponentDom.scala @@ -1,7 +1,7 @@ package japgolly.scalajs.react import japgolly.scalajs.react.util.DomUtil._ -import japgolly.scalajs.react.util.Util.{catchAll, identityFn} +import japgolly.scalajs.react.util.Util.identityFn import org.scalajs.dom import org.scalajs.dom.html import scala.scalajs.js.| @@ -15,8 +15,8 @@ sealed trait ComponentDom { final def asMounted(): ComponentDom.Mounted = mounted getOrElse sys.error("DOM node isn't mounted.") - def toElement: Option[dom.Element] = None - def toText: Option[dom.Text] = None + def toElement: Option[dom.Element] = + None final def toHtml: Option[html.Element] = toElement.flatMap(_.domToHtml) @@ -24,11 +24,17 @@ sealed trait ComponentDom { final def toNode: Option[dom.Node] = mounted.map(_.node) + final def toText: Option[dom.Text] = + mounted.flatMap { + case Node(t: dom.Text) => Some(t) + case _ => None + } + /** For testing purposes. */ final def show(sanitiseHtml: String => String = identityFn): String = mounted match { case Some(Element(e)) => sanitiseHtml(e.outerHTML) - case Some(Text(t)) => (catchAll(t.wholeText) orElse catchAll(t.textContent) orElse catchAll(t.innerText)).get + case Some(Node(n)) => n.nodeValue case None => "" } } @@ -38,7 +44,7 @@ object ComponentDom { def apply(i: facade.ReactDOM.DomNode | Null | Unit): ComponentDom = (i: Any) match { case e: dom.Element => Element(e) - case t: dom.Text => Text(t) + case n: dom.Node => Node(n) case null | () => Unmounted } @@ -63,7 +69,7 @@ object ComponentDom { final def asElement(): dom.Element = this match { case Element(e) => e - case Text(t) => sys error s"Expected a dom.Element; got $t" + case x => throw new RuntimeException(s"Expected a dom.Element; got ${x.raw}") } /** unsafe! may throw an exception */ @@ -76,11 +82,12 @@ object ComponentDom { /** unsafe! may throw an exception */ final def asText(): dom.Text = - this match { - case Text(t) => t - case Element(e) => sys error s"Expected a dom.Text; got $e" + node match { + case t: dom.Text => t + case n => throw new RuntimeException(s"Expected a dom.Text; got $n") } + @deprecated("Call .node and pattern match as needed", "2.2.0") def fold[A](text: dom.Text => A, element: dom.Element => A): A } @@ -88,14 +95,20 @@ object ComponentDom { override def toElement = Some(element) override def node = element override def raw = element + @deprecated("Call .node and pattern match as needed", "2.2.0") override def fold[A](text: dom.Text => A, f: dom.Element => A) = f(element) } - final case class Text(text: dom.Text) extends Mounted { - override def toText = Some(text) - override def node = text - override def raw = text - override def fold[A](f: dom.Text => A, element: dom.Element => A) = f(text) + final case class Node(node: dom.Node) extends Mounted { + override def raw = node + @deprecated("Call .node and pattern match as needed", "2.2.0") + override def fold[A](text: dom.Text => A, element: dom.Element => A) = + node match { + case t: dom.Text => text(t) + case _ => throw new RuntimeException(s"The .fold method is now deprecated and doesn't support non-Text, non-Element nodes") + } } + @deprecated("Use Node instead", "2.2.0") type Text = Node + @deprecated("Use Node instead", "2.2.0") val Text = Node } diff --git a/library/coreGeneric/src/main/scala/japgolly/scalajs/react/React.scala b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/React.scala index e820bda64..7f2d2efeb 100644 --- a/library/coreGeneric/src/main/scala/japgolly/scalajs/react/React.scala +++ b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/React.scala @@ -2,7 +2,7 @@ package japgolly.scalajs.react import japgolly.scalajs.react.internal.Box import japgolly.scalajs.react.internal.CoreGeneral._ -import japgolly.scalajs.react.util.Effect.Async +import japgolly.scalajs.react.util.Effect.{Async, Sync} import japgolly.scalajs.react.vdom.{VdomElement, VdomNode} import scala.scalajs.js @@ -10,6 +10,9 @@ object React { @inline def raw: facade.React = facade.React @inline def version: String = facade.React.version + lazy val majorVersion: Int = + version.takeWhile(_.isDigit).toInt + /** Create a new context. * * If you'd like to retain type information about the JS type used under-the-hood with React, @@ -57,6 +60,15 @@ object React { .cmapCtorProps[P](Box(_)) } + /** Similar to `useTransition` but allows uses where hooks are not available. + * + * @param callback A _synchronous_ function which causes state updates that can be deferred. + * + * @since 2.2.0 / React 18.0.0 + */ + def startTransition[F[_]](callback: => F[Unit])(implicit F: Sync[F]) = + F.delay(facade.React.startTransition(F.toJsFn(callback))) + val Profiler = feature.Profiler /** StrictMode is a tool for highlighting potential problems in an application. diff --git a/library/coreGeneric/src/main/scala/japgolly/scalajs/react/ReactDOM.scala b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/ReactDOM.scala index 8bf99a881..6f99d5d2d 100644 --- a/library/coreGeneric/src/main/scala/japgolly/scalajs/react/ReactDOM.scala +++ b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/ReactDOM.scala @@ -2,63 +2,56 @@ package japgolly.scalajs.react import japgolly.scalajs.react.util.Effect._ import japgolly.scalajs.react.util.NotAllowed -import japgolly.scalajs.react.vdom.VdomNode import org.scalajs.dom import scala.scalajs.js.| object ReactDOM { - def raw = facade.ReactDOM - def version = facade.ReactDOM.version + val raw = facade.ReactDOM + @inline def version = facade.ReactDOM.version /** For mounted components, use .getDOMNode */ def findDOMNode(componentOrElement: dom.Element | facade.React.ComponentUntyped): Option[ComponentDom.Mounted] = ComponentDom.findDOMNode(componentOrElement).mounted - def hydrate(element : VdomNode, - container: facade.ReactDOM.Container): facade.React.ComponentUntyped = - facade.ReactDOM.hydrate(element.rawNode, container) + def flushSync[F[_], A](fa: F[A])(implicit F: Sync[F]): F[A] = + F.delay(facade.ReactDOM.flushSync(F.toJsFn(fa))) - def hydrate[G[_]](element : VdomNode, - container: facade.ReactDOM.Container, - callback : => G[Unit])(implicit G: Dispatch[G]): facade.React.ComponentUntyped = - facade.ReactDOM.hydrate(element.rawNode, container, G.dispatchFn(callback)) + def hydrate[G[_], A](element: A, container: raw.Container, callback : => G[Unit]) + (implicit G: Dispatch[G], r: Renderable[A]): facade.React.ComponentUntyped = + facade.ReactDOM.hydrate(r(element), container, G.dispatchFn(callback)) /** Hydrate the container if is has children, else render into that container. */ - def hydrateOrRender(element : VdomNode, - container: dom.Element): facade.React.ComponentUntyped = + def hydrateOrRender[G[_], A](element: A, container: dom.Element, callback: => G[Unit]) + (implicit G: Dispatch[G], r: Renderable[A]): facade.React.ComponentUntyped = if (container.hasChildNodes()) - hydrate(element, container) + hydrate(element, container, callback) else - element.renderIntoDOM(container) + raw.render(r(element), container, G.dispatchFn(callback)) + + // =================================================================================================================== + // Deprecated stuff + + @deprecated("Import vdom and use ReactPortal()", "") + def createPortal(child: NotAllowed, container: Any) = child.result + + @deprecated("Use hydrateRoot instead", "2.2.0 / React v18") + def hydrate[A](element: A, container: raw.Container)(implicit r: Renderable[A]): facade.React.ComponentUntyped = + facade.ReactDOM.hydrate(r(element), container) /** Hydrate the container if is has children, else render into that container. */ - def hydrateOrRender[G[_]](element : VdomNode, - container: dom.Element, - callback : => G[Unit])(implicit G: Dispatch[G]): facade.React.ComponentUntyped = + @deprecated("Use hydrateOrRenderIntoNewRoot instead", "2.2.0 / React v18") + def hydrateOrRender[A](element: A, container: dom.Element)(implicit r: Renderable[A]): facade.React.ComponentUntyped = if (container.hasChildNodes()) - hydrate(element, container, callback) + hydrate(element, container) else - element.renderIntoDOM(container, callback) + raw.render(r(element), container) - def unmountComponentAtNode(container: dom.Node): Boolean = - raw.unmountComponentAtNode(container) - - // .hydrate is not here because currently, SSR with scalajs-react isn't directly supported. - // .raw.hydrate can be used if needed. - - // There are three ways of providing this functionality: - // 1. ReactDOM.render here (problem: lose return type precision) - // 2. ReactDOM.render{Js,Scala,Vdom,etc} - // 3. Add .renderIntoDOM to mountable types (current solution) - @deprecated("Use .renderIntoDOM on unmounted components.", "") + @deprecated("Use createRoot and root.render instead", "2.2.0 / React v18") def render(element : NotAllowed, container: Any, callback : Any = null) = element.result - @deprecated("Import vdom and use ReactPortal()", "") - def createPortal(child: NotAllowed, container: Any) = child.result - - def flushSync[F[_], A](fa: F[A])(implicit F: Sync[F]): F[A] = - F.delay(facade.ReactDOM.flushSync(F.toJsFn(fa))) - -} \ No newline at end of file + @deprecated("Use root.unmount() instead", "2.2.0 / React v18") + def unmountComponentAtNode(container: dom.Node): Boolean = + raw.unmountComponentAtNode(container) +} diff --git a/library/coreGeneric/src/main/scala/japgolly/scalajs/react/ReactDOMClient.scala b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/ReactDOMClient.scala new file mode 100644 index 000000000..b73fb3f23 --- /dev/null +++ b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/ReactDOMClient.scala @@ -0,0 +1,54 @@ +package japgolly.scalajs.react + +import org.scalajs.dom + +object ReactDOMClient { + val raw = facade.ReactDOMClient + + /** Create a React root for the supplied container and return the root. The root can be used to render a React element + * into the DOM with `.render`. + * + * @since v2.2.0 / React v18 + */ + def createRoot(container: raw.RootContainer): ReactRoot = + ReactRoot(raw.createRoot(container)) + + /** Create a React root for the supplied container and return the root. The root can be used to render a React element + * into the DOM with `.render`. + * + * @since v2.2.0 / React v18 + */ + def createRoot(container: raw.RootContainer, options: ReactOptions.CreateRoot): ReactRoot = + ReactRoot(raw.createRoot(container, options.raw())) + + /** Same as [[createRoot()]], but is used to hydrate a container whose HTML contents were rendered by + * [[ReactDOMServer]]. React will attempt to attach event listeners to the existing markup. + * + * @since v2.2.0 / React v18 + */ + def hydrateRoot[A](container: raw.HydrationContainer, element: A)(implicit r: Renderable[A]): ReactRoot = + ReactRoot(raw.hydrateRoot(container, r(element))) + + /** Same as [[createRoot()]], but is used to hydrate a container whose HTML contents were rendered by + * [[ReactDOMServer]]. React will attempt to attach event listeners to the existing markup. + * + * @since v2.2.0 / React v18 + */ + def hydrateRoot[A](container: raw.HydrationContainer, element: A, options: ReactOptions.HydrateRoot) + (implicit r: Renderable[A]): ReactRoot = + ReactRoot(raw.hydrateRoot(container, r(element), options.raw())) + + /** Hydrate the container if is has children, else render into that container. */ + def hydrateOrRenderIntoNewRoot[A](container : dom.Element, + element : A, + creationOptions : ReactOptions.CreateRoot = ReactOptions.CreateRoot(), + hydrationOptions: ReactOptions.HydrateRoot = ReactOptions.HydrateRoot(), + )(implicit r : Renderable[A]): ReactRoot = + if (container.hasChildNodes()) + hydrateRoot(container, element, hydrationOptions) + else { + val root = createRoot(container, creationOptions) + root.render(element) + root + } +} diff --git a/library/coreGeneric/src/main/scala/japgolly/scalajs/react/ReactDOMServer.scala b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/ReactDOMServer.scala index 9632286e6..997e2f42a 100644 --- a/library/coreGeneric/src/main/scala/japgolly/scalajs/react/ReactDOMServer.scala +++ b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/ReactDOMServer.scala @@ -3,8 +3,8 @@ package japgolly.scalajs.react import japgolly.scalajs.react.vdom.VdomNode object ReactDOMServer { - def raw = facade.ReactDOMServer - def version = facade.ReactDOMServer.version + @inline def raw = facade.ReactDOMServer + @inline def version = facade.ReactDOMServer.version /** * Render a React.Element to its initial HTML. This should only be used on the server. React will return an HTML diff --git a/library/coreGeneric/src/main/scala/japgolly/scalajs/react/ReactOptions.scala b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/ReactOptions.scala new file mode 100644 index 000000000..06d425791 --- /dev/null +++ b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/ReactOptions.scala @@ -0,0 +1,40 @@ +package japgolly.scalajs.react + +import scala.scalajs.js + +/** Classes for specifying options for React. */ +object ReactOptions { + + /** Options for [[ReactDOM.createRoot()]]. */ + final case class CreateRoot(identifierPrefix : js.UndefOr[String ] = js.undefined, + onRecoverableError : js.UndefOr[Any => Unit] = js.undefined, + unstable_concurrentUpdatesByDefault: js.UndefOr[Boolean ] = js.undefined, + unstable_strictMode : js.UndefOr[Boolean ] = js.undefined, + ) { self => + def raw(): facade.CreateRootOptions = { + val o = js.Dynamic.literal().asInstanceOf[facade.CreateRootOptions] + o.identifierPrefix = self.identifierPrefix + o.onRecoverableError = self.onRecoverableError + o.unstable_concurrentUpdatesByDefault = self.unstable_concurrentUpdatesByDefault + o.unstable_strictMode = self.unstable_strictMode + o + } + } + + /** Options for [[ReactDOM.hydrateRoot()]]. */ + final case class HydrateRoot(identifierPrefix : js.UndefOr[String ] = js.undefined, + onRecoverableError : js.UndefOr[Any => Unit] = js.undefined, + unstable_concurrentUpdatesByDefault: js.UndefOr[Boolean ] = js.undefined, + unstable_strictMode : js.UndefOr[Boolean ] = js.undefined, + ) { self => + def raw(): facade.HydrateRootOptions = { + val o = js.Dynamic.literal().asInstanceOf[facade.HydrateRootOptions] + o.identifierPrefix = self.identifierPrefix + o.onRecoverableError = self.onRecoverableError + o.unstable_concurrentUpdatesByDefault = self.unstable_concurrentUpdatesByDefault + o.unstable_strictMode = self.unstable_strictMode + o + } + } + +} diff --git a/library/coreGeneric/src/main/scala/japgolly/scalajs/react/ReactRoot.scala b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/ReactRoot.scala new file mode 100644 index 000000000..c7857313b --- /dev/null +++ b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/ReactRoot.scala @@ -0,0 +1,16 @@ +package japgolly.scalajs.react + +/** A location in the DOM into which React has initialised itself, and now manages. + * + * Can be used to render a React element into the DOM with `.render`. + * + * @since v2.2.0 / React 18 + */ +@inline final case class ReactRoot(raw: facade.RootType) { + + @inline def render[A](node: A)(implicit r: Renderable[A]): Unit = + raw.render(r(node)) + + @inline def unmount(): Unit = + raw.unmount() +} diff --git a/library/coreGeneric/src/main/scala/japgolly/scalajs/react/Renderable.scala b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/Renderable.scala new file mode 100644 index 000000000..922f09916 --- /dev/null +++ b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/Renderable.scala @@ -0,0 +1,28 @@ +package japgolly.scalajs.react + +import japgolly.scalajs.react.component.Generic.{UnmountedRaw => Component} +import japgolly.scalajs.react.vdom.VdomNode + +/** Typeclass for anything that React can render. + * + * @since v2.2.0 / React 18 + */ +@inline final case class Renderable[-A](raw: A => facade.React.Node) extends AnyVal { + @inline def apply(a: A): facade.React.Node = + raw(a) +} + +object Renderable { + + @inline implicit def long: Renderable[Long] = + Renderable(_.toString) + + @inline implicit def raw[A](implicit ev: A => facade.React.Node): Renderable[A] = + Renderable(ev) + + @inline implicit def vdom: Renderable[VdomNode] = + Renderable(_.rawNode) + + @inline implicit def component: Renderable[Component] = + Renderable(_.raw) +} diff --git a/library/coreGeneric/src/main/scala/japgolly/scalajs/react/component/Delayed.scala b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/component/Delayed.scala new file mode 100644 index 000000000..cb70776ad --- /dev/null +++ b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/component/Delayed.scala @@ -0,0 +1,97 @@ +package japgolly.scalajs.react.component + +import japgolly.scalajs.react.util.Trampoline +import japgolly.scalajs.react.vdom.VdomNode + +final class Delayed[+A] private[react] (private[Delayed] val trampoline: Trampoline[A]) extends AnyVal { self => + @inline def eval(): A = + trampoline.run + + @inline def map[B](f: A => B): Delayed[B] = + new Delayed(trampoline.map(f)) + + @inline def flatMap[B](f: A => Delayed[B]): Delayed[B] = + new Delayed(trampoline.flatMap(f.andThen(_.trampoline))) +} + +object Delayed { + @inline def apply[A](a: => A): Delayed[A] = + new Delayed(Trampoline.delay(() => a)) + + implicit class DelayedOps[I, O](private val h: I => Delayed[O]) extends AnyVal { + @inline def contramap[A](f: A => I): A => Delayed[O] = f andThen h + } + + @inline implicit def autoLift[A](a: => A)(implicit f: A => VdomNode): Delayed[VdomNode] = + Delayed(f(a)) + + @inline def fromFunction[O](f: () => O): Delayed[O] = + Delayed(f()) + + @inline def fromFunction[I, O](f: I => O): I => Delayed[O] = + i => Delayed(f(i)) + + @inline def fromFunction[I1, I2, O](f: (I1, I2) => O): (I1, I2) => Delayed[O] = + (i1, i2) => Delayed(f(i1, i2)) + + @inline def fromFunction[I1, I2, I3, O](f: (I1, I2, I3) => O): (I1, I2, I3) => Delayed[O] = + (i1, i2, i3) => Delayed(f(i1, i2, i3)) + + @inline def fromFunction[I1, I2, I3, I4, O](f: (I1, I2, I3, I4) => O): (I1, I2, I3, I4) => Delayed[O] = + (i1, i2, i3, i4) => Delayed(f(i1, i2, i3, i4)) + + @inline def fromFunction[I1, I2, I3, I4, I5, O](f: (I1, I2, I3, I4, I5) => O): (I1, I2, I3, I4, I5) => Delayed[O] = + (i1, i2, i3, i4, i5) => Delayed(f(i1, i2, i3, i4, i5)) + + @inline def fromFunction[I1, I2, I3, I4, I5, I6, O](f: (I1, I2, I3, I4, I5, I6) => O): (I1, I2, I3, I4, I5, I6) => Delayed[O] = + (i1, i2, i3, i4, i5, i6) => Delayed(f(i1, i2, i3, i4, i5, i6)) + + @inline def fromFunction[I1, I2, I3, I4, I5, I6, I7, O](f: (I1, I2, I3, I4, I5, I6, I7) => O): (I1, I2, I3, I4, I5, I6, I7) => Delayed[O] = + (i1, i2, i3, i4, i5, i6, i7) => Delayed(f(i1, i2, i3, i4, i5, i6, i7)) + + @inline def fromFunction[I1, I2, I3, I4, I5, I6, I7, I8, O](f: (I1, I2, I3, I4, I5, I6, I7, I8) => O): (I1, I2, I3, I4, I5, I6, I7, I8) => Delayed[O] = + (i1, i2, i3, i4, i5, i6, i7, i8) => Delayed(f(i1, i2, i3, i4, i5, i6, i7, i8)) + + @inline def fromFunction[I1, I2, I3, I4, I5, I6, I7, I8, I9, O](f: (I1, I2, I3, I4, I5, I6, I7, I8, I9) => O): (I1, I2, I3, I4, I5, I6, I7, I8, I9) => Delayed[O] = + (i1, i2, i3, i4, i5, i6, i7, i8, i9) => Delayed(f(i1, i2, i3, i4, i5, i6, i7, i8, i9)) + + @inline def fromFunction[I1, I2, I3, I4, I5, I6, I7, I8, I9, I10, O](f: (I1, I2, I3, I4, I5, I6, I7, I8, I9, I10) => O): (I1, I2, I3, I4, I5, I6, I7, I8, I9, I10) => Delayed[O] = + (i1, i2, i3, i4, i5, i6, i7, i8, i9, i10) => Delayed(f(i1, i2, i3, i4, i5, i6, i7, i8, i9, i10)) + + @inline def fromFunction[I1, I2, I3, I4, I5, I6, I7, I8, I9, I10, I11, O](f: (I1, I2, I3, I4, I5, I6, I7, I8, I9, I10, I11) => O): (I1, I2, I3, I4, I5, I6, I7, I8, I9, I10, I11) => Delayed[O] = + (i1, i2, i3, i4, i5, i6, i7, i8, i9, i10, i11) => Delayed(f(i1, i2, i3, i4, i5, i6, i7, i8, i9, i10, i11)) + + @inline def fromFunction[I1, I2, I3, I4, I5, I6, I7, I8, I9, I10, I11, I12, O](f: (I1, I2, I3, I4, I5, I6, I7, I8, I9, I10, I11, I12) => O): (I1, I2, I3, I4, I5, I6, I7, I8, I9, I10, I11, I12) => Delayed[O] = + (i1, i2, i3, i4, i5, i6, i7, i8, i9, i10, i11, i12) => Delayed(f(i1, i2, i3, i4, i5, i6, i7, i8, i9, i10, i11, i12)) + + @inline def fromFunction[I1, I2, I3, I4, I5, I6, I7, I8, I9, I10, I11, I12, I13, O](f: (I1, I2, I3, I4, I5, I6, I7, I8, I9, I10, I11, I12, I13) => O): (I1, I2, I3, I4, I5, I6, I7, I8, I9, I10, I11, I12, I13) => Delayed[O] = + (i1, i2, i3, i4, i5, i6, i7, i8, i9, i10, i11, i12, i13) => Delayed(f(i1, i2, i3, i4, i5, i6, i7, i8, i9, i10, i11, i12, i13)) + + @inline def fromFunction[I1, I2, I3, I4, I5, I6, I7, I8, I9, I10, I11, I12, I13, I14, O](f: (I1, I2, I3, I4, I5, I6, I7, I8, I9, I10, I11, I12, I13, I14) => O): (I1, I2, I3, I4, I5, I6, I7, I8, I9, I10, I11, I12, I13, I14) => Delayed[O] = + (i1, i2, i3, i4, i5, i6, i7, i8, i9, i10, i11, i12, i13, i14) => Delayed(f(i1, i2, i3, i4, i5, i6, i7, i8, i9, i10, i11, i12, i13, i14)) + + @inline def fromFunction[I1, I2, I3, I4, I5, I6, I7, I8, I9, I10, I11, I12, I13, I14, I15, O](f: (I1, I2, I3, I4, I5, I6, I7, I8, I9, I10, I11, I12, I13, I14, I15) => O): (I1, I2, I3, I4, I5, I6, I7, I8, I9, I10, I11, I12, I13, I14, I15) => Delayed[O] = + (i1, i2, i3, i4, i5, i6, i7, i8, i9, i10, i11, i12, i13, i14, i15) => Delayed(f(i1, i2, i3, i4, i5, i6, i7, i8, i9, i10, i11, i12, i13, i14, i15)) + + @inline def fromFunction[I1, I2, I3, I4, I5, I6, I7, I8, I9, I10, I11, I12, I13, I14, I15, I16, O](f: (I1, I2, I3, I4, I5, I6, I7, I8, I9, I10, I11, I12, I13, I14, I15, I16) => O): (I1, I2, I3, I4, I5, I6, I7, I8, I9, I10, I11, I12, I13, I14, I15, I16) => Delayed[O] = + (i1, i2, i3, i4, i5, i6, i7, i8, i9, i10, i11, i12, i13, i14, i15, i16) => Delayed(f(i1, i2, i3, i4, i5, i6, i7, i8, i9, i10, i11, i12, i13, i14, i15, i16)) + + @inline def fromFunction[I1, I2, I3, I4, I5, I6, I7, I8, I9, I10, I11, I12, I13, I14, I15, I16, I17, O](f: (I1, I2, I3, I4, I5, I6, I7, I8, I9, I10, I11, I12, I13, I14, I15, I16, I17) => O): (I1, I2, I3, I4, I5, I6, I7, I8, I9, I10, I11, I12, I13, I14, I15, I16, I17) => Delayed[O] = + (i1, i2, i3, i4, i5, i6, i7, i8, i9, i10, i11, i12, i13, i14, i15, i16, i17) => Delayed(f(i1, i2, i3, i4, i5, i6, i7, i8, i9, i10, i11, i12, i13, i14, i15, i16, i17)) + + @inline def fromFunction[I1, I2, I3, I4, I5, I6, I7, I8, I9, I10, I11, I12, I13, I14, I15, I16, I17, I18, O](f: (I1, I2, I3, I4, I5, I6, I7, I8, I9, I10, I11, I12, I13, I14, I15, I16, I17, I18) => O): (I1, I2, I3, I4, I5, I6, I7, I8, I9, I10, I11, I12, I13, I14, I15, I16, I17, I18) => Delayed[O] = + (i1, i2, i3, i4, i5, i6, i7, i8, i9, i10, i11, i12, i13, i14, i15, i16, i17, i18) => Delayed(f(i1, i2, i3, i4, i5, i6, i7, i8, i9, i10, i11, i12, i13, i14, i15, i16, i17, i18)) + + @inline def fromFunction[I1, I2, I3, I4, I5, I6, I7, I8, I9, I10, I11, I12, I13, I14, I15, I16, I17, I18, I19, O](f: (I1, I2, I3, I4, I5, I6, I7, I8, I9, I10, I11, I12, I13, I14, I15, I16, I17, I18, I19) => O): (I1, I2, I3, I4, I5, I6, I7, I8, I9, I10, I11, I12, I13, I14, I15, I16, I17, I18, I19) => Delayed[O] = + (i1, i2, i3, i4, i5, i6, i7, i8, i9, i10, i11, i12, i13, i14, i15, i16, i17, i18, i19) => Delayed(f(i1, i2, i3, i4, i5, i6, i7, i8, i9, i10, i11, i12, i13, i14, i15, i16, i17, i18, i19)) + + @inline def fromFunction[I1, I2, I3, I4, I5, I6, I7, I8, I9, I10, I11, I12, I13, I14, I15, I16, I17, I18, I19, I20, O](f: (I1, I2, I3, I4, I5, I6, I7, I8, I9, I10, I11, I12, I13, I14, I15, I16, I17, I18, I19, I20) => O): (I1, I2, I3, I4, I5, I6, I7, I8, I9, I10, I11, I12, I13, I14, I15, I16, I17, I18, I19, I20) => Delayed[O] = + (i1, i2, i3, i4, i5, i6, i7, i8, i9, i10, i11, i12, i13, i14, i15, i16, i17, i18, i19, i20) => Delayed(f(i1, i2, i3, i4, i5, i6, i7, i8, i9, i10, i11, i12, i13, i14, i15, i16, i17, i18, i19, i20)) + + @inline def fromFunction[I1, I2, I3, I4, I5, I6, I7, I8, I9, I10, I11, I12, I13, I14, I15, I16, I17, I18, I19, I20, I21, O](f: (I1, I2, I3, I4, I5, I6, I7, I8, I9, I10, I11, I12, I13, I14, I15, I16, I17, I18, I19, I20, I21) => O): (I1, I2, I3, I4, I5, I6, I7, I8, I9, I10, I11, I12, I13, I14, I15, I16, I17, I18, I19, I20, I21) => Delayed[O] = + (i1, i2, i3, i4, i5, i6, i7, i8, i9, i10, i11, i12, i13, i14, i15, i16, i17, i18, i19, i20, i21) => Delayed(f(i1, i2, i3, i4, i5, i6, i7, i8, i9, i10, i11, i12, i13, i14, i15, i16, i17, i18, i19, i20, i21)) + + @inline def fromFunction[I1, I2, I3, I4, I5, I6, I7, I8, I9, I10, I11, I12, I13, I14, I15, I16, I17, I18, I19, I20, I21, I22, O](f: (I1, I2, I3, I4, I5, I6, I7, I8, I9, I10, I11, I12, I13, I14, I15, I16, I17, I18, I19, I20, I21, I22) => O): (I1, I2, I3, I4, I5, I6, I7, I8, I9, I10, I11, I12, I13, I14, I15, I16, I17, I18, I19, I20, I21, I22) => Delayed[O] = + (i1, i2, i3, i4, i5, i6, i7, i8, i9, i10, i11, i12, i13, i14, i15, i16, i17, i18, i19, i20, i21, i22) => Delayed(f(i1, i2, i3, i4, i5, i6, i7, i8, i9, i10, i11, i12, i13, i14, i15, i16, i17, i18, i19, i20, i21, i22)) +} + diff --git a/library/coreGeneric/src/main/scala/japgolly/scalajs/react/component/DerivedDisplayName.scala b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/component/DerivedDisplayName.scala new file mode 100644 index 000000000..71bad4213 --- /dev/null +++ b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/component/DerivedDisplayName.scala @@ -0,0 +1,12 @@ +package japgolly.scalajs.react.component + +import sourcecode.FullName + +trait DerivedDisplayName { + protected def derivedDisplayName(implicit name: FullName): String = { + // Heuristic to split the name into package and class name + val parts = name.value.split('.') + val (packageName, className) = parts.span(_.headOption.forall(_.isLower)) + className.mkString(".") + Some(packageName).filter(_.nonEmpty).fold("")(_.mkString(" (", ".", ")")) + } +} diff --git a/library/coreGeneric/src/main/scala/japgolly/scalajs/react/component/Generic.scala b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/component/Generic.scala index 901a02552..055a31d97 100644 --- a/library/coreGeneric/src/main/scala/japgolly/scalajs/react/component/Generic.scala +++ b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/component/Generic.scala @@ -95,6 +95,7 @@ object Generic { final def mountRawOrNull(c: facade.React.ComponentUntyped | Null): M = if (c == null) null.asInstanceOf[M] else mountRaw(JsUtil.notNull(c)) + @deprecated("Use ReactDOM.createRoot and root.render instead", "2.2.0 / React v18") def renderIntoDOM(container: facade.ReactDOM.Container): Mounted = mountRaw(facade.ReactDOM.render(raw, container)) diff --git a/library/coreGeneric/src/main/scala/japgolly/scalajs/react/component/JsFn.scala b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/component/JsFn.scala index 94c44c62a..70a2d4c89 100644 --- a/library/coreGeneric/src/main/scala/japgolly/scalajs/react/component/JsFn.scala +++ b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/component/JsFn.scala @@ -68,10 +68,11 @@ object JsFn extends JsBaseComponentTemplate[facade.React.StatelessFunctionalComp generic[UnusedObject, Children.Varargs](p => render(PropsChildren(p.children)))(s) } - private def staticDisplayName = "" + private def readDisplayName(a: facade.HasDisplayName): String = + a.displayName.getOrElse("") override protected def rawComponentDisplayName[A <: js.Object](r: facade.React.StatelessFunctionalComponent[A]) = - staticDisplayName + readDisplayName(r) // =================================================================================================================== @@ -113,11 +114,12 @@ object JsFn extends JsBaseComponentTemplate[facade.React.StatelessFunctionalComp sealed trait UnmountedSimple[P, M] extends Generic.UnmountedSimple[P, M] { override type Raw <: facade.React.ComponentElement[_ <: js.Object] override final type Ref = Nothing - override final def displayName = staticDisplayName + override final def displayName = readDisplayName(raw.`type`) override def mapUnmountedProps[P2](f: P => P2): UnmountedSimple[P2, M] override def mapMounted[M2](f: M => M2): UnmountedSimple[P, M2] + @deprecated("Use ReactDOM.createRoot and root.render instead", "2.2.0 / React v18") override final def renderIntoDOM(container: facade.ReactDOM.Container): this.Mounted = postRender(facade.ReactDOM.render(raw, container)) diff --git a/library/coreGeneric/src/main/scala/japgolly/scalajs/react/component/JsForwardRef.scala b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/component/JsForwardRef.scala index d5e4db52a..a99962df9 100644 --- a/library/coreGeneric/src/main/scala/japgolly/scalajs/react/component/JsForwardRef.scala +++ b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/component/JsForwardRef.scala @@ -140,6 +140,7 @@ object JsForwardRef { override def mapUnmountedProps[P2](f: P => P2): UnmountedSimple[P2, R, M] override def mapMounted[M2](f: M => M2): UnmountedSimple[P, R, M2] + @deprecated("Use ReactDOM.createRoot and root.render instead", "2.2.0 / React v18") override final def renderIntoDOM(container: facade.ReactDOM.Container): this.Mounted = postRender(facade.ReactDOM.render(raw, container)) diff --git a/library/coreGeneric/src/main/scala/japgolly/scalajs/react/component/ScalaFn.scala b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/component/ScalaFn.scala index 0fc3d6a75..7469dc59f 100644 --- a/library/coreGeneric/src/main/scala/japgolly/scalajs/react/component/ScalaFn.scala +++ b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/component/ScalaFn.scala @@ -5,49 +5,85 @@ import japgolly.scalajs.react.internal._ import japgolly.scalajs.react.vdom.VdomNode import japgolly.scalajs.react.{Children, CtorType, PropsChildren, Reusability, facade} import scala.scalajs.js +import sourcecode.FullName -object ScalaFn { +object ScalaFn extends DerivedDisplayName { type Component[P, CT[-p, +u] <: CtorType[p, u]] = JsFn.ComponentWithRoot[P, CT, Unmounted[P], Box[P], CT, JsFn.Unmounted[Box[P]]] type Unmounted[P] = JsFn.UnmountedWithRoot[P, Mounted, Box[P]] type Mounted = JsFn.Mounted private def create[P, C <: Children, CT[-p, +u] <: CtorType[p, u]] - (render: Box[P] with facade.PropsWithChildren => VdomNode) + (displayName: String) + (render: Box[P] with facade.PropsWithChildren => Delayed[VdomNode]) (implicit s: CtorType.Summoner.Aux[Box[P], C, CT]): Component[P, CT] = { - val jsRender = render.andThen(_.rawNode): js.Function1[Box[P] with facade.PropsWithChildren, facade.React.Node] + val jsRender = render.andThen(_.eval().rawNode): js.Function1[Box[P] with facade.PropsWithChildren, facade.React.Node] val rawComponent = jsRender.asInstanceOf[facade.React.StatelessFunctionalComponent[Box[P]]] + rawComponent.setDisplayName = displayName JsFn.force[Box[P], C](rawComponent)(s) .cmapCtorProps[P](Box(_)) .mapUnmounted(_.mapUnmountedProps(_.unbox)) } - @inline def withHooks[P] = - HookComponentBuilder.apply[P] + @inline def withDisplayName(name: String): DisplayNameApplied = + new DisplayNameApplied(name) + + @inline def withHooks[P](implicit name: FullName): HookComponentBuilder.ComponentP.First[P] = + HookComponentBuilder.apply[P](derivedDisplayName) // =================================================================================================================== - def apply[P](render: P => VdomNode)(implicit s: CtorType.Summoner[Box[P], Children.None]): Component[P, s.CT] = - create[P, Children.None, s.CT](b => render(b.unbox))(s) + def apply[P](render: P => Delayed[VdomNode])(implicit s: CtorType.Summoner[Box[P], Children.None], name: FullName): Component[P, s.CT] = + create[P, Children.None, s.CT](derivedDisplayName)(b => render(b.unbox))(s) - def withChildren[P](render: (P, PropsChildren) => VdomNode)(implicit s: CtorType.Summoner[Box[P], Children.Varargs]): Component[P, s.CT] = - create[P, Children.Varargs, s.CT](b => render(b.unbox, PropsChildren(b.children)))(s) + def withChildren[P](render: (P, PropsChildren) => Delayed[VdomNode])(implicit s: CtorType.Summoner[Box[P], Children.Varargs], name: FullName): Component[P, s.CT] = + create[P, Children.Varargs, s.CT](derivedDisplayName)(b => render(b.unbox, PropsChildren(b.children)))(s) - def justChildren(render: PropsChildren => VdomNode): Component[Unit, CtorType.Children] = - create(b => render(PropsChildren(b.children))) + def justChildren(render: PropsChildren => Delayed[VdomNode])(implicit name: FullName): Component[Unit, CtorType.Children] = + create(derivedDisplayName)(b => render(PropsChildren(b.children))) // =================================================================================================================== - def withReuse[P](render: P => VdomNode)(implicit s: CtorType.Summoner[Box[P], Children.None], r: Reusability[P]): Component[P, s.CT] = - withHooks[P].renderWithReuse(render)(s, r) + def withReuse[P](render: P => Delayed[VdomNode])(implicit s: CtorType.Summoner[Box[P], Children.None], r: Reusability[P], name: FullName): Component[P, s.CT] = + withHooks[P].renderWithReuse(render.andThen(_.eval()))(s, r) + + def withReuseBy[P, A](reusableInputs: P => A)(render: A => Delayed[VdomNode])(implicit s: CtorType.Summoner[Box[P], Children.None], r: Reusability[A], name: FullName): Component[P, s.CT] = + withHooks[P].renderWithReuseBy(reusableInputs)(render.andThen(_.eval()))(s, r) + + def withChildrenAndReuse[P](render: (P, PropsChildren) => Delayed[VdomNode])(implicit s: CtorType.Summoner[Box[P], Children.Varargs], rp: Reusability[P], rc: Reusability[PropsChildren], name: FullName): Component[P, s.CT] = + withHooks[P].withPropsChildren.renderWithReuse(i => render(i.props, i.propsChildren).eval()) + + def withChildrenAndReuse[P, A](reusableInputs: (P, PropsChildren) => A)(render: A => Delayed[VdomNode])(implicit s: CtorType.Summoner[Box[P], Children.Varargs], r: Reusability[A], name: FullName): Component[P, s.CT] = + withHooks[P].withPropsChildren.renderWithReuseBy(i => reusableInputs(i.props, i.propsChildren))(render.andThen(_.eval())) + + class DisplayNameApplied private[ScalaFn](displayName: String) { + @inline def withHooks[P]: HookComponentBuilder.ComponentP.First[P] = + HookComponentBuilder.apply[P](displayName) + + // =================================================================================================================== - def withReuseBy[P, A](reusableInputs: P => A)(render: A => VdomNode)(implicit s: CtorType.Summoner[Box[P], Children.None], r: Reusability[A]): Component[P, s.CT] = - withHooks[P].renderWithReuseBy(reusableInputs)(render)(s, r) + def apply[P](render: P => Delayed[VdomNode])(implicit s: CtorType.Summoner[Box[P], Children.None]): Component[P, s.CT] = + create[P, Children.None, s.CT](displayName)(b => render(b.unbox))(s) - def withChildrenAndReuse[P](render: (P, PropsChildren) => VdomNode)(implicit s: CtorType.Summoner[Box[P], Children.Varargs], rp: Reusability[P], rc: Reusability[PropsChildren]): Component[P, s.CT] = - withHooks[P].withPropsChildren.renderWithReuse(i => render(i.props, i.propsChildren)) + def withChildren[P](render: (P, PropsChildren) => Delayed[VdomNode])(implicit s: CtorType.Summoner[Box[P], Children.Varargs]): Component[P, s.CT] = + create[P, Children.Varargs, s.CT](displayName)(b => render(b.unbox, PropsChildren(b.children)))(s) - def withChildrenAndReuse[P, A](reusableInputs: (P, PropsChildren) => A)(render: A => VdomNode)(implicit s: CtorType.Summoner[Box[P], Children.Varargs], r: Reusability[A]): Component[P, s.CT] = - withHooks[P].withPropsChildren.renderWithReuseBy(i => reusableInputs(i.props, i.propsChildren))(render) + def justChildren(render: PropsChildren => Delayed[VdomNode]): Component[Unit, CtorType.Children] = + create(displayName)(b => render(PropsChildren(b.children))) + + // =================================================================================================================== + + def withReuse[P](render: P => Delayed[VdomNode])(implicit s: CtorType.Summoner[Box[P], Children.None], r: Reusability[P]): Component[P, s.CT] = + withHooks[P].renderWithReuse(render.andThen(_.eval()))(s, r) + + def withReuseBy[P, A](reusableInputs: P => A)(render: A => Delayed[VdomNode])(implicit s: CtorType.Summoner[Box[P], Children.None], r: Reusability[A]): Component[P, s.CT] = + withHooks[P].renderWithReuseBy(reusableInputs)(render.andThen(_.eval()))(s, r) + + def withChildrenAndReuse[P](render: (P, PropsChildren) => Delayed[VdomNode])(implicit s: CtorType.Summoner[Box[P], Children.Varargs], rp: Reusability[P], rc: Reusability[PropsChildren]): Component[P, s.CT] = + withHooks[P].withPropsChildren.renderWithReuse(i => render(i.props, i.propsChildren).eval()) + + def withChildrenAndReuse[P, A](reusableInputs: (P, PropsChildren) => A)(render: A => Delayed[VdomNode])(implicit s: CtorType.Summoner[Box[P], Children.Varargs], r: Reusability[A]): Component[P, s.CT] = + withHooks[P].withPropsChildren.renderWithReuseBy(i => reusableInputs(i.props, i.propsChildren))(render.andThen(_.eval())) + } } diff --git a/library/coreGeneric/src/main/scala/japgolly/scalajs/react/component/ScalaForwardRef.scala b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/component/ScalaForwardRef.scala index 47cd65c20..d6b88ee8b 100644 --- a/library/coreGeneric/src/main/scala/japgolly/scalajs/react/component/ScalaForwardRef.scala +++ b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/component/ScalaForwardRef.scala @@ -6,6 +6,8 @@ import japgolly.scalajs.react.vdom.VdomNode import japgolly.scalajs.react.{Children, CtorType, PropsChildren, Ref, facade} import scala.annotation.nowarn import scala.scalajs.js +import sourcecode.FullName + object ScalaForwardRef { type Component[P, R, CT[-p, +u] <: CtorType[p, u]] = JsForwardRef.ComponentWithRoot[P, R, CT, Unmounted[P, R], Box[P], CT, JsForwardRef.Unmounted[Box[P], R]] @@ -13,27 +15,28 @@ object ScalaForwardRef { type Mounted = JsForwardRef.Mounted } -object ReactForwardRefInternals { +object ReactForwardRefInternals extends DerivedDisplayName { sealed trait Dsl extends Any { protected type R protected type RefValue protected def create[P, C <: Children, CT[-p, +u] <: CtorType[p, u]] - (render: (Box[P] with facade.PropsWithChildren, Option[R]) => VdomNode) + (displayName: String) + (render: (Box[P] with facade.PropsWithChildren, Option[R]) => Delayed[VdomNode]) (implicit s: CtorType.Summoner.Aux[Box[P], C, CT]): Component[P, RefValue, CT] - final def apply(render: Option[R] => VdomNode): Component[Unit, RefValue, CtorType.Nullary] = - create((_, r) => render(r)) + final def apply(render: Option[R] => Delayed[VdomNode])(implicit name: FullName): Component[Unit, RefValue, CtorType.Nullary] = + create(derivedDisplayName)((_, r) => render(r)) - final def apply[P](render: (P, Option[R]) => VdomNode): Component[P, RefValue, CtorType.Props] = - create((p, r) => render(p.unbox, r)) + final def apply[P](render: (P, Option[R]) => Delayed[VdomNode])(implicit name: FullName): Component[P, RefValue, CtorType.Props] = + create(derivedDisplayName)((p, r) => render(p.unbox, r)) - final def withChildren[P](render: (P, PropsChildren, Option[R]) => VdomNode): Component[P, RefValue, CtorType.PropsAndChildren] = - create((b, r) => render(b.unbox, PropsChildren(b.children), r)) + final def withChildren[P](render: (P, PropsChildren, Option[R]) => Delayed[VdomNode])(implicit name: FullName): Component[P, RefValue, CtorType.PropsAndChildren] = + create(derivedDisplayName)((b, r) => render(b.unbox, PropsChildren(b.children), r)) - final def justChildren(render: (PropsChildren, Option[R]) => VdomNode): Component[Unit, RefValue, CtorType.Children] = - create((b, r) => render(PropsChildren(b.children), r)) + final def justChildren(render: (PropsChildren, Option[R]) => Delayed[VdomNode])(implicit name: FullName): Component[Unit, RefValue, CtorType.Children] = + create(derivedDisplayName)((b, r) => render(PropsChildren(b.children), r)) } // extends AnyVal with Dsl makes scalac 2.11 explode @@ -42,11 +45,29 @@ object ReactForwardRefInternals { override protected type RefValue = RM override protected def create[P, C <: Children, CT[-p, +u] <: CtorType[p, u]] - (render: (Box[P] with facade.PropsWithChildren, Option[R]) => VdomNode) + (displayName: String) + (render: (Box[P] with facade.PropsWithChildren, Option[R]) => Delayed[VdomNode]) (implicit s: CtorType.Summoner.Aux[Box[P], C, CT]): Component[P, RefValue, CT] = - ReactForwardRef.create[P, RefValue, C, CT]((p, r) => render(p, r.map(_.map( + ReactForwardRef.create[P, RefValue, C, CT](displayName)((p, r) => render(p, r.map(_.map( Js.mounted[P0, S0](_).withRawType[RM] - )))) + ))).eval()) + + @inline def withDisplayName(name: String): DisplayNameApplied = + new DisplayNameApplied(name) + + class DisplayNameApplied private[ToJsComponent](displayName: String) { + final def apply(render: Option[R] => Delayed[VdomNode]): Component[Unit, RefValue, CtorType.Nullary] = + create(displayName)((_, r) => render(r)) + + final def apply[P](render: (P, Option[R]) => Delayed[VdomNode]): Component[P, RefValue, CtorType.Props] = + create(displayName)((p, r) => render(p.unbox, r)) + + final def withChildren[P](render: (P, PropsChildren, Option[R]) => Delayed[VdomNode]): Component[P, RefValue, CtorType.PropsAndChildren] = + create(displayName)((b, r) => render(b.unbox, PropsChildren(b.children), r)) + + final def justChildren(render: (PropsChildren, Option[R]) => Delayed[VdomNode]): Component[Unit, RefValue, CtorType.Children] = + create(displayName)((b, r) => render(PropsChildren(b.children), r)) + } } // extends AnyVal with Dsl makes scalac 2.11 explode @@ -55,9 +76,27 @@ object ReactForwardRefInternals { override protected type RefValue = Scala.RawMounted[P2, S, B] override protected def create[P, C <: Children, CT[-p, +u] <: CtorType[p, u]] - (render: (Box[P] with facade.PropsWithChildren, Option[R]) => VdomNode) + (displayName: String) + (render: (Box[P] with facade.PropsWithChildren, Option[R]) => Delayed[VdomNode]) (implicit s: CtorType.Summoner.Aux[Box[P], C, CT]): Component[P, RefValue, CT] = - ReactForwardRef.create[P, RefValue, C, CT]((p, r) => render(p, r.map(_.map(_.mountedImpure)))) + ReactForwardRef.create[P, RefValue, C, CT](displayName)((p, r) => render(p, r.map(_.map(_.mountedImpure))).eval()) + + @inline def withDisplayName(name: String): DisplayNameApplied = + new DisplayNameApplied(name) + + class DisplayNameApplied private[ToScalaComponent](displayName: String) { + final def apply(render: Option[R] => Delayed[VdomNode]): Component[Unit, RefValue, CtorType.Nullary] = + create(displayName)((_, r) => render(r)) + + final def apply[P](render: (P, Option[R]) => Delayed[VdomNode]): Component[P, RefValue, CtorType.Props] = + create(displayName)((p, r) => render(p.unbox, r)) + + final def withChildren[P](render: (P, PropsChildren, Option[R]) => Delayed[VdomNode]): Component[P, RefValue, CtorType.PropsAndChildren] = + create(displayName)((b, r) => render(b.unbox, PropsChildren(b.children), r)) + + final def justChildren(render: (PropsChildren, Option[R]) => Delayed[VdomNode]): Component[Unit, RefValue, CtorType.Children] = + create(displayName)((b, r) => render(PropsChildren(b.children), r)) + } } } @@ -65,31 +104,40 @@ object ReactForwardRef { outer => import ReactForwardRefInternals._ private[component] def create[P, R, C <: Children, CT[-p, +u] <: CtorType[p, u]] - (render: (Box[P] with facade.PropsWithChildren, Option[Ref.Simple[R]]) => VdomNode) + (displayName: String) + (render: (Box[P] with facade.PropsWithChildren, Option[Ref.Simple[R]]) => Delayed[VdomNode]) (implicit s: CtorType.Summoner.Aux[Box[P], C, CT]): Component[P, R, CT] = { val jsRender: js.Function2[Box[P] with facade.PropsWithChildren, facade.React.ForwardedRef[R], facade.React.Node] = (p: Box[P] with facade.PropsWithChildren, r: facade.React.ForwardedRef[R]) => - render(p, Ref.forwardedFromJs(r)).rawNode + render(p, Ref.forwardedFromJs(r)).eval().rawNode val rawComponent = facade.React.forwardRef(jsRender) + rawComponent.displayName = displayName JsForwardRef.force[Box[P], C, R](rawComponent)(s) .cmapCtorProps[P](Box(_)) .mapUnmounted(_.mapUnmountedProps(_.unbox)) } - def apply[R](render: Option[Ref.Simple[R]] => VdomNode): Component[Unit, R, CtorType.Nullary] = - create((_, r) => render(r)) + private def derivedDisplayName(implicit name: FullName): String = + name.value + + @inline def withDisplayName(name: String): DisplayNameApplied = + new DisplayNameApplied(name) + - def apply[P, R](render: (P, Option[Ref.Simple[R]]) => VdomNode): Component[P, R, CtorType.Props] = - create((p, r) => render(p.unbox, r)) + def apply[R](render: Option[Ref.Simple[R]] => Delayed[VdomNode])(implicit name: FullName): Component[Unit, R, CtorType.Nullary] = + create(derivedDisplayName)((_, r) => render(r)) - def withChildren[P, R](render: (P, PropsChildren, Option[Ref.Simple[R]]) => VdomNode): Component[P, R, CtorType.PropsAndChildren] = - create((b, r) => render(b.unbox, PropsChildren(b.children), r)) + def apply[P, R](render: (P, Option[Ref.Simple[R]]) => Delayed[VdomNode])(implicit name: FullName): Component[P, R, CtorType.Props] = + create(derivedDisplayName)((p, r) => render(p.unbox, r)) - def justChildren[R](render: (PropsChildren, Option[Ref.Simple[R]]) => VdomNode): Component[Unit, R, CtorType.Children] = - create((b, r) => render(PropsChildren(b.children), r)) + def withChildren[P, R](render: (P, PropsChildren, Option[Ref.Simple[R]]) => Delayed[VdomNode])(implicit name: FullName): Component[P, R, CtorType.PropsAndChildren] = + create(derivedDisplayName)((b, r) => render(b.unbox, PropsChildren(b.children), r)) + + def justChildren[R](render: (PropsChildren, Option[Ref.Simple[R]]) => Delayed[VdomNode])(implicit name: FullName): Component[Unit, R, CtorType.Children] = + create(derivedDisplayName)((b, r) => render(PropsChildren(b.children), r)) // =================================================================================================================== @@ -104,4 +152,18 @@ object ReactForwardRef { outer => def toScalaComponent[P, S, B]: ToScalaComponent[P, S, B] = new ToScalaComponent(()) + + class DisplayNameApplied private[ReactForwardRef](displayName: String) { + def apply[R](render: Option[Ref.Simple[R]] => Delayed[VdomNode]): Component[Unit, R, CtorType.Nullary] = + create(displayName)((_, r) => render(r)) + + def apply[P, R](render: (P, Option[Ref.Simple[R]]) => Delayed[VdomNode]): Component[P, R, CtorType.Props] = + create(displayName)((p, r) => render(p.unbox, r)) + + def withChildren[P, R](render: (P, PropsChildren, Option[Ref.Simple[R]]) => Delayed[VdomNode]): Component[P, R, CtorType.PropsAndChildren] = + create(displayName)((b, r) => render(b.unbox, PropsChildren(b.children), r)) + + def justChildren[R](render: (PropsChildren, Option[Ref.Simple[R]]) => Delayed[VdomNode]): Component[Unit, R, CtorType.Children] = + create(displayName)((b, r) => render(PropsChildren(b.children), r)) + } } diff --git a/library/coreGeneric/src/main/scala/japgolly/scalajs/react/feature/Profiler.scala b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/feature/Profiler.scala index 0233df4e1..a881fa59b 100644 --- a/library/coreGeneric/src/main/scala/japgolly/scalajs/react/feature/Profiler.scala +++ b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/feature/Profiler.scala @@ -5,6 +5,7 @@ import japgolly.scalajs.react.util.Effect.Sync import japgolly.scalajs.react.util.JsUtil import japgolly.scalajs.react.vdom.PackageBase._ import java.time.Duration +import scala.annotation.nowarn import scala.scalajs.js /** The Profiler measures how often a React application renders and what the "cost" of rendering is. Its purpose is to @@ -67,13 +68,14 @@ object Profiler { * @param startTime Timestamp when React began rendering the current update. * @param commitTime Timestamp when React committed the current update. This value is shared between all profilers in a commit, enabling them to be grouped if desirable. */ + @nowarn("cat=deprecation") final case class OnRenderData(id : String, phase : String, actualDurationMs: Double, baseDurationMs : Double, startTime : Double, commitTime : Double, - rawInteractions : js.Iterable[facade.Interaction], + @deprecated("Removed in React 18", "2.2.0") rawInteractions: js.Iterable[facade.Interaction], ) { def phaseIsMount: Boolean = @@ -93,6 +95,7 @@ object Profiler { /** Set of "interactions" that were being traced when the update was scheduled * (e.g. when render or setState were called). */ + @deprecated("Removed in React 18", "2.2.0") lazy val interactions: Vector[Interaction] = rawInteractions.iterator.map(Interaction.fromRaw).toVector } @@ -119,6 +122,7 @@ object Profiler { * and its return value will be returned to the caller. Any code run within that callback will be attributed to that * interaction. Calls to unstable_wrap() will schedule async work within the same zone. */ + @deprecated("Removed in React 18", "2.2.0") def unstable_trace[A](name: String)(body: => A): A = facade.React.SecretInternals.SchedulerTracing.unstable_trace( name, diff --git a/library/coreGeneric/src/main/scala/japgolly/scalajs/react/hooks/Api.scala b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/hooks/Api.scala index f905850f2..39fee8369 100644 --- a/library/coreGeneric/src/main/scala/japgolly/scalajs/react/hooks/Api.scala +++ b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/hooks/Api.scala @@ -6,6 +6,7 @@ import japgolly.scalajs.react.feature.Context import japgolly.scalajs.react.hooks.Hooks._ import japgolly.scalajs.react.internal.Box import japgolly.scalajs.react.util.DefaultEffects +import japgolly.scalajs.react.util.Effect.Sync import japgolly.scalajs.react.util.Util.identityFn import japgolly.scalajs.react.vdom.{TopNode, VdomNode} import japgolly.scalajs.react.{Children, CtorType, Ref, Reusability, Reusable} @@ -195,8 +196,7 @@ object Api { * * By default, effects run after every completed render. * If you'd only like to execute the effect when your component is mounted, then use [[useEffectOnMount]]. - * If you'd only like to execute the effect when certain values have changed, provide those certain values as - * the second argument. + * If you'd only like to execute the effect when certain values have changed, then use [[useEffectWithDeps]]. * * @see https://reactjs.org/docs/hooks-reference.html#useeffect */ @@ -208,13 +208,12 @@ object Api { * * By default, effects run after every completed render. * If you'd only like to execute the effect when your component is mounted, then use [[useEffectOnMount]]. - * If you'd only like to execute the effect when certain values have changed, provide those certain values as - * the second argument. + * If you'd only like to execute the effect when certain values have changed, then use [[useEffectWithDeps]]. * * @see https://reactjs.org/docs/hooks-reference.html#useeffect */ - final def useEffectBy[A](init: Ctx => A)(implicit a: UseEffectArg[A], step: Step): step.Self = - self(ctx => UseEffect.unsafeCreate(init(ctx))) + final def useEffectBy[A](effect: Ctx => A)(implicit a: UseEffectArg[A], step: Step): step.Self = + self(ctx => UseEffect.unsafeCreate(effect(ctx))) /** The callback passed to useEffect will run after the render is committed to the screen. Think of effects as an * escape hatch from React’s purely functional world into the imperative world. @@ -239,7 +238,7 @@ object Api { /** The callback passed to useEffect will run after the render is committed to the screen. Think of effects as an * escape hatch from React’s purely functional world into the imperative world. * - * This will only execute the effect when values in the second argument, change. + * This will only execute the effect when values in the first argument change. * * @see https://reactjs.org/docs/hooks-reference.html#useeffect */ @@ -249,14 +248,14 @@ object Api { /** The callback passed to useEffect will run after the render is committed to the screen. Think of effects as an * escape hatch from React’s purely functional world into the imperative world. * - * This will only execute the effect when values in the second argument, change. + * This will only execute the effect when values in the first argument change. * * @see https://reactjs.org/docs/hooks-reference.html#useeffect */ final def useEffectWithDepsBy[D, A](deps: Ctx => D)(effect: Ctx => D => A)(implicit a: UseEffectArg[A], r: Reusability[D], step: Step): step.Self = customBy(ctx => ReusableEffect.useEffect(deps(ctx))(effect(ctx))) - /** When invoked, forces a re-render of your component. */ + /** Provides a Callback that when invoked forces a re-render of your component. */ final def useForceUpdate(implicit step: Step): step.Next[Reusable[DefaultEffects.Sync[Unit]]] = custom(CustomHook.useForceUpdate) @@ -267,8 +266,7 @@ object Api { * Prefer the standard [[useEffect]] when possible to avoid blocking visual updates. * * If you'd only like to execute the effect when your component is mounted, then use [[useLayoutEffectOnMount]]. - * If you'd only like to execute the effect when certain values have changed, provide those certain values as - * the second argument. + * If you'd only like to execute the effect when certain values have changed, then use [[useLayoutEffectWithDeps]]. * * @see https://reactjs.org/docs/hooks-reference.html#useLayoutEffect */ @@ -282,13 +280,12 @@ object Api { * Prefer the standard [[useEffect]] when possible to avoid blocking visual updates. * * If you'd only like to execute the effect when your component is mounted, then use [[useLayoutEffectOnMount]]. - * If you'd only like to execute the effect when certain values have changed, provide those certain values as - * the second argument. + * If you'd only like to execute the effect when certain values have changed, then use [[useLayoutEffectWithDeps]]. * * @see https://reactjs.org/docs/hooks-reference.html#useLayoutEffect */ - final def useLayoutEffectBy[A](init: Ctx => A)(implicit a: UseEffectArg[A], step: Step): step.Self = - self(ctx => UseEffect.unsafeCreateLayout(init(ctx))) + final def useLayoutEffectBy[A](effect: Ctx => A)(implicit a: UseEffectArg[A], step: Step): step.Self = + self(ctx => UseEffect.unsafeCreateLayout(effect(ctx))) /** The signature is identical to [[useEffect]], but it fires synchronously after all DOM mutations. Use this to * read layout from the DOM and synchronously re-render. Updates scheduled inside useLayoutEffect will be flushed @@ -322,7 +319,7 @@ object Api { * * Prefer the standard [[useEffect]] when possible to avoid blocking visual updates. * - * This will only execute the effect when values in the second argument, change. + * This will only execute the effect when values in the first argument change. * * @see https://reactjs.org/docs/hooks-reference.html#useLayoutEffect */ @@ -335,13 +332,93 @@ object Api { * * Prefer the standard [[useEffect]] when possible to avoid blocking visual updates. * - * This will only execute the effect when values in the second argument, change. + * This will only execute the effect when values in the first argument change. * * @see https://reactjs.org/docs/hooks-reference.html#useLayoutEffect */ final def useLayoutEffectWithDepsBy[D, A](deps: Ctx => D)(effect: Ctx => D => A)(implicit a: UseEffectArg[A], r: Reusability[D], step: Step): step.Self = customBy(ctx => ReusableEffect.useLayoutEffect(deps(ctx))(effect(ctx))) + /** The signature is identical to [[useEffect]], but it fires synchronously after all DOM mutations, but before any + * layout Effects fire. Use this to insert styles before any Effects fire that may need to read layout. Updates + * scheduled inside useLayoutEffect will be flushed synchronously, before the browser has a chance to paint. + * + * Prefer the standard [[useEffect]] when possible to avoid blocking visual updates. + * + * If you'd only like to execute the effect when your component is mounted, then use [[useInsertionEffectOnMount]]. + * If you'd only like to execute the effect when certain values have changed, then use [[useInsertionEffectWithDeps]]. + * + * @see https://react.dev/reference/react/useInsertionEffect#useInsertionEffect + */ + final def useInsertionEffect[A](effect: A)(implicit a: UseEffectArg[A], step: Step): step.Self = + useInsertionEffectBy(_ => effect) + + /** The signature is identical to [[useEffect]], but it fires synchronously after all DOM mutations, but before any + * layout Effects fire. Use this to insert styles before any Effects fire that may need to read layout. Updates + * scheduled inside useLayoutEffect will be flushed synchronously, before the browser has a chance to paint. + * + * Prefer the standard [[useEffect]] when possible to avoid blocking visual updates. + * + * If you'd only like to execute the effect when your component is mounted, then use [[useInsertionEffectOnMount]]. + * If you'd only like to execute the effect when certain values have changed, then use [[useInsertionEffectWithDeps]]. + * + * @see https://react.dev/reference/react/useInsertionEffect#useInsertionEffect + */ + final def useInsertionEffectBy[A](effect: Ctx => A)(implicit a: UseEffectArg[A], step: Step): step.Self = + self(ctx => UseEffect.unsafeCreateInsertion(effect(ctx))) + + /** The signature is identical to [[useEffect]], but it fires synchronously after all DOM mutations, but before any + * layout Effects fire. Use this to insert styles before any Effects fire that may need to read layout. Updates + * scheduled inside useLayoutEffect will be flushed synchronously, before the browser has a chance to paint. + * + * Prefer the standard [[useEffect]] when possible to avoid blocking visual updates. + * + * This will only execute the effect when your component is mounted. + * + * @see https://react.dev/reference/react/useInsertionEffect#useInsertionEffect + */ + final def useInsertionEffectOnMount[A](effect: A)(implicit a: UseEffectArg[A], step: Step): step.Self = + useInsertionEffectOnMountBy(_ => effect) + + /** The signature is identical to [[useEffect]], but it fires synchronously after all DOM mutations, but before any + * layout Effects fire. Use this to insert styles before any Effects fire that may need to read layout. Updates + * scheduled inside useLayoutEffect will be flushed synchronously, before the browser has a chance to paint. + * + * Prefer the standard [[useEffect]] when possible to avoid blocking visual updates. + * + * This will only execute the effect when your component is mounted. + * + * @see https://react.dev/reference/react/useInsertionEffect#useInsertionEffect + */ + final def useInsertionEffectOnMountBy[A](effect: Ctx => A)(implicit a: UseEffectArg[A], step: Step): step.Self = + self(ctx => UseEffect.unsafeCreateInsertionOnMount(effect(ctx))) + + /** The signature is identical to [[useEffect]], but it fires synchronously after all DOM mutations, but before any + * layout Effects fire. Use this to insert styles before any Effects fire that may need to read layout. Updates + * scheduled inside useLayoutEffect will be flushed synchronously, before the browser has a chance to paint. + * + * Prefer the standard [[useEffect]] when possible to avoid blocking visual updates. + * + * This will only execute the effect when values in the first argument change. + * + * @see https://react.dev/reference/react/useInsertionEffect#useInsertionEffect + */ + final def useInsertionEffectWithDeps[D, A](deps: => D)(effect: D => A)(implicit a: UseEffectArg[A], r: Reusability[D], step: Step): step.Self = + custom(ReusableEffect.useInsertionEffect(deps)(effect)) + + /** The signature is identical to [[useEffect]], but it fires synchronously after all DOM mutations, but before any + * layout Effects fire. Use this to insert styles before any Effects fire that may need to read layout. Updates + * scheduled inside useLayoutEffect will be flushed synchronously, before the browser has a chance to paint. + * + * Prefer the standard [[useEffect]] when possible to avoid blocking visual updates. + * + * This will only execute the effect when values in the first argument change. + * + * @see https://react.dev/reference/react/useInsertionEffect#useInsertionEffect + */ + final def useInsertionEffectWithDepsBy[D, A](deps: Ctx => D)(effect: Ctx => D => A)(implicit a: UseEffectArg[A], r: Reusability[D], step: Step): step.Self = + customBy(ctx => ReusableEffect.useInsertionEffect(deps(ctx))(effect(ctx))) + /** Returns a memoized value. * * Pass a “create” function and any dependencies. useMemo will only recompute the memoized value when one @@ -477,6 +554,81 @@ object Api { */ final def useStateWithReuseBy[S: ClassTag: Reusability](initialState: Ctx => S)(implicit step: Step): step.Next[UseStateWithReuse[S]] = next(ctx => UseStateWithReuse.unsafeCreate(initialState(ctx))) + + /** Generates unique IDs that can be passed to accessibility attributes. + * + * @see https://react.dev/reference/react/useId + */ + final def useId(implicit step: Step): step.Next[String] = + customBy(_ => UseId()) + + /** Allows components to avoid undesirable loading states by waiting for content to load + * before transitioning to the next screen. It also allows components to defer slower, + * data fetching updates until subsequent renders so that more crucial updates can be + * rendered immediately. + * + * **If some state update causes a component to suspend, that state update should be wrapped in a transition.** + * + * @see {@link https://react.dev/reference/react/useTransition} + */ + final def useTransition(implicit step: Step): step.Next[UseTransition] = + customBy(_ => UseTransition()) + + /** + * Lets you subscribe to an external store. + * + * @see + * {@link https://react.dev/reference/react/useSyncExternalStore} + */ + @inline final def useSyncExternalStore[F[_], A](subscribe: F[Unit] => F[F[Unit]], getSnapshot: F[A])(implicit F: Sync[F], step: Step): step.Next[A] = + useSyncExternalStore(subscribe, getSnapshot, js.undefined) + + /** + * Lets you subscribe to an external store. + * + * @see + * {@link https://react.dev/reference/react/useSyncExternalStore} + */ + final def useSyncExternalStore[F[_], A](subscribe: F[Unit] => F[F[Unit]], getSnapshot: F[A], getServerSnapshot: js.UndefOr[F[A]])(implicit F: Sync[F], step: Step): step.Next[A] = + customBy(_ => UseSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)) + + /** + * Lets you subscribe to an external store. + * + * @see + * {@link https://react.dev/reference/react/useSyncExternalStore} + */ + @inline final def useSyncExternalStoreBy[F[_], A](subscribe: Ctx => F[Unit] => F[F[Unit]], getSnapshot: Ctx => F[A])(implicit F: Sync[F], step: Step): step.Next[A] = + useSyncExternalStoreBy(subscribe, getSnapshot, js.undefined) + /** + * Lets you subscribe to an external store. + * + * @see + * {@link https://react.dev/reference/react/useSyncExternalStore} + */ + final def useSyncExternalStoreBy[F[_], A](subscribe: Ctx => F[Unit] => F[F[Unit]], getSnapshot: Ctx => F[A], getServerSnapshot: js.UndefOr[Ctx => F[A]])(implicit F: Sync[F], step: Step): step.Next[A] = + customBy(ctx => UseSyncExternalStore(subscribe(ctx), getSnapshot(ctx), getServerSnapshot.map(_(ctx)))) + + /** + * Lets you defer updating a part of the UI. + * + * @see + * {@link https://react.dev/reference/react/useDeferredValue} + */ + final def useDeferredValue[A](value: Ctx => A)(implicit step: Step): step.Next[A] = + // initialValue was added in React 19 - Replace when we upgrade to React 19 + // customBy(ctx => UseDeferredValue(value(ctx), js.undefined)) + customBy(ctx => UseDeferredValue(value(ctx))) + + // initialValue was added in React 19 - Uncomment when we upgrade to React 19 + // /** + // * Lets you defer updating a part of the UI. + // * + // * @see + // * {@link https://react.dev/reference/react/useDeferredValue} + // */ + // final def useDeferredValue[A](value: Ctx => A, initialValue: Ctx => A)(implicit step: Step): step.Next[A] = + // customBy(ctx => UseDeferredValue(value(ctx), initialValue(ctx))) } // =================================================================================================================== @@ -554,18 +706,17 @@ object Api { * * By default, effects run after every completed render. * If you'd only like to execute the effect when your component is mounted, then use [[useEffectOnMount]]. - * If you'd only like to execute the effect when certain values have changed, provide those certain values as - * the second argument. + * If you'd only like to execute the effect when certain values have changed, then use [[useEffectWithDeps]]. * * @see https://reactjs.org/docs/hooks-reference.html#useeffect */ - final def useEffectBy[A](init: CtxFn[A])(implicit a: UseEffectArg[A], step: Step): step.Self = - useEffectBy(step.squash(init)(_)) + final def useEffectBy[A](effect: CtxFn[A])(implicit a: UseEffectArg[A], step: Step): step.Self = + useEffectBy(step.squash(effect)(_)) /** The callback passed to useEffect will run after the render is committed to the screen. Think of effects as an * escape hatch from React’s purely functional world into the imperative world. * - * This will only execute the effect when values in the second argument, change. + * This will only execute the effect when values in the first argument change. * * @see https://reactjs.org/docs/hooks-reference.html#useeffect */ @@ -589,13 +740,12 @@ object Api { * Prefer the standard [[useEffect]] when possible to avoid blocking visual updates. * * If you'd only like to execute the effect when your component is mounted, then use [[useLayoutEffectOnMount]]. - * If you'd only like to execute the effect when certain values have changed, provide those certain values as - * the second argument. + * If you'd only like to execute the effect when certain values have changed, then use [[useLayoutEffectWithDeps]]. * * @see https://reactjs.org/docs/hooks-reference.html#useLayoutEffect */ - final def useLayoutEffectBy[A](init: CtxFn[A])(implicit a: UseEffectArg[A], step: Step): step.Self = - useLayoutEffectBy(step.squash(init)(_)) + final def useLayoutEffectBy[A](effect: CtxFn[A])(implicit a: UseEffectArg[A], step: Step): step.Self = + useLayoutEffectBy(step.squash(effect)(_)) /** The signature is identical to [[useEffect]], but it fires synchronously after all DOM mutations. Use this to * read layout from the DOM and synchronously re-render. Updates scheduled inside useLayoutEffect will be flushed @@ -603,7 +753,7 @@ object Api { * * Prefer the standard [[useEffect]] when possible to avoid blocking visual updates. * - * This will only execute the effect when values in the second argument, change. + * This will only execute the effect when values in the first argument change. * * @see https://reactjs.org/docs/hooks-reference.html#useLayoutEffect */ @@ -623,6 +773,46 @@ object Api { final def useLayoutEffectOnMountBy[A](effect: CtxFn[A])(implicit a: UseEffectArg[A], step: Step): step.Self = useLayoutEffectOnMountBy(step.squash(effect)(_)) + /** The signature is identical to [[useEffect]], but it fires synchronously after all DOM mutations, but before any + * layout Effects fire. Use this to insert styles before any Effects fire that may need to read layout. Updates + * scheduled inside useLayoutEffect will be flushed synchronously, before the browser has a chance to paint. + * + * Prefer the standard [[useEffect]] when possible to avoid blocking visual updates. + * + * If you'd only like to execute the effect when your component is mounted, then use [[useInsertionEffectOnMount]]. + * If you'd only like to execute the effect when certain values have changed, then use [[useInsertionEffectWithDeps]]. + * + * @see https://react.dev/reference/react/useInsertionEffect#useInsertionEffect + */ + final def useInsertionEffectBy[A](effect: CtxFn[A])(implicit a: UseEffectArg[A], step: Step): step.Self = + useInsertionEffectBy(step.squash(effect)(_)) + + /** The signature is identical to [[useEffect]], but it fires synchronously after all DOM mutations, but before any + * layout Effects fire. Use this to insert styles before any Effects fire that may need to read layout. Updates + * scheduled inside useLayoutEffect will be flushed synchronously, before the browser has a chance to paint. + * + * Prefer the standard [[useEffect]] when possible to avoid blocking visual updates. + * + * This will only execute the effect when values in the first argument change. + * + * @see https://react.dev/reference/react/useInsertionEffect#useInsertionEffect + */ + final def useInsertionEffectWithDepsBy[D, A](deps: CtxFn[D])(effect: CtxFn[D => A])(implicit a: UseEffectArg[A], r: Reusability[D], step: Step): step.Self = + useInsertionEffectWithDepsBy(step.squash(deps)(_))(step.squash(effect)(_)) + + /** The signature is identical to [[useEffect]], but it fires synchronously after all DOM mutations, but before any + * layout Effects fire. Use this to insert styles before any Effects fire that may need to read layout. Updates + * scheduled inside useLayoutEffect will be flushed synchronously, before the browser has a chance to paint. + * + * Prefer the standard [[useEffect]] when possible to avoid blocking visual updates. + * + * This will only execute the effect when your component is mounted. + * + * @see https://react.dev/reference/react/useInsertionEffect#useInsertionEffect + */ + final def useInsertionEffectOnMountBy[A](effect: CtxFn[A])(implicit a: UseEffectArg[A], step: Step): step.Self = + useInsertionEffectOnMountBy(step.squash(effect)(_)) + /** Returns a memoized value. * * Pass a “create” function and any dependencies. useMemo will only recompute the memoized value when one @@ -674,6 +864,43 @@ object Api { */ final def useStateWithReuseBy[S: ClassTag: Reusability](initialState: CtxFn[S])(implicit step: Step): step.Next[UseStateWithReuse[S]] = useStateWithReuseBy(step.squash(initialState)(_)) + + /** + * Lets you subscribe to an external store. + * + * @see + * {@link https://react.dev/reference/react/useSyncExternalStore} + */ + @inline final def useSyncExternalStoreBy[F[_], A](subscribe: CtxFn[F[Unit] => F[F[Unit]]], getSnapshot: CtxFn[F[A]])(implicit F: Sync[F], step: Step): step.Next[A] = + useSyncExternalStoreBy(subscribe, getSnapshot, js.undefined) + /** + * Lets you subscribe to an external store. + * + * @see + * {@link https://react.dev/reference/react/useSyncExternalStore} + */ + final def useSyncExternalStoreBy[F[_], A](subscribe: CtxFn[F[Unit] => F[F[Unit]]], getSnapshot: CtxFn[F[A]], getServerSnapshot: js.UndefOr[CtxFn[F[A]]])(implicit F: Sync[F], step: Step): step.Next[A] = + useSyncExternalStoreBy(ctx => step.squash(subscribe)(ctx), ctx => step.squash(getSnapshot)(ctx), getServerSnapshot.map(f => ctx => step.squash(f)(ctx))) + + /** + * Lets you defer updating a part of the UI. + * + * @see + * {@link https://react.dev/reference/react/useDeferredValue} + */ + @inline final def useDeferredValue[A](value: CtxFn[A])(implicit step: Step): step.Next[A] = + useDeferredValue(ctx => step.squash(value)(ctx)) + + // initialValue was added in React 19 - Uncomment when we upgrade to React 19 + // /** + // * Lets you defer updating a part of the UI. + // * + // * @see + // * {@link https://react.dev/reference/react/useDeferredValue} + // */ + // final def useDeferredValue[A](value: CtxFn[A], initialValue: CtxFn[A])(implicit step: Step): step.Next[A] = + // // useDeferredValue(ctx => step.squash(value)(ctx), initialValue.map(f => ctx => step.squash(f)(ctx))) + // useDeferredValue(ctx => step.squash(value)(ctx), ctx => step.squash(initialValue)(ctx)) } // =================================================================================================================== diff --git a/library/coreGeneric/src/main/scala/japgolly/scalajs/react/hooks/CustomHook.scala b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/hooks/CustomHook.scala index 89deaefb9..7ba02ebee 100644 --- a/library/coreGeneric/src/main/scala/japgolly/scalajs/react/hooks/CustomHook.scala +++ b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/hooks/CustomHook.scala @@ -35,6 +35,12 @@ final class CustomHook[I, O] private[CustomHook] (val unsafeInit: I => O) extend val o2 = next.unsafeInit(is._2) O.merge(o1, o2) } + + def toHookResult(implicit ev: I =:= Unit): HookResult[O] = + HookResult(unsafeInit(ev.flip(()))) + + def toHookResult: I => HookResult[O] = + (i: I) => HookResult(unsafeInit(i)) } object CustomHook { @@ -42,6 +48,12 @@ object CustomHook { def apply[I]: Builder.First[I] = new Builder.First(_ => ()) + def fromHookResult[O](hook: HookResult[O]): CustomHook[Unit, O] = + CustomHook.unchecked(_ => hook.eval()) + + def fromHookResult[I, O](hook: I => HookResult[O]): CustomHook[I, O] = + CustomHook.unchecked(i => hook(i).eval()) + /** Provides you with a means to do whatever you want without the static guarantees that the normal DSL provides. * It's up to you to ensure you don't vioalte React's hook rules. */ @@ -79,7 +91,7 @@ object CustomHook { step.self(init, f) override protected def next[H](f: I => H)(implicit step: Step): step.Next[H] = - step.next(init, f) + step.next(init, f, "") def build: CustomHook[I, Unit] = CustomHook.unchecked(init) @@ -95,13 +107,13 @@ object CustomHook { def apply[O](f: Ctx => O): CustomHook[I, O] } - final class Subsequent[I, Ctx, CtxFn[_]](buildFn: BuildFn[I, Ctx]) extends Api.Secondary[Ctx, CtxFn, SubsequentStep[I, Ctx, CtxFn]] { + final class Subsequent[I, Ctx, CtxFn[_]](displayName: String)(buildFn: BuildFn[I, Ctx]) extends Api.Secondary[Ctx, CtxFn, SubsequentStep[I, Ctx, CtxFn]] { override protected def self(f: Ctx => Any)(implicit step: Step): step.Self = - step.self(buildFn, f) + step.self(buildFn, f, displayName) override protected def next[H](f: Ctx => H)(implicit step: Step): step.Next[H] = - step.next[H](buildFn, f) + step.next[H](buildFn, f, displayName) def build: CustomHook[I, Unit] = buildReturning(_ => ()) @@ -125,7 +137,7 @@ object CustomHook { f(i) }) - def next[H1](initFirst: I => Unit, hook1: I => H1): Next[H1] = { + def next[H1](initFirst: I => Unit, hook1: I => H1, displayName: String): Next[H1] = { type Ctx = HookCtx.I1[I, H1] val build: BuildFn[I, Ctx] = new BuildFn[I, Ctx] { @@ -137,7 +149,7 @@ object CustomHook { f(ctx) } } - new Subsequent[I, HookCtx.I1[I, H1], ({ type F[A] = (I, H1) => A})#F](build) + new Subsequent[I, HookCtx.I1[I, H1], ({ type F[A] = (I, H1) => A})#F](displayName)(build) } } @@ -146,8 +158,8 @@ object CustomHook { trait SubsequentStep[I, _Ctx, _CtxFn[_]] extends Api.SubsequentStep[_Ctx, _CtxFn] { final type Self = Subsequent[I, Ctx, CtxFn] - final def self: (BuildFn[I, Ctx], Ctx => Any) => Self = - (buildPrev, initNextHook) => { + final def self: (BuildFn[I, Ctx], Ctx => Any, String) => Self = + (buildPrev, initNextHook, displayName) => { val buildNext: BuildFn[I, Ctx] = new BuildFn[I, Ctx] { override def apply[O](f: Ctx => O) = { @@ -157,9 +169,9 @@ object CustomHook { } } } - new Subsequent[I, Ctx, CtxFn](buildNext) + new Subsequent[I, Ctx, CtxFn](displayName)(buildNext) } - def next[A]: (BuildFn[I, Ctx], Ctx => A) => Next[A] + def next[A]: (BuildFn[I, Ctx], Ctx => A, String) => Next[A] } object SubsequentStep extends Custom_SubsequentSteps { diff --git a/library/coreGeneric/src/main/scala/japgolly/scalajs/react/hooks/HookComponentBuilder.scala b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/hooks/HookComponentBuilder.scala index 33f9e7e2d..8dd4a86c6 100644 --- a/library/coreGeneric/src/main/scala/japgolly/scalajs/react/hooks/HookComponentBuilder.scala +++ b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/hooks/HookComponentBuilder.scala @@ -8,27 +8,26 @@ import japgolly.scalajs.react.{Children, CtorType, PropsChildren, Reusability} object HookComponentBuilder { - def apply[P]: ComponentP.First[P] = - new ComponentP.First(_ => ()) + def apply[P](displayName: String): ComponentP.First[P] = + new ComponentP.First(displayName)(_ => ()) // =================================================================================================================== // Component with Props object ComponentP { - final class First[P](init: P => Unit) extends Api.PrimaryWithRender[P, Children.None, P, FirstStep[P]] { - + final class First[P](displayName: String)(init: P => Unit) extends Api.PrimaryWithRender[P, Children.None, P, FirstStep[P]] { override protected def self(f: P => Any)(implicit step: Step): step.Self = - step.self(init, f) + step.self(init, f, displayName) override protected def next[H](f: P => H)(implicit step: Step): step.Next[H] = - step.next(init, f) + step.next(init, f, displayName) def withPropsChildren: ComponentPC.First[P] = - new ComponentPC.First(ctx => init(ctx.props)) + new ComponentPC.First(displayName)(ctx => init(ctx.props)) override def render(f: P => VdomNode)(implicit s: CtorType.Summoner[Box[P], Children.None]): Component[P, s.CT] = - ScalaFn{ p => + ScalaFn.withDisplayName(displayName){ p => init(p) f(p) } @@ -40,22 +39,22 @@ object HookComponentBuilder { type RenderFn[-P, +Ctx] = (Ctx => VdomNode) => P => VdomNode - final class Subsequent[P, Ctx, CtxFn[_]](renderFn: RenderFn[P, Ctx]) extends Api.SecondaryWithRender[P, Children.None, Ctx, CtxFn, SubsequentStep[P, Ctx, CtxFn]] { + final class Subsequent[P, Ctx, CtxFn[_]](displayName: String)(renderFn: RenderFn[P, Ctx]) extends Api.SecondaryWithRender[P, Children.None, Ctx, CtxFn, SubsequentStep[P, Ctx, CtxFn]] { override protected def self(f: Ctx => Any)(implicit step: Step): step.Self = - step.self(renderFn, f) + step.self(renderFn, f, displayName) override protected def next[H](f: Ctx => H)(implicit step: Step): step.Next[H] = - step.next[H](renderFn, f) + step.next[H](renderFn, f, displayName) override def render(f: Ctx => VdomNode)(implicit s: CtorType.Summoner[Box[P], Children.None]): Component[P, s.CT] = - ScalaFn(renderFn(f)) + ScalaFn.withDisplayName(displayName)(renderFn(f).andThen(HookResult(_))) override def renderWithReuseBy[A](reusableInputs: Ctx => A)(f: A => VdomNode)(implicit s: CtorType.Summoner[Box[P], Children.None], r: Reusability[A]): Component[P, s.CT] = { val hook = CustomHook.shouldComponentUpdate(f) - ScalaFn(renderFn { ctx => + ScalaFn.withDisplayName(displayName)(renderFn { ctx => hook.unsafeInit(() => reusableInputs(ctx)) - }) + }.andThen(HookResult(_))) } } @@ -65,13 +64,13 @@ object HookComponentBuilder { override type Self = First[P] override type Next[H1] = Subsequent[P, HookCtx.P1[P, H1], ({ type F[A] = (P, H1) => A})#F] - def self(initFirst: P => Unit, f: P => Any): Self = - new First[P](p => { + def self(initFirst: P => Unit, f: P => Any, displayName: String): Self = + new First[P](displayName)(p => { initFirst(p) f(p) }) - def next[H1](initFirst: P => Unit, hook1: P => H1): Next[H1] = { + def next[H1](initFirst: P => Unit, hook1: P => H1, displayName: String): Next[H1] = { type Ctx = HookCtx.P1[P, H1] val render: RenderFn[P, Ctx] = f => p => { @@ -79,7 +78,7 @@ object HookComponentBuilder { val h1 = hook1(p) f(HookCtx(p, h1)) } - new Subsequent[P, HookCtx.P1[P, H1], ({ type F[A] = (P, H1) => A})#F](render) + new Subsequent[P, HookCtx.P1[P, H1], ({ type F[A] = (P, H1) => A})#F](displayName)(render) } } @@ -88,16 +87,16 @@ object HookComponentBuilder { trait SubsequentStep[P, _Ctx, _CtxFn[_]] extends Api.SubsequentStep[_Ctx, _CtxFn] { final type Self = Subsequent[P, Ctx, CtxFn] - final def self: (RenderFn[P, Ctx], Ctx => Any) => Self = - (renderPrev, initNextHook) => { + final def self: (RenderFn[P, Ctx], Ctx => Any, String) => Self = + (renderPrev, initNextHook, displayName) => { val renderNext: ComponentP.RenderFn[P, Ctx] = render => renderPrev { ctx => initNextHook(ctx) render(ctx) } - new ComponentP.Subsequent[P, Ctx, CtxFn](renderNext) + new ComponentP.Subsequent[P, Ctx, CtxFn](displayName)(renderNext) } - def next[A]: (RenderFn[P, Ctx], Ctx => A) => Next[A] + def next[A]: (RenderFn[P, Ctx], Ctx => A, String) => Next[A] } object SubsequentStep extends ComponentP_SubsequentSteps { @@ -110,20 +109,20 @@ object HookComponentBuilder { object ComponentPC { - final class First[P](init: HookCtx.PC0[P] => Unit) extends Api.PrimaryWithRender[P, Children.Varargs, HookCtx.PC0[P], FirstStep[P]] { + final class First[P](displayName: String)(init: HookCtx.PC0[P] => Unit) extends Api.PrimaryWithRender[P, Children.Varargs, HookCtx.PC0[P], FirstStep[P]] { type Ctx = HookCtx.PC0[P] override protected def self(f: Ctx => Any)(implicit step: Step): step.Self = - step.self(init, f) + step.self(init, f, displayName) override protected def next[H](f: Ctx => H)(implicit step: Step): step.Next[H] = - step.next(init, f) + step.next(init, f, displayName) override def render(f: Ctx => VdomNode)(implicit s: CtorType.Summoner[Box[P], Children.Varargs]): Component[P, s.CT] = - ScalaFn.withChildren((p: P, pc: PropsChildren) => f(HookCtx.withChildren(p, pc))) + ScalaFn.withDisplayName(displayName).withChildren((p: P, pc: PropsChildren) => f(HookCtx.withChildren(p, pc))) def render(f: (P, PropsChildren) => VdomNode)(implicit s: CtorType.Summoner[Box[P], Children.Varargs]): Component[P, s.CT] = - ScalaFn.withChildren(f) + ScalaFn.withDisplayName(displayName).withChildren((p, c) => HookResult(f(p, c))) override def renderWithReuseBy[A](reusableInputs: Ctx => A)(f: A => VdomNode)(implicit s: CtorType.Summoner[Box[P], Children.Varargs], r: Reusability[A]): Component[P, s.CT] = customBy(ctx => CustomHook.shouldComponentUpdate(f).apply(() => reusableInputs(ctx))) @@ -132,22 +131,26 @@ object HookComponentBuilder { type RenderFn[-P, +Ctx] = (Ctx => VdomNode) => (P, PropsChildren) => VdomNode - final class Subsequent[P, Ctx, CtxFn[_]](renderFn: RenderFn[P, Ctx]) extends Api.SecondaryWithRender[P, Children.Varargs, Ctx, CtxFn, SubsequentStep[P, Ctx, CtxFn]] { + final class Subsequent[P, Ctx, CtxFn[_]](displayName: String)(renderFn: RenderFn[P, Ctx]) extends Api.SecondaryWithRender[P, Children.Varargs, Ctx, CtxFn, SubsequentStep[P, Ctx, CtxFn]] { override protected def self(f: Ctx => Any)(implicit step: Step): step.Self = - step.self(renderFn, f) + step.self(renderFn, f, displayName) override protected def next[H](f: Ctx => H)(implicit step: Step): step.Next[H] = - step.next[H](renderFn, f) + step.next[H](renderFn, f, displayName) override def render(f: Ctx => VdomNode)(implicit s: CtorType.Summoner[Box[P], Children.Varargs]): Component[P, s.CT] = - ScalaFn.withChildren(renderFn(f)) + ScalaFn.withDisplayName(displayName).withChildren((p, c) => HookResult(renderFn(f)(p, c))) override def renderWithReuseBy[A](reusableInputs: Ctx => A)(f: A => VdomNode)(implicit s: CtorType.Summoner[Box[P], Children.Varargs], r: Reusability[A]): Component[P, s.CT] = { val hook = CustomHook.shouldComponentUpdate(f) - ScalaFn.withChildren(renderFn { ctx => - hook.unsafeInit(() => reusableInputs(ctx)) - }) + ScalaFn.withDisplayName(displayName).withChildren((p, c) => + HookResult( + renderFn { ctx => + hook.unsafeInit(() => reusableInputs(ctx)) + }(p, c) + ) + ) } } @@ -157,13 +160,13 @@ object HookComponentBuilder { override type Self = First[P] override type Next[H1] = Subsequent[P, HookCtx.PC1[P, H1], ({ type F[A] = (P, PropsChildren, H1) => A})#F] - def self(initFirst: HookCtx.PC0[P] => Unit, f: HookCtx.PC0[P] => Any): Self = - new First[P](ctx0 => { + def self(initFirst: HookCtx.PC0[P] => Unit, f: HookCtx.PC0[P] => Any, displayName: String): Self = + new First[P](displayName)(ctx0 => { initFirst(ctx0) f(ctx0) }) - def next[H1](initFirst: HookCtx.PC0[P] => Unit, hook1: HookCtx.PC0[P] => H1): Next[H1] = { + def next[H1](initFirst: HookCtx.PC0[P] => Unit, hook1: HookCtx.PC0[P] => H1, displayName: String): Next[H1] = { type Ctx = HookCtx.PC1[P, H1] val render: RenderFn[P, Ctx] = f => (p, pc) => { @@ -172,7 +175,7 @@ object HookComponentBuilder { val h1 = hook1(ctx0) f(HookCtx.withChildren(p, pc, h1)) } - new Subsequent[P, HookCtx.PC1[P, H1], ({ type F[A] = (P, PropsChildren, H1) => A})#F](render) + new Subsequent[P, HookCtx.PC1[P, H1], ({ type F[A] = (P, PropsChildren, H1) => A})#F](displayName)(render) } } @@ -181,16 +184,16 @@ object HookComponentBuilder { trait SubsequentStep[P, _Ctx, _CtxFn[_]] extends Api.SubsequentStep[_Ctx, _CtxFn] { final type Self = Subsequent[P, Ctx, CtxFn] - final def self: (RenderFn[P, Ctx], Ctx => Any) => Self = - (renderPrev, initNextHook) => { + final def self: (RenderFn[P, Ctx], Ctx => Any, String) => Self = + (renderPrev, initNextHook, displayName) => { val renderNext: ComponentPC.RenderFn[P, Ctx] = render => renderPrev { ctx => initNextHook(ctx) render(ctx) } - new ComponentPC.Subsequent[P, Ctx, CtxFn](renderNext) + new ComponentPC.Subsequent[P, Ctx, CtxFn](displayName)(renderNext) } - def next[A]: (RenderFn[P, Ctx], Ctx => A) => Next[A] + def next[A]: (RenderFn[P, Ctx], Ctx => A, String) => Next[A] } object SubsequentStep extends ComponentPC_SubsequentSteps { diff --git a/library/coreGeneric/src/main/scala/japgolly/scalajs/react/hooks/Hooks.scala b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/hooks/Hooks.scala index 16a7abd48..d9a58e5a9 100644 --- a/library/coreGeneric/src/main/scala/japgolly/scalajs/react/hooks/Hooks.scala +++ b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/hooks/Hooks.scala @@ -1,6 +1,7 @@ package japgolly.scalajs.react.hooks import japgolly.scalajs.react.component.{Js => JsComponent, Scala => ScalaComponent} +import japgolly.scalajs.react.facade.React import japgolly.scalajs.react.feature.Context import japgolly.scalajs.react.internal.Box import japgolly.scalajs.react.util.Effect._ @@ -30,10 +31,15 @@ object Hooks { override def fromJs = g } - implicit def callback[F[_]](implicit F: Dispatch[F]): UseCallbackArg[F[Unit]] = + implicit def dispatchUnit[F[_]](implicit F: Dispatch[F]): UseCallbackArg[F[Unit]] = apply[F[Unit], js.Function0[Unit]]( F.dispatchFn(_))( f => Reusable.byRef(f).withValue(F.delay(f()))) + + implicit def syncValue[F[_], A](implicit F: UnsafeSync[F]): UseCallbackArg[F[A]] = + apply[F[A], js.Function0[A]]( + F.toJsFn(_))( + f => Reusable.byRef(f).withValue(F.delay(f()))) } object UseCallback { @@ -99,6 +105,12 @@ object Hooks { def unsafeCreateLayoutOnMount[A](effect: A)(implicit a: UseEffectArg[A]): Unit = facade.React.useLayoutEffect(a.toJs(effect), new js.Array[Any]) + + def unsafeCreateInsertion[A](effect: A)(implicit a: UseEffectArg[A]): Unit = + facade.React.useInsertionEffect(a.toJs(effect)) + + def unsafeCreateInsertionOnMount[A](effect: A)(implicit a: UseEffectArg[A]): Unit = + facade.React.useInsertionEffect(a.toJs(effect), new js.Array[Any]) } object ReusableEffect { @@ -112,6 +124,11 @@ object Hooks { CustomHook.reusableDeps[D] .apply(() => deps) .map { case (d, rev) => facade.React.useLayoutEffect(a.toJs(effect(d)), js.Array[Int](rev)) } + + def useInsertionEffect[D, A](deps: => D)(effect: D => A)(implicit a: UseEffectArg[A], r: Reusability[D]): CustomHook[Unit, Unit] = + CustomHook.reusableDeps[D] + .apply(() => deps) + .map { case (d, rev) => facade.React.useInsertionEffect(a.toJs(effect(d)), js.Array[Int](rev)) } } // =================================================================================================================== @@ -258,9 +275,9 @@ object Hooks { } object UseStateF { - def apply[F[_], S, O](r: facade.React.UseState[S], oss: Reusable[facade.React.UseStateSetter[O]])(implicit f: Sync[F]): UseStateF[F, S] = + def apply[F[_], S, O](r: facade.React.UseState[S], oss: Reusable[facade.React.UseStateSetter[O]])(implicit FF: Sync[F]): UseStateF[F, S] = new UseStateF[F, S] { - override protected[hooks] implicit def F = f + override protected[hooks] implicit def F: Sync[F] = FF override val raw = r override type OriginalState = O override val originalSetState = oss @@ -438,4 +455,61 @@ object Hooks { F.delay { value = f(value) } } + + // =================================================================================================================== + + object UseId { + def apply(): CustomHook[Unit, String] = + CustomHook.delay(facade.React.useId()) + } + + // =================================================================================================================== + + type UseTransition = UseTransitionF[D.Sync] + + object UseTransition { + @inline def apply(): CustomHook[Unit, UseTransition] = + CustomHook.delay(UseTransitionF(facade.React.useTransition())(D.Sync)) + } + + trait UseTransitionF[F[_]] { + protected[hooks] implicit def F: Sync[F] + val raw: facade.React.UseTransition + + /** Whether we’re waiting for the transition to finish + */ + def isPending: Boolean = raw._1 + + /** A function that takes a callback. We can use it to tell React which state we want to defer. + */ + def startTransition(cb: => F[Unit]): F[Unit] = + F.delay(raw._2(F.toJsFn(cb))) + } + + object UseTransitionF { + def apply[F[_]](r: facade.React.UseTransition)(implicit FF: Sync[F]): UseTransitionF[F] = + new UseTransitionF[F] { + override protected[hooks] implicit def F: Sync[F] = FF + + override val raw: React.UseTransition = r + } + } + + object UseSyncExternalStore { + def apply[F[_], A](subscribe: F[Unit] => F[F[Unit]], getSnapshot: F[A], getServerSnapshot: js.UndefOr[F[A]] = js.undefined)(implicit F: Sync[F]): CustomHook[Unit, A] = { + val subscribeJs: facade.React.UseSyncExternalStoreSubscribeArg = (update: js.Function0[Unit]) => F.runSync(F.map(subscribe(F.fromJsFn0(update)))(F.toJsFn(_))) + val getSnapshotJs: js.Function0[A] = F.toJsFn(getSnapshot) + val getServerSnapshotJs: js.UndefOr[js.Function0[A]] = getServerSnapshot.map(F.toJsFn(_)) + CustomHook.delay(facade.React.useSyncExternalStore(subscribeJs, getSnapshotJs, getServerSnapshotJs)) + } + } + + object UseDeferredValue { + // initialValue was added in React 19 - Replace when we upgrade to React 19 + // def apply[A](value: A, initialValue: js.UndefOr[A] = js.undefined): CustomHook[Unit, A] = + // CustomHook.delay(facade.React.useDeferredValue(value, initialValue)) + + def apply[A](value: A): CustomHook[Unit, A] = + CustomHook.delay(facade.React.useDeferredValue(value)) + } } diff --git a/library/coreGeneric/src/main/scala/japgolly/scalajs/react/hooks/StepBoilerplate.scala b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/hooks/StepBoilerplate.scala index 0b68dbc61..ee43b5f57 100644 --- a/library/coreGeneric/src/main/scala/japgolly/scalajs/react/hooks/StepBoilerplate.scala +++ b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/hooks/StepBoilerplate.scala @@ -47,14 +47,14 @@ trait ComponentP_SubsequentSteps { self: ComponentP.SubsequentStep.type => new ComponentP.SubsequentStep[P, HookCtx.P1[P, H1], ({ type F[A] = (P, H1) => A})#F] { override type Next[H2] = ComponentP.Subsequent.AtStep1[P, H1]#Next[H2] override def next[H2] = - (renderPrev, initNextHook) => { + (renderPrev, initNextHook, displayName) => { val renderNext: ComponentP.RenderFn[P, HookCtx.P2[P, H1, H2]] = render => renderPrev { ctx1 => val h2 = initNextHook(ctx1) val ctx2 = HookCtx(ctx1.props, ctx1.hook1, h2) render(ctx2) } - new ComponentP.Subsequent[P, HookCtx.P2[P, H1, H2], ({ type F[A] = (P, H1, H2) => A})#F](renderNext) + new ComponentP.Subsequent[P, HookCtx.P2[P, H1, H2], ({ type F[A] = (P, H1, H2) => A})#F](displayName)(renderNext) } override def squash[A] = f => _.apply1(f) } @@ -69,14 +69,14 @@ trait ComponentP_SubsequentSteps { self: ComponentP.SubsequentStep.type => new ComponentP.SubsequentStep[P, HookCtx.P2[P, H1, H2], ({ type F[A] = (P, H1, H2) => A})#F] { override type Next[H3] = ComponentP.Subsequent.AtStep2[P, H1, H2]#Next[H3] override def next[H3] = - (renderPrev, initNextHook) => { + (renderPrev, initNextHook, displayName) => { val renderNext: ComponentP.RenderFn[P, HookCtx.P3[P, H1, H2, H3]] = render => renderPrev { ctx2 => val h3 = initNextHook(ctx2) val ctx3 = HookCtx(ctx2.props, ctx2.hook1, ctx2.hook2, h3) render(ctx3) } - new ComponentP.Subsequent[P, HookCtx.P3[P, H1, H2, H3], ({ type F[A] = (P, H1, H2, H3) => A})#F](renderNext) + new ComponentP.Subsequent[P, HookCtx.P3[P, H1, H2, H3], ({ type F[A] = (P, H1, H2, H3) => A})#F](displayName)(renderNext) } override def squash[A] = f => _.apply2(f) } @@ -91,14 +91,14 @@ trait ComponentP_SubsequentSteps { self: ComponentP.SubsequentStep.type => new ComponentP.SubsequentStep[P, HookCtx.P3[P, H1, H2, H3], ({ type F[A] = (P, H1, H2, H3) => A})#F] { override type Next[H4] = ComponentP.Subsequent.AtStep3[P, H1, H2, H3]#Next[H4] override def next[H4] = - (renderPrev, initNextHook) => { + (renderPrev, initNextHook, displayName) => { val renderNext: ComponentP.RenderFn[P, HookCtx.P4[P, H1, H2, H3, H4]] = render => renderPrev { ctx3 => val h4 = initNextHook(ctx3) val ctx4 = HookCtx(ctx3.props, ctx3.hook1, ctx3.hook2, ctx3.hook3, h4) render(ctx4) } - new ComponentP.Subsequent[P, HookCtx.P4[P, H1, H2, H3, H4], ({ type F[A] = (P, H1, H2, H3, H4) => A})#F](renderNext) + new ComponentP.Subsequent[P, HookCtx.P4[P, H1, H2, H3, H4], ({ type F[A] = (P, H1, H2, H3, H4) => A})#F](displayName)(renderNext) } override def squash[A] = f => _.apply3(f) } @@ -113,14 +113,14 @@ trait ComponentP_SubsequentSteps { self: ComponentP.SubsequentStep.type => new ComponentP.SubsequentStep[P, HookCtx.P4[P, H1, H2, H3, H4], ({ type F[A] = (P, H1, H2, H3, H4) => A})#F] { override type Next[H5] = ComponentP.Subsequent.AtStep4[P, H1, H2, H3, H4]#Next[H5] override def next[H5] = - (renderPrev, initNextHook) => { + (renderPrev, initNextHook, displayName) => { val renderNext: ComponentP.RenderFn[P, HookCtx.P5[P, H1, H2, H3, H4, H5]] = render => renderPrev { ctx4 => val h5 = initNextHook(ctx4) val ctx5 = HookCtx(ctx4.props, ctx4.hook1, ctx4.hook2, ctx4.hook3, ctx4.hook4, h5) render(ctx5) } - new ComponentP.Subsequent[P, HookCtx.P5[P, H1, H2, H3, H4, H5], ({ type F[A] = (P, H1, H2, H3, H4, H5) => A})#F](renderNext) + new ComponentP.Subsequent[P, HookCtx.P5[P, H1, H2, H3, H4, H5], ({ type F[A] = (P, H1, H2, H3, H4, H5) => A})#F](displayName)(renderNext) } override def squash[A] = f => _.apply4(f) } @@ -135,14 +135,14 @@ trait ComponentP_SubsequentSteps { self: ComponentP.SubsequentStep.type => new ComponentP.SubsequentStep[P, HookCtx.P5[P, H1, H2, H3, H4, H5], ({ type F[A] = (P, H1, H2, H3, H4, H5) => A})#F] { override type Next[H6] = ComponentP.Subsequent.AtStep5[P, H1, H2, H3, H4, H5]#Next[H6] override def next[H6] = - (renderPrev, initNextHook) => { + (renderPrev, initNextHook, displayName) => { val renderNext: ComponentP.RenderFn[P, HookCtx.P6[P, H1, H2, H3, H4, H5, H6]] = render => renderPrev { ctx5 => val h6 = initNextHook(ctx5) val ctx6 = HookCtx(ctx5.props, ctx5.hook1, ctx5.hook2, ctx5.hook3, ctx5.hook4, ctx5.hook5, h6) render(ctx6) } - new ComponentP.Subsequent[P, HookCtx.P6[P, H1, H2, H3, H4, H5, H6], ({ type F[A] = (P, H1, H2, H3, H4, H5, H6) => A})#F](renderNext) + new ComponentP.Subsequent[P, HookCtx.P6[P, H1, H2, H3, H4, H5, H6], ({ type F[A] = (P, H1, H2, H3, H4, H5, H6) => A})#F](displayName)(renderNext) } override def squash[A] = f => _.apply5(f) } @@ -157,14 +157,14 @@ trait ComponentP_SubsequentSteps { self: ComponentP.SubsequentStep.type => new ComponentP.SubsequentStep[P, HookCtx.P6[P, H1, H2, H3, H4, H5, H6], ({ type F[A] = (P, H1, H2, H3, H4, H5, H6) => A})#F] { override type Next[H7] = ComponentP.Subsequent.AtStep6[P, H1, H2, H3, H4, H5, H6]#Next[H7] override def next[H7] = - (renderPrev, initNextHook) => { + (renderPrev, initNextHook, displayName) => { val renderNext: ComponentP.RenderFn[P, HookCtx.P7[P, H1, H2, H3, H4, H5, H6, H7]] = render => renderPrev { ctx6 => val h7 = initNextHook(ctx6) val ctx7 = HookCtx(ctx6.props, ctx6.hook1, ctx6.hook2, ctx6.hook3, ctx6.hook4, ctx6.hook5, ctx6.hook6, h7) render(ctx7) } - new ComponentP.Subsequent[P, HookCtx.P7[P, H1, H2, H3, H4, H5, H6, H7], ({ type F[A] = (P, H1, H2, H3, H4, H5, H6, H7) => A})#F](renderNext) + new ComponentP.Subsequent[P, HookCtx.P7[P, H1, H2, H3, H4, H5, H6, H7], ({ type F[A] = (P, H1, H2, H3, H4, H5, H6, H7) => A})#F](displayName)(renderNext) } override def squash[A] = f => _.apply6(f) } @@ -179,14 +179,14 @@ trait ComponentP_SubsequentSteps { self: ComponentP.SubsequentStep.type => new ComponentP.SubsequentStep[P, HookCtx.P7[P, H1, H2, H3, H4, H5, H6, H7], ({ type F[A] = (P, H1, H2, H3, H4, H5, H6, H7) => A})#F] { override type Next[H8] = ComponentP.Subsequent.AtStep7[P, H1, H2, H3, H4, H5, H6, H7]#Next[H8] override def next[H8] = - (renderPrev, initNextHook) => { + (renderPrev, initNextHook, displayName) => { val renderNext: ComponentP.RenderFn[P, HookCtx.P8[P, H1, H2, H3, H4, H5, H6, H7, H8]] = render => renderPrev { ctx7 => val h8 = initNextHook(ctx7) val ctx8 = HookCtx(ctx7.props, ctx7.hook1, ctx7.hook2, ctx7.hook3, ctx7.hook4, ctx7.hook5, ctx7.hook6, ctx7.hook7, h8) render(ctx8) } - new ComponentP.Subsequent[P, HookCtx.P8[P, H1, H2, H3, H4, H5, H6, H7, H8], ({ type F[A] = (P, H1, H2, H3, H4, H5, H6, H7, H8) => A})#F](renderNext) + new ComponentP.Subsequent[P, HookCtx.P8[P, H1, H2, H3, H4, H5, H6, H7, H8], ({ type F[A] = (P, H1, H2, H3, H4, H5, H6, H7, H8) => A})#F](displayName)(renderNext) } override def squash[A] = f => _.apply7(f) } @@ -201,14 +201,14 @@ trait ComponentP_SubsequentSteps { self: ComponentP.SubsequentStep.type => new ComponentP.SubsequentStep[P, HookCtx.P8[P, H1, H2, H3, H4, H5, H6, H7, H8], ({ type F[A] = (P, H1, H2, H3, H4, H5, H6, H7, H8) => A})#F] { override type Next[H9] = ComponentP.Subsequent.AtStep8[P, H1, H2, H3, H4, H5, H6, H7, H8]#Next[H9] override def next[H9] = - (renderPrev, initNextHook) => { + (renderPrev, initNextHook, displayName) => { val renderNext: ComponentP.RenderFn[P, HookCtx.P9[P, H1, H2, H3, H4, H5, H6, H7, H8, H9]] = render => renderPrev { ctx8 => val h9 = initNextHook(ctx8) val ctx9 = HookCtx(ctx8.props, ctx8.hook1, ctx8.hook2, ctx8.hook3, ctx8.hook4, ctx8.hook5, ctx8.hook6, ctx8.hook7, ctx8.hook8, h9) render(ctx9) } - new ComponentP.Subsequent[P, HookCtx.P9[P, H1, H2, H3, H4, H5, H6, H7, H8, H9], ({ type F[A] = (P, H1, H2, H3, H4, H5, H6, H7, H8, H9) => A})#F](renderNext) + new ComponentP.Subsequent[P, HookCtx.P9[P, H1, H2, H3, H4, H5, H6, H7, H8, H9], ({ type F[A] = (P, H1, H2, H3, H4, H5, H6, H7, H8, H9) => A})#F](displayName)(renderNext) } override def squash[A] = f => _.apply8(f) } @@ -223,14 +223,14 @@ trait ComponentP_SubsequentSteps { self: ComponentP.SubsequentStep.type => new ComponentP.SubsequentStep[P, HookCtx.P9[P, H1, H2, H3, H4, H5, H6, H7, H8, H9], ({ type F[A] = (P, H1, H2, H3, H4, H5, H6, H7, H8, H9) => A})#F] { override type Next[H10] = ComponentP.Subsequent.AtStep9[P, H1, H2, H3, H4, H5, H6, H7, H8, H9]#Next[H10] override def next[H10] = - (renderPrev, initNextHook) => { + (renderPrev, initNextHook, displayName) => { val renderNext: ComponentP.RenderFn[P, HookCtx.P10[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10]] = render => renderPrev { ctx9 => val h10 = initNextHook(ctx9) val ctx10 = HookCtx(ctx9.props, ctx9.hook1, ctx9.hook2, ctx9.hook3, ctx9.hook4, ctx9.hook5, ctx9.hook6, ctx9.hook7, ctx9.hook8, ctx9.hook9, h10) render(ctx10) } - new ComponentP.Subsequent[P, HookCtx.P10[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10], ({ type F[A] = (P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10) => A})#F](renderNext) + new ComponentP.Subsequent[P, HookCtx.P10[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10], ({ type F[A] = (P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10) => A})#F](displayName)(renderNext) } override def squash[A] = f => _.apply9(f) } @@ -245,14 +245,14 @@ trait ComponentP_SubsequentSteps { self: ComponentP.SubsequentStep.type => new ComponentP.SubsequentStep[P, HookCtx.P10[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10], ({ type F[A] = (P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10) => A})#F] { override type Next[H11] = ComponentP.Subsequent.AtStep10[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10]#Next[H11] override def next[H11] = - (renderPrev, initNextHook) => { + (renderPrev, initNextHook, displayName) => { val renderNext: ComponentP.RenderFn[P, HookCtx.P11[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11]] = render => renderPrev { ctx10 => val h11 = initNextHook(ctx10) val ctx11 = HookCtx(ctx10.props, ctx10.hook1, ctx10.hook2, ctx10.hook3, ctx10.hook4, ctx10.hook5, ctx10.hook6, ctx10.hook7, ctx10.hook8, ctx10.hook9, ctx10.hook10, h11) render(ctx11) } - new ComponentP.Subsequent[P, HookCtx.P11[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11], ({ type F[A] = (P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11) => A})#F](renderNext) + new ComponentP.Subsequent[P, HookCtx.P11[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11], ({ type F[A] = (P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11) => A})#F](displayName)(renderNext) } override def squash[A] = f => _.apply10(f) } @@ -267,14 +267,14 @@ trait ComponentP_SubsequentSteps { self: ComponentP.SubsequentStep.type => new ComponentP.SubsequentStep[P, HookCtx.P11[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11], ({ type F[A] = (P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11) => A})#F] { override type Next[H12] = ComponentP.Subsequent.AtStep11[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11]#Next[H12] override def next[H12] = - (renderPrev, initNextHook) => { + (renderPrev, initNextHook, displayName) => { val renderNext: ComponentP.RenderFn[P, HookCtx.P12[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12]] = render => renderPrev { ctx11 => val h12 = initNextHook(ctx11) val ctx12 = HookCtx(ctx11.props, ctx11.hook1, ctx11.hook2, ctx11.hook3, ctx11.hook4, ctx11.hook5, ctx11.hook6, ctx11.hook7, ctx11.hook8, ctx11.hook9, ctx11.hook10, ctx11.hook11, h12) render(ctx12) } - new ComponentP.Subsequent[P, HookCtx.P12[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12], ({ type F[A] = (P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12) => A})#F](renderNext) + new ComponentP.Subsequent[P, HookCtx.P12[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12], ({ type F[A] = (P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12) => A})#F](displayName)(renderNext) } override def squash[A] = f => _.apply11(f) } @@ -289,14 +289,14 @@ trait ComponentP_SubsequentSteps { self: ComponentP.SubsequentStep.type => new ComponentP.SubsequentStep[P, HookCtx.P12[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12], ({ type F[A] = (P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12) => A})#F] { override type Next[H13] = ComponentP.Subsequent.AtStep12[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12]#Next[H13] override def next[H13] = - (renderPrev, initNextHook) => { + (renderPrev, initNextHook, displayName) => { val renderNext: ComponentP.RenderFn[P, HookCtx.P13[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13]] = render => renderPrev { ctx12 => val h13 = initNextHook(ctx12) val ctx13 = HookCtx(ctx12.props, ctx12.hook1, ctx12.hook2, ctx12.hook3, ctx12.hook4, ctx12.hook5, ctx12.hook6, ctx12.hook7, ctx12.hook8, ctx12.hook9, ctx12.hook10, ctx12.hook11, ctx12.hook12, h13) render(ctx13) } - new ComponentP.Subsequent[P, HookCtx.P13[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13], ({ type F[A] = (P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13) => A})#F](renderNext) + new ComponentP.Subsequent[P, HookCtx.P13[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13], ({ type F[A] = (P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13) => A})#F](displayName)(renderNext) } override def squash[A] = f => _.apply12(f) } @@ -311,14 +311,14 @@ trait ComponentP_SubsequentSteps { self: ComponentP.SubsequentStep.type => new ComponentP.SubsequentStep[P, HookCtx.P13[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13], ({ type F[A] = (P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13) => A})#F] { override type Next[H14] = ComponentP.Subsequent.AtStep13[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13]#Next[H14] override def next[H14] = - (renderPrev, initNextHook) => { + (renderPrev, initNextHook, displayName) => { val renderNext: ComponentP.RenderFn[P, HookCtx.P14[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14]] = render => renderPrev { ctx13 => val h14 = initNextHook(ctx13) val ctx14 = HookCtx(ctx13.props, ctx13.hook1, ctx13.hook2, ctx13.hook3, ctx13.hook4, ctx13.hook5, ctx13.hook6, ctx13.hook7, ctx13.hook8, ctx13.hook9, ctx13.hook10, ctx13.hook11, ctx13.hook12, ctx13.hook13, h14) render(ctx14) } - new ComponentP.Subsequent[P, HookCtx.P14[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14], ({ type F[A] = (P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14) => A})#F](renderNext) + new ComponentP.Subsequent[P, HookCtx.P14[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14], ({ type F[A] = (P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14) => A})#F](displayName)(renderNext) } override def squash[A] = f => _.apply13(f) } @@ -333,14 +333,14 @@ trait ComponentP_SubsequentSteps { self: ComponentP.SubsequentStep.type => new ComponentP.SubsequentStep[P, HookCtx.P14[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14], ({ type F[A] = (P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14) => A})#F] { override type Next[H15] = ComponentP.Subsequent.AtStep14[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14]#Next[H15] override def next[H15] = - (renderPrev, initNextHook) => { + (renderPrev, initNextHook, displayName) => { val renderNext: ComponentP.RenderFn[P, HookCtx.P15[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15]] = render => renderPrev { ctx14 => val h15 = initNextHook(ctx14) val ctx15 = HookCtx(ctx14.props, ctx14.hook1, ctx14.hook2, ctx14.hook3, ctx14.hook4, ctx14.hook5, ctx14.hook6, ctx14.hook7, ctx14.hook8, ctx14.hook9, ctx14.hook10, ctx14.hook11, ctx14.hook12, ctx14.hook13, ctx14.hook14, h15) render(ctx15) } - new ComponentP.Subsequent[P, HookCtx.P15[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15], ({ type F[A] = (P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15) => A})#F](renderNext) + new ComponentP.Subsequent[P, HookCtx.P15[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15], ({ type F[A] = (P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15) => A})#F](displayName)(renderNext) } override def squash[A] = f => _.apply14(f) } @@ -355,14 +355,14 @@ trait ComponentP_SubsequentSteps { self: ComponentP.SubsequentStep.type => new ComponentP.SubsequentStep[P, HookCtx.P15[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15], ({ type F[A] = (P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15) => A})#F] { override type Next[H16] = ComponentP.Subsequent.AtStep15[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15]#Next[H16] override def next[H16] = - (renderPrev, initNextHook) => { + (renderPrev, initNextHook, displayName) => { val renderNext: ComponentP.RenderFn[P, HookCtx.P16[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16]] = render => renderPrev { ctx15 => val h16 = initNextHook(ctx15) val ctx16 = HookCtx(ctx15.props, ctx15.hook1, ctx15.hook2, ctx15.hook3, ctx15.hook4, ctx15.hook5, ctx15.hook6, ctx15.hook7, ctx15.hook8, ctx15.hook9, ctx15.hook10, ctx15.hook11, ctx15.hook12, ctx15.hook13, ctx15.hook14, ctx15.hook15, h16) render(ctx16) } - new ComponentP.Subsequent[P, HookCtx.P16[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16], ({ type F[A] = (P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16) => A})#F](renderNext) + new ComponentP.Subsequent[P, HookCtx.P16[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16], ({ type F[A] = (P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16) => A})#F](displayName)(renderNext) } override def squash[A] = f => _.apply15(f) } @@ -377,14 +377,14 @@ trait ComponentP_SubsequentSteps { self: ComponentP.SubsequentStep.type => new ComponentP.SubsequentStep[P, HookCtx.P16[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16], ({ type F[A] = (P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16) => A})#F] { override type Next[H17] = ComponentP.Subsequent.AtStep16[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16]#Next[H17] override def next[H17] = - (renderPrev, initNextHook) => { + (renderPrev, initNextHook, displayName) => { val renderNext: ComponentP.RenderFn[P, HookCtx.P17[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17]] = render => renderPrev { ctx16 => val h17 = initNextHook(ctx16) val ctx17 = HookCtx(ctx16.props, ctx16.hook1, ctx16.hook2, ctx16.hook3, ctx16.hook4, ctx16.hook5, ctx16.hook6, ctx16.hook7, ctx16.hook8, ctx16.hook9, ctx16.hook10, ctx16.hook11, ctx16.hook12, ctx16.hook13, ctx16.hook14, ctx16.hook15, ctx16.hook16, h17) render(ctx17) } - new ComponentP.Subsequent[P, HookCtx.P17[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17], ({ type F[A] = (P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17) => A})#F](renderNext) + new ComponentP.Subsequent[P, HookCtx.P17[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17], ({ type F[A] = (P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17) => A})#F](displayName)(renderNext) } override def squash[A] = f => _.apply16(f) } @@ -399,14 +399,14 @@ trait ComponentP_SubsequentSteps { self: ComponentP.SubsequentStep.type => new ComponentP.SubsequentStep[P, HookCtx.P17[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17], ({ type F[A] = (P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17) => A})#F] { override type Next[H18] = ComponentP.Subsequent.AtStep17[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17]#Next[H18] override def next[H18] = - (renderPrev, initNextHook) => { + (renderPrev, initNextHook, displayName) => { val renderNext: ComponentP.RenderFn[P, HookCtx.P18[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18]] = render => renderPrev { ctx17 => val h18 = initNextHook(ctx17) val ctx18 = HookCtx(ctx17.props, ctx17.hook1, ctx17.hook2, ctx17.hook3, ctx17.hook4, ctx17.hook5, ctx17.hook6, ctx17.hook7, ctx17.hook8, ctx17.hook9, ctx17.hook10, ctx17.hook11, ctx17.hook12, ctx17.hook13, ctx17.hook14, ctx17.hook15, ctx17.hook16, ctx17.hook17, h18) render(ctx18) } - new ComponentP.Subsequent[P, HookCtx.P18[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18], ({ type F[A] = (P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18) => A})#F](renderNext) + new ComponentP.Subsequent[P, HookCtx.P18[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18], ({ type F[A] = (P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18) => A})#F](displayName)(renderNext) } override def squash[A] = f => _.apply17(f) } @@ -421,14 +421,14 @@ trait ComponentP_SubsequentSteps { self: ComponentP.SubsequentStep.type => new ComponentP.SubsequentStep[P, HookCtx.P18[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18], ({ type F[A] = (P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18) => A})#F] { override type Next[H19] = ComponentP.Subsequent.AtStep18[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18]#Next[H19] override def next[H19] = - (renderPrev, initNextHook) => { + (renderPrev, initNextHook, displayName) => { val renderNext: ComponentP.RenderFn[P, HookCtx.P19[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19]] = render => renderPrev { ctx18 => val h19 = initNextHook(ctx18) val ctx19 = HookCtx(ctx18.props, ctx18.hook1, ctx18.hook2, ctx18.hook3, ctx18.hook4, ctx18.hook5, ctx18.hook6, ctx18.hook7, ctx18.hook8, ctx18.hook9, ctx18.hook10, ctx18.hook11, ctx18.hook12, ctx18.hook13, ctx18.hook14, ctx18.hook15, ctx18.hook16, ctx18.hook17, ctx18.hook18, h19) render(ctx19) } - new ComponentP.Subsequent[P, HookCtx.P19[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19], ({ type F[A] = (P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19) => A})#F](renderNext) + new ComponentP.Subsequent[P, HookCtx.P19[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19], ({ type F[A] = (P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19) => A})#F](displayName)(renderNext) } override def squash[A] = f => _.apply18(f) } @@ -443,14 +443,14 @@ trait ComponentP_SubsequentSteps { self: ComponentP.SubsequentStep.type => new ComponentP.SubsequentStep[P, HookCtx.P19[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19], ({ type F[A] = (P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19) => A})#F] { override type Next[H20] = ComponentP.Subsequent.AtStep19[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19]#Next[H20] override def next[H20] = - (renderPrev, initNextHook) => { + (renderPrev, initNextHook, displayName) => { val renderNext: ComponentP.RenderFn[P, HookCtx.P20[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19, H20]] = render => renderPrev { ctx19 => val h20 = initNextHook(ctx19) val ctx20 = HookCtx(ctx19.props, ctx19.hook1, ctx19.hook2, ctx19.hook3, ctx19.hook4, ctx19.hook5, ctx19.hook6, ctx19.hook7, ctx19.hook8, ctx19.hook9, ctx19.hook10, ctx19.hook11, ctx19.hook12, ctx19.hook13, ctx19.hook14, ctx19.hook15, ctx19.hook16, ctx19.hook17, ctx19.hook18, ctx19.hook19, h20) render(ctx20) } - new ComponentP.Subsequent[P, HookCtx.P20[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19, H20], ({ type F[A] = (P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19, H20) => A})#F](renderNext) + new ComponentP.Subsequent[P, HookCtx.P20[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19, H20], ({ type F[A] = (P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19, H20) => A})#F](displayName)(renderNext) } override def squash[A] = f => _.apply19(f) } @@ -465,14 +465,14 @@ trait ComponentP_SubsequentSteps { self: ComponentP.SubsequentStep.type => new ComponentP.SubsequentStep[P, HookCtx.P20[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19, H20], ({ type F[A] = (P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19, H20) => A})#F] { override type Next[H21] = ComponentP.Subsequent.AtStep20[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19, H20]#Next[H21] override def next[H21] = - (renderPrev, initNextHook) => { + (renderPrev, initNextHook, displayName) => { val renderNext: ComponentP.RenderFn[P, HookCtx.P21[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19, H20, H21]] = render => renderPrev { ctx20 => val h21 = initNextHook(ctx20) val ctx21 = HookCtx(ctx20.props, ctx20.hook1, ctx20.hook2, ctx20.hook3, ctx20.hook4, ctx20.hook5, ctx20.hook6, ctx20.hook7, ctx20.hook8, ctx20.hook9, ctx20.hook10, ctx20.hook11, ctx20.hook12, ctx20.hook13, ctx20.hook14, ctx20.hook15, ctx20.hook16, ctx20.hook17, ctx20.hook18, ctx20.hook19, ctx20.hook20, h21) render(ctx21) } - new ComponentP.Subsequent[P, HookCtx.P21[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19, H20, H21], ({ type F[A] = (P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19, H20, H21) => A})#F](renderNext) + new ComponentP.Subsequent[P, HookCtx.P21[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19, H20, H21], ({ type F[A] = (P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19, H20, H21) => A})#F](displayName)(renderNext) } override def squash[A] = f => _.apply20(f) } @@ -515,14 +515,14 @@ trait ComponentPC_SubsequentSteps { self: ComponentPC.SubsequentStep.type => new ComponentPC.SubsequentStep[P, HookCtx.PC1[P, H1], ({ type F[A] = (P, PropsChildren, H1) => A})#F] { override type Next[H2] = ComponentPC.Subsequent.AtStep1[P, H1]#Next[H2] override def next[H2] = - (renderPrev, initNextHook) => { + (renderPrev, initNextHook, displayName) => { val renderNext: ComponentPC.RenderFn[P, HookCtx.PC2[P, H1, H2]] = render => renderPrev { ctx1 => val h2 = initNextHook(ctx1) val ctx2 = HookCtx.withChildren(ctx1.props, ctx1.propsChildren, ctx1.hook1, h2) render(ctx2) } - new ComponentPC.Subsequent[P, HookCtx.PC2[P, H1, H2], ({ type F[A] = (P, PropsChildren, H1, H2) => A})#F](renderNext) + new ComponentPC.Subsequent[P, HookCtx.PC2[P, H1, H2], ({ type F[A] = (P, PropsChildren, H1, H2) => A})#F](displayName)(renderNext) } override def squash[A] = f => _.apply1(f) } @@ -537,14 +537,14 @@ trait ComponentPC_SubsequentSteps { self: ComponentPC.SubsequentStep.type => new ComponentPC.SubsequentStep[P, HookCtx.PC2[P, H1, H2], ({ type F[A] = (P, PropsChildren, H1, H2) => A})#F] { override type Next[H3] = ComponentPC.Subsequent.AtStep2[P, H1, H2]#Next[H3] override def next[H3] = - (renderPrev, initNextHook) => { + (renderPrev, initNextHook, displayName) => { val renderNext: ComponentPC.RenderFn[P, HookCtx.PC3[P, H1, H2, H3]] = render => renderPrev { ctx2 => val h3 = initNextHook(ctx2) val ctx3 = HookCtx.withChildren(ctx2.props, ctx2.propsChildren, ctx2.hook1, ctx2.hook2, h3) render(ctx3) } - new ComponentPC.Subsequent[P, HookCtx.PC3[P, H1, H2, H3], ({ type F[A] = (P, PropsChildren, H1, H2, H3) => A})#F](renderNext) + new ComponentPC.Subsequent[P, HookCtx.PC3[P, H1, H2, H3], ({ type F[A] = (P, PropsChildren, H1, H2, H3) => A})#F](displayName)(renderNext) } override def squash[A] = f => _.apply2(f) } @@ -559,14 +559,14 @@ trait ComponentPC_SubsequentSteps { self: ComponentPC.SubsequentStep.type => new ComponentPC.SubsequentStep[P, HookCtx.PC3[P, H1, H2, H3], ({ type F[A] = (P, PropsChildren, H1, H2, H3) => A})#F] { override type Next[H4] = ComponentPC.Subsequent.AtStep3[P, H1, H2, H3]#Next[H4] override def next[H4] = - (renderPrev, initNextHook) => { + (renderPrev, initNextHook, displayName) => { val renderNext: ComponentPC.RenderFn[P, HookCtx.PC4[P, H1, H2, H3, H4]] = render => renderPrev { ctx3 => val h4 = initNextHook(ctx3) val ctx4 = HookCtx.withChildren(ctx3.props, ctx3.propsChildren, ctx3.hook1, ctx3.hook2, ctx3.hook3, h4) render(ctx4) } - new ComponentPC.Subsequent[P, HookCtx.PC4[P, H1, H2, H3, H4], ({ type F[A] = (P, PropsChildren, H1, H2, H3, H4) => A})#F](renderNext) + new ComponentPC.Subsequent[P, HookCtx.PC4[P, H1, H2, H3, H4], ({ type F[A] = (P, PropsChildren, H1, H2, H3, H4) => A})#F](displayName)(renderNext) } override def squash[A] = f => _.apply3(f) } @@ -581,14 +581,14 @@ trait ComponentPC_SubsequentSteps { self: ComponentPC.SubsequentStep.type => new ComponentPC.SubsequentStep[P, HookCtx.PC4[P, H1, H2, H3, H4], ({ type F[A] = (P, PropsChildren, H1, H2, H3, H4) => A})#F] { override type Next[H5] = ComponentPC.Subsequent.AtStep4[P, H1, H2, H3, H4]#Next[H5] override def next[H5] = - (renderPrev, initNextHook) => { + (renderPrev, initNextHook, displayName) => { val renderNext: ComponentPC.RenderFn[P, HookCtx.PC5[P, H1, H2, H3, H4, H5]] = render => renderPrev { ctx4 => val h5 = initNextHook(ctx4) val ctx5 = HookCtx.withChildren(ctx4.props, ctx4.propsChildren, ctx4.hook1, ctx4.hook2, ctx4.hook3, ctx4.hook4, h5) render(ctx5) } - new ComponentPC.Subsequent[P, HookCtx.PC5[P, H1, H2, H3, H4, H5], ({ type F[A] = (P, PropsChildren, H1, H2, H3, H4, H5) => A})#F](renderNext) + new ComponentPC.Subsequent[P, HookCtx.PC5[P, H1, H2, H3, H4, H5], ({ type F[A] = (P, PropsChildren, H1, H2, H3, H4, H5) => A})#F](displayName)(renderNext) } override def squash[A] = f => _.apply4(f) } @@ -603,14 +603,14 @@ trait ComponentPC_SubsequentSteps { self: ComponentPC.SubsequentStep.type => new ComponentPC.SubsequentStep[P, HookCtx.PC5[P, H1, H2, H3, H4, H5], ({ type F[A] = (P, PropsChildren, H1, H2, H3, H4, H5) => A})#F] { override type Next[H6] = ComponentPC.Subsequent.AtStep5[P, H1, H2, H3, H4, H5]#Next[H6] override def next[H6] = - (renderPrev, initNextHook) => { + (renderPrev, initNextHook, displayName) => { val renderNext: ComponentPC.RenderFn[P, HookCtx.PC6[P, H1, H2, H3, H4, H5, H6]] = render => renderPrev { ctx5 => val h6 = initNextHook(ctx5) val ctx6 = HookCtx.withChildren(ctx5.props, ctx5.propsChildren, ctx5.hook1, ctx5.hook2, ctx5.hook3, ctx5.hook4, ctx5.hook5, h6) render(ctx6) } - new ComponentPC.Subsequent[P, HookCtx.PC6[P, H1, H2, H3, H4, H5, H6], ({ type F[A] = (P, PropsChildren, H1, H2, H3, H4, H5, H6) => A})#F](renderNext) + new ComponentPC.Subsequent[P, HookCtx.PC6[P, H1, H2, H3, H4, H5, H6], ({ type F[A] = (P, PropsChildren, H1, H2, H3, H4, H5, H6) => A})#F](displayName)(renderNext) } override def squash[A] = f => _.apply5(f) } @@ -625,14 +625,14 @@ trait ComponentPC_SubsequentSteps { self: ComponentPC.SubsequentStep.type => new ComponentPC.SubsequentStep[P, HookCtx.PC6[P, H1, H2, H3, H4, H5, H6], ({ type F[A] = (P, PropsChildren, H1, H2, H3, H4, H5, H6) => A})#F] { override type Next[H7] = ComponentPC.Subsequent.AtStep6[P, H1, H2, H3, H4, H5, H6]#Next[H7] override def next[H7] = - (renderPrev, initNextHook) => { + (renderPrev, initNextHook, displayName) => { val renderNext: ComponentPC.RenderFn[P, HookCtx.PC7[P, H1, H2, H3, H4, H5, H6, H7]] = render => renderPrev { ctx6 => val h7 = initNextHook(ctx6) val ctx7 = HookCtx.withChildren(ctx6.props, ctx6.propsChildren, ctx6.hook1, ctx6.hook2, ctx6.hook3, ctx6.hook4, ctx6.hook5, ctx6.hook6, h7) render(ctx7) } - new ComponentPC.Subsequent[P, HookCtx.PC7[P, H1, H2, H3, H4, H5, H6, H7], ({ type F[A] = (P, PropsChildren, H1, H2, H3, H4, H5, H6, H7) => A})#F](renderNext) + new ComponentPC.Subsequent[P, HookCtx.PC7[P, H1, H2, H3, H4, H5, H6, H7], ({ type F[A] = (P, PropsChildren, H1, H2, H3, H4, H5, H6, H7) => A})#F](displayName)(renderNext) } override def squash[A] = f => _.apply6(f) } @@ -647,14 +647,14 @@ trait ComponentPC_SubsequentSteps { self: ComponentPC.SubsequentStep.type => new ComponentPC.SubsequentStep[P, HookCtx.PC7[P, H1, H2, H3, H4, H5, H6, H7], ({ type F[A] = (P, PropsChildren, H1, H2, H3, H4, H5, H6, H7) => A})#F] { override type Next[H8] = ComponentPC.Subsequent.AtStep7[P, H1, H2, H3, H4, H5, H6, H7]#Next[H8] override def next[H8] = - (renderPrev, initNextHook) => { + (renderPrev, initNextHook, displayName) => { val renderNext: ComponentPC.RenderFn[P, HookCtx.PC8[P, H1, H2, H3, H4, H5, H6, H7, H8]] = render => renderPrev { ctx7 => val h8 = initNextHook(ctx7) val ctx8 = HookCtx.withChildren(ctx7.props, ctx7.propsChildren, ctx7.hook1, ctx7.hook2, ctx7.hook3, ctx7.hook4, ctx7.hook5, ctx7.hook6, ctx7.hook7, h8) render(ctx8) } - new ComponentPC.Subsequent[P, HookCtx.PC8[P, H1, H2, H3, H4, H5, H6, H7, H8], ({ type F[A] = (P, PropsChildren, H1, H2, H3, H4, H5, H6, H7, H8) => A})#F](renderNext) + new ComponentPC.Subsequent[P, HookCtx.PC8[P, H1, H2, H3, H4, H5, H6, H7, H8], ({ type F[A] = (P, PropsChildren, H1, H2, H3, H4, H5, H6, H7, H8) => A})#F](displayName)(renderNext) } override def squash[A] = f => _.apply7(f) } @@ -669,14 +669,14 @@ trait ComponentPC_SubsequentSteps { self: ComponentPC.SubsequentStep.type => new ComponentPC.SubsequentStep[P, HookCtx.PC8[P, H1, H2, H3, H4, H5, H6, H7, H8], ({ type F[A] = (P, PropsChildren, H1, H2, H3, H4, H5, H6, H7, H8) => A})#F] { override type Next[H9] = ComponentPC.Subsequent.AtStep8[P, H1, H2, H3, H4, H5, H6, H7, H8]#Next[H9] override def next[H9] = - (renderPrev, initNextHook) => { + (renderPrev, initNextHook, displayName) => { val renderNext: ComponentPC.RenderFn[P, HookCtx.PC9[P, H1, H2, H3, H4, H5, H6, H7, H8, H9]] = render => renderPrev { ctx8 => val h9 = initNextHook(ctx8) val ctx9 = HookCtx.withChildren(ctx8.props, ctx8.propsChildren, ctx8.hook1, ctx8.hook2, ctx8.hook3, ctx8.hook4, ctx8.hook5, ctx8.hook6, ctx8.hook7, ctx8.hook8, h9) render(ctx9) } - new ComponentPC.Subsequent[P, HookCtx.PC9[P, H1, H2, H3, H4, H5, H6, H7, H8, H9], ({ type F[A] = (P, PropsChildren, H1, H2, H3, H4, H5, H6, H7, H8, H9) => A})#F](renderNext) + new ComponentPC.Subsequent[P, HookCtx.PC9[P, H1, H2, H3, H4, H5, H6, H7, H8, H9], ({ type F[A] = (P, PropsChildren, H1, H2, H3, H4, H5, H6, H7, H8, H9) => A})#F](displayName)(renderNext) } override def squash[A] = f => _.apply8(f) } @@ -691,14 +691,14 @@ trait ComponentPC_SubsequentSteps { self: ComponentPC.SubsequentStep.type => new ComponentPC.SubsequentStep[P, HookCtx.PC9[P, H1, H2, H3, H4, H5, H6, H7, H8, H9], ({ type F[A] = (P, PropsChildren, H1, H2, H3, H4, H5, H6, H7, H8, H9) => A})#F] { override type Next[H10] = ComponentPC.Subsequent.AtStep9[P, H1, H2, H3, H4, H5, H6, H7, H8, H9]#Next[H10] override def next[H10] = - (renderPrev, initNextHook) => { + (renderPrev, initNextHook, displayName) => { val renderNext: ComponentPC.RenderFn[P, HookCtx.PC10[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10]] = render => renderPrev { ctx9 => val h10 = initNextHook(ctx9) val ctx10 = HookCtx.withChildren(ctx9.props, ctx9.propsChildren, ctx9.hook1, ctx9.hook2, ctx9.hook3, ctx9.hook4, ctx9.hook5, ctx9.hook6, ctx9.hook7, ctx9.hook8, ctx9.hook9, h10) render(ctx10) } - new ComponentPC.Subsequent[P, HookCtx.PC10[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10], ({ type F[A] = (P, PropsChildren, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10) => A})#F](renderNext) + new ComponentPC.Subsequent[P, HookCtx.PC10[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10], ({ type F[A] = (P, PropsChildren, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10) => A})#F](displayName)(renderNext) } override def squash[A] = f => _.apply9(f) } @@ -713,14 +713,14 @@ trait ComponentPC_SubsequentSteps { self: ComponentPC.SubsequentStep.type => new ComponentPC.SubsequentStep[P, HookCtx.PC10[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10], ({ type F[A] = (P, PropsChildren, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10) => A})#F] { override type Next[H11] = ComponentPC.Subsequent.AtStep10[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10]#Next[H11] override def next[H11] = - (renderPrev, initNextHook) => { + (renderPrev, initNextHook, displayName) => { val renderNext: ComponentPC.RenderFn[P, HookCtx.PC11[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11]] = render => renderPrev { ctx10 => val h11 = initNextHook(ctx10) val ctx11 = HookCtx.withChildren(ctx10.props, ctx10.propsChildren, ctx10.hook1, ctx10.hook2, ctx10.hook3, ctx10.hook4, ctx10.hook5, ctx10.hook6, ctx10.hook7, ctx10.hook8, ctx10.hook9, ctx10.hook10, h11) render(ctx11) } - new ComponentPC.Subsequent[P, HookCtx.PC11[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11], ({ type F[A] = (P, PropsChildren, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11) => A})#F](renderNext) + new ComponentPC.Subsequent[P, HookCtx.PC11[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11], ({ type F[A] = (P, PropsChildren, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11) => A})#F](displayName)(renderNext) } override def squash[A] = f => _.apply10(f) } @@ -735,14 +735,14 @@ trait ComponentPC_SubsequentSteps { self: ComponentPC.SubsequentStep.type => new ComponentPC.SubsequentStep[P, HookCtx.PC11[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11], ({ type F[A] = (P, PropsChildren, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11) => A})#F] { override type Next[H12] = ComponentPC.Subsequent.AtStep11[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11]#Next[H12] override def next[H12] = - (renderPrev, initNextHook) => { + (renderPrev, initNextHook, displayName) => { val renderNext: ComponentPC.RenderFn[P, HookCtx.PC12[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12]] = render => renderPrev { ctx11 => val h12 = initNextHook(ctx11) val ctx12 = HookCtx.withChildren(ctx11.props, ctx11.propsChildren, ctx11.hook1, ctx11.hook2, ctx11.hook3, ctx11.hook4, ctx11.hook5, ctx11.hook6, ctx11.hook7, ctx11.hook8, ctx11.hook9, ctx11.hook10, ctx11.hook11, h12) render(ctx12) } - new ComponentPC.Subsequent[P, HookCtx.PC12[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12], ({ type F[A] = (P, PropsChildren, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12) => A})#F](renderNext) + new ComponentPC.Subsequent[P, HookCtx.PC12[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12], ({ type F[A] = (P, PropsChildren, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12) => A})#F](displayName)(renderNext) } override def squash[A] = f => _.apply11(f) } @@ -757,14 +757,14 @@ trait ComponentPC_SubsequentSteps { self: ComponentPC.SubsequentStep.type => new ComponentPC.SubsequentStep[P, HookCtx.PC12[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12], ({ type F[A] = (P, PropsChildren, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12) => A})#F] { override type Next[H13] = ComponentPC.Subsequent.AtStep12[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12]#Next[H13] override def next[H13] = - (renderPrev, initNextHook) => { + (renderPrev, initNextHook, displayName) => { val renderNext: ComponentPC.RenderFn[P, HookCtx.PC13[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13]] = render => renderPrev { ctx12 => val h13 = initNextHook(ctx12) val ctx13 = HookCtx.withChildren(ctx12.props, ctx12.propsChildren, ctx12.hook1, ctx12.hook2, ctx12.hook3, ctx12.hook4, ctx12.hook5, ctx12.hook6, ctx12.hook7, ctx12.hook8, ctx12.hook9, ctx12.hook10, ctx12.hook11, ctx12.hook12, h13) render(ctx13) } - new ComponentPC.Subsequent[P, HookCtx.PC13[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13], ({ type F[A] = (P, PropsChildren, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13) => A})#F](renderNext) + new ComponentPC.Subsequent[P, HookCtx.PC13[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13], ({ type F[A] = (P, PropsChildren, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13) => A})#F](displayName)(renderNext) } override def squash[A] = f => _.apply12(f) } @@ -779,14 +779,14 @@ trait ComponentPC_SubsequentSteps { self: ComponentPC.SubsequentStep.type => new ComponentPC.SubsequentStep[P, HookCtx.PC13[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13], ({ type F[A] = (P, PropsChildren, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13) => A})#F] { override type Next[H14] = ComponentPC.Subsequent.AtStep13[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13]#Next[H14] override def next[H14] = - (renderPrev, initNextHook) => { + (renderPrev, initNextHook, displayName) => { val renderNext: ComponentPC.RenderFn[P, HookCtx.PC14[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14]] = render => renderPrev { ctx13 => val h14 = initNextHook(ctx13) val ctx14 = HookCtx.withChildren(ctx13.props, ctx13.propsChildren, ctx13.hook1, ctx13.hook2, ctx13.hook3, ctx13.hook4, ctx13.hook5, ctx13.hook6, ctx13.hook7, ctx13.hook8, ctx13.hook9, ctx13.hook10, ctx13.hook11, ctx13.hook12, ctx13.hook13, h14) render(ctx14) } - new ComponentPC.Subsequent[P, HookCtx.PC14[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14], ({ type F[A] = (P, PropsChildren, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14) => A})#F](renderNext) + new ComponentPC.Subsequent[P, HookCtx.PC14[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14], ({ type F[A] = (P, PropsChildren, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14) => A})#F](displayName)(renderNext) } override def squash[A] = f => _.apply13(f) } @@ -801,14 +801,14 @@ trait ComponentPC_SubsequentSteps { self: ComponentPC.SubsequentStep.type => new ComponentPC.SubsequentStep[P, HookCtx.PC14[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14], ({ type F[A] = (P, PropsChildren, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14) => A})#F] { override type Next[H15] = ComponentPC.Subsequent.AtStep14[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14]#Next[H15] override def next[H15] = - (renderPrev, initNextHook) => { + (renderPrev, initNextHook, displayName) => { val renderNext: ComponentPC.RenderFn[P, HookCtx.PC15[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15]] = render => renderPrev { ctx14 => val h15 = initNextHook(ctx14) val ctx15 = HookCtx.withChildren(ctx14.props, ctx14.propsChildren, ctx14.hook1, ctx14.hook2, ctx14.hook3, ctx14.hook4, ctx14.hook5, ctx14.hook6, ctx14.hook7, ctx14.hook8, ctx14.hook9, ctx14.hook10, ctx14.hook11, ctx14.hook12, ctx14.hook13, ctx14.hook14, h15) render(ctx15) } - new ComponentPC.Subsequent[P, HookCtx.PC15[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15], ({ type F[A] = (P, PropsChildren, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15) => A})#F](renderNext) + new ComponentPC.Subsequent[P, HookCtx.PC15[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15], ({ type F[A] = (P, PropsChildren, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15) => A})#F](displayName)(renderNext) } override def squash[A] = f => _.apply14(f) } @@ -823,14 +823,14 @@ trait ComponentPC_SubsequentSteps { self: ComponentPC.SubsequentStep.type => new ComponentPC.SubsequentStep[P, HookCtx.PC15[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15], ({ type F[A] = (P, PropsChildren, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15) => A})#F] { override type Next[H16] = ComponentPC.Subsequent.AtStep15[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15]#Next[H16] override def next[H16] = - (renderPrev, initNextHook) => { + (renderPrev, initNextHook, displayName) => { val renderNext: ComponentPC.RenderFn[P, HookCtx.PC16[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16]] = render => renderPrev { ctx15 => val h16 = initNextHook(ctx15) val ctx16 = HookCtx.withChildren(ctx15.props, ctx15.propsChildren, ctx15.hook1, ctx15.hook2, ctx15.hook3, ctx15.hook4, ctx15.hook5, ctx15.hook6, ctx15.hook7, ctx15.hook8, ctx15.hook9, ctx15.hook10, ctx15.hook11, ctx15.hook12, ctx15.hook13, ctx15.hook14, ctx15.hook15, h16) render(ctx16) } - new ComponentPC.Subsequent[P, HookCtx.PC16[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16], ({ type F[A] = (P, PropsChildren, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16) => A})#F](renderNext) + new ComponentPC.Subsequent[P, HookCtx.PC16[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16], ({ type F[A] = (P, PropsChildren, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16) => A})#F](displayName)(renderNext) } override def squash[A] = f => _.apply15(f) } @@ -845,14 +845,14 @@ trait ComponentPC_SubsequentSteps { self: ComponentPC.SubsequentStep.type => new ComponentPC.SubsequentStep[P, HookCtx.PC16[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16], ({ type F[A] = (P, PropsChildren, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16) => A})#F] { override type Next[H17] = ComponentPC.Subsequent.AtStep16[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16]#Next[H17] override def next[H17] = - (renderPrev, initNextHook) => { + (renderPrev, initNextHook, displayName) => { val renderNext: ComponentPC.RenderFn[P, HookCtx.PC17[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17]] = render => renderPrev { ctx16 => val h17 = initNextHook(ctx16) val ctx17 = HookCtx.withChildren(ctx16.props, ctx16.propsChildren, ctx16.hook1, ctx16.hook2, ctx16.hook3, ctx16.hook4, ctx16.hook5, ctx16.hook6, ctx16.hook7, ctx16.hook8, ctx16.hook9, ctx16.hook10, ctx16.hook11, ctx16.hook12, ctx16.hook13, ctx16.hook14, ctx16.hook15, ctx16.hook16, h17) render(ctx17) } - new ComponentPC.Subsequent[P, HookCtx.PC17[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17], ({ type F[A] = (P, PropsChildren, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17) => A})#F](renderNext) + new ComponentPC.Subsequent[P, HookCtx.PC17[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17], ({ type F[A] = (P, PropsChildren, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17) => A})#F](displayName)(renderNext) } override def squash[A] = f => _.apply16(f) } @@ -867,14 +867,14 @@ trait ComponentPC_SubsequentSteps { self: ComponentPC.SubsequentStep.type => new ComponentPC.SubsequentStep[P, HookCtx.PC17[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17], ({ type F[A] = (P, PropsChildren, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17) => A})#F] { override type Next[H18] = ComponentPC.Subsequent.AtStep17[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17]#Next[H18] override def next[H18] = - (renderPrev, initNextHook) => { + (renderPrev, initNextHook, displayName) => { val renderNext: ComponentPC.RenderFn[P, HookCtx.PC18[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18]] = render => renderPrev { ctx17 => val h18 = initNextHook(ctx17) val ctx18 = HookCtx.withChildren(ctx17.props, ctx17.propsChildren, ctx17.hook1, ctx17.hook2, ctx17.hook3, ctx17.hook4, ctx17.hook5, ctx17.hook6, ctx17.hook7, ctx17.hook8, ctx17.hook9, ctx17.hook10, ctx17.hook11, ctx17.hook12, ctx17.hook13, ctx17.hook14, ctx17.hook15, ctx17.hook16, ctx17.hook17, h18) render(ctx18) } - new ComponentPC.Subsequent[P, HookCtx.PC18[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18], ({ type F[A] = (P, PropsChildren, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18) => A})#F](renderNext) + new ComponentPC.Subsequent[P, HookCtx.PC18[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18], ({ type F[A] = (P, PropsChildren, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18) => A})#F](displayName)(renderNext) } override def squash[A] = f => _.apply17(f) } @@ -889,14 +889,14 @@ trait ComponentPC_SubsequentSteps { self: ComponentPC.SubsequentStep.type => new ComponentPC.SubsequentStep[P, HookCtx.PC18[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18], ({ type F[A] = (P, PropsChildren, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18) => A})#F] { override type Next[H19] = ComponentPC.Subsequent.AtStep18[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18]#Next[H19] override def next[H19] = - (renderPrev, initNextHook) => { + (renderPrev, initNextHook, displayName) => { val renderNext: ComponentPC.RenderFn[P, HookCtx.PC19[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19]] = render => renderPrev { ctx18 => val h19 = initNextHook(ctx18) val ctx19 = HookCtx.withChildren(ctx18.props, ctx18.propsChildren, ctx18.hook1, ctx18.hook2, ctx18.hook3, ctx18.hook4, ctx18.hook5, ctx18.hook6, ctx18.hook7, ctx18.hook8, ctx18.hook9, ctx18.hook10, ctx18.hook11, ctx18.hook12, ctx18.hook13, ctx18.hook14, ctx18.hook15, ctx18.hook16, ctx18.hook17, ctx18.hook18, h19) render(ctx19) } - new ComponentPC.Subsequent[P, HookCtx.PC19[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19], ({ type F[A] = (P, PropsChildren, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19) => A})#F](renderNext) + new ComponentPC.Subsequent[P, HookCtx.PC19[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19], ({ type F[A] = (P, PropsChildren, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19) => A})#F](displayName)(renderNext) } override def squash[A] = f => _.apply18(f) } @@ -911,14 +911,14 @@ trait ComponentPC_SubsequentSteps { self: ComponentPC.SubsequentStep.type => new ComponentPC.SubsequentStep[P, HookCtx.PC19[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19], ({ type F[A] = (P, PropsChildren, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19) => A})#F] { override type Next[H20] = ComponentPC.Subsequent.AtStep19[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19]#Next[H20] override def next[H20] = - (renderPrev, initNextHook) => { + (renderPrev, initNextHook, displayName) => { val renderNext: ComponentPC.RenderFn[P, HookCtx.PC20[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19, H20]] = render => renderPrev { ctx19 => val h20 = initNextHook(ctx19) val ctx20 = HookCtx.withChildren(ctx19.props, ctx19.propsChildren, ctx19.hook1, ctx19.hook2, ctx19.hook3, ctx19.hook4, ctx19.hook5, ctx19.hook6, ctx19.hook7, ctx19.hook8, ctx19.hook9, ctx19.hook10, ctx19.hook11, ctx19.hook12, ctx19.hook13, ctx19.hook14, ctx19.hook15, ctx19.hook16, ctx19.hook17, ctx19.hook18, ctx19.hook19, h20) render(ctx20) } - new ComponentPC.Subsequent[P, HookCtx.PC20[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19, H20], ({ type F[A] = (P, PropsChildren, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19, H20) => A})#F](renderNext) + new ComponentPC.Subsequent[P, HookCtx.PC20[P, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19, H20], ({ type F[A] = (P, PropsChildren, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19, H20) => A})#F](displayName)(renderNext) } override def squash[A] = f => _.apply19(f) } @@ -962,7 +962,7 @@ trait Custom_SubsequentSteps { self: Custom.SubsequentStep.type => new Custom.SubsequentStep[I, HookCtx.I1[I, H1], ({ type F[A] = (I, H1) => A})#F] { override type Next[H2] = Custom.Subsequent.AtStep1[I, H1]#Next[H2] override def next[H2] = - (buildPrev, initNextHook) => { + (buildPrev, initNextHook, displayName) => { val buildNext: Custom.BuildFn[I, HookCtx.I2[I, H1, H2]] = new Custom.BuildFn[I, HookCtx.I2[I, H1, H2]] { override def apply[O](f: HookCtx.I2[I, H1, H2] => O) = { @@ -973,7 +973,7 @@ trait Custom_SubsequentSteps { self: Custom.SubsequentStep.type => } } } - new Custom.Subsequent[I, HookCtx.I2[I, H1, H2], ({ type F[A] = (I, H1, H2) => A})#F](buildNext) + new Custom.Subsequent[I, HookCtx.I2[I, H1, H2], ({ type F[A] = (I, H1, H2) => A})#F](displayName)(buildNext) } override def squash[A] = f => _.apply1(f) } @@ -988,7 +988,7 @@ trait Custom_SubsequentSteps { self: Custom.SubsequentStep.type => new Custom.SubsequentStep[I, HookCtx.I2[I, H1, H2], ({ type F[A] = (I, H1, H2) => A})#F] { override type Next[H3] = Custom.Subsequent.AtStep2[I, H1, H2]#Next[H3] override def next[H3] = - (buildPrev, initNextHook) => { + (buildPrev, initNextHook, displayName) => { val buildNext: Custom.BuildFn[I, HookCtx.I3[I, H1, H2, H3]] = new Custom.BuildFn[I, HookCtx.I3[I, H1, H2, H3]] { override def apply[O](f: HookCtx.I3[I, H1, H2, H3] => O) = { @@ -999,7 +999,7 @@ trait Custom_SubsequentSteps { self: Custom.SubsequentStep.type => } } } - new Custom.Subsequent[I, HookCtx.I3[I, H1, H2, H3], ({ type F[A] = (I, H1, H2, H3) => A})#F](buildNext) + new Custom.Subsequent[I, HookCtx.I3[I, H1, H2, H3], ({ type F[A] = (I, H1, H2, H3) => A})#F](displayName)(buildNext) } override def squash[A] = f => _.apply2(f) } @@ -1014,7 +1014,7 @@ trait Custom_SubsequentSteps { self: Custom.SubsequentStep.type => new Custom.SubsequentStep[I, HookCtx.I3[I, H1, H2, H3], ({ type F[A] = (I, H1, H2, H3) => A})#F] { override type Next[H4] = Custom.Subsequent.AtStep3[I, H1, H2, H3]#Next[H4] override def next[H4] = - (buildPrev, initNextHook) => { + (buildPrev, initNextHook, displayName) => { val buildNext: Custom.BuildFn[I, HookCtx.I4[I, H1, H2, H3, H4]] = new Custom.BuildFn[I, HookCtx.I4[I, H1, H2, H3, H4]] { override def apply[O](f: HookCtx.I4[I, H1, H2, H3, H4] => O) = { @@ -1025,7 +1025,7 @@ trait Custom_SubsequentSteps { self: Custom.SubsequentStep.type => } } } - new Custom.Subsequent[I, HookCtx.I4[I, H1, H2, H3, H4], ({ type F[A] = (I, H1, H2, H3, H4) => A})#F](buildNext) + new Custom.Subsequent[I, HookCtx.I4[I, H1, H2, H3, H4], ({ type F[A] = (I, H1, H2, H3, H4) => A})#F](displayName)(buildNext) } override def squash[A] = f => _.apply3(f) } @@ -1040,7 +1040,7 @@ trait Custom_SubsequentSteps { self: Custom.SubsequentStep.type => new Custom.SubsequentStep[I, HookCtx.I4[I, H1, H2, H3, H4], ({ type F[A] = (I, H1, H2, H3, H4) => A})#F] { override type Next[H5] = Custom.Subsequent.AtStep4[I, H1, H2, H3, H4]#Next[H5] override def next[H5] = - (buildPrev, initNextHook) => { + (buildPrev, initNextHook, displayName) => { val buildNext: Custom.BuildFn[I, HookCtx.I5[I, H1, H2, H3, H4, H5]] = new Custom.BuildFn[I, HookCtx.I5[I, H1, H2, H3, H4, H5]] { override def apply[O](f: HookCtx.I5[I, H1, H2, H3, H4, H5] => O) = { @@ -1051,7 +1051,7 @@ trait Custom_SubsequentSteps { self: Custom.SubsequentStep.type => } } } - new Custom.Subsequent[I, HookCtx.I5[I, H1, H2, H3, H4, H5], ({ type F[A] = (I, H1, H2, H3, H4, H5) => A})#F](buildNext) + new Custom.Subsequent[I, HookCtx.I5[I, H1, H2, H3, H4, H5], ({ type F[A] = (I, H1, H2, H3, H4, H5) => A})#F](displayName)(buildNext) } override def squash[A] = f => _.apply4(f) } @@ -1066,7 +1066,7 @@ trait Custom_SubsequentSteps { self: Custom.SubsequentStep.type => new Custom.SubsequentStep[I, HookCtx.I5[I, H1, H2, H3, H4, H5], ({ type F[A] = (I, H1, H2, H3, H4, H5) => A})#F] { override type Next[H6] = Custom.Subsequent.AtStep5[I, H1, H2, H3, H4, H5]#Next[H6] override def next[H6] = - (buildPrev, initNextHook) => { + (buildPrev, initNextHook, displayName) => { val buildNext: Custom.BuildFn[I, HookCtx.I6[I, H1, H2, H3, H4, H5, H6]] = new Custom.BuildFn[I, HookCtx.I6[I, H1, H2, H3, H4, H5, H6]] { override def apply[O](f: HookCtx.I6[I, H1, H2, H3, H4, H5, H6] => O) = { @@ -1077,7 +1077,7 @@ trait Custom_SubsequentSteps { self: Custom.SubsequentStep.type => } } } - new Custom.Subsequent[I, HookCtx.I6[I, H1, H2, H3, H4, H5, H6], ({ type F[A] = (I, H1, H2, H3, H4, H5, H6) => A})#F](buildNext) + new Custom.Subsequent[I, HookCtx.I6[I, H1, H2, H3, H4, H5, H6], ({ type F[A] = (I, H1, H2, H3, H4, H5, H6) => A})#F](displayName)(buildNext) } override def squash[A] = f => _.apply5(f) } @@ -1092,7 +1092,7 @@ trait Custom_SubsequentSteps { self: Custom.SubsequentStep.type => new Custom.SubsequentStep[I, HookCtx.I6[I, H1, H2, H3, H4, H5, H6], ({ type F[A] = (I, H1, H2, H3, H4, H5, H6) => A})#F] { override type Next[H7] = Custom.Subsequent.AtStep6[I, H1, H2, H3, H4, H5, H6]#Next[H7] override def next[H7] = - (buildPrev, initNextHook) => { + (buildPrev, initNextHook, displayName) => { val buildNext: Custom.BuildFn[I, HookCtx.I7[I, H1, H2, H3, H4, H5, H6, H7]] = new Custom.BuildFn[I, HookCtx.I7[I, H1, H2, H3, H4, H5, H6, H7]] { override def apply[O](f: HookCtx.I7[I, H1, H2, H3, H4, H5, H6, H7] => O) = { @@ -1103,7 +1103,7 @@ trait Custom_SubsequentSteps { self: Custom.SubsequentStep.type => } } } - new Custom.Subsequent[I, HookCtx.I7[I, H1, H2, H3, H4, H5, H6, H7], ({ type F[A] = (I, H1, H2, H3, H4, H5, H6, H7) => A})#F](buildNext) + new Custom.Subsequent[I, HookCtx.I7[I, H1, H2, H3, H4, H5, H6, H7], ({ type F[A] = (I, H1, H2, H3, H4, H5, H6, H7) => A})#F](displayName)(buildNext) } override def squash[A] = f => _.apply6(f) } @@ -1118,7 +1118,7 @@ trait Custom_SubsequentSteps { self: Custom.SubsequentStep.type => new Custom.SubsequentStep[I, HookCtx.I7[I, H1, H2, H3, H4, H5, H6, H7], ({ type F[A] = (I, H1, H2, H3, H4, H5, H6, H7) => A})#F] { override type Next[H8] = Custom.Subsequent.AtStep7[I, H1, H2, H3, H4, H5, H6, H7]#Next[H8] override def next[H8] = - (buildPrev, initNextHook) => { + (buildPrev, initNextHook, displayName) => { val buildNext: Custom.BuildFn[I, HookCtx.I8[I, H1, H2, H3, H4, H5, H6, H7, H8]] = new Custom.BuildFn[I, HookCtx.I8[I, H1, H2, H3, H4, H5, H6, H7, H8]] { override def apply[O](f: HookCtx.I8[I, H1, H2, H3, H4, H5, H6, H7, H8] => O) = { @@ -1129,7 +1129,7 @@ trait Custom_SubsequentSteps { self: Custom.SubsequentStep.type => } } } - new Custom.Subsequent[I, HookCtx.I8[I, H1, H2, H3, H4, H5, H6, H7, H8], ({ type F[A] = (I, H1, H2, H3, H4, H5, H6, H7, H8) => A})#F](buildNext) + new Custom.Subsequent[I, HookCtx.I8[I, H1, H2, H3, H4, H5, H6, H7, H8], ({ type F[A] = (I, H1, H2, H3, H4, H5, H6, H7, H8) => A})#F](displayName)(buildNext) } override def squash[A] = f => _.apply7(f) } @@ -1144,7 +1144,7 @@ trait Custom_SubsequentSteps { self: Custom.SubsequentStep.type => new Custom.SubsequentStep[I, HookCtx.I8[I, H1, H2, H3, H4, H5, H6, H7, H8], ({ type F[A] = (I, H1, H2, H3, H4, H5, H6, H7, H8) => A})#F] { override type Next[H9] = Custom.Subsequent.AtStep8[I, H1, H2, H3, H4, H5, H6, H7, H8]#Next[H9] override def next[H9] = - (buildPrev, initNextHook) => { + (buildPrev, initNextHook, displayName) => { val buildNext: Custom.BuildFn[I, HookCtx.I9[I, H1, H2, H3, H4, H5, H6, H7, H8, H9]] = new Custom.BuildFn[I, HookCtx.I9[I, H1, H2, H3, H4, H5, H6, H7, H8, H9]] { override def apply[O](f: HookCtx.I9[I, H1, H2, H3, H4, H5, H6, H7, H8, H9] => O) = { @@ -1155,7 +1155,7 @@ trait Custom_SubsequentSteps { self: Custom.SubsequentStep.type => } } } - new Custom.Subsequent[I, HookCtx.I9[I, H1, H2, H3, H4, H5, H6, H7, H8, H9], ({ type F[A] = (I, H1, H2, H3, H4, H5, H6, H7, H8, H9) => A})#F](buildNext) + new Custom.Subsequent[I, HookCtx.I9[I, H1, H2, H3, H4, H5, H6, H7, H8, H9], ({ type F[A] = (I, H1, H2, H3, H4, H5, H6, H7, H8, H9) => A})#F](displayName)(buildNext) } override def squash[A] = f => _.apply8(f) } @@ -1170,7 +1170,7 @@ trait Custom_SubsequentSteps { self: Custom.SubsequentStep.type => new Custom.SubsequentStep[I, HookCtx.I9[I, H1, H2, H3, H4, H5, H6, H7, H8, H9], ({ type F[A] = (I, H1, H2, H3, H4, H5, H6, H7, H8, H9) => A})#F] { override type Next[H10] = Custom.Subsequent.AtStep9[I, H1, H2, H3, H4, H5, H6, H7, H8, H9]#Next[H10] override def next[H10] = - (buildPrev, initNextHook) => { + (buildPrev, initNextHook, displayName) => { val buildNext: Custom.BuildFn[I, HookCtx.I10[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10]] = new Custom.BuildFn[I, HookCtx.I10[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10]] { override def apply[O](f: HookCtx.I10[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10] => O) = { @@ -1181,7 +1181,7 @@ trait Custom_SubsequentSteps { self: Custom.SubsequentStep.type => } } } - new Custom.Subsequent[I, HookCtx.I10[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10], ({ type F[A] = (I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10) => A})#F](buildNext) + new Custom.Subsequent[I, HookCtx.I10[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10], ({ type F[A] = (I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10) => A})#F](displayName)(buildNext) } override def squash[A] = f => _.apply9(f) } @@ -1196,7 +1196,7 @@ trait Custom_SubsequentSteps { self: Custom.SubsequentStep.type => new Custom.SubsequentStep[I, HookCtx.I10[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10], ({ type F[A] = (I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10) => A})#F] { override type Next[H11] = Custom.Subsequent.AtStep10[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10]#Next[H11] override def next[H11] = - (buildPrev, initNextHook) => { + (buildPrev, initNextHook, displayName) => { val buildNext: Custom.BuildFn[I, HookCtx.I11[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11]] = new Custom.BuildFn[I, HookCtx.I11[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11]] { override def apply[O](f: HookCtx.I11[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11] => O) = { @@ -1207,7 +1207,7 @@ trait Custom_SubsequentSteps { self: Custom.SubsequentStep.type => } } } - new Custom.Subsequent[I, HookCtx.I11[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11], ({ type F[A] = (I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11) => A})#F](buildNext) + new Custom.Subsequent[I, HookCtx.I11[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11], ({ type F[A] = (I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11) => A})#F](displayName)(buildNext) } override def squash[A] = f => _.apply10(f) } @@ -1222,7 +1222,7 @@ trait Custom_SubsequentSteps { self: Custom.SubsequentStep.type => new Custom.SubsequentStep[I, HookCtx.I11[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11], ({ type F[A] = (I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11) => A})#F] { override type Next[H12] = Custom.Subsequent.AtStep11[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11]#Next[H12] override def next[H12] = - (buildPrev, initNextHook) => { + (buildPrev, initNextHook, displayName) => { val buildNext: Custom.BuildFn[I, HookCtx.I12[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12]] = new Custom.BuildFn[I, HookCtx.I12[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12]] { override def apply[O](f: HookCtx.I12[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12] => O) = { @@ -1233,7 +1233,7 @@ trait Custom_SubsequentSteps { self: Custom.SubsequentStep.type => } } } - new Custom.Subsequent[I, HookCtx.I12[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12], ({ type F[A] = (I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12) => A})#F](buildNext) + new Custom.Subsequent[I, HookCtx.I12[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12], ({ type F[A] = (I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12) => A})#F](displayName)(buildNext) } override def squash[A] = f => _.apply11(f) } @@ -1248,7 +1248,7 @@ trait Custom_SubsequentSteps { self: Custom.SubsequentStep.type => new Custom.SubsequentStep[I, HookCtx.I12[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12], ({ type F[A] = (I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12) => A})#F] { override type Next[H13] = Custom.Subsequent.AtStep12[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12]#Next[H13] override def next[H13] = - (buildPrev, initNextHook) => { + (buildPrev, initNextHook, displayName) => { val buildNext: Custom.BuildFn[I, HookCtx.I13[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13]] = new Custom.BuildFn[I, HookCtx.I13[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13]] { override def apply[O](f: HookCtx.I13[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13] => O) = { @@ -1259,7 +1259,7 @@ trait Custom_SubsequentSteps { self: Custom.SubsequentStep.type => } } } - new Custom.Subsequent[I, HookCtx.I13[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13], ({ type F[A] = (I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13) => A})#F](buildNext) + new Custom.Subsequent[I, HookCtx.I13[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13], ({ type F[A] = (I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13) => A})#F](displayName)(buildNext) } override def squash[A] = f => _.apply12(f) } @@ -1274,7 +1274,7 @@ trait Custom_SubsequentSteps { self: Custom.SubsequentStep.type => new Custom.SubsequentStep[I, HookCtx.I13[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13], ({ type F[A] = (I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13) => A})#F] { override type Next[H14] = Custom.Subsequent.AtStep13[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13]#Next[H14] override def next[H14] = - (buildPrev, initNextHook) => { + (buildPrev, initNextHook, displayName) => { val buildNext: Custom.BuildFn[I, HookCtx.I14[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14]] = new Custom.BuildFn[I, HookCtx.I14[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14]] { override def apply[O](f: HookCtx.I14[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14] => O) = { @@ -1285,7 +1285,7 @@ trait Custom_SubsequentSteps { self: Custom.SubsequentStep.type => } } } - new Custom.Subsequent[I, HookCtx.I14[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14], ({ type F[A] = (I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14) => A})#F](buildNext) + new Custom.Subsequent[I, HookCtx.I14[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14], ({ type F[A] = (I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14) => A})#F](displayName)(buildNext) } override def squash[A] = f => _.apply13(f) } @@ -1300,7 +1300,7 @@ trait Custom_SubsequentSteps { self: Custom.SubsequentStep.type => new Custom.SubsequentStep[I, HookCtx.I14[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14], ({ type F[A] = (I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14) => A})#F] { override type Next[H15] = Custom.Subsequent.AtStep14[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14]#Next[H15] override def next[H15] = - (buildPrev, initNextHook) => { + (buildPrev, initNextHook, displayName) => { val buildNext: Custom.BuildFn[I, HookCtx.I15[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15]] = new Custom.BuildFn[I, HookCtx.I15[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15]] { override def apply[O](f: HookCtx.I15[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15] => O) = { @@ -1311,7 +1311,7 @@ trait Custom_SubsequentSteps { self: Custom.SubsequentStep.type => } } } - new Custom.Subsequent[I, HookCtx.I15[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15], ({ type F[A] = (I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15) => A})#F](buildNext) + new Custom.Subsequent[I, HookCtx.I15[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15], ({ type F[A] = (I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15) => A})#F](displayName)(buildNext) } override def squash[A] = f => _.apply14(f) } @@ -1326,7 +1326,7 @@ trait Custom_SubsequentSteps { self: Custom.SubsequentStep.type => new Custom.SubsequentStep[I, HookCtx.I15[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15], ({ type F[A] = (I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15) => A})#F] { override type Next[H16] = Custom.Subsequent.AtStep15[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15]#Next[H16] override def next[H16] = - (buildPrev, initNextHook) => { + (buildPrev, initNextHook, displayName) => { val buildNext: Custom.BuildFn[I, HookCtx.I16[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16]] = new Custom.BuildFn[I, HookCtx.I16[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16]] { override def apply[O](f: HookCtx.I16[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16] => O) = { @@ -1337,7 +1337,7 @@ trait Custom_SubsequentSteps { self: Custom.SubsequentStep.type => } } } - new Custom.Subsequent[I, HookCtx.I16[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16], ({ type F[A] = (I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16) => A})#F](buildNext) + new Custom.Subsequent[I, HookCtx.I16[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16], ({ type F[A] = (I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16) => A})#F](displayName)(buildNext) } override def squash[A] = f => _.apply15(f) } @@ -1352,7 +1352,7 @@ trait Custom_SubsequentSteps { self: Custom.SubsequentStep.type => new Custom.SubsequentStep[I, HookCtx.I16[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16], ({ type F[A] = (I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16) => A})#F] { override type Next[H17] = Custom.Subsequent.AtStep16[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16]#Next[H17] override def next[H17] = - (buildPrev, initNextHook) => { + (buildPrev, initNextHook, displayName) => { val buildNext: Custom.BuildFn[I, HookCtx.I17[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17]] = new Custom.BuildFn[I, HookCtx.I17[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17]] { override def apply[O](f: HookCtx.I17[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17] => O) = { @@ -1363,7 +1363,7 @@ trait Custom_SubsequentSteps { self: Custom.SubsequentStep.type => } } } - new Custom.Subsequent[I, HookCtx.I17[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17], ({ type F[A] = (I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17) => A})#F](buildNext) + new Custom.Subsequent[I, HookCtx.I17[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17], ({ type F[A] = (I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17) => A})#F](displayName)(buildNext) } override def squash[A] = f => _.apply16(f) } @@ -1378,7 +1378,7 @@ trait Custom_SubsequentSteps { self: Custom.SubsequentStep.type => new Custom.SubsequentStep[I, HookCtx.I17[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17], ({ type F[A] = (I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17) => A})#F] { override type Next[H18] = Custom.Subsequent.AtStep17[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17]#Next[H18] override def next[H18] = - (buildPrev, initNextHook) => { + (buildPrev, initNextHook, displayName) => { val buildNext: Custom.BuildFn[I, HookCtx.I18[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18]] = new Custom.BuildFn[I, HookCtx.I18[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18]] { override def apply[O](f: HookCtx.I18[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18] => O) = { @@ -1389,7 +1389,7 @@ trait Custom_SubsequentSteps { self: Custom.SubsequentStep.type => } } } - new Custom.Subsequent[I, HookCtx.I18[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18], ({ type F[A] = (I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18) => A})#F](buildNext) + new Custom.Subsequent[I, HookCtx.I18[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18], ({ type F[A] = (I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18) => A})#F](displayName)(buildNext) } override def squash[A] = f => _.apply17(f) } @@ -1404,7 +1404,7 @@ trait Custom_SubsequentSteps { self: Custom.SubsequentStep.type => new Custom.SubsequentStep[I, HookCtx.I18[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18], ({ type F[A] = (I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18) => A})#F] { override type Next[H19] = Custom.Subsequent.AtStep18[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18]#Next[H19] override def next[H19] = - (buildPrev, initNextHook) => { + (buildPrev, initNextHook, displayName) => { val buildNext: Custom.BuildFn[I, HookCtx.I19[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19]] = new Custom.BuildFn[I, HookCtx.I19[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19]] { override def apply[O](f: HookCtx.I19[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19] => O) = { @@ -1415,7 +1415,7 @@ trait Custom_SubsequentSteps { self: Custom.SubsequentStep.type => } } } - new Custom.Subsequent[I, HookCtx.I19[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19], ({ type F[A] = (I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19) => A})#F](buildNext) + new Custom.Subsequent[I, HookCtx.I19[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19], ({ type F[A] = (I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19) => A})#F](displayName)(buildNext) } override def squash[A] = f => _.apply18(f) } @@ -1430,7 +1430,7 @@ trait Custom_SubsequentSteps { self: Custom.SubsequentStep.type => new Custom.SubsequentStep[I, HookCtx.I19[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19], ({ type F[A] = (I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19) => A})#F] { override type Next[H20] = Custom.Subsequent.AtStep19[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19]#Next[H20] override def next[H20] = - (buildPrev, initNextHook) => { + (buildPrev, initNextHook, displayName) => { val buildNext: Custom.BuildFn[I, HookCtx.I20[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19, H20]] = new Custom.BuildFn[I, HookCtx.I20[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19, H20]] { override def apply[O](f: HookCtx.I20[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19, H20] => O) = { @@ -1441,7 +1441,7 @@ trait Custom_SubsequentSteps { self: Custom.SubsequentStep.type => } } } - new Custom.Subsequent[I, HookCtx.I20[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19, H20], ({ type F[A] = (I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19, H20) => A})#F](buildNext) + new Custom.Subsequent[I, HookCtx.I20[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19, H20], ({ type F[A] = (I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19, H20) => A})#F](displayName)(buildNext) } override def squash[A] = f => _.apply19(f) } @@ -1456,7 +1456,7 @@ trait Custom_SubsequentSteps { self: Custom.SubsequentStep.type => new Custom.SubsequentStep[I, HookCtx.I20[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19, H20], ({ type F[A] = (I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19, H20) => A})#F] { override type Next[H21] = Custom.Subsequent.AtStep20[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19, H20]#Next[H21] override def next[H21] = - (buildPrev, initNextHook) => { + (buildPrev, initNextHook, displayName) => { val buildNext: Custom.BuildFn[I, HookCtx.I21[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19, H20, H21]] = new Custom.BuildFn[I, HookCtx.I21[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19, H20, H21]] { override def apply[O](f: HookCtx.I21[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19, H20, H21] => O) = { @@ -1467,7 +1467,7 @@ trait Custom_SubsequentSteps { self: Custom.SubsequentStep.type => } } } - new Custom.Subsequent[I, HookCtx.I21[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19, H20, H21], ({ type F[A] = (I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19, H20, H21) => A})#F](buildNext) + new Custom.Subsequent[I, HookCtx.I21[I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19, H20, H21], ({ type F[A] = (I, H1, H2, H3, H4, H5, H6, H7, H8, H9, H10, H11, H12, H13, H14, H15, H16, H17, H18, H19, H20, H21) => A})#F](displayName)(buildNext) } override def squash[A] = f => _.apply20(f) } diff --git a/library/coreGeneric/src/main/scala/japgolly/scalajs/react/hooks/UseCallbackBoilerplate.scala b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/hooks/UseCallbackBoilerplate.scala index b46f2e056..f21f448bb 100644 --- a/library/coreGeneric/src/main/scala/japgolly/scalajs/react/hooks/UseCallbackBoilerplate.scala +++ b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/hooks/UseCallbackBoilerplate.scala @@ -20,109 +20,219 @@ trait UseCallbackArgInstances { z => (a) => Z.dispatch(z(a)))( z => Reusable.byRef(z).withValue((a) => Z.delay(z(a)))) + implicit def ci1[A, Y, Z[_]](implicit Z: UnsafeSync[Z]): UseCallbackArg[(A) => Z[Y]] = + UseCallbackArg[(A) => Z[Y], js.Function1[A, Y]]( + z => (a) => Z.runSync(z(a)))( + z => Reusable.byRef(z).withValue((a) => Z.delay(z(a)))) + implicit def c2[A, B, Z[_]](implicit Z: Dispatch[Z]): UseCallbackArg[(A, B) => Z[Unit]] = UseCallbackArg[(A, B) => Z[Unit], js.Function2[A, B, Unit]]( z => (a, b) => Z.dispatch(z(a, b)))( z => Reusable.byRef(z).withValue((a, b) => Z.delay(z(a, b)))) + implicit def ci2[A, B, Y, Z[_]](implicit Z: UnsafeSync[Z]): UseCallbackArg[(A, B) => Z[Y]] = + UseCallbackArg[(A, B) => Z[Y], js.Function2[A, B, Y]]( + z => (a, b) => Z.runSync(z(a, b)))( + z => Reusable.byRef(z).withValue((a, b) => Z.delay(z(a, b)))) + implicit def c3[A, B, C, Z[_]](implicit Z: Dispatch[Z]): UseCallbackArg[(A, B, C) => Z[Unit]] = UseCallbackArg[(A, B, C) => Z[Unit], js.Function3[A, B, C, Unit]]( z => (a, b, c) => Z.dispatch(z(a, b, c)))( z => Reusable.byRef(z).withValue((a, b, c) => Z.delay(z(a, b, c)))) + implicit def ci3[A, B, C, Y, Z[_]](implicit Z: UnsafeSync[Z]): UseCallbackArg[(A, B, C) => Z[Y]] = + UseCallbackArg[(A, B, C) => Z[Y], js.Function3[A, B, C, Y]]( + z => (a, b, c) => Z.runSync(z(a, b, c)))( + z => Reusable.byRef(z).withValue((a, b, c) => Z.delay(z(a, b, c)))) + implicit def c4[A, B, C, D, Z[_]](implicit Z: Dispatch[Z]): UseCallbackArg[(A, B, C, D) => Z[Unit]] = UseCallbackArg[(A, B, C, D) => Z[Unit], js.Function4[A, B, C, D, Unit]]( z => (a, b, c, d) => Z.dispatch(z(a, b, c, d)))( z => Reusable.byRef(z).withValue((a, b, c, d) => Z.delay(z(a, b, c, d)))) + implicit def ci4[A, B, C, D, Y, Z[_]](implicit Z: UnsafeSync[Z]): UseCallbackArg[(A, B, C, D) => Z[Y]] = + UseCallbackArg[(A, B, C, D) => Z[Y], js.Function4[A, B, C, D, Y]]( + z => (a, b, c, d) => Z.runSync(z(a, b, c, d)))( + z => Reusable.byRef(z).withValue((a, b, c, d) => Z.delay(z(a, b, c, d)))) + implicit def c5[A, B, C, D, E, Z[_]](implicit Z: Dispatch[Z]): UseCallbackArg[(A, B, C, D, E) => Z[Unit]] = UseCallbackArg[(A, B, C, D, E) => Z[Unit], js.Function5[A, B, C, D, E, Unit]]( z => (a, b, c, d, e) => Z.dispatch(z(a, b, c, d, e)))( z => Reusable.byRef(z).withValue((a, b, c, d, e) => Z.delay(z(a, b, c, d, e)))) + implicit def ci5[A, B, C, D, E, Y, Z[_]](implicit Z: UnsafeSync[Z]): UseCallbackArg[(A, B, C, D, E) => Z[Y]] = + UseCallbackArg[(A, B, C, D, E) => Z[Y], js.Function5[A, B, C, D, E, Y]]( + z => (a, b, c, d, e) => Z.runSync(z(a, b, c, d, e)))( + z => Reusable.byRef(z).withValue((a, b, c, d, e) => Z.delay(z(a, b, c, d, e)))) + implicit def c6[A, B, C, D, E, F, Z[_]](implicit Z: Dispatch[Z]): UseCallbackArg[(A, B, C, D, E, F) => Z[Unit]] = UseCallbackArg[(A, B, C, D, E, F) => Z[Unit], js.Function6[A, B, C, D, E, F, Unit]]( z => (a, b, c, d, e, f) => Z.dispatch(z(a, b, c, d, e, f)))( z => Reusable.byRef(z).withValue((a, b, c, d, e, f) => Z.delay(z(a, b, c, d, e, f)))) + implicit def ci6[A, B, C, D, E, F, Y, Z[_]](implicit Z: UnsafeSync[Z]): UseCallbackArg[(A, B, C, D, E, F) => Z[Y]] = + UseCallbackArg[(A, B, C, D, E, F) => Z[Y], js.Function6[A, B, C, D, E, F, Y]]( + z => (a, b, c, d, e, f) => Z.runSync(z(a, b, c, d, e, f)))( + z => Reusable.byRef(z).withValue((a, b, c, d, e, f) => Z.delay(z(a, b, c, d, e, f)))) + implicit def c7[A, B, C, D, E, F, G, Z[_]](implicit Z: Dispatch[Z]): UseCallbackArg[(A, B, C, D, E, F, G) => Z[Unit]] = UseCallbackArg[(A, B, C, D, E, F, G) => Z[Unit], js.Function7[A, B, C, D, E, F, G, Unit]]( z => (a, b, c, d, e, f, g) => Z.dispatch(z(a, b, c, d, e, f, g)))( z => Reusable.byRef(z).withValue((a, b, c, d, e, f, g) => Z.delay(z(a, b, c, d, e, f, g)))) + implicit def ci7[A, B, C, D, E, F, G, Y, Z[_]](implicit Z: UnsafeSync[Z]): UseCallbackArg[(A, B, C, D, E, F, G) => Z[Y]] = + UseCallbackArg[(A, B, C, D, E, F, G) => Z[Y], js.Function7[A, B, C, D, E, F, G, Y]]( + z => (a, b, c, d, e, f, g) => Z.runSync(z(a, b, c, d, e, f, g)))( + z => Reusable.byRef(z).withValue((a, b, c, d, e, f, g) => Z.delay(z(a, b, c, d, e, f, g)))) + implicit def c8[A, B, C, D, E, F, G, H, Z[_]](implicit Z: Dispatch[Z]): UseCallbackArg[(A, B, C, D, E, F, G, H) => Z[Unit]] = UseCallbackArg[(A, B, C, D, E, F, G, H) => Z[Unit], js.Function8[A, B, C, D, E, F, G, H, Unit]]( z => (a, b, c, d, e, f, g, h) => Z.dispatch(z(a, b, c, d, e, f, g, h)))( z => Reusable.byRef(z).withValue((a, b, c, d, e, f, g, h) => Z.delay(z(a, b, c, d, e, f, g, h)))) + implicit def ci8[A, B, C, D, E, F, G, H, Y, Z[_]](implicit Z: UnsafeSync[Z]): UseCallbackArg[(A, B, C, D, E, F, G, H) => Z[Y]] = + UseCallbackArg[(A, B, C, D, E, F, G, H) => Z[Y], js.Function8[A, B, C, D, E, F, G, H, Y]]( + z => (a, b, c, d, e, f, g, h) => Z.runSync(z(a, b, c, d, e, f, g, h)))( + z => Reusable.byRef(z).withValue((a, b, c, d, e, f, g, h) => Z.delay(z(a, b, c, d, e, f, g, h)))) + implicit def c9[A, B, C, D, E, F, G, H, I, Z[_]](implicit Z: Dispatch[Z]): UseCallbackArg[(A, B, C, D, E, F, G, H, I) => Z[Unit]] = UseCallbackArg[(A, B, C, D, E, F, G, H, I) => Z[Unit], js.Function9[A, B, C, D, E, F, G, H, I, Unit]]( z => (a, b, c, d, e, f, g, h, i) => Z.dispatch(z(a, b, c, d, e, f, g, h, i)))( z => Reusable.byRef(z).withValue((a, b, c, d, e, f, g, h, i) => Z.delay(z(a, b, c, d, e, f, g, h, i)))) + implicit def ci9[A, B, C, D, E, F, G, H, I, Y, Z[_]](implicit Z: UnsafeSync[Z]): UseCallbackArg[(A, B, C, D, E, F, G, H, I) => Z[Y]] = + UseCallbackArg[(A, B, C, D, E, F, G, H, I) => Z[Y], js.Function9[A, B, C, D, E, F, G, H, I, Y]]( + z => (a, b, c, d, e, f, g, h, i) => Z.runSync(z(a, b, c, d, e, f, g, h, i)))( + z => Reusable.byRef(z).withValue((a, b, c, d, e, f, g, h, i) => Z.delay(z(a, b, c, d, e, f, g, h, i)))) + implicit def c10[A, B, C, D, E, F, G, H, I, J, Z[_]](implicit Z: Dispatch[Z]): UseCallbackArg[(A, B, C, D, E, F, G, H, I, J) => Z[Unit]] = UseCallbackArg[(A, B, C, D, E, F, G, H, I, J) => Z[Unit], js.Function10[A, B, C, D, E, F, G, H, I, J, Unit]]( z => (a, b, c, d, e, f, g, h, i, j) => Z.dispatch(z(a, b, c, d, e, f, g, h, i, j)))( z => Reusable.byRef(z).withValue((a, b, c, d, e, f, g, h, i, j) => Z.delay(z(a, b, c, d, e, f, g, h, i, j)))) + implicit def ci10[A, B, C, D, E, F, G, H, I, J, Y, Z[_]](implicit Z: UnsafeSync[Z]): UseCallbackArg[(A, B, C, D, E, F, G, H, I, J) => Z[Y]] = + UseCallbackArg[(A, B, C, D, E, F, G, H, I, J) => Z[Y], js.Function10[A, B, C, D, E, F, G, H, I, J, Y]]( + z => (a, b, c, d, e, f, g, h, i, j) => Z.runSync(z(a, b, c, d, e, f, g, h, i, j)))( + z => Reusable.byRef(z).withValue((a, b, c, d, e, f, g, h, i, j) => Z.delay(z(a, b, c, d, e, f, g, h, i, j)))) + implicit def c11[A, B, C, D, E, F, G, H, I, J, K, Z[_]](implicit Z: Dispatch[Z]): UseCallbackArg[(A, B, C, D, E, F, G, H, I, J, K) => Z[Unit]] = UseCallbackArg[(A, B, C, D, E, F, G, H, I, J, K) => Z[Unit], js.Function11[A, B, C, D, E, F, G, H, I, J, K, Unit]]( z => (a, b, c, d, e, f, g, h, i, j, k) => Z.dispatch(z(a, b, c, d, e, f, g, h, i, j, k)))( z => Reusable.byRef(z).withValue((a, b, c, d, e, f, g, h, i, j, k) => Z.delay(z(a, b, c, d, e, f, g, h, i, j, k)))) + implicit def ci11[A, B, C, D, E, F, G, H, I, J, K, Y, Z[_]](implicit Z: UnsafeSync[Z]): UseCallbackArg[(A, B, C, D, E, F, G, H, I, J, K) => Z[Y]] = + UseCallbackArg[(A, B, C, D, E, F, G, H, I, J, K) => Z[Y], js.Function11[A, B, C, D, E, F, G, H, I, J, K, Y]]( + z => (a, b, c, d, e, f, g, h, i, j, k) => Z.runSync(z(a, b, c, d, e, f, g, h, i, j, k)))( + z => Reusable.byRef(z).withValue((a, b, c, d, e, f, g, h, i, j, k) => Z.delay(z(a, b, c, d, e, f, g, h, i, j, k)))) + implicit def c12[A, B, C, D, E, F, G, H, I, J, K, L, Z[_]](implicit Z: Dispatch[Z]): UseCallbackArg[(A, B, C, D, E, F, G, H, I, J, K, L) => Z[Unit]] = UseCallbackArg[(A, B, C, D, E, F, G, H, I, J, K, L) => Z[Unit], js.Function12[A, B, C, D, E, F, G, H, I, J, K, L, Unit]]( z => (a, b, c, d, e, f, g, h, i, j, k, l) => Z.dispatch(z(a, b, c, d, e, f, g, h, i, j, k, l)))( z => Reusable.byRef(z).withValue((a, b, c, d, e, f, g, h, i, j, k, l) => Z.delay(z(a, b, c, d, e, f, g, h, i, j, k, l)))) + implicit def ci12[A, B, C, D, E, F, G, H, I, J, K, L, Y, Z[_]](implicit Z: UnsafeSync[Z]): UseCallbackArg[(A, B, C, D, E, F, G, H, I, J, K, L) => Z[Y]] = + UseCallbackArg[(A, B, C, D, E, F, G, H, I, J, K, L) => Z[Y], js.Function12[A, B, C, D, E, F, G, H, I, J, K, L, Y]]( + z => (a, b, c, d, e, f, g, h, i, j, k, l) => Z.runSync(z(a, b, c, d, e, f, g, h, i, j, k, l)))( + z => Reusable.byRef(z).withValue((a, b, c, d, e, f, g, h, i, j, k, l) => Z.delay(z(a, b, c, d, e, f, g, h, i, j, k, l)))) + implicit def c13[A, B, C, D, E, F, G, H, I, J, K, L, M, Z[_]](implicit Z: Dispatch[Z]): UseCallbackArg[(A, B, C, D, E, F, G, H, I, J, K, L, M) => Z[Unit]] = UseCallbackArg[(A, B, C, D, E, F, G, H, I, J, K, L, M) => Z[Unit], js.Function13[A, B, C, D, E, F, G, H, I, J, K, L, M, Unit]]( z => (a, b, c, d, e, f, g, h, i, j, k, l, m) => Z.dispatch(z(a, b, c, d, e, f, g, h, i, j, k, l, m)))( z => Reusable.byRef(z).withValue((a, b, c, d, e, f, g, h, i, j, k, l, m) => Z.delay(z(a, b, c, d, e, f, g, h, i, j, k, l, m)))) + implicit def ci13[A, B, C, D, E, F, G, H, I, J, K, L, M, Y, Z[_]](implicit Z: UnsafeSync[Z]): UseCallbackArg[(A, B, C, D, E, F, G, H, I, J, K, L, M) => Z[Y]] = + UseCallbackArg[(A, B, C, D, E, F, G, H, I, J, K, L, M) => Z[Y], js.Function13[A, B, C, D, E, F, G, H, I, J, K, L, M, Y]]( + z => (a, b, c, d, e, f, g, h, i, j, k, l, m) => Z.runSync(z(a, b, c, d, e, f, g, h, i, j, k, l, m)))( + z => Reusable.byRef(z).withValue((a, b, c, d, e, f, g, h, i, j, k, l, m) => Z.delay(z(a, b, c, d, e, f, g, h, i, j, k, l, m)))) + implicit def c14[A, B, C, D, E, F, G, H, I, J, K, L, M, N, Z[_]](implicit Z: Dispatch[Z]): UseCallbackArg[(A, B, C, D, E, F, G, H, I, J, K, L, M, N) => Z[Unit]] = UseCallbackArg[(A, B, C, D, E, F, G, H, I, J, K, L, M, N) => Z[Unit], js.Function14[A, B, C, D, E, F, G, H, I, J, K, L, M, N, Unit]]( z => (a, b, c, d, e, f, g, h, i, j, k, l, m, n) => Z.dispatch(z(a, b, c, d, e, f, g, h, i, j, k, l, m, n)))( z => Reusable.byRef(z).withValue((a, b, c, d, e, f, g, h, i, j, k, l, m, n) => Z.delay(z(a, b, c, d, e, f, g, h, i, j, k, l, m, n)))) + implicit def ci14[A, B, C, D, E, F, G, H, I, J, K, L, M, N, Y, Z[_]](implicit Z: UnsafeSync[Z]): UseCallbackArg[(A, B, C, D, E, F, G, H, I, J, K, L, M, N) => Z[Y]] = + UseCallbackArg[(A, B, C, D, E, F, G, H, I, J, K, L, M, N) => Z[Y], js.Function14[A, B, C, D, E, F, G, H, I, J, K, L, M, N, Y]]( + z => (a, b, c, d, e, f, g, h, i, j, k, l, m, n) => Z.runSync(z(a, b, c, d, e, f, g, h, i, j, k, l, m, n)))( + z => Reusable.byRef(z).withValue((a, b, c, d, e, f, g, h, i, j, k, l, m, n) => Z.delay(z(a, b, c, d, e, f, g, h, i, j, k, l, m, n)))) + implicit def c15[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, Z[_]](implicit Z: Dispatch[Z]): UseCallbackArg[(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O) => Z[Unit]] = UseCallbackArg[(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O) => Z[Unit], js.Function15[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, Unit]]( z => (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o) => Z.dispatch(z(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o)))( z => Reusable.byRef(z).withValue((a, b, c, d, e, f, g, h, i, j, k, l, m, n, o) => Z.delay(z(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o)))) + implicit def ci15[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, Y, Z[_]](implicit Z: UnsafeSync[Z]): UseCallbackArg[(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O) => Z[Y]] = + UseCallbackArg[(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O) => Z[Y], js.Function15[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, Y]]( + z => (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o) => Z.runSync(z(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o)))( + z => Reusable.byRef(z).withValue((a, b, c, d, e, f, g, h, i, j, k, l, m, n, o) => Z.delay(z(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o)))) + implicit def c16[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Z[_]](implicit Z: Dispatch[Z]): UseCallbackArg[(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P) => Z[Unit]] = UseCallbackArg[(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P) => Z[Unit], js.Function16[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Unit]]( z => (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p) => Z.dispatch(z(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p)))( z => Reusable.byRef(z).withValue((a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p) => Z.delay(z(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p)))) + implicit def ci16[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Y, Z[_]](implicit Z: UnsafeSync[Z]): UseCallbackArg[(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P) => Z[Y]] = + UseCallbackArg[(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P) => Z[Y], js.Function16[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Y]]( + z => (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p) => Z.runSync(z(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p)))( + z => Reusable.byRef(z).withValue((a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p) => Z.delay(z(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p)))) + implicit def c17[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, Z[_]](implicit Z: Dispatch[Z]): UseCallbackArg[(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q) => Z[Unit]] = UseCallbackArg[(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q) => Z[Unit], js.Function17[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, Unit]]( z => (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q) => Z.dispatch(z(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q)))( z => Reusable.byRef(z).withValue((a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q) => Z.delay(z(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q)))) + implicit def ci17[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, Y, Z[_]](implicit Z: UnsafeSync[Z]): UseCallbackArg[(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q) => Z[Y]] = + UseCallbackArg[(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q) => Z[Y], js.Function17[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, Y]]( + z => (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q) => Z.runSync(z(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q)))( + z => Reusable.byRef(z).withValue((a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q) => Z.delay(z(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q)))) + implicit def c18[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, Z[_]](implicit Z: Dispatch[Z]): UseCallbackArg[(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R) => Z[Unit]] = UseCallbackArg[(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R) => Z[Unit], js.Function18[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, Unit]]( z => (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r) => Z.dispatch(z(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r)))( z => Reusable.byRef(z).withValue((a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r) => Z.delay(z(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r)))) + implicit def ci18[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, Y, Z[_]](implicit Z: UnsafeSync[Z]): UseCallbackArg[(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R) => Z[Y]] = + UseCallbackArg[(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R) => Z[Y], js.Function18[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, Y]]( + z => (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r) => Z.runSync(z(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r)))( + z => Reusable.byRef(z).withValue((a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r) => Z.delay(z(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r)))) + implicit def c19[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, Z[_]](implicit Z: Dispatch[Z]): UseCallbackArg[(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S) => Z[Unit]] = UseCallbackArg[(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S) => Z[Unit], js.Function19[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, Unit]]( z => (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s) => Z.dispatch(z(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s)))( z => Reusable.byRef(z).withValue((a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s) => Z.delay(z(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s)))) + implicit def ci19[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, Y, Z[_]](implicit Z: UnsafeSync[Z]): UseCallbackArg[(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S) => Z[Y]] = + UseCallbackArg[(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S) => Z[Y], js.Function19[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, Y]]( + z => (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s) => Z.runSync(z(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s)))( + z => Reusable.byRef(z).withValue((a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s) => Z.delay(z(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s)))) + implicit def c20[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, Z[_]](implicit Z: Dispatch[Z]): UseCallbackArg[(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T) => Z[Unit]] = UseCallbackArg[(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T) => Z[Unit], js.Function20[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, Unit]]( z => (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t) => Z.dispatch(z(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t)))( z => Reusable.byRef(z).withValue((a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t) => Z.delay(z(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t)))) + implicit def ci20[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, Y, Z[_]](implicit Z: UnsafeSync[Z]): UseCallbackArg[(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T) => Z[Y]] = + UseCallbackArg[(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T) => Z[Y], js.Function20[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, Y]]( + z => (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t) => Z.runSync(z(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t)))( + z => Reusable.byRef(z).withValue((a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t) => Z.delay(z(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t)))) + implicit def c21[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, Z[_]](implicit Z: Dispatch[Z]): UseCallbackArg[(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U) => Z[Unit]] = UseCallbackArg[(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U) => Z[Unit], js.Function21[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, Unit]]( z => (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u) => Z.dispatch(z(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u)))( z => Reusable.byRef(z).withValue((a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u) => Z.delay(z(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u)))) + implicit def ci21[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, Y, Z[_]](implicit Z: UnsafeSync[Z]): UseCallbackArg[(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U) => Z[Y]] = + UseCallbackArg[(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U) => Z[Y], js.Function21[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, Y]]( + z => (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u) => Z.runSync(z(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u)))( + z => Reusable.byRef(z).withValue((a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u) => Z.delay(z(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u)))) + implicit def c22[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, Z[_]](implicit Z: Dispatch[Z]): UseCallbackArg[(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V) => Z[Unit]] = UseCallbackArg[(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V) => Z[Unit], js.Function22[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, Unit]]( z => (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v) => Z.dispatch(z(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v)))( z => Reusable.byRef(z).withValue((a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v) => Z.delay(z(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v)))) + implicit def ci22[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, Y, Z[_]](implicit Z: UnsafeSync[Z]): UseCallbackArg[(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V) => Z[Y]] = + UseCallbackArg[(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V) => Z[Y], js.Function22[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, Y]]( + z => (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v) => Z.runSync(z(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v)))( + z => Reusable.byRef(z).withValue((a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v) => Z.delay(z(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v)))) + } diff --git a/library/coreGeneric/src/main/scala/japgolly/scalajs/react/hooks/all.scala b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/hooks/all.scala new file mode 100644 index 000000000..b98696e07 --- /dev/null +++ b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/hooks/all.scala @@ -0,0 +1,5 @@ +package japgolly.scalajs.react.hooks + +trait all extends react17 with react18 with extra + +object all extends all diff --git a/library/coreGeneric/src/main/scala/japgolly/scalajs/react/hooks/extra.scala b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/hooks/extra.scala new file mode 100644 index 000000000..385a5c0a7 --- /dev/null +++ b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/hooks/extra.scala @@ -0,0 +1,80 @@ +package japgolly.scalajs.react.hooks + +import japgolly.scalajs.react.component.{Js => JsComponent} +import japgolly.scalajs.react.hooks.Hooks.{UseRef, UseStateWithReuse} +import japgolly.scalajs.react.util.DefaultEffects +import japgolly.scalajs.react.vdom.TopNode +import japgolly.scalajs.react.{CtorType, Ref, Reusability, Reusable, ScalaComponent} +import scala.reflect.ClassTag +import scala.scalajs.js + + +trait extra { + /** Provides a Callback that when invoked forces a re-render of your component. */ + @inline final val useForceUpdate: HookResult[Reusable[DefaultEffects.Sync[Unit]]] = + CustomHook.useForceUpdate.toHookResult + + + /** Create a mutable ref that will persist for the full lifetime of the component. */ + @inline final val useRefToAnyVdom: HookResult[Ref.ToAnyVdom] = + HookResult(UseRef.unsafeCreateToAnyVdom()) + + /** Create a mutable ref that will persist for the full lifetime of the component. */ + @inline final def useRefToVdom[N <: TopNode: ClassTag]: HookResult[Ref.ToVdom[N]] = + HookResult(UseRef.unsafeCreateToVdom[N]()) + + /** Create a mutable ref that will persist for the full lifetime of the component. */ + @inline final def useRefToScalaComponent[P, S, B]: HookResult[Ref.ToScalaComponent[P, S, B]] = + HookResult(UseRef.unsafeCreateToScalaComponent[P, S, B]()) + + /** Create a mutable ref that will persist for the full lifetime of the component. */ + @inline final def useRefToScalaComponent[P, S, B, CT[-p, +u] <: CtorType[p, u]]( + c: ScalaComponent.Component[P, S, B, CT] + ): HookResult[Ref.WithScalaComponent[P, S, B, CT]] = + HookResult(UseRef.unsafeCreateToScalaComponent(c)) + + /** Create a mutable ref that will persist for the full lifetime of the component. */ + @inline final def useRefToJsComponent[P <: js.Object, S <: js.Object] + : HookResult[Ref.ToJsComponent[P, S, JsComponent.RawMounted[P, S]]] = + HookResult(UseRef.unsafeCreateToJsComponent[P, S]()) + + /** Create a mutable ref that will persist for the full lifetime of the component. */ + @inline final def useRefToJsComponentWithMountedFacade[P <: js.Object, S <: js.Object, F <: js.Object] + : HookResult[Ref.ToJsComponent[P, S, JsComponent.RawMounted[P, S] with F]] = + HookResult(UseRef.unsafeCreateToJsComponentWithMountedFacade[P, S, F]()) + + /** Create a mutable ref that will persist for the full lifetime of the component. */ + @inline final def useRefToJsComponent[F[_], A[_], P1, S1, CT1[-p, +u] <: CtorType[p, + u + ], R <: JsComponent.RawMounted[P0, S0], P0 <: js.Object, S0 <: js.Object, CT0[-p, +u] <: CtorType[p, + u + ]]( + a: Ref.WithJsComponentArg[F, A, P1, S1, CT1, R, P0, S0] + ): HookResult[Ref.WithJsComponent[F, A, P1, S1, CT1, R, P0, S0]] = + HookResult(UseRef.unsafeCreateToJsComponent(a)) + + /** + * Returns a stateful value, and a function to update it. + * + * During the initial render, the returned state is the same as the value passed as the first + * argument (initialState). + * + * During subsequent re-renders, the first value returned by useState will always be the most recent + * state after applying updates. + */ + @inline final def useStateWithReuse[S: ClassTag: Reusability]( + initialState: => S + ): HookResult[UseStateWithReuse[S]] = + HookResult(UseStateWithReuse.unsafeCreate(initialState)) + + /** + * Given a reusable value, returns the original value that is being reused whenver reusability + * applies, together with a revisition a number that increments only when the value isn't reused. + * + * Useful for using `Reusability` logic in facades to JS hooks that accept dependencies: you can + * pass the revision as a dependency to the JS hook. + */ + @inline final def useReused[D: Reusability](deps: => D): HookResult[(D, Int)] = + CustomHook.reusableDeps[D].toHookResult(() => deps) + +} diff --git a/library/coreGeneric/src/main/scala/japgolly/scalajs/react/hooks/package.scala b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/hooks/package.scala new file mode 100644 index 000000000..9c4c7762a --- /dev/null +++ b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/hooks/package.scala @@ -0,0 +1,9 @@ +package japgolly.scalajs.react + +import japgolly.scalajs.react.component.Delayed + +package object hooks { + // Offers nicer API for hooks + type HookResult[+A] = Delayed[A] + val HookResult = Delayed +} diff --git a/library/coreGeneric/src/main/scala/japgolly/scalajs/react/hooks/react17.scala b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/hooks/react17.scala new file mode 100644 index 000000000..c934b154e --- /dev/null +++ b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/hooks/react17.scala @@ -0,0 +1,206 @@ +package japgolly.scalajs.react.hooks + +import japgolly.scalajs.react.feature.Context +import japgolly.scalajs.react.hooks.Hooks._ +import japgolly.scalajs.react.{Reusability, _} + +trait react17 { + /** + * Returns a memoized callback. + * + * Pass an inline callback and dependencies. useCallback will return a memoized version of the + * callback that only changes if one of the dependencies has changed. This is useful when passing + * callbacks to optimized child components that rely on reference equality to prevent unnecessary + * renders. + * + * @see + * https://reactjs.org/docs/hooks-reference.html#usecallback + */ + @inline final def useCallback[A](callback: A)(implicit isCallbackArg: UseCallbackArg[A]): HookResult[Reusable[A]] = + UseCallback(callback).toHookResult + + /** + * Returns a memoized callback. + * + * Pass an inline callback and dependencies. useCallback will return a memoized version of the + * callback that only changes if one of the dependencies has changed. This is useful when passing + * callbacks to optimized child components that rely on reference equality to prevent unnecessary + * renders. + * + * @see + * https://reactjs.org/docs/hooks-reference.html#usecallback + */ + @inline final def useCallbackWithDeps[D: Reusability, A](deps: => D)(callback: D => A)( + implicit isCallbackArg: UseCallbackArg[A] + ): HookResult[Reusable[A]] = + UseCallback.withDeps(deps)(callback).toHookResult + + /** + * Accepts a context object and returns the current context value for that context. The current + * context value is determined by the value prop of the nearest `` above the + * calling component in the tree. + * + * When the nearest `` above the component updates, this Hook will trigger a + * rerender with the latest context value passed to that `MyContext` provider. Even if an ancestor + * uses `React.memo` or `shouldComponentUpdate`, a rerender will still happen starting at the + * component itself using `useContext`. + * + * A component calling `useContext` will always re-render when the context value changes. If + * re-rendering the component is expensive, you can optimize it by using memoization. + * + * `useContext(MyContext)` only lets you read the context and subscribe to its changes. You still + * need a `` above in the tree to provide the value for this context. + * + * @see + * https://reactjs.org/docs/hooks-reference.html#usecontext + */ + @inline final def useContext[A](ctx: Context[A]): HookResult[A] = + HookResult(UseContext.unsafeCreate(ctx)) + + /** + * Used to display a label for custom hooks in React DevTools. + * + * @see + * https://reactjs.org/docs/hooks-reference.html#usedebugvalue + */ + @inline final def useDebugValue(desc: => Any): HookResult[Unit] = + HookResult(UseDebugValue.unsafeCreate(desc)) + + /** + * The callback passed to useEffect will run after the render is committed to the screen. Think of + * effects as an escape hatch from React’s purely functional world into the imperative world. + * + * By default, effects run after every completed render. If you'd only like to execute the effect + * when your component is mounted, then use [[useEffectOnMount]]. If you'd only like to execute the + * effect when certain values have changed, then use [[useEffectWithDeps]]. + * + * @see + * https://reactjs.org/docs/hooks-reference.html#useeffect + */ + @inline final def useEffect[A](effect: A)(implicit isEffectArg: UseEffectArg[A]): HookResult[Unit] = + HookResult(UseEffect.unsafeCreate(effect)) + + /** + * The callback passed to useEffect will run after the render is committed to the screen. Think of + * effects as an escape hatch from React’s purely functional world into the imperative world. + * + * This will only execute the effect when your component is mounted. + * + * @see + * https://reactjs.org/docs/hooks-reference.html#useeffect + */ + @inline final def useEffectOnMount[A](effect: A)(implicit isEffectArg: UseEffectArg[A]): HookResult[Unit] = + HookResult(UseEffect.unsafeCreateOnMount(effect)) + + /** + * The callback passed to useEffect will run after the render is committed to the screen. Think of + * effects as an escape hatch from React’s purely functional world into the imperative world. + * + * This will only execute the effect when values in the first argument change. + * + * @see + * https://reactjs.org/docs/hooks-reference.html#useeffect + */ + @inline final def useEffectWithDeps[D: Reusability, A](deps: => D)(effect: D => A)( + implicit isEffectArg: UseEffectArg[A] + ): HookResult[Unit] = + ReusableEffect.useEffect(deps)(effect).toHookResult + + /** + * The signature is identical to [[useEffect]], but it fires synchronously after all DOM mutations. + * Use this to read layout from the DOM and synchronously re-render. Updates scheduled inside + * useLayoutEffect will be flushed synchronously, before the browser has a chance to paint. + * + * Prefer the standard [[useEffect]] when possible to avoid blocking visual updates. + * + * If you'd only like to execute the effect when your component is mounted, then use + * [[useLayoutEffectOnMount]]. If you'd only like to execute the effect when certain values have + * changed, then use [[useLayoutEffectWithDeps]]. + * + * @see + * https://reactjs.org/docs/hooks-reference.html#useLayoutEffect + */ + @inline final def useLayoutEffect[A](effect: A)(implicit isEffectArg: UseEffectArg[A]): HookResult[Unit] = + HookResult(UseEffect.unsafeCreateLayout(effect)) + + /** + * The signature is identical to [[useEffect]], but it fires synchronously after all DOM mutations. + * Use this to read layout from the DOM and synchronously re-render. Updates scheduled inside + * useLayoutEffect will be flushed synchronously, before the browser has a chance to paint. + * + * Prefer the standard [[useEffect]] when possible to avoid blocking visual updates. + * + * This will only execute the effect when your component is mounted. + * + * @see + * https://reactjs.org/docs/hooks-reference.html#useLayoutEffect + */ + @inline final def useLayoutEffectOnMount[A](effect: A)(implicit isEffectArg: UseEffectArg[A]): HookResult[Unit] = + HookResult(UseEffect.unsafeCreateLayoutOnMount(effect)) + + /** + * The signature is identical to [[useEffect]], but it fires synchronously after all DOM mutations. + * Use this to read layout from the DOM and synchronously re-render. Updates scheduled inside + * useLayoutEffect will be flushed synchronously, before the browser has a chance to paint. + * + * Prefer the standard [[useEffect]] when possible to avoid blocking visual updates. + * + * This will only execute the effect when values in the first argument change. + * + * @see + * https://reactjs.org/docs/hooks-reference.html#useLayoutEffect + */ + @inline final def useLayoutEffectWithDeps[D: Reusability, A](deps: => D)(effect: D => A)( + implicit isEffectArg: UseEffectArg[A] + ): HookResult[Unit] = + ReusableEffect.useLayoutEffect(deps)(effect).toHookResult + + /** + * Returns a memoized value. + * + * Pass a “create” function and any dependencies. useMemo will only recompute the memoized value + * when one of the dependencies has changed. This optimization helps to avoid expensive calculations + * on every render. + * + * Remember that the function passed to useMemo runs during rendering. Don’t do anything there that + * you wouldn’t normally do while rendering. For example, side effects belong in [[useEffect]], not + * useMemo. + * + * @see + * https://reactjs.org/docs/hooks-reference.html#usememo + */ + @inline final def useMemo[D: Reusability, A](deps: => D)(create: D => A): HookResult[Reusable[A]] = + UseMemo(deps)(create).toHookResult + + /** + * An alternative to [[useState]]. Accepts a reducer of type `(state, action) => newState`, and + * returns the current state paired with a dispatch method. (If you’re familiar with Redux, you + * already know how this works.) + * + * useReducer is usually preferable to useState when you have complex state logic that involves + * multiple sub-values or when the next state depends on the previous one. useReducer also lets you + * optimize performance for components that trigger deep updates because you can pass dispatch down + * instead of callbacks. + * + * @see + * https://reactjs.org/docs/hooks-reference.html#usereducer + */ + @inline final def useReducer[S, A](reducer: (S, A) => S, initialState: => S): HookResult[UseReducer[S, A]] = + HookResult(UseReducer.unsafeCreate(reducer, initialState)) + + /** Create a mutable ref that will persist for the full lifetime of the component. */ + @inline final def useRef[A](initialValue: => A): HookResult[UseRef[A]] = + HookResult(UseRef.unsafeCreate(initialValue)) + + /** + * Returns a stateful value, and a function to update it. + * + * During the initial render, the returned state is the same as the value passed as the first + * argument (initialState). + * + * During subsequent re-renders, the first value returned by useState will always be the most recent + * state after applying updates. + */ + @inline final def useState[A](initial: => A): HookResult[UseState[A]] = + HookResult(UseState.unsafeCreate(initial)) +} \ No newline at end of file diff --git a/library/coreGeneric/src/main/scala/japgolly/scalajs/react/hooks/react18.scala b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/hooks/react18.scala new file mode 100644 index 000000000..a3ab3f5d2 --- /dev/null +++ b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/hooks/react18.scala @@ -0,0 +1,100 @@ +package japgolly.scalajs.react.hooks + +import japgolly.scalajs.react.Reusability +import japgolly.scalajs.react.hooks.HookResult +import japgolly.scalajs.react.hooks.Hooks._ +import japgolly.scalajs.react.util.Effect.Sync +import scala.scalajs.js + +trait react18 { + /** + * Generates unique IDs that can be passed to accessibility attributes. + * + * @see + * https://react.dev/reference/react/useId + */ + @inline final def useId: HookResult[String] = + UseId().toHookResult + + /** + * Allows components to avoid undesirable loading states by waiting for content to load before + * transitioning to the next screen. It also allows components to defer slower, data fetching + * updates until subsequent renders so that more crucial updates can be rendered immediately. + * + * **If some state update causes a component to suspend, that state update should be wrapped in a + * transition.** + * + * @see + * {@link https://react.dev/reference/react/useTransition} + */ + @inline final def useTransition: HookResult[UseTransition] = + UseTransition().toHookResult + + /** + * Lets you subscribe to an external store. + * + * @see + * {@link https://react.dev/reference/react/useSyncExternalStore} + */ + @inline final def useSyncExternalStore[F[_], A]( + subscribe: F[Unit] => F[F[Unit]], + getSnapshot: F[A], + getServerSnapshot: js.UndefOr[F[A]] = js.undefined + )(implicit F: Sync[F]): HookResult[A] = + UseSyncExternalStore(subscribe, getSnapshot, getServerSnapshot).toHookResult + + + /** The signature is identical to [[useEffect]], but it fires synchronously after all DOM mutations, but before any + * layout Effects fire. Use this to insert styles before any Effects fire that may need to read layout. Updates + * scheduled inside useLayoutEffect will be flushed synchronously, before the browser has a chance to paint. + * + * Prefer the standard [[useEffect]] when possible to avoid blocking visual updates. + * + * If you'd only like to execute the effect when your component is mounted, then use [[useInsertionEffectOnMount]]. + * If you'd only like to execute the effect when certain values have changed, then use [[useInsertionEffectWithDeps]]. + * + * @see https://react.dev/reference/react/useInsertionEffect#useInsertionEffect + */ + @inline final def useInsertionEffect[A](effect: A)(implicit isEffectArg: UseEffectArg[A]): HookResult[Unit] = + HookResult(UseEffect.unsafeCreateInsertion(effect)) + + /** The signature is identical to [[useEffect]], but it fires synchronously after all DOM mutations, but before any + * layout Effects fire. Use this to insert styles before any Effects fire that may need to read layout. Updates + * scheduled inside useLayoutEffect will be flushed synchronously, before the browser has a chance to paint. + * + * Prefer the standard [[useEffect]] when possible to avoid blocking visual updates. + * + * This will only execute the effect when your component is mounted. + * + * @see https://react.dev/reference/react/useInsertionEffect#useInsertionEffect + */ + @inline final def useInsertionEffectOnMount[A](effect: A)(implicit isEffectArg: UseEffectArg[A]): HookResult[Unit] = + HookResult(UseEffect.unsafeCreateInsertionOnMount(effect)) + + /** The signature is identical to [[useEffect]], but it fires synchronously after all DOM mutations, but before any + * layout Effects fire. Use this to insert styles before any Effects fire that may need to read layout. Updates + * scheduled inside useLayoutEffect will be flushed synchronously, before the browser has a chance to paint. + * + * Prefer the standard [[useEffect]] when possible to avoid blocking visual updates. + * + * This will only execute the effect when values in the first argument change. + * + * @see https://react.dev/reference/react/useInsertionEffect#useInsertionEffect + */ + @inline final def useInsertionEffectWithDeps[D: Reusability, A](deps: => D)(effect: D => A)( + implicit isEffectArg: UseEffectArg[A] + ): HookResult[Unit] = + ReusableEffect.useInsertionEffect(deps)(effect).toHookResult + + /** + * Lets you defer updating a part of the UI. + * + * @see + * {@link https://react.dev/reference/react/useDeferredValue} + */ + // initialValue was added in React 19 - Replace when we upgrade to React 19 + // @inline final def useDeferredValue[A](value: A, initialValue: js.UndefOr[A] = js.undefined): HookResult[A] = + // UseDeferredValue(value, initialValue).toHookResult + @inline final def useDeferredValue[A](value: A): HookResult[A] = + UseDeferredValue(value).toHookResult +} \ No newline at end of file diff --git a/library/coreGeneric/src/main/scala/japgolly/scalajs/react/internal/CoreGeneral.scala b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/internal/CoreGeneral.scala index 5f0142583..5aa52e26b 100644 --- a/library/coreGeneric/src/main/scala/japgolly/scalajs/react/internal/CoreGeneral.scala +++ b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/internal/CoreGeneral.scala @@ -11,7 +11,8 @@ trait CoreGeneral extends ReactEventTypes with ReactExtensions with DomUtil - with FacadeExports { + with FacadeExports + with hooks.all { import japgolly.scalajs.react.util.DefaultEffects._ @@ -54,6 +55,9 @@ trait CoreGeneral final val Hooks = hooks.Hooks final val HooksApi = hooks.Api + final type HookResult[+A] = hooks.HookResult[A] + final val HookResult = hooks.HookResult + final val ReactEffect = Effect final type ~=>[-A, +B] = Reusable[A => B] diff --git a/library/doc b/library/doc new file mode 120000 index 000000000..6af34c9a8 --- /dev/null +++ b/library/doc @@ -0,0 +1 @@ +../doc \ No newline at end of file diff --git a/library/extra/src/main/scala/japgolly/scalajs/react/extra/StateSnapshot.scala b/library/extra/src/main/scala/japgolly/scalajs/react/extra/StateSnapshot.scala index 8f09f5a5d..6544fc962 100644 --- a/library/extra/src/main/scala/japgolly/scalajs/react/extra/StateSnapshot.scala +++ b/library/extra/src/main/scala/japgolly/scalajs/react/extra/StateSnapshot.scala @@ -3,7 +3,9 @@ package japgolly.scalajs.react.extra.internal import japgolly.scalajs.react.component.{Generic => GenericComponent} import japgolly.scalajs.react.extra.StateSnapshotF import japgolly.scalajs.react.extra.StateSnapshotF.StateSnapshot -import japgolly.scalajs.react.hooks.{Api => HooksApi, CustomHook} +import japgolly.scalajs.react.extra.internal.StateSnapshot.withReuse.useStateSnapshotWithReuse +import japgolly.scalajs.react.hooks.all._ +import japgolly.scalajs.react.hooks.{Api => HooksApi, CustomHook, HookResult} import japgolly.scalajs.react.internal.{Iso, Lens} import japgolly.scalajs.react.util.DefaultEffects.{Async => DA, Sync => DS} import japgolly.scalajs.react.util.Effect.Sync @@ -47,19 +49,19 @@ object StateSnapshot { def apply[S](value: S): FromValue[S] = new FromValue(value) - /** @since 2.0.0 */ - def hook[S](initialValue: => S)(implicit rs: Reusability[S]): CustomHook[Unit, StateSnapshot[S]] = - CustomHook[Unit] - .useState(initialValue) - .useRef(List.empty[DS[Unit]]) - .useEffectBy { (_, _, delayedCallbacks) => + /** @since 3.0.0 */ + def useStateSnapshotWithReuse[S](initialValue: => S)(implicit rs: Reusability[S]): HookResult[StateSnapshot[S]] = + for { + state <- useState(initialValue) + delayedCallbacks <- useRef(List.empty[DS[Unit]]) + _ <- useEffect { val cbs = delayedCallbacks.value if (cbs.isEmpty) DS.empty else DS.chain(DS.runAll(cbs: _*), delayedCallbacks.set(Nil)) } - .buildReturning { (_, state, delayedCallbacks) => + } yield { val setFn: SetFn[S] = (os, cb) => os match { case Some(s) => @@ -74,6 +76,10 @@ object StateSnapshot { new StateSnapshot[S](state.value, state.originalSetState.withValue(setFn), rs) } + /** @since 2.0.0 */ + def hook[S](initialValue: => S)(implicit rs: Reusability[S]): CustomHook[Unit, StateSnapshot[S]] = + CustomHook.fromHookResult(useStateSnapshotWithReuse(initialValue)) + /** This is meant to be called once and reused so that the setState callback stays the same. */ def prepare[S](f: SetFn[S]): FromSetStateFn[S] = new FromSetStateFn(reusableSetFn(f)) @@ -183,9 +189,13 @@ object StateSnapshot { def apply[S](value: S): FromValue[S] = new FromValue(value) + /** @since 3.0.0 */ + def useStateSnapshot[S](initialValue: => S): HookResult[StateSnapshot[S]] = + useStateSnapshotWithReuse(initialValue)(Reusability.never) + /** @since 2.0.0 */ def hook[S](initialValue: => S): CustomHook[Unit, StateSnapshot[S]] = - withReuse.hook(initialValue)(Reusability.never) + CustomHook.fromHookResult(useStateSnapshot(initialValue)) def of[I, S](i: I)(implicit t: StateAccessor.ReadImpureWritePure[I, S]): StateSnapshot[S] = apply(t.state(i)).setStateVia(i) @@ -270,5 +280,11 @@ object StateSnapshot { implicit def hooksExtUseStateSnapshot2[Ctx, CtxFn[_], Step <: HooksApi.SubsequentStep[Ctx, CtxFn]](api: HooksApi.Secondary[Ctx, CtxFn, Step]): Secondary[Ctx, CtxFn, Step] = new Secondary(api) + + @inline def useStateSnapshot[S](initialState: => S): HookResult[StateSnapshot[S]] = + StateSnapshot.useStateSnapshot(initialState) + + @inline def useStateSnapshotWithReuse[S](initialState: => S)(implicit r: Reusability[S]): HookResult[StateSnapshot[S]] = + StateSnapshot.withReuse.useStateSnapshotWithReuse(initialState) } } diff --git a/library/extra/src/main/scala/japgolly/scalajs/react/extra/router/Dsl.scala b/library/extra/src/main/scala/japgolly/scalajs/react/extra/router/Dsl.scala index bc9aa8aed..fcf185d3d 100644 --- a/library/extra/src/main/scala/japgolly/scalajs/react/extra/router/Dsl.scala +++ b/library/extra/src/main/scala/japgolly/scalajs/react/extra/router/Dsl.scala @@ -87,7 +87,7 @@ object StaticDsl { val / = literal("/") } - abstract class RouteCommon[R[X] <: RouteCommon[R, X], A] { + trait RouteCommon[R[X] <: RouteCommon[R, X], A] { def parseThen(f: Option[A] => Option[A]): R[A] @@ -159,7 +159,7 @@ object StaticDsl { // val g = p.matcher("").groupCount // if (g != matchGroups) // sys.error(s"Error in regex: /${p.pattern}/. Expected $matchGroups match groups but detected $g.") - new Route(p, m => parse(i => m.group(i + 1)), a => Path(build(a))) + Route.forPattern(p, m => parse(i => m.group(i + 1)), a => Path(build(a))) } } @@ -192,30 +192,28 @@ object StaticDsl { } /** - * A complete route. + * A `Route` translates a `Path` into an instance of model `A` and vice versa. */ - final class Route[A](pattern: Pattern, - parseFn: Matcher => Option[A], - buildFn: A => Path) extends RouteCommon[Route, A] with RouterMacros.ForRoute[A] { - override def toString = - s"Route($pattern)" - + case class Route[A](parse: Path => Option[A], pathFor: A => Path) extends RouteCommon[Route, A] { override def parseThen(f: Option[A] => Option[A]): Route[A] = - new Route(pattern, f compose parseFn, buildFn) + Route(f compose parse, pathFor) override def pmap[B](b: A => Option[B])(a: B => A): Route[B] = - new Route(pattern, parseFn(_) flatMap b, buildFn compose a) - - def parse(path: Path): Option[A] = { - val m = pattern.matcher(path.value) - if (m.matches) - parseFn(m) - else - None - } + Route(parse(_) flatMap b, pathFor compose a) + } - def pathFor(a: A): Path = - buildFn(a) + object Route { + def forPattern[A](pattern: Pattern, parseFn: Matcher => Option[A], buildFn: A => Path): Route[A] = + new Route( + p => { + val m = pattern.matcher(p.value) + if (m.matches) + parseFn(m) + else + None + }, + buildFn + ) } // =================================================================================================================== diff --git a/library/extra/src/main/scala/japgolly/scalajs/react/extra/router/Router.scala b/library/extra/src/main/scala/japgolly/scalajs/react/extra/router/Router.scala index bba909ee4..f539f8902 100644 --- a/library/extra/src/main/scala/japgolly/scalajs/react/extra/router/Router.scala +++ b/library/extra/src/main/scala/japgolly/scalajs/react/extra/router/Router.scala @@ -1,5 +1,6 @@ package japgolly.scalajs.react.extra.router +import japgolly.scalajs.react.React.startTransition import japgolly.scalajs.react._ import japgolly.scalajs.react.extra._ import japgolly.scalajs.react.util.DefaultEffects @@ -50,7 +51,7 @@ object RouterWithProps { .render ($ => lgc.render($.state, $.props)) .componentDidMount ($ => cfg.postRenderFn(None, $.state.page, $.props)) .componentDidUpdate (i => cfg.postRenderFn(Some(i.prevState.page), i.currentState.page, i.currentProps)) - .configure (ListenableF.listenToUnit(_ => lgc, $ => F.flatMap(lgc.syncToWindowUrl)(s => F.transSync($.setState(s))))) + .configure (ListenableF.listenToUnit(_ => lgc, $ => F.flatMap(lgc.syncToWindowUrl)(s => F.transSync(startTransition($.setState(s)))))) .configure (EL.install_("popstate", lgc.ctl.refresh, dom.window)) .configureWhen(isIE11())(EL.install_("hashchange", lgc.ctl.refresh, dom.window)) } @@ -212,12 +213,12 @@ final class RouterLogicF[F[_], Page, Props](val baseUrl: BaseUrl, cfg: RouterWit val ctlByPath: RouterCtlF[F, Path] = new RouterCtlF[F, Path] { - override protected implicit def F = cfg.effect - override def baseUrl = impbaseurl - override def byPath = this - override val refresh = interpret(BroadcastSync) - override def pathFor(path: Path) = path - override def set(p: Path, v: SetRouteVia) = interpret(setPath(p, v)) + override protected implicit def F: Sync[F] = cfg.effect + override def baseUrl = impbaseurl + override def byPath = this + override val refresh = interpret(BroadcastSync) + override def pathFor(path: Path) = path + override def set(p: Path, v: SetRouteVia) = interpret(setPath(p, v)) } val ctl: RouterCtlF[F, Page] = diff --git a/library/facadeMain/src/main/scala/japgolly/scalajs/react/facade/Act.scala b/library/facadeMain/src/main/scala/japgolly/scalajs/react/facade/Act.scala new file mode 100644 index 000000000..5b21f262a --- /dev/null +++ b/library/facadeMain/src/main/scala/japgolly/scalajs/react/facade/Act.scala @@ -0,0 +1,41 @@ +package japgolly.scalajs.react.facade + +import scalajs.js +import scalajs.js.annotation.JSName + +/** + * @since React 18.3.0 / scalajs-react 3.0.0 + */ +@js.native +trait Act extends js.Object { + /** When writing UI tests, tasks like rendering, user events, or data fetching can be considered as "units" of + * interaction with a user interface. React provides a helper called act() that makes sure all updates related to + * these "units" have been processed and applied to the DOM before you make any assertions: + * + * {{{ + * act(() => { + * // render components + * }); + * // make assertions + * }}} + * + * This helps make your tests run closer to what real users would experience when using your application. + */ + final def act(body: js.Function0[Any]): js.Thenable[Unit] = js.native + +/** When writing UI tests, tasks like rendering, user events, or data fetching can be considered as "units" of + * interaction with a user interface. React provides a helper called act() that makes sure all updates related to + * these "units" have been processed and applied to the DOM before you make any assertions: + * + * {{{ + * await act(async () => { + * // render components + * }); + * // make assertions + * }}} + * + * This helps make your tests run closer to what real users would experience when using your application. + */ + @JSName("act") + final def actAsync[A](body: js.Function0[js.Thenable[A]]): js.Thenable[A] = js.native +} diff --git a/library/facadeMain/src/main/scala/japgolly/scalajs/react/facade/Hooks.scala b/library/facadeMain/src/main/scala/japgolly/scalajs/react/facade/Hooks.scala index d892d3eb1..1d47b5087 100644 --- a/library/facadeMain/src/main/scala/japgolly/scalajs/react/facade/Hooks.scala +++ b/library/facadeMain/src/main/scala/japgolly/scalajs/react/facade/Hooks.scala @@ -1,6 +1,5 @@ package japgolly.scalajs.react.facade -import scala.annotation.nowarn import scala.scalajs.js import scala.scalajs.js.| @@ -9,7 +8,6 @@ import scala.scalajs.js.| * @since React 16.8.0 / scalajs-react 2.0.0 */ @js.native -@nowarn("cat=unused") trait Hooks extends js.Object { final type HookDeps = js.UndefOr[js.Array[_]] | Null @@ -17,6 +15,8 @@ trait Hooks extends js.Object { final type UseStateSetter[S] = js.Function1[S | js.Function1[S, S], Unit] final type UseState[S] = js.Tuple2[S, UseStateSetter[S]] + final type UseTransition = js.Tuple2[Boolean, js.Function1[js.Function0[Unit], Unit]] + final def useState[S](initial: S | js.Function0[S]): UseState[S] = js.native final type UseEffectArg = js.Function0[js.UndefOr[js.Function0[Any]]] @@ -26,6 +26,9 @@ trait Hooks extends js.Object { final def useLayoutEffect(effect: js.Function0[js.UndefOr[js.Function0[Any]]], deps : js.UndefOr[HookDeps] = js.native): Unit = js.native + final def useInsertionEffect(effect: js.Function0[js.UndefOr[js.Function0[Any]]], + deps : js.UndefOr[HookDeps] = js.native): Unit = js.native + final def useContext[A](ctx: React.Context[A]): A = js.native final type UseReducerDispatch[-A] = js.Function1[A, Unit] @@ -46,4 +49,19 @@ trait Hooks extends js.Object { final def useDebugValue(desc: Any): Unit = js.native final def useDebugValue[A](value: A, desc: A => Any): Unit = js.native + + final def useId(): String = js.native + + final def useTransition(): UseTransition = js.native + + final type UseSyncExternalStoreSubscribeArg = js.Function1[js.Function0[Unit], js.Function0[Unit]] + final def useSyncExternalStore[A]( + subscribe: UseSyncExternalStoreSubscribeArg, + getSnapshot: js.Function0[A], + getServerSnapshot: js.UndefOr[js.Function0[A]] = js.undefined + ): A = js.native + + // initialValue was added in React 19 - Replace when we upgrade to React 19 + // final def useDeferredValue[A](value: A, initialValue: js.UndefOr[A] = js.undefined): A = js.native + final def useDeferredValue[A](value: A): A = js.native } diff --git a/library/facadeMain/src/main/scala/japgolly/scalajs/react/facade/Profiler.scala b/library/facadeMain/src/main/scala/japgolly/scalajs/react/facade/Profiler.scala index 74764a497..d709412be 100644 --- a/library/facadeMain/src/main/scala/japgolly/scalajs/react/facade/Profiler.scala +++ b/library/facadeMain/src/main/scala/japgolly/scalajs/react/facade/Profiler.scala @@ -8,11 +8,13 @@ trait Interaction extends js.Object { val __count : Int = js.native val id : Int = js.native val name : String = js.native - val timestamp: Double = js.native + val timestamp: Double = js.native } object Profiler { + // TODO: the interactions field has been removed from React 18 + type OnRender = js.Function7[ String, // id: the "id" prop of the Profiler tree that has just committed String, // phase: either "mount" (if the tree just mounted) or "update" (if it re-rendered) diff --git a/library/facadeMain/src/main/scala/japgolly/scalajs/react/facade/React.scala b/library/facadeMain/src/main/scala/japgolly/scalajs/react/facade/React.scala index 212986a9d..c72e356de 100644 --- a/library/facadeMain/src/main/scala/japgolly/scalajs/react/facade/React.scala +++ b/library/facadeMain/src/main/scala/japgolly/scalajs/react/facade/React.scala @@ -145,7 +145,7 @@ object React extends React { var current: A } - type StatelessFunctionalComponent[Props <: js.Object] = js.Function1[Props, Node] + type StatelessFunctionalComponent[Props <: js.Object] = js.Function1[Props, Node] with HasMutableDisplayName trait ValueProps[A] extends js.Object { val value: A @@ -180,8 +180,7 @@ object React extends React { } @js.native -@nowarn("cat=unused") -trait React extends Hooks { +trait React extends Hooks with Act { import React._ final def createContext[A](defaultValue: A): React.Context[A] = js.native @@ -220,6 +219,8 @@ trait React extends Hooks { /** @since 16.6.0 */ final def memo[P <: js.Object, A](f: js.Function1[P, A], areEqual: js.Function2[P, P, Boolean] = js.native): js.Object = js.native + final def startTransition(callback: js.Function0[Unit]): Unit = js.native + final val version: String = js.native /** React.Children provides utilities for dealing with the this.props.children opaque data structure. */ diff --git a/library/facadeMain/src/main/scala/japgolly/scalajs/react/facade/ReactDOM.scala b/library/facadeMain/src/main/scala/japgolly/scalajs/react/facade/ReactDOM.scala index 613674bfe..9fa879c64 100644 --- a/library/facadeMain/src/main/scala/japgolly/scalajs/react/facade/ReactDOM.scala +++ b/library/facadeMain/src/main/scala/japgolly/scalajs/react/facade/ReactDOM.scala @@ -14,24 +14,31 @@ object ReactDOM extends ReactDOM @nowarn("cat=unused") trait ReactDOM extends js.Object { + final type Container = dom.Element | dom.Document | dom.DocumentFragment + val version: String = js.native - final type Container = dom.Element | dom.Document + @deprecated("Use createRoot instead", "2.2.0 / React v18") + final def render(element: React.Node, container: Container): React.ComponentUntyped = js.native final def render(element : React.Node, container: Container, - callback : js.Function0[Any] = js.native): React.ComponentUntyped = js.native + callback : js.Function0[Any]): React.ComponentUntyped = js.native + + @deprecated("Use hydrateRoot instead", "2.2.0 / React v18") + final def hydrate(element: React.Node, container: Container): React.ComponentUntyped = js.native final def hydrate(element : React.Node, container: Container, - callback : js.Function0[Any] = js.native): React.ComponentUntyped = js.native + callback : js.Function0[Any]): React.ComponentUntyped = js.native + @deprecated("Use root.unmount() instead", "2.2.0 / React v18") final def unmountComponentAtNode(container: dom.Node): Boolean = js.native // ========================================================================== // NOTE: Ensure that ComponentDom is kept up-to-date with this type // - final type DomNode = dom.Element | dom.Text + final type DomNode = dom.Node // ========================================================================== @throws[js.JavaScriptException]("if arg isn't a React component or its unmounted") diff --git a/library/facadeMain/src/main/scala/japgolly/scalajs/react/facade/ReactDOMClient.scala b/library/facadeMain/src/main/scala/japgolly/scalajs/react/facade/ReactDOMClient.scala new file mode 100644 index 000000000..4aa79414b --- /dev/null +++ b/library/facadeMain/src/main/scala/japgolly/scalajs/react/facade/ReactDOMClient.scala @@ -0,0 +1,80 @@ +package japgolly.scalajs.react.facade + +import org.scalajs.dom +import scala.annotation.nowarn +import scala.scalajs.js +import scala.scalajs.js.annotation._ +import scala.scalajs.js.| + +@JSImport("react-dom/client", JSImport.Namespace, "ReactDOM") +@js.native +object ReactDOMClient extends ReactDOMClient + +@js.native +@nowarn("cat=unused") +trait ReactDOMClient extends js.Object { + final type HydrationContainer = dom.Element | dom.Document + final type RootContainer = dom.Element | dom.DocumentFragment + + /** Create a React root for the supplied container and return the root. The root can be used to render a React element into the DOM with `.render`. */ + final def createRoot(container: RootContainer, options: CreateRootOptions = js.native): RootType = js.native + + /** Same as createRoot(), but is used to hydrate a container whose HTML contents were rendered by ReactDOMServer. React will attempt to attach event listeners to the existing markup. */ + final def hydrateRoot(container: HydrationContainer, element: React.Node, options: HydrateRootOptions = js.native): RootType = js.native +} + +@js.native +@nowarn("cat=unused") +trait RootType extends js.Object { + def render(element: React.Node): Unit = js.native + def unmount(): Unit = js.native +} + +@js.native +trait CreateRootOptions extends js.Object { + var identifierPrefix : js.UndefOr[String] + var onRecoverableError : js.UndefOr[Any => Unit] + var unstable_concurrentUpdatesByDefault: js.UndefOr[Boolean] + var unstable_strictMode : js.UndefOr[Boolean] + // var transitionCallbacks : js.UndefOr[TransitionTracingCallbacks] +} + +@js.native +trait HydrateRootOptions extends js.Object { + var identifierPrefix : js.UndefOr[String] + var onRecoverableError : js.UndefOr[Any => Unit] + var unstable_concurrentUpdatesByDefault: js.UndefOr[Boolean] + var unstable_strictMode : js.UndefOr[Boolean] + // var hydratedSources : js.UndefOr[Array[MutableSource[any]]] + // var onHydrated : js.UndefOr[Comment => Unit] + // var onDeleted : js.UndefOr[Comment => Unit] +} + +// @js.native +// trait RecoverableError extends js.Object { +// val message: String +// } + +// @js.native +// trait TransitionTracingCallbacks extends js.Object { +// var onMarkerComplete : js.UndefOr[(String, String, Double, Double) => Unit] +// var onMarkerIncomplete : js.UndefOr[(String, String, Double, Array[Deletions]) => Unit] +// var onMarkerProgress : js.UndefOr[(String, String, Double, Double, Array[HasName]) => Unit] +// var onTransitionComplete : js.UndefOr[(String, Double, Double) => Unit] +// var onTransitionIncomplete: js.UndefOr[(String, Double, Array[Deletions]) => Unit] +// var onTransitionProgress : js.UndefOr[(String, Double, Double, Array[HasName]) => Unit] +// var onTransitionStart : js.UndefOr[(String, Double) => Unit] +// } + +// @js.native +// trait HasName extends js.Object { +// val name: String +// } + +// @js.native +// trait Deletions extends js.Object { +// val `type`: String +// val name: js.UndefOr[String] +// val newName: js.UndefOr[String] +// val endTime: Double +// } diff --git a/library/facadeMain/src/main/scala/japgolly/scalajs/react/facade/SecretInternals.scala b/library/facadeMain/src/main/scala/japgolly/scalajs/react/facade/SecretInternals.scala index e1a866f43..d457008f8 100644 --- a/library/facadeMain/src/main/scala/japgolly/scalajs/react/facade/SecretInternals.scala +++ b/library/facadeMain/src/main/scala/japgolly/scalajs/react/facade/SecretInternals.scala @@ -4,9 +4,12 @@ import scala.scalajs.js @js.native trait SecretInternals extends js.Object { + + @deprecated("Removed in React 18", "2.2.0") final val SchedulerTracing: SchedulerTracing = js.native } +@deprecated("Removed in React 18", "2.2.0") @js.native trait SchedulerTracing extends js.Object { diff --git a/library/facadeMain/src/main/scala/japgolly/scalajs/react/facade/package.scala b/library/facadeMain/src/main/scala/japgolly/scalajs/react/facade/package.scala index bd496f299..adde3dfce 100644 --- a/library/facadeMain/src/main/scala/japgolly/scalajs/react/facade/package.scala +++ b/library/facadeMain/src/main/scala/japgolly/scalajs/react/facade/package.scala @@ -27,4 +27,9 @@ package object facade { val displayName: js.UndefOr[String] = js.native } + @js.native + trait HasMutableDisplayName extends HasDisplayName { + @js.annotation.JSName("displayName") + var setDisplayName: js.UndefOr[String] = js.native + } } diff --git a/library/facadeTest/src/main/scala/japgolly/scalajs/react/test/facade/ReactTestUtils.scala b/library/facadeTest/src/main/scala/japgolly/scalajs/react/test/facade/ReactTestUtils.scala index c37112e50..63f54cc57 100644 --- a/library/facadeTest/src/main/scala/japgolly/scalajs/react/test/facade/ReactTestUtils.scala +++ b/library/facadeTest/src/main/scala/japgolly/scalajs/react/test/facade/ReactTestUtils.scala @@ -12,7 +12,6 @@ import scala.scalajs.js.| object ReactTestUtils extends ReactTestUtils @js.native -@nowarn("cat=unused") trait ReactTestUtils extends js.Object { final val Simulate: Simulate = js.native @@ -30,6 +29,7 @@ trait ReactTestUtils extends js.Object { * * This helps make your tests run closer to what real users would experience when using your application. */ + @deprecated("Use React.act", "3.0.0") final def act(body: js.Function0[Any]): js.Thenable[Unit] = js.native /** When writing UI tests, tasks like rendering, user events, or data fetching can be considered as "units" of @@ -46,6 +46,7 @@ trait ReactTestUtils extends js.Object { * This helps make your tests run closer to what real users would experience when using your application. */ @JSName("act") + @deprecated("Use React.actAsync", "3.0.0") final def actAsync(body: js.Function0[js.Thenable[Any]]): js.Thenable[Unit] = js.native /** Render a component into a detached DOM node in the document. This function requires a DOM. */ diff --git a/library/ghpages/html/dev2.html b/library/ghpages/html/dev2.html index 31ab646df..1bd6eaf65 100644 --- a/library/ghpages/html/dev2.html +++ b/library/ghpages/html/dev2.html @@ -3,8 +3,8 @@ - - + + diff --git a/library/ghpages/html/dev3.html b/library/ghpages/html/dev3.html index 719ecfd2e..02c6b6496 100644 --- a/library/ghpages/html/dev3.html +++ b/library/ghpages/html/dev3.html @@ -3,8 +3,8 @@ - - + + diff --git a/library/ghpages/html/prod.html b/library/ghpages/html/prod.html index e1605a900..7e24c78ed 100644 --- a/library/ghpages/html/prod.html +++ b/library/ghpages/html/prod.html @@ -3,8 +3,8 @@ - - + + diff --git a/library/ghpages/src/main/scala/ghpages/GhPages.scala b/library/ghpages/src/main/scala/ghpages/GhPages.scala index 3ec113fe8..c127acbca 100644 --- a/library/ghpages/src/main/scala/ghpages/GhPages.scala +++ b/library/ghpages/src/main/scala/ghpages/GhPages.scala @@ -66,8 +66,9 @@ object GhPages { def main(args: Array[String]): Unit = { val container = dom.document.getElementById("root") + val root = ReactDOMClient.createRoot(container) dom.console.info("Router logging is enabled. Enjoy!") val router = Router(baseUrl, routerConfig.logToConsole) - router() renderIntoDOM container + root.render(router()) } } diff --git a/library/ghpages/src/main/scala/ghpages/examples/HooksExample.scala b/library/ghpages/src/main/scala/ghpages/examples/HooksExample.scala index 94f0aaf51..a3e222580 100644 --- a/library/ghpages/src/main/scala/ghpages/examples/HooksExample.scala +++ b/library/ghpages/src/main/scala/ghpages/examples/HooksExample.scala @@ -31,7 +31,6 @@ object HooksExample { |} |""".stripMargin - val source = GhPagesMacros.exampleSource // EXAMPLE:START @@ -57,11 +56,32 @@ object HooksExample { <.button( ^.onClick --> count.modState(_ + 1), "Click me" - ), + ) ) ) } + // ALTERNATIVELY + + object Example2 { + val Component = ScalaFnComponent[Unit] { _ => + for { + count <- useState(0) + _ <- useEffect(Callback { + // Update the document title using the browser API + document.title = s"You clicked ${count.value} times" + }) + } yield + <.div( + <.p(s"You clicked ${count.value} times"), + <.button( + ^.onClick --> count.modState(_ + 1), + "Click me" + ) + ) + } + } + // EXAMPLE:END def content = SideBySide.Content(jsSource, source, main()) diff --git a/library/package.json b/library/package.json new file mode 100644 index 000000000..95182470d --- /dev/null +++ b/library/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "jsdom": "^25.0.1" + } +} diff --git a/library/project/Build.scala b/library/project/Build.scala index 2647b2121..6e6b46dee 100644 --- a/library/project/Build.scala +++ b/library/project/Build.scala @@ -227,10 +227,10 @@ object ScalaJsReact { ), jsDependencies ++= Seq( Dep.sizzleJs(Test).value, + (ProvidedJS / "polyfill.js") % Test, (ProvidedJS / "component-es6.js" dependsOn Dep.reactDom.dev) % Test, (ProvidedJS / "component-fn.js" dependsOn Dep.reactDom.dev) % Test, (ProvidedJS / "forward-ref.js" dependsOn Dep.reactDom.dev) % Test, - (ProvidedJS / "polyfill.js" dependsOn Dep.reactDom.dev) % Test, ), ) @@ -239,6 +239,7 @@ object ScalaJsReact { .configure(commonSettings, publicationSettings, hasNoTests, effectGenericModule) .settings( moduleName := "test", + libraryDependencies += Dep.microlibsTestUtil.value, ) lazy val testUtilMacros = project diff --git a/library/project/Dependencies.scala b/library/project/Dependencies.scala index 46ab36677..4b6f98476 100644 --- a/library/project/Dependencies.scala +++ b/library/project/Dependencies.scala @@ -9,24 +9,25 @@ object Dependencies { object Ver { // Externally observable - val cats = "2.7.0" - val catsEffect = "3.3.11" + val cats = "2.12.0" + val catsEffect = "3.5.4" val microlibs = "4.1.0" val monocle2 = "2.1.0" - val monocle3 = "3.1.0" - val scala2 = "2.13.8" - val scala3 = "3.1.2" - val scalaJsDom = "2.0.0" - val sourcecode = "0.2.8" + val monocle3 = "3.2.0" + val scala2 = "2.13.15" + val scala3 = "3.3.0" + val scalaJsDom = "2.8.0" + val sourcecode = "0.4.2" // Internal val betterMonadicFor = "0.3.1" val catsTestkitScalaTest = "2.1.5" val disciplineScalaTest = "2.1.5" - val kindProjector = "0.13.2" - val macrotaskExecutor = "1.0.0" + val fastTextEncoding = "1.0.6" + val kindProjector = "0.13.3" + val macrotaskExecutor = "1.1.1" val nyaya = "1.0.0" - val reactJs = "17.0.2" + val reactJs = "18.3.1" val scalaJsJavaTime = "1.0.0" val scalaJsSecureRandom = "1.0.0" val scalaTest = "3.2.11" @@ -76,25 +77,34 @@ object Dependencies { val reactDoutestUtils = ReactArtifact("react-dom-test-utils") } - final case class ReactArtifact(filename: String) { - val dev = s"umd/$filename.development.js" - val prod = s"umd/$filename.production.min.js" - } + def fastTextEncodingJs = "text.min.js" // 1.0.6 webjar only contains minified version def globalDependencyOverrides = Def.setting(Seq( Dep.scalaJsDom.value, Dep.univEq.value, Dep.univEqCats.value, + "org.webjars.npm" % "scheduler" % "0.22.0", // Required for React 18.3.1 )) + final case class ReactArtifact(filename: String) { + val dev = s"umd/$filename.development.js" + val prod = s"umd/$filename.production.min.js" + } + def addReactJsDependencies(scope: Configuration): Project => Project = _.enablePlugins(JSDependenciesPlugin) .settings( jsDependencies ++= Seq( + /** For testing React 18 */ + "org.webjars.npm" % "fast-text-encoding" % Ver.fastTextEncoding % scope + / fastTextEncodingJs + minified "text.min.js", + "org.webjars.npm" % "react" % Ver.reactJs % scope - / "umd/react.development.js" - minified "umd/react.production.min.js" + / "umd/react.development.js" + minified "umd/react.production.min.js" + dependsOn fastTextEncodingJs commonJSName "React", "org.webjars.npm" % "react-dom" % Ver.reactJs % scope @@ -110,8 +120,8 @@ object Dependencies { commonJSName "ReactTestUtils", "org.webjars.npm" % "react-dom" % Ver.reactJs % scope - / "umd/react-dom-server.browser.development.js" - minified "umd/react-dom-server.browser.production.min.js" + / "umd/react-dom-server-legacy.browser.development.js" + minified "umd/react-dom-server-legacy.browser.production.min.js" dependsOn "umd/react-dom.development.js" commonJSName "ReactDOMServer"), diff --git a/library/project/GenHooks.scala b/library/project/GenHooks.scala index c9194789d..1f8029cf2 100644 --- a/library/project/GenHooks.scala +++ b/library/project/GenHooks.scala @@ -58,6 +58,13 @@ object GenHooks { | z => ($as) => Z.dispatch(z($as)))( | z => Reusable.byRef(z).withValue(($as) => Z.delay(z($as)))) |""".stripMargin + + useCallbackArgs += + s""" implicit def ci$n[$As, Y, Z[_]](implicit Z: UnsafeSync[Z]): UseCallbackArg[($As) => Z[Y]] = + | UseCallbackArg[($As) => Z[Y], js.Function$n[$As, Y]]( + | z => ($as) => Z.runSync(z($as)))( + | z => Reusable.byRef(z).withValue(($as) => Z.delay(z($as)))) + |""".stripMargin if (n <= 21) { hookCtxCtorsI += s" def apply[I, $Hns](input: I, $hookParams): I$n[I, $Hns] =\n new I$n(input, $hookArgs)" @@ -116,7 +123,7 @@ object GenHooks { | new Custom.SubsequentStep[I, HookCtx.I$s[I, $preHns], ${hookCtxFnI(s)}] { | override type Next[H$n] = Custom.Subsequent.AtStep$s[I, $preHns]#Next[H$n] | override def next[H$n] = - | (buildPrev, initNextHook) => { + | (buildPrev, initNextHook, displayName) => { | val buildNext: Custom.BuildFn[I, HookCtx.I$n[I, $Hns]] = | new Custom.BuildFn[I, HookCtx.I$n[I, $Hns]] { | override def apply[O](f: HookCtx.I$n[I, $Hns] => O) = { @@ -127,7 +134,7 @@ object GenHooks { | } | } | } - | new Custom.Subsequent[I, HookCtx.I$n[I, $Hns], ${hookCtxFnI(n)}](buildNext) + | new Custom.Subsequent[I, HookCtx.I$n[I, $Hns], ${hookCtxFnI(n)}](displayName)(buildNext) | } | override def squash[A] = f => _.apply$s(f) | } @@ -145,14 +152,14 @@ object GenHooks { | new ComponentP.SubsequentStep[P, HookCtx.P$s[P, $preHns], ${hookCtxFnP(s)}] { | override type Next[H$n] = ComponentP.Subsequent.AtStep$s[P, $preHns]#Next[H$n] | override def next[H$n] = - | (renderPrev, initNextHook) => { + | (renderPrev, initNextHook, displayName) => { | val renderNext: ComponentP.RenderFn[P, HookCtx.P$n[P, $Hns]] = | render => renderPrev { ctx$s => | val h$n = initNextHook(ctx$s) | val ctx$n = HookCtx(ctx$s.props, $preCtxArgs, h$n) | render(ctx$n) | } - | new ComponentP.Subsequent[P, HookCtx.P$n[P, $Hns], ${hookCtxFnP(n)}](renderNext) + | new ComponentP.Subsequent[P, HookCtx.P$n[P, $Hns], ${hookCtxFnP(n)}](displayName)(renderNext) | } | override def squash[A] = f => _.apply$s(f) | } @@ -171,14 +178,14 @@ object GenHooks { | new ComponentPC.SubsequentStep[P, HookCtx.PC$s[P, $preHns], ${hookCtxFnPC(s)}] { | override type Next[H$n] = ComponentPC.Subsequent.AtStep$s[P, $preHns]#Next[H$n] | override def next[H$n] = - | (renderPrev, initNextHook) => { + | (renderPrev, initNextHook, displayName) => { | val renderNext: ComponentPC.RenderFn[P, HookCtx.PC$n[P, $Hns]] = | render => renderPrev { ctx$s => | val h$n = initNextHook(ctx$s) | val ctx$n = HookCtx.withChildren(ctx$s.props, ctx$s.propsChildren, $preCtxArgs, h$n) | render(ctx$n) | } - | new ComponentPC.Subsequent[P, HookCtx.PC$n[P, $Hns], ${hookCtxFnPC(n)}](renderNext) + | new ComponentPC.Subsequent[P, HookCtx.PC$n[P, $Hns], ${hookCtxFnPC(n)}](displayName)(renderNext) | } | override def squash[A] = f => _.apply$s(f) | } diff --git a/library/project/build.properties b/library/project/build.properties index 4ff6415f2..8cf07b7c2 100644 --- a/library/project/build.properties +++ b/library/project/build.properties @@ -1 +1 @@ -sbt.version=1.6.2 \ No newline at end of file +sbt.version=1.9.8 \ No newline at end of file diff --git a/library/project/plugins.sbt b/library/project/plugins.sbt index 1efb3bd84..639a7a5c2 100644 --- a/library/project/plugins.sbt +++ b/library/project/plugins.sbt @@ -2,7 +2,7 @@ libraryDependencies ++= Seq( "org.scala-js" %% "scalajs-env-jsdom-nodejs" % "1.1.0", "org.scala-js" %% "scalajs-env-selenium" % "1.1.1") -addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.10.1") -addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.5.10") +addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.12.1") +addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.9.0") addSbtPlugin("org.scala-js" % "sbt-jsdependencies" % "1.0.2") -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.10.0") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.16.0") diff --git a/library/scalafix.sbt b/library/scalafix.sbt index a1100c70c..14bc20874 100644 --- a/library/scalafix.sbt +++ b/library/scalafix.sbt @@ -8,10 +8,4 @@ ThisBuild / scalacOptions ++= { ThisBuild / semanticdbEnabled := true // NOTE: Upgrade downstream-tests/scalafix.sbt too! -ThisBuild / semanticdbVersion := "4.5.9" - -ThisBuild / scalafixScalaBinaryVersion := "2.13" - -ThisBuild / scalafixDependencies ++= Seq( - "com.github.liancheng" %% "organize-imports" % "0.6.0" -) +ThisBuild / semanticdbVersion := "4.12.0" diff --git a/library/testUtil/src/main/scala/japgolly/scalajs/react/test/ReactTestUtils.scala b/library/testUtil/src/main/scala/japgolly/scalajs/react/test/ReactTestUtils.scala index 1e816219f..b43d45986 100644 --- a/library/testUtil/src/main/scala/japgolly/scalajs/react/test/ReactTestUtils.scala +++ b/library/testUtil/src/main/scala/japgolly/scalajs/react/test/ReactTestUtils.scala @@ -15,6 +15,7 @@ import scala.concurrent.{ExecutionContext, Future} import scala.scalajs.js import scala.scalajs.js.| +@deprecated("Migrate to React 18 and ReactTestUtils2. This will be replaced by ReactTestUtils2 in scalajs-react 3.0.0.", "2.2.0 / React 18") object ReactTestUtils extends ReactTestUtils { @inline def raw = japgolly.scalajs.react.test.facade.ReactTestUtils @@ -132,6 +133,7 @@ object ReactTestUtils extends ReactTestUtils { } // Internals } +@deprecated("Migrate to React 18 and ReactTestUtils2. This will be replaced by ReactTestUtils2 in scalajs-react 3.0.0.", "2.2.0 / React 18") trait ReactTestUtils extends japgolly.scalajs.react.test.internal.ReactTestUtilExtensions { import ReactTestUtils._ import ReactTestUtils.Internals._ diff --git a/library/testUtil/src/main/scala/japgolly/scalajs/react/test/ReactTestUtils2.scala b/library/testUtil/src/main/scala/japgolly/scalajs/react/test/ReactTestUtils2.scala new file mode 100644 index 000000000..80f7db4bb --- /dev/null +++ b/library/testUtil/src/main/scala/japgolly/scalajs/react/test/ReactTestUtils2.scala @@ -0,0 +1,140 @@ +package japgolly.scalajs.react.test + +import japgolly.scalajs.react._ +import japgolly.scalajs.react.hooks.Hooks +import japgolly.scalajs.react.internal.CoreGeneral._ +import japgolly.scalajs.react.test.internal.WithDsl +import japgolly.scalajs.react.util.DefaultEffects.{Sync => DS} +import japgolly.scalajs.react.util.Effect._ +import japgolly.scalajs.react.util.{ImplicitUnit, JsUtil} +import org.scalajs.dom.html.Element +import org.scalajs.dom.{console, document} + +object ReactTestUtils2 extends ReactTestUtils2 { + IsReactActEnvironment = true + + private[ReactTestUtils2] object Internals { + val reactDataAttrRegex = """\s+data-react\S*?\s*?=\s*?".*?"""".r + val reactTextCommentRegex = """""".r + + def warnOnError(prefix: String)(a: => Any): Unit = + try { + a + () + } catch { + case t: Throwable => + console.warn(s"$prefix: $t") + } + } // Internals +} + +trait ReactTestUtils2 extends japgolly.scalajs.react.test.internal.ReactTestUtilExtensions { + import ReactTestUtils2.Internals._ + + private val reactRaw = japgolly.scalajs.react.facade.React + + type Unmounted = GenericComponent.Unmounted[_, Unit] + + type CompType = GenericComponent.ComponentRaw {type Raw <: japgolly.scalajs.react.facade.React.ComponentClassUntyped } + + // =================================================================================================================== + + def IsReactActEnvironment(): Boolean = + JsUtil.global().IS_REACT_ACT_ENVIRONMENT.asInstanceOf[Boolean] + + def IsReactActEnvironment_=(b: Boolean): Unit = + JsUtil.global().IS_REACT_ACT_ENVIRONMENT = b + + /** When writing UI tests, tasks like rendering, user events, or data fetching can be considered as "units" of + * interaction with a user interface. React provides a helper called act() that makes sure all updates related to + * these "units" have been processed and applied to the DOM before you make any assertions: + * + * {{{ + * act { + * // render components + * } + * // make assertions + * }}} + * + * This helps make your tests run closer to what real users would experience when using your application. + */ + def act[A](body: => A): A = { + var a = Option.empty[A] + reactRaw.act(() => { a = Some(body) }) + a.getOrElse(throw new RuntimeException("React.act didn't seem to complete.")) + } + + /** When writing UI tests, tasks like rendering, user events, or data fetching can be considered as "units" of + * interaction with a user interface. React provides a helper called act() that makes sure all updates related to + * these "units" have been processed and applied to the DOM before you make any assertions: + * + * {{{ + * await act(async () => { + * // render components + * }); + * // make assertions + * }}} + * + * This helps make your tests run closer to what real users would experience when using your application. + */ + def actAsync[F[_], A](body: F[A])(implicit F: Async[F]): F[A] = { + F.flatMap(F.delay(new Hooks.Var(Option.empty[A]))) { ref => + def setAsync(a: A): F[Unit] = F.delay(DS.runSync(ref.set(Some(a)))) + val body2 = F.flatMap(body)(setAsync) + val body3 = F.fromJsPromise(reactRaw.actAsync(F.toJsPromise(body2))) + F.map(body3)(_ => ref.value.getOrElse(throw new RuntimeException("React.act didn't seem to complete."))) + } + } + + @inline def actAsync[F[_], A](body: => A)(implicit F: Async[F]): F[A] = + actAsync(F.delay(body)) + + def newElement(): Element = { + val cont = document.createElement("div").domAsHtml + document.body.appendChild(cont) + cont + } + + def removeElement(e: Element): Unit = + warnOnError("Failed to remove element: " + e) { + document.body.removeChild(e) + } + + /** Turn `<div data-reactroot="">hello</div>` + * into `<div>hello</div>` + */ + def removeReactInternals(html: String): String = { + var h = html + h = reactTextCommentRegex.replaceAllIn(h, "") + h = reactDataAttrRegex.replaceAllIn(h, "") + h + } + + val withElement: WithDsl[Element, ImplicitUnit] = + WithDsl(newElement())(removeElement) + + val withReactRoot: WithDsl[TestReactRoot, ImplicitUnit] = + withElement.mapResource(TestReactRoot(_))(_.unmount()) + + def withRendered[A](unmounted: A): WithDsl[TestDomWithRoot, Renderable[A]] = + WithDsl.apply[TestDomWithRoot, Renderable[A]] { (renderable, cleanup) => + val root = withReactRoot.setup(implicitly, cleanup) + act(root.render(unmounted)(renderable)) + root.selectFirstChild() + } + + def renderAsync[F[_], A]( + unmounted: A + )(implicit F: Async[F], renderable: Renderable[A]): F[TestDomWithRoot] = + F.flatMap(F.pure(ReactTestUtils2.withReactRoot.setup(implicitly, new WithDsl.Cleanup)))( + root => F.map(actAsync(root.render(unmounted)))(_ => root.selectFirstChild()) + ) + + def withRenderedAsync[F[_], A]( + unmounted: A + )(use: TestDomWithRoot => F[Unit] + )(implicit F: Async[F], renderable: Renderable[A]): F[Unit] = + F.flatMap(renderAsync(unmounted)) { d => + F.finallyRun(use(d), actAsync(d.unmount())) + } +} diff --git a/library/testUtil/src/main/scala/japgolly/scalajs/react/test/Simulate.scala b/library/testUtil/src/main/scala/japgolly/scalajs/react/test/Simulate.scala index 1bd821875..7cdcef922 100644 --- a/library/testUtil/src/main/scala/japgolly/scalajs/react/test/Simulate.scala +++ b/library/testUtil/src/main/scala/japgolly/scalajs/react/test/Simulate.scala @@ -1,12 +1,14 @@ package japgolly.scalajs.react.test +import japgolly.scalajs.react.React +import japgolly.scalajs.react.test.facade import scala.scalajs.js /** https://reactjs.org/docs/test-utils.html#simulate */ object Simulate { import ReactEventType._ - def raw = japgolly.scalajs.react.test.facade.ReactTestUtils.Simulate + val raw = facade.ReactTestUtils.Simulate private def mod(e: js.Object, eventType: ReactEventType): js.Object = js.Object.assign( @@ -21,58 +23,64 @@ object Simulate { js.Dynamic.literal(detail = detail), e) - def auxClick (t: ReactOrDomNode, eventData: js.Object = null): Unit = raw.auxClick (t, mod(eventData, Mouse, 1)) - def beforeInput (t: ReactOrDomNode, eventData: js.Object = null): Unit = raw.beforeInput (t, mod(eventData, Basic)) - def blur (t: ReactOrDomNode, eventData: js.Object = null): Unit = raw.blur (t, mod(eventData, Focus)) - def change (t: ReactOrDomNode, eventData: js.Object = null): Unit = raw.change (t, mod(eventData, Form)) - def click (t: ReactOrDomNode, eventData: js.Object = null): Unit = raw.click (t, mod(eventData, Mouse, 1)) - def compositionEnd (t: ReactOrDomNode, eventData: js.Object = null): Unit = raw.compositionEnd (t, mod(eventData, Composition)) - def compositionStart (t: ReactOrDomNode, eventData: js.Object = null): Unit = raw.compositionStart (t, mod(eventData, Composition)) - def compositionUpdate (t: ReactOrDomNode, eventData: js.Object = null): Unit = raw.compositionUpdate (t, mod(eventData, Composition)) - def contextMenu (t: ReactOrDomNode, eventData: js.Object = null): Unit = raw.contextMenu (t, mod(eventData, Mouse)) - def copy (t: ReactOrDomNode, eventData: js.Object = null): Unit = raw.copy (t, mod(eventData, Clipboard)) - def cut (t: ReactOrDomNode, eventData: js.Object = null): Unit = raw.cut (t, mod(eventData, Clipboard)) - def doubleClick (t: ReactOrDomNode, eventData: js.Object = null): Unit = raw.doubleClick (t, mod(eventData, Mouse, 2)) - def dragEnd (t: ReactOrDomNode, eventData: js.Object = null): Unit = raw.dragEnd (t, mod(eventData, Drag)) - def dragEnter (t: ReactOrDomNode, eventData: js.Object = null): Unit = raw.dragEnter (t, mod(eventData, Drag)) - def dragExit (t: ReactOrDomNode, eventData: js.Object = null): Unit = raw.dragExit (t, mod(eventData, Drag)) - def dragLeave (t: ReactOrDomNode, eventData: js.Object = null): Unit = raw.dragLeave (t, mod(eventData, Drag)) - def dragOver (t: ReactOrDomNode, eventData: js.Object = null): Unit = raw.dragOver (t, mod(eventData, Drag)) - def dragStart (t: ReactOrDomNode, eventData: js.Object = null): Unit = raw.dragStart (t, mod(eventData, Drag)) - def drag (t: ReactOrDomNode, eventData: js.Object = null): Unit = raw.drag (t, mod(eventData, Drag)) - def drop (t: ReactOrDomNode, eventData: js.Object = null): Unit = raw.drop (t, mod(eventData, Drag)) - def error (t: ReactOrDomNode, eventData: js.Object = null): Unit = raw.error (t, mod(eventData, Basic)) - def focus (t: ReactOrDomNode, eventData: js.Object = null): Unit = raw.focus (t, mod(eventData, Focus)) - def gotPointerCapture (t: ReactOrDomNode, eventData: js.Object = null): Unit = raw.gotPointerCapture (t, mod(eventData, Pointer)) - def input (t: ReactOrDomNode, eventData: js.Object = null): Unit = raw.input (t, mod(eventData, Form)) - def keyDown (t: ReactOrDomNode, eventData: js.Object = null): Unit = raw.keyDown (t, mod(eventData, Keyboard)) - def keyPress (t: ReactOrDomNode, eventData: js.Object = null): Unit = raw.keyPress (t, mod(eventData, Keyboard)) - def keyUp (t: ReactOrDomNode, eventData: js.Object = null): Unit = raw.keyUp (t, mod(eventData, Keyboard)) - def load (t: ReactOrDomNode, eventData: js.Object = null): Unit = raw.load (t, mod(eventData, Basic)) - def lostPointerCapture(t: ReactOrDomNode, eventData: js.Object = null): Unit = raw.lostPointerCapture(t, mod(eventData, Pointer)) - def mouseDown (t: ReactOrDomNode, eventData: js.Object = null): Unit = raw.mouseDown (t, mod(eventData, Mouse, 1)) - def mouseEnter (t: ReactOrDomNode, eventData: js.Object = null): Unit = raw.mouseEnter (t, mod(eventData, Mouse)) - def mouseLeave (t: ReactOrDomNode, eventData: js.Object = null): Unit = raw.mouseLeave (t, mod(eventData, Mouse)) - def mouseMove (t: ReactOrDomNode, eventData: js.Object = null): Unit = raw.mouseMove (t, mod(eventData, Mouse)) - def mouseOut (t: ReactOrDomNode, eventData: js.Object = null): Unit = raw.mouseOut (t, mod(eventData, Mouse)) - def mouseOver (t: ReactOrDomNode, eventData: js.Object = null): Unit = raw.mouseOver (t, mod(eventData, Mouse)) - def mouseUp (t: ReactOrDomNode, eventData: js.Object = null): Unit = raw.mouseUp (t, mod(eventData, Mouse, 1)) - def paste (t: ReactOrDomNode, eventData: js.Object = null): Unit = raw.paste (t, mod(eventData, Clipboard)) - def pointerCancel (t: ReactOrDomNode, eventData: js.Object = null): Unit = raw.pointerCancel (t, mod(eventData, Pointer)) - def pointerDown (t: ReactOrDomNode, eventData: js.Object = null): Unit = raw.pointerDown (t, mod(eventData, Pointer)) - def pointerEnter (t: ReactOrDomNode, eventData: js.Object = null): Unit = raw.pointerEnter (t, mod(eventData, Pointer)) - def pointerLeave (t: ReactOrDomNode, eventData: js.Object = null): Unit = raw.pointerLeave (t, mod(eventData, Pointer)) - def pointerMove (t: ReactOrDomNode, eventData: js.Object = null): Unit = raw.pointerMove (t, mod(eventData, Pointer)) - def pointerOut (t: ReactOrDomNode, eventData: js.Object = null): Unit = raw.pointerOut (t, mod(eventData, Pointer)) - def pointerOver (t: ReactOrDomNode, eventData: js.Object = null): Unit = raw.pointerOver (t, mod(eventData, Pointer)) - def pointerUp (t: ReactOrDomNode, eventData: js.Object = null): Unit = raw.pointerUp (t, mod(eventData, Pointer)) - def reset (t: ReactOrDomNode, eventData: js.Object = null): Unit = raw.reset (t, mod(eventData, Form)) - def scroll (t: ReactOrDomNode, eventData: js.Object = null): Unit = raw.scroll (t, mod(eventData, UI)) - def select (t: ReactOrDomNode, eventData: js.Object = null): Unit = raw.select (t, mod(eventData, Basic)) - def submit (t: ReactOrDomNode, eventData: js.Object = null): Unit = raw.submit (t, mod(eventData, Form)) - def touchCancel (t: ReactOrDomNode, eventData: js.Object = null): Unit = raw.touchCancel (t, mod(eventData, Touch)) - def touchEnd (t: ReactOrDomNode, eventData: js.Object = null): Unit = raw.touchEnd (t, mod(eventData, Touch)) - def touchMove (t: ReactOrDomNode, eventData: js.Object = null): Unit = raw.touchMove (t, mod(eventData, Touch)) - def touchStart (t: ReactOrDomNode, eventData: js.Object = null): Unit = raw.touchStart (t, mod(eventData, Touch)) - def wheel (t: ReactOrDomNode, eventData: js.Object = null): Unit = raw.wheel (t, mod(eventData, Wheel)) + private def wrap(f: => Unit): Unit = + if (React.majorVersion >= 18 && ReactTestUtils2.IsReactActEnvironment()) + ReactTestUtils2.act(f) + else + f + + def auxClick (t: ReactOrDomNode, eventData: js.Object = null): Unit = wrap(raw.auxClick (t, mod(eventData, Mouse, 1))) + def beforeInput (t: ReactOrDomNode, eventData: js.Object = null): Unit = wrap(raw.beforeInput (t, mod(eventData, Basic))) + def blur (t: ReactOrDomNode, eventData: js.Object = null): Unit = wrap(raw.blur (t, mod(eventData, Focus))) + def change (t: ReactOrDomNode, eventData: js.Object = null): Unit = wrap(raw.change (t, mod(eventData, Form))) + def click (t: ReactOrDomNode, eventData: js.Object = null): Unit = wrap(raw.click (t, mod(eventData, Mouse, 1))) + def compositionEnd (t: ReactOrDomNode, eventData: js.Object = null): Unit = wrap(raw.compositionEnd (t, mod(eventData, Composition))) + def compositionStart (t: ReactOrDomNode, eventData: js.Object = null): Unit = wrap(raw.compositionStart (t, mod(eventData, Composition))) + def compositionUpdate (t: ReactOrDomNode, eventData: js.Object = null): Unit = wrap(raw.compositionUpdate (t, mod(eventData, Composition))) + def contextMenu (t: ReactOrDomNode, eventData: js.Object = null): Unit = wrap(raw.contextMenu (t, mod(eventData, Mouse))) + def copy (t: ReactOrDomNode, eventData: js.Object = null): Unit = wrap(raw.copy (t, mod(eventData, Clipboard))) + def cut (t: ReactOrDomNode, eventData: js.Object = null): Unit = wrap(raw.cut (t, mod(eventData, Clipboard))) + def doubleClick (t: ReactOrDomNode, eventData: js.Object = null): Unit = wrap(raw.doubleClick (t, mod(eventData, Mouse, 2))) + def dragEnd (t: ReactOrDomNode, eventData: js.Object = null): Unit = wrap(raw.dragEnd (t, mod(eventData, Drag))) + def dragEnter (t: ReactOrDomNode, eventData: js.Object = null): Unit = wrap(raw.dragEnter (t, mod(eventData, Drag))) + def dragExit (t: ReactOrDomNode, eventData: js.Object = null): Unit = wrap(raw.dragExit (t, mod(eventData, Drag))) + def dragLeave (t: ReactOrDomNode, eventData: js.Object = null): Unit = wrap(raw.dragLeave (t, mod(eventData, Drag))) + def dragOver (t: ReactOrDomNode, eventData: js.Object = null): Unit = wrap(raw.dragOver (t, mod(eventData, Drag))) + def dragStart (t: ReactOrDomNode, eventData: js.Object = null): Unit = wrap(raw.dragStart (t, mod(eventData, Drag))) + def drag (t: ReactOrDomNode, eventData: js.Object = null): Unit = wrap(raw.drag (t, mod(eventData, Drag))) + def drop (t: ReactOrDomNode, eventData: js.Object = null): Unit = wrap(raw.drop (t, mod(eventData, Drag))) + def error (t: ReactOrDomNode, eventData: js.Object = null): Unit = wrap(raw.error (t, mod(eventData, Basic))) + def focus (t: ReactOrDomNode, eventData: js.Object = null): Unit = wrap(raw.focus (t, mod(eventData, Focus))) + def gotPointerCapture (t: ReactOrDomNode, eventData: js.Object = null): Unit = wrap(raw.gotPointerCapture (t, mod(eventData, Pointer))) + def input (t: ReactOrDomNode, eventData: js.Object = null): Unit = wrap(raw.input (t, mod(eventData, Form))) + def keyDown (t: ReactOrDomNode, eventData: js.Object = null): Unit = wrap(raw.keyDown (t, mod(eventData, Keyboard))) + def keyPress (t: ReactOrDomNode, eventData: js.Object = null): Unit = wrap(raw.keyPress (t, mod(eventData, Keyboard))) + def keyUp (t: ReactOrDomNode, eventData: js.Object = null): Unit = wrap(raw.keyUp (t, mod(eventData, Keyboard))) + def load (t: ReactOrDomNode, eventData: js.Object = null): Unit = wrap(raw.load (t, mod(eventData, Basic))) + def lostPointerCapture(t: ReactOrDomNode, eventData: js.Object = null): Unit = wrap(raw.lostPointerCapture(t, mod(eventData, Pointer))) + def mouseDown (t: ReactOrDomNode, eventData: js.Object = null): Unit = wrap(raw.mouseDown (t, mod(eventData, Mouse, 1))) + def mouseEnter (t: ReactOrDomNode, eventData: js.Object = null): Unit = wrap(raw.mouseEnter (t, mod(eventData, Mouse))) + def mouseLeave (t: ReactOrDomNode, eventData: js.Object = null): Unit = wrap(raw.mouseLeave (t, mod(eventData, Mouse))) + def mouseMove (t: ReactOrDomNode, eventData: js.Object = null): Unit = wrap(raw.mouseMove (t, mod(eventData, Mouse))) + def mouseOut (t: ReactOrDomNode, eventData: js.Object = null): Unit = wrap(raw.mouseOut (t, mod(eventData, Mouse))) + def mouseOver (t: ReactOrDomNode, eventData: js.Object = null): Unit = wrap(raw.mouseOver (t, mod(eventData, Mouse))) + def mouseUp (t: ReactOrDomNode, eventData: js.Object = null): Unit = wrap(raw.mouseUp (t, mod(eventData, Mouse, 1))) + def paste (t: ReactOrDomNode, eventData: js.Object = null): Unit = wrap(raw.paste (t, mod(eventData, Clipboard))) + def pointerCancel (t: ReactOrDomNode, eventData: js.Object = null): Unit = wrap(raw.pointerCancel (t, mod(eventData, Pointer))) + def pointerDown (t: ReactOrDomNode, eventData: js.Object = null): Unit = wrap(raw.pointerDown (t, mod(eventData, Pointer))) + def pointerEnter (t: ReactOrDomNode, eventData: js.Object = null): Unit = wrap(raw.pointerEnter (t, mod(eventData, Pointer))) + def pointerLeave (t: ReactOrDomNode, eventData: js.Object = null): Unit = wrap(raw.pointerLeave (t, mod(eventData, Pointer))) + def pointerMove (t: ReactOrDomNode, eventData: js.Object = null): Unit = wrap(raw.pointerMove (t, mod(eventData, Pointer))) + def pointerOut (t: ReactOrDomNode, eventData: js.Object = null): Unit = wrap(raw.pointerOut (t, mod(eventData, Pointer))) + def pointerOver (t: ReactOrDomNode, eventData: js.Object = null): Unit = wrap(raw.pointerOver (t, mod(eventData, Pointer))) + def pointerUp (t: ReactOrDomNode, eventData: js.Object = null): Unit = wrap(raw.pointerUp (t, mod(eventData, Pointer))) + def reset (t: ReactOrDomNode, eventData: js.Object = null): Unit = wrap(raw.reset (t, mod(eventData, Form))) + def scroll (t: ReactOrDomNode, eventData: js.Object = null): Unit = wrap(raw.scroll (t, mod(eventData, UI))) + def select (t: ReactOrDomNode, eventData: js.Object = null): Unit = wrap(raw.select (t, mod(eventData, Basic))) + def submit (t: ReactOrDomNode, eventData: js.Object = null): Unit = wrap(raw.submit (t, mod(eventData, Form))) + def touchCancel (t: ReactOrDomNode, eventData: js.Object = null): Unit = wrap(raw.touchCancel (t, mod(eventData, Touch))) + def touchEnd (t: ReactOrDomNode, eventData: js.Object = null): Unit = wrap(raw.touchEnd (t, mod(eventData, Touch))) + def touchMove (t: ReactOrDomNode, eventData: js.Object = null): Unit = wrap(raw.touchMove (t, mod(eventData, Touch))) + def touchStart (t: ReactOrDomNode, eventData: js.Object = null): Unit = wrap(raw.touchStart (t, mod(eventData, Touch))) + def wheel (t: ReactOrDomNode, eventData: js.Object = null): Unit = wrap(raw.wheel (t, mod(eventData, Wheel))) } diff --git a/library/testUtil/src/main/scala/japgolly/scalajs/react/test/TestContainer.scala b/library/testUtil/src/main/scala/japgolly/scalajs/react/test/TestContainer.scala new file mode 100644 index 000000000..067c83130 --- /dev/null +++ b/library/testUtil/src/main/scala/japgolly/scalajs/react/test/TestContainer.scala @@ -0,0 +1,45 @@ +package japgolly.scalajs.react.test + +import japgolly.scalajs.react.{facade => mainFacade} +import org.scalajs.dom + +object TestContainer { + def apply(c: mainFacade.ReactDOM.Container): TestContainer = + new TestContainer { + override type Self = TestDom + override protected def Self(n2: dom.Node) = TestDom(n2) + override def container = c + override def toString = s"TestContainer($c)" + } +} + +// ===================================================================================================================== + +/** Wraps a DOM container and provides utilities for testing its state. + * + * As an example `testContainer.innerHTML.assert("
Welcome
")` + * + * @since 2.2.0 + */ +trait TestContainer extends TestDom { + + def container: mainFacade.ReactDOM.Container + + final def node = + fold(identity, identity, identity) + + def fold[A](onElement : dom.Element => A, + onDocument : dom.Document => A, + onDocumentFragment: dom.DocumentFragment => A): A = + (container: Any) match { + case x: dom.Element => onElement (x) + case x: dom.Document => onDocument (x) + case x: dom.DocumentFragment => onDocumentFragment(x) + } + + def isEmpty(): Boolean = + node.childNodes.length == 0 + + @inline final def nonEmpty(): Boolean = + !isEmpty() +} diff --git a/library/testUtil/src/main/scala/japgolly/scalajs/react/test/TestDom.scala b/library/testUtil/src/main/scala/japgolly/scalajs/react/test/TestDom.scala new file mode 100644 index 000000000..24a5b0a36 --- /dev/null +++ b/library/testUtil/src/main/scala/japgolly/scalajs/react/test/TestDom.scala @@ -0,0 +1,172 @@ +package japgolly.scalajs.react.test + +import japgolly.scalajs.react.test.internal.HtmlAssertionDsl +import japgolly.scalajs.react.util.JsUtil +import org.scalajs.dom +import scala.reflect.ClassTag +import scala.util.Try + +object TestDom { + def apply(n: dom.Node): TestDom = + new TestDom { + override type Self = TestDom + + override protected def Self(n2: dom.Node) = TestDom(n2) + override def node = n + override def toString = s"TestDom($node)" + } +} + +// ===================================================================================================================== + +/** Wraps a DOM and provides utilities for testing its state. + * + * As an example `testDom.outerHTML.assert("
Welcome
")` + * + * @since 2.2.0 + */ +trait TestDom { + + def node: dom.Node + + assert(node.isInstanceOf[dom.Node], "Invalid test DOM. Expected a DOM node but got: " + node) + + // ------------------------------------------------------------------------------------------------------------------- + // Returning `TestDom` + + type Self <: TestDom + + protected def Self(n: dom.Node): Self + + def select(f: dom.Node => dom.Node): Self = + Self(f(node)) + + def select(selectors: String): Self = { + val all = querySelectorAll(selectors) + all.length match { + case 1 => Self(all.head) + case 0 => throw new RuntimeException(s"No child of $node found matching '$selectors'") + case n => throw new RuntimeException(s"Found $n children of $node found matching '$selectors', expected 1. Use .selectFirst() instead to select the first matching result.") + } + } + + def selectFirst(selectors: String): Self = + querySelectorOption(selectors) match { + case Some(e) => Self(e) + case None => throw new RuntimeException(s"No child of $node found matching '$selectors'") + } + + def selectFirstChild(): Self = + Self(firstChild()) + + // ------------------------------------------------------------------------------------------------------------------- + // Returning DOM + + /** Cast the DOM as `A` or throw an exception. */ + def as[A](implicit ct: ClassTag[A]): A = + (node: Any) match { + case a: A => + a + case _ => + val cls = ct.runtimeClass + val name = Try(cls.getSimpleName()).getOrElse(cls.getName()) + throw new RuntimeException(s"Expected DOM to be a $name, got: $node") + } + + def asButton(): dom.HTMLButtonElement = + as[dom.HTMLButtonElement] + + def asDocument(): dom.Document = + as[dom.Document] + + def asDocumentFragment(): dom.DocumentFragment = + as[dom.DocumentFragment] + + def asElement(): dom.Element = + as[dom.Element] + + def asHtml(): dom.HTMLElement = + as[dom.HTMLElement] + + def asInput(): dom.HTMLInputElement = + as[dom.HTMLInputElement] + + def asSelect(): dom.HTMLSelectElement = + as[dom.HTMLSelectElement] + + def asTextArea(): dom.HTMLTextAreaElement = + as[dom.HTMLTextAreaElement] + + def children(): Vector[dom.Node] = + node.childNodes.toVector + + def firstChild(): dom.Node = + node.childNodes(0) + + def querySelector(selectors: String): dom.Element = { + JsUtil.querySelectorFn(node) + .map(_(selectors)) + .getOrElse(throw new RuntimeException(s".querySelector() isn't available on $node")) + } + + def querySelectorOption(selectors: String): Option[dom.Element] = + JsUtil.querySelectorFn(node).toOption.flatMap(f => Option(f(selectors))) + + def querySelectorAll(selectors: String): Vector[dom.Element] = + JsUtil.querySelectorAllFn(node).map(_(selectors).toVector).getOrElse(Vector.empty) + + // /** + // * Traverse all components in tree and accumulate all components where test(component) is true. + // * This is not that useful on its own, but it's used as a primitive for other test utils. + // */ + // def findAllInRenderedTree(tree: Mounted, test: MountedOutput => Boolean): Vector[MountedOutput] = + // raw.findAllInRenderedTree(tree.raw, (m: RawM) => test(wrapMO(m))).iterator.map(wrapMO(_)).toVector + + // /** + // * Finds all instance of components in the rendered tree that are DOM components with the class name + // * matching className. + // */ + // def scryRenderedDOMComponentsWithClass(tree: Mounted, className: String): Vector[MountedOutput] = + // raw.scryRenderedDOMComponentsWithClass(tree.raw, className).iterator.map(wrapMO(_)).toVector + + // /** + // * Like [[scryRenderedDOMComponentsWithClass()]] but expects there to be one result, and returns that one result, or + // * throws exception if there is any other number of matches besides one. + // */ + // def findRenderedDOMComponentWithClass(tree: Mounted, className: String): MountedOutput = + // wrapMO(raw.findRenderedDOMComponentWithClass(tree.raw, className)) + + // /** + // * Finds all instance of components in the rendered tree that are DOM components with the tag name + // * matching tagName. + // */ + // def scryRenderedDOMComponentsWithTag(tree: Mounted, tagName: String): Vector[MountedOutput] = + // raw.scryRenderedDOMComponentsWithTag(tree.raw, tagName).iterator.map(wrapMO(_)).toVector + + // /** + // * Like [[scryRenderedDOMComponentsWithTag()]] but expects there to be one result, and returns that one result, or + // * throws exception if there is any other number of matches besides one. + // */ + // def findRenderedDOMComponentWithTag(tree: Mounted, tagName: String): MountedOutput = + // wrapMO(raw.findRenderedDOMComponentWithTag(tree.raw, tagName)) + + // /** Finds all instances of components with type equal to componentClass. */ + // def scryRenderedComponentsWithType(tree: Mounted, c: CompType): Vector[MountedOutput] = + // raw.scryRenderedComponentsWithType(tree.raw, c.raw).iterator.map(wrapMO(_)).toVector + + // /** + // * Same as [[scryRenderedComponentsWithType()]] but expects there to be one result and returns that one result, or throws + // * exception if there is any other number of matches besides one. + // */ + // def findRenderedComponentWithType(tree: Mounted, c: CompType): MountedOutput = + // wrapMO(raw.findRenderedComponentWithType(tree.raw, c.raw)) + + // ------------------------------------------------------------------------------------------------------------------- + // Testing + + def innerHTML: HtmlAssertionDsl = + HtmlAssertionDsl.node("innerHTML", node, _.innerHTML) + + def outerHTML: HtmlAssertionDsl = + HtmlAssertionDsl.node("outerHTML", node, _.outerHTML) +} diff --git a/library/testUtil/src/main/scala/japgolly/scalajs/react/test/TestDomWithRoot.scala b/library/testUtil/src/main/scala/japgolly/scalajs/react/test/TestDomWithRoot.scala new file mode 100644 index 000000000..9865f1284 --- /dev/null +++ b/library/testUtil/src/main/scala/japgolly/scalajs/react/test/TestDomWithRoot.scala @@ -0,0 +1,24 @@ +package japgolly.scalajs.react.test + +import org.scalajs.dom + +object TestDomWithRoot { + def apply(r: TestReactRoot, n: dom.Node): TestDomWithRoot = + new TestDomWithRoot { + override type Self = TestDomWithRoot + override protected def Self(n2: dom.Node) = TestDomWithRoot(root, n2) + override val root = r + override def node = n + override def toString = s"TestDomWithRoot($node)" + } +} + +// ===================================================================================================================== + +trait TestDomWithRoot extends TestDom { + override type Self <: TestDomWithRoot + val root: TestReactRoot + + @inline def unmount(): Unit = + root.unmount() +} diff --git a/library/testUtil/src/main/scala/japgolly/scalajs/react/test/TestReactRoot.scala b/library/testUtil/src/main/scala/japgolly/scalajs/react/test/TestReactRoot.scala new file mode 100644 index 000000000..a2311797a --- /dev/null +++ b/library/testUtil/src/main/scala/japgolly/scalajs/react/test/TestReactRoot.scala @@ -0,0 +1,46 @@ +package japgolly.scalajs.react.test + +import japgolly.scalajs.react.{facade => mainFacade, _} +import org.scalajs.dom + +object TestReactRoot { + + def apply(container: mainFacade.ReactDOMClient.RootContainer): TestReactRoot = + apply(ReactDOMClient.createRoot(container), container) + + def apply(root: ReactRoot, container: mainFacade.ReactDOM.Container): TestReactRoot = { + @inline def r = root + @inline def c = container + new TestReactRoot { + override type Self = TestDomWithRoot + override protected def Self(n2: dom.Node) = TestDomWithRoot(this, n2) + override def root = r + + override def container = c + override def toString = s"TestReactRoot($root, $container)" + } + } +} + +// ===================================================================================================================== + +/** Wraps a React Root (introduced in React 18) and provides utilities for testing its state. + * + * As an example `testRoot.innerHTML.assert("
Welcome
")` + * + * @since 2.2.0 / React 18 + */ +trait TestReactRoot extends TestContainer { + override type Self <: TestDomWithRoot + + def root: ReactRoot + + @inline def raw = + root.raw + + def render[A](unmounted: A)(implicit r: Renderable[A]): Unit = + ReactTestUtils2.act(root.render(unmounted)) + + def unmount(): Unit = + ReactTestUtils2.act(root.unmount()) +} diff --git a/library/testUtil/src/main/scala/japgolly/scalajs/react/test/internal/HtmlAssertionDsl.scala b/library/testUtil/src/main/scala/japgolly/scalajs/react/test/internal/HtmlAssertionDsl.scala new file mode 100644 index 000000000..019a72ee3 --- /dev/null +++ b/library/testUtil/src/main/scala/japgolly/scalajs/react/test/internal/HtmlAssertionDsl.scala @@ -0,0 +1,146 @@ +package japgolly.scalajs.react.test.internal + +import japgolly.microlibs.testutil.TestUtil +import japgolly.microlibs.testutil.TestUtil._ +import japgolly.scalajs.react.test.ReactTestUtils2 +import java.util.regex.Pattern +import org.scalajs.dom +import sourcecode.Line + +object HtmlAssertionDsl { + + def apply(name: String, html: => String): HtmlAssertionDsl = + apply(name, html, html) + + def apply(name: String, rawHtml: => String, html: => String): HtmlAssertionDsl = + new HtmlAssertionDsl { + override protected def dslName = name + override def raw() = rawHtml + override def apply() = html + } + + def node(name : String, + node : => dom.Node, + onElement: dom.Element => String, + onNode : dom.Node => String = _.nodeValue): HtmlAssertionDsl = { + def read(sanitise: String => String): String = + node match { + case e: dom.Element => sanitise(onElement(e)) + case n => onNode(n) + } + apply( + name = name, + rawHtml = read(identity), + html = read(ReactTestUtils2.removeReactInternals)) + } +} + + +// ===================================================================================================================== + +trait HtmlAssertionDsl extends Function0[String] { + import japgolly.microlibs.testutil.TestUtilInternals._ + + override def toString() = + s"HtmlAssertionDsl:$dslName" + + protected def dslName: String + + /** Return the raw content as a String, which may include internal React attributes. */ + def raw(): String + + /** Return the raw content as a String, with any internal React attributes removed. */ + override def apply(): String + + def assert(expect: String)(implicit l: Line): Unit = + assert(null, expect) + + def assert(name: => String, expect: String)(implicit l: Line): Unit = + assertEqO(Option(name), apply(), expect) + + def assertContains(substr: String)(implicit q: Line): Unit = + TestUtil.assertContains(apply(), substr) + + def assertContainsCI(substr: String)(implicit q: Line): Unit = + TestUtil.assertContainsCI(apply(), substr) + + def assertNotContains(substr: String)(implicit q: Line): Unit = + TestUtil.assertNotContains(apply(), substr) + + def assertNotContainsCI(substr: String)(implicit q: Line): Unit = + TestUtil.assertNotContainsCI(apply(), substr) + + def assertContainsAny(substrs: String*)(implicit q: Line): Unit = + TestUtil.assertContainsAny(apply(), substrs: _*) + + def assertContainsAll(substrs: String*)(implicit q: Line): Unit = + TestUtil.assertContainsAll(apply(), substrs: _*) + + def assertNotContainsAny(substrs: String*)(implicit q: Line): Unit = + TestUtil.assertNotContainsAny(apply(), substrs: _*) + + def assertNotContainsAll(substrs: String*)(implicit q: Line): Unit = + TestUtil.assertNotContainsAll(apply(), substrs: _*) + + def assertContainsAnyCI(substrs: String*)(implicit q: Line): Unit = + TestUtil.assertContainsAnyCI(apply(), substrs: _*) + + def assertContainsAllCI(substrs: String*)(implicit q: Line): Unit = + TestUtil.assertContainsAllCI(apply(), substrs: _*) + + def assertNotContainsAnyCI(substrs: String*)(implicit q: Line): Unit = + TestUtil.assertNotContainsAnyCI(apply(), substrs: _*) + + def assertNotContainsAllCI(substrs: String*)(implicit q: Line): Unit = + TestUtil.assertNotContainsAllCI(apply(), substrs: _*) + + def assertMatches(regex: String)(implicit l: Line): Unit = + assertMatches(null, Pattern.compile(regex)) + + def assertMatches(pattern: Pattern)(implicit l: Line): Unit = + assertMatches(null, pattern) + + def assertMatches(name: => String, regex: String)(implicit l: Line): Unit = + assertMatches(name, Pattern.compile(regex)) + + def assertMatches(name: => String, pattern: Pattern)(implicit l: Line): Unit = + test2(name)("assertMatches", pattern.matcher(_).matches())("regexp", BOLD_BRIGHT_CYAN, pattern.pattern) + + def assertStartsWith(substr: String)(implicit l: Line): Unit = + assertStartsWith(null, substr) + + def assertStartsWith(name: => String, substr: String)(implicit l: Line): Unit = + test2(name)("assertStartsWith", _.startsWith(substr))("substr", BOLD_BRIGHT_CYAN, substr) + + def assertEndsWith(substr: String)(implicit l: Line): Unit = + assertEndsWith(null, substr) + + def assertEndsWith(name: => String, substr: String)(implicit l: Line): Unit = + test2(name)("assertEndsWith", _.endsWith(substr))("substr", BOLD_BRIGHT_CYAN, substr) + + def assertStartsWithCI(substr: String)(implicit l: Line): Unit = + assertStartsWithCI(null, substr) + + def assertStartsWithCI(name: => String, substr: String)(implicit l: Line): Unit = + test2(name)("assertStartsWithCI", ci(_).startsWith(ci(substr)))("substr", BOLD_BRIGHT_CYAN, substr) + + def assertEndsWithCI(substr: String)(implicit l: Line): Unit = + assertEndsWithCI(null, substr) + + def assertEndsWithCI(name: => String, substr: String)(implicit l: Line): Unit = + test2(name)("assertEndsWithCI", ci(_).endsWith(ci(substr)))("substr", BOLD_BRIGHT_CYAN, substr) + + private def ci(s: String) = s.toLowerCase + + private def test2(name: => String) + (methodName: String, test: String => Boolean) + (title1: String, colour1: String, value1: Any) + (implicit l: Line): Unit = { + val actual = apply() + if (!test(actual)) { + val desc = Option(name) + printFail2(desc)(title1, colour1, value1)("actual", BOLD_BRIGHT_RED, actual) + failMethod(s"$dslName.$methodName", desc) + } + } +} diff --git a/library/testUtil/src/main/scala/japgolly/scalajs/react/test/internal/ReactTestUtilExtensions.scala b/library/testUtil/src/main/scala/japgolly/scalajs/react/test/internal/ReactTestUtilExtensions.scala index 6543a8cf3..e3f0d9057 100644 --- a/library/testUtil/src/main/scala/japgolly/scalajs/react/test/internal/ReactTestUtilExtensions.scala +++ b/library/testUtil/src/main/scala/japgolly/scalajs/react/test/internal/ReactTestUtilExtensions.scala @@ -1,17 +1,21 @@ package japgolly.scalajs.react.test.internal import japgolly.scalajs.react.component.Generic -import japgolly.scalajs.react.test.ReactTestUtils +import japgolly.scalajs.react.test.ReactTestUtils2 import japgolly.scalajs.react.util.Effect.{Id, UnsafeSync} +import org.scalajs.dom object ReactTestUtilExtensions { implicit final class ReactTestExt_MountedSimple[F[_], A[_], P, S](private val m: Generic.MountedSimple[F, A, P, S]) extends AnyVal { def outerHtmlScrubbed()(implicit F: UnsafeSync[F]): String = - F.runSync(m.getDOMNode).asMounted().fold(_.textContent, e => ReactTestUtils.removeReactInternals(e.outerHTML)) + F.runSync(m.getDOMNode).asMounted().node match { + case e: dom.Element => ReactTestUtils2.removeReactInternals(e.outerHTML) + case n => n.nodeValue + } def showDom()(implicit F: UnsafeSync[F]): String = - F.runSync(m.getDOMNode).show(ReactTestUtils.removeReactInternals) + F.runSync(m.getDOMNode).show(ReactTestUtils2.removeReactInternals) } } diff --git a/library/testUtil/src/main/scala/japgolly/scalajs/react/test/internal/WithDsl.scala b/library/testUtil/src/main/scala/japgolly/scalajs/react/test/internal/WithDsl.scala new file mode 100644 index 000000000..e0e39af0f --- /dev/null +++ b/library/testUtil/src/main/scala/japgolly/scalajs/react/test/internal/WithDsl.scala @@ -0,0 +1,118 @@ +package japgolly.scalajs.react.test.internal + +import japgolly.scalajs.react.test.ReactTestUtilsConfig.aroundReact +import japgolly.scalajs.react.util.Effect._ +import japgolly.scalajs.react.util.ImplicitUnit +import japgolly.scalajs.react.util.syntax._ +import scala.concurrent.{ExecutionContext, Future} + +object WithDsl { + + def apply[A, I](f: (I, Cleanup) => A): WithDsl[A, I] = + new WithDsl[A, I] { + override val setup = f + } + + def apply[A](create: => A)(destroy: A => Unit): WithDsl[A, ImplicitUnit] = + apply[A, ImplicitUnit] { (_, cleanup) => + val a = create + cleanup.register(destroy(a)) + a + } + + def aroundReactAsync[F[_], A](body: F[A])(implicit F: Async[F]): F[A] = { + val start = F.delay { + val stop = aroundReact.start() + F.delay(stop()) + } + F.flatMap(start) { stop => + F.finallyRun(body, stop) + } + } + + def aroundReactFuture[A](body: => Future[A])(implicit ec: ExecutionContext): Future[A] = { + val stop = aroundReact.start() + val f = body + f.onComplete { _ => stop() } + f + } + + def attemptFuture[A](f: => Future[A]): Future[A] = + try f catch { case err: Exception => Future.failed(err) } + + class Cleanup { + private var fns = () => () + + def register(f: => Unit): Unit = { + val g = fns + fns = () => { f; g() } + } + + def apply(): Unit = + fns() + } +} + +// ===================================================================================================================== + +/** DSL for working with managed resources. + * + * @tparam A The resource type. + * @tparam I Type of an implicit value required on resource use. + * + * @since 2.2.0 + */ +trait WithDsl[A, I] { self => + import WithDsl._ + + val setup: (I, Cleanup) => A + + protected def init(i: I): (A, Cleanup) = { + val cleanup = new Cleanup + val a = setup(i, cleanup) + (a, cleanup) + } + + def apply[B](use: A => B)(implicit i: I): B = { + val (a, cleanup) = init(i) + try + use(a) + finally + cleanup() + } + + def async[G[_], B](use: A => G[B])(implicit i: I, G: Async[G]): G[B] = + aroundReactAsync { + for { + x <- G.delay(init(i)) + b <- G.finallyRun(use(x._1), G.delay(x._2())) + } yield b + } + + def future[B](use: A => Future[B])(implicit i: I, ec: ExecutionContext): Future[B] = + aroundReactFuture { + val (a, cleanup) = init(i) + attemptFuture(use(a)).andThen { case _ => cleanup() } + } + + def map[B](f: A => B): WithDsl[B, I] = + mapFull { (a, _) => f(a) } + + def mapResource[B](f: A => B)(cleanup: B => Unit): WithDsl[B, I] = + mapFull { (a, c) => + val b = f(a) + c.register(cleanup(b)) + b + } + + private def mapFull[B](f: (A, Cleanup) => B): WithDsl[B, I] = + new WithDsl[B, I] { + override val setup = (i, cleanup) => { + val a = self.setup(i, cleanup) + f(a, cleanup) + } + } + + def tap[B](f: A => B): WithDsl[A, I] = + mapFull { (a, _) => f(a); a } +} diff --git a/library/tests/src/test/scala/japgolly/scalajs/react/MiscTest.scala b/library/tests/src/test/scala/japgolly/scalajs/react/MiscTest.scala index fbcbee5ad..eae3c6cea 100644 --- a/library/tests/src/test/scala/japgolly/scalajs/react/MiscTest.scala +++ b/library/tests/src/test/scala/japgolly/scalajs/react/MiscTest.scala @@ -8,7 +8,6 @@ import japgolly.scalajs.react.util.JsUtil import japgolly.scalajs.react.vdom.html_<^._ import java.time.Duration import monocle._ -import org.scalajs.dom.html import scala.scalajs.js import scala.util.Try import utest._ @@ -70,11 +69,11 @@ object MiscTest extends TestSuite { <.option(^.value := "c")("c")) ).build - val c = ReactTestUtils.renderIntoDocument(s()) - val sel = c.getDOMNode.asMounted().domCast[html.Select] - val options = sel.options.asInstanceOf[js.Array[html.Option]] // https://github.com/scala-js/scala-js-dom/pull/107 - val selectedOptions = options filter (_.selected) map (_.value) - assert(selectedOptions.toSet == Set("a", "c")) + ReactTestUtils2.withRendered(s()) { d => + val sel = d.asSelect() + val selectedOptions =sel.options.filter(_.selected).map(_.value) + assertSet(selectedOptions.toSet, Set("a", "c")) + } } "renderScopeZoomState" - { @@ -93,13 +92,14 @@ object MiscTest extends TestSuite { .render { $ => val add7 = $.modState(_ + 7) val add1 = $.modState(_ + 1) - <.button(^.onClick --> (add1 >> add7)) + <.button(<.span($.state), ^.onClick --> (add1 >> add7)) } .build - val c = ReactTestUtils.renderIntoDocument(C()) - assertEq(c.state, 3) - Simulation.click run c - assertEq(c.state, 11) + ReactTestUtils2.withRendered(C()) { d => + d.select("span").innerHTML.assert("3") + Simulation.click run d.node + d.select("span").innerHTML.assert("11") + } } "zoomState" - { @@ -197,10 +197,8 @@ object MiscTest extends TestSuite { <.div("i = ", i) ) ).build - React.Profiler.unstable_trace("poop") { - ReactTestUtils.withRenderedIntoDocument(comp(234)) { mounted => - assertOuterHTML(mounted.getDOMNode.toHtml.get, "
i = 234
") - } + ReactTestUtils2.withRendered(comp(234)) { d => + d.outerHTML.assert("
i = 234
") } assertEq(results.length, 1) val r = results.head @@ -208,9 +206,6 @@ object MiscTest extends TestSuite { assertEq(r.phase, "mount") assertEq(r.phaseIsMount, true) assertEq(r.phaseIsUpdate, false) - assertEq(r.interactions.length, 1) - val i = r.interactions.head - assertEq(i.name, "poop") } "durationFromDOMHighResTimeStamp" - { diff --git a/library/tests/src/test/scala/japgolly/scalajs/react/core/HooksTest.scala b/library/tests/src/test/scala/japgolly/scalajs/react/core/HooksTest.scala index bcf224b22..50e30baea 100644 --- a/library/tests/src/test/scala/japgolly/scalajs/react/core/HooksTest.scala +++ b/library/tests/src/test/scala/japgolly/scalajs/react/core/HooksTest.scala @@ -3,11 +3,14 @@ package japgolly.scalajs.react.core import japgolly.scalajs.react.Hooks.UseEffectArg import japgolly.scalajs.react._ import japgolly.scalajs.react.extra._ +import japgolly.scalajs.react.hooks.HookResult import japgolly.scalajs.react.test.DomTester import japgolly.scalajs.react.test.ReactTestUtils._ import japgolly.scalajs.react.test.TestUtil._ import japgolly.scalajs.react.vdom.html_<^._ import org.scalajs.dom.html.Input +import scala.collection.mutable +import scala.scalajs.js import utest._ object HooksTest extends TestSuite { @@ -214,19 +217,54 @@ object HooksTest extends TestSuite { val hook = addII ++ addI_ ++ add_S val _ : CustomHook[Int, (Int, String)] = hook - val comp = ScalaFnComponent.withHooks[PI] + val comp = ScalaFnComponent.withDisplayName("WithCustomHooks") + .withHooks[PI] .customBy(p => hook(p.pi)) .render((_, h) => h.toString) + assertEq(comp.displayName, "WithCustomHooks") test(comp(PI(3))) { t => t.assertText("(3,ah)") } assertEq(ints.values, Vector(3, 30, 100)) } + private def testCustomMonadicHookComposition(): Unit = { + + locally { + type LI = Long => HookResult[Int] + type IU = Int => HookResult[Unit] + + assertType[(LI, IU)].map(h => h._1.andThen(_.flatMap(h._2))).is[Long => HookResult[Unit]] + } + + val ints = new Recorder[Int] + + val addII = (i: Int) => useEffect(ints.addCB(i)).map(_ => i) + val addI_ = addII.andThen(_.map(_ => ())).contramap[Int](_ * 10) + val add_S = useEffect(ints.addCB(100)).map(_ => "ah") + + def hook(i: Int) = + for { + x <- addII(i) + _ <- addI_(i) + s <- add_S + } yield (x, s) + val _ : Int => HookResult[(Int, String)] = hook(_) + + val comp = + ScalaFnComponent.withDisplayName("WithCustomHooks")[PI](p => hook(p.pi).map(_.toString)) + + assertEq(comp.displayName, "WithCustomHooks") + test(comp(PI(3))) { t => + t.assertText("(3,ah)") + } + assertEq(ints.values, Vector(3, 30, 100)) + } + private def testLazyVal(): Unit = { val counter = new Counter - val comp = ScalaFnComponent.withHooks[PI] + val comp = ScalaFnComponent.withDisplayName("TestComponent").withHooks[PI] .localLazyVal(counter.inc()) .localLazyValBy((p, _) => p.pi + counter.inc()) .localLazyValBy($ => $.props.pi + counter.inc()) @@ -240,6 +278,7 @@ object HooksTest extends TestSuite { <.button(^.onClick --> s.modState(_ + 1))) } + assertEq(comp.displayName, "TestComponent") test(comp(PI(10))) { t => t.assertText("P=PI(10), v1=3, v2=12, v3=11") t.clickButton(); t.assertText("P=PI(10), v1=6, v2=15, v3=14") @@ -281,6 +320,7 @@ object HooksTest extends TestSuite { <.button(^.onClick --> s.modState(_ + 1))) } + assertEq(comp.displayName, "HooksTest.comp (japgolly.scalajs.react.core)") test(comp(PI(10))) { t => t.assertText("P=PI(10), v1=101, v2=112, v3=113") t.clickButton(); t.assertText("P=PI(10), v1=104, v2=115, v3=116") @@ -345,6 +385,36 @@ object HooksTest extends TestSuite { } } + private def testMonadicUseCallback(): Unit = { + val counter = new Counter + val comp = ScalaFnComponent[Unit]( _ => + for { + c1 <- useCallback(counter.incCB(9)) + c2 <- useCallback((i: Int) => counter.incCB(i)) + s <- useState(0) + } yield + <.div( + "S=", counter.value, + ", R1=", ReusableCallbackComponent(c1), + ", R2=", ReusableSetIntComponent(c2), + <.button(^.onClick --> s.modState(_ + 1)) + ) + ) + + val inc9 = 1 + val inc1 = 2 + val inc2 = 3 + val rndr = 4 + + test(comp()) { t => + assertEq(counter.value, 0); t.assertText("S=0, R1=1, R2=1") + t.clickButton(inc1); assertEq(counter.value, 1); t.assertText("S=0, R1=1, R2=1") + t.clickButton(inc9); assertEq(counter.value, 10); t.assertText("S=0, R1=1, R2=1") + t.clickButton(inc2); assertEq(counter.value, 12); t.assertText("S=0, R1=1, R2=1") + t.clickButton(rndr); assertEq(counter.value, 12); t.assertText("S=12, R1=1, R2=1") + } + } + private def testUseCallbackBy(): Unit = { val counter = new Counter val comp = ScalaFnComponent.withHooks[PI] @@ -463,6 +533,52 @@ object HooksTest extends TestSuite { } } + private def testMonadicUseCallbackWithDeps(): Unit = { + val counter = new Counter + val comp = ScalaFnComponent[PI]( p => + for { + c0 <- useCallbackWithDeps(p.pi)( _ => counter.incCB(p.pi)) + d1 <- useState(10) + d2 <- useState(20) + c1 <- useCallbackWithDeps( d1.value)(counter.incCB) + c2 <- useCallbackWithDeps(d2.value)(_ => (i: Int) => counter.incCB(d2.value + i - 1)) + } yield + <.div( + "S=", counter.value, + ", C0=", ReusableCallbackComponent(c0), + ", C1=", ReusableCallbackComponent(c1), + ", C2=", ReusableSetIntComponent(c2), + <.button(^.onClick --> d1.modState(_ + 100)), + <.button(^.onClick --> d2.modState(_ + 100)), + ) + ) + + val c0 = 1 // s += 3 + val c1 = 2 // s += d1 + val c2a = 3 // s += d2 + val c2b = 4 // s += d2+1 + val d1 = 5 // d1 += 100 + val d2 = 6 // d2 += 100 + + test(comp(PI(3))) { t => + assertEq(counter.value, 0); t.assertText("S=0, C0=1, C1=1, C2=1") + t.clickButton(c1); assertEq(counter.value, 10); t.assertText("S=0, C0=1, C1=1, C2=1") + t.clickButton(c0); assertEq(counter.value, 13); t.assertText("S=0, C0=1, C1=1, C2=1") + t.clickButton(c2b); assertEq(counter.value, 34); t.assertText("S=0, C0=1, C1=1, C2=1") + t.clickButton(c2a); assertEq(counter.value, 54); t.assertText("S=0, C0=1, C1=1, C2=1") + t.clickButton(d2); assertEq(counter.value, 54); t.assertText("S=54, C0=1, C1=1, C2=2") // d2=120 + t.clickButton(c2a); assertEq(counter.value, 174); t.assertText("S=54, C0=1, C1=1, C2=2") + t.clickButton(c0); assertEq(counter.value, 177); t.assertText("S=54, C0=1, C1=1, C2=2") + t.clickButton(c1); assertEq(counter.value, 187); t.assertText("S=54, C0=1, C1=1, C2=2") + t.clickButton(d1); assertEq(counter.value, 187); t.assertText("S=187, C0=1, C1=2, C2=2") // d1=110 + t.clickButton(c2a); assertEq(counter.value, 307); t.assertText("S=187, C0=1, C1=2, C2=2") + t.clickButton(c1); assertEq(counter.value, 417); t.assertText("S=187, C0=1, C1=2, C2=2") + t.clickButton(d1); assertEq(counter.value, 417); t.assertText("S=417, C0=1, C1=3, C2=2") // d1=210 + t.clickButton(d1); assertEq(counter.value, 417); t.assertText("S=417, C0=1, C1=4, C2=2") // d1=310 + t.clickButton(c1); assertEq(counter.value, 727); t.assertText("S=417, C0=1, C1=4, C2=2") + } + } + private def testUseContext(): Unit = { val ctx = React.createContext(100) @@ -483,6 +599,24 @@ object HooksTest extends TestSuite { } } + private def testMonadicUseContext(): Unit = { + val ctx = React.createContext(100) + + val compC = ScalaFnComponent[Unit]( _ => useContext(ctx).map(c => c)) + + val comp = ScalaFnComponent[Unit] { _ => + <.div( + compC(), + ":", + ctx.provide(123)(compC()), + ) + } + + test(comp()) { t => + t.assertText("100:123") + } + } + // Can't really observe this but can at least confirm that usage doesn't throw private def testUseDebugValue(): Unit = { val comp = ScalaFnComponent.withHooks[PI] @@ -497,6 +631,22 @@ object HooksTest extends TestSuite { } } + // Can't really observe this but can at least confirm that usage doesn't throw + private def testMonadicUseDebugValue(): Unit = { + val comp = ScalaFnComponent[PI](p => + for { + _ <- useDebugValue("hehe") + _ <- useDebugValue(p.pi) + s <- useState(0) + _ <- useDebugValue(p.pi + s.value) + } yield <.div(p.pi) + ) + + test(comp(PI(3))) { t => + t.assertText("3") + } + } + trait X_UseEffect_Primary[Ctx, Step <: HooksApi.AbstractStep] { def X_useEffect[A](effect: A)(implicit a: UseEffectArg[A], step: Step): step.Self def X_useEffectBy[A](init: Ctx => A)(implicit a: UseEffectArg[A], step: Step): step.Self @@ -512,9 +662,9 @@ object HooksTest extends TestSuite { } private object UseEffect extends UseEffectTests { - override protected implicit def hooksExt1[Ctx, Step <: HooksApi.AbstractStep](api: HooksApi.Primary[Ctx, Step]) = + override protected implicit def hooksExt1[Ctx, Step <: HooksApi.AbstractStep](api: HooksApi.Primary[Ctx, Step]): Primary[Ctx, Step] = new Primary(api) - override protected implicit def hooksExt2[Ctx, CtxFn[_], Step <: HooksApi.SubsequentStep[Ctx, CtxFn]](api: HooksApi.Secondary[Ctx, CtxFn, Step]) = + override protected implicit def hooksExt2[Ctx, CtxFn[_], Step <: HooksApi.SubsequentStep[Ctx, CtxFn]](api: HooksApi.Secondary[Ctx, CtxFn, Step]): Secondary[Ctx, CtxFn, Step] = new Secondary(api) protected class Primary[Ctx, Step <: HooksApi.AbstractStep](api: HooksApi.Primary[Ctx, Step]) extends X_UseEffect_Primary[Ctx, Step] { override def X_useEffect[A](effect: A)(implicit a: UseEffectArg[A], step: Step) = @@ -541,9 +691,9 @@ object HooksTest extends TestSuite { } private object UseLayoutEffect extends UseEffectTests { - override protected implicit def hooksExt1[Ctx, Step <: HooksApi.AbstractStep](api: HooksApi.Primary[Ctx, Step]) = + override protected implicit def hooksExt1[Ctx, Step <: HooksApi.AbstractStep](api: HooksApi.Primary[Ctx, Step]): Primary[Ctx, Step] = new Primary(api) - override protected implicit def hooksExt2[Ctx, CtxFn[_], Step <: HooksApi.SubsequentStep[Ctx, CtxFn]](api: HooksApi.Secondary[Ctx, CtxFn, Step]) = + override protected implicit def hooksExt2[Ctx, CtxFn[_], Step <: HooksApi.SubsequentStep[Ctx, CtxFn]](api: HooksApi.Secondary[Ctx, CtxFn, Step]): Secondary[Ctx, CtxFn, Step] = new Secondary(api) protected class Primary[Ctx, Step <: HooksApi.AbstractStep](api: HooksApi.Primary[Ctx, Step]) extends X_UseEffect_Primary[Ctx, Step] { override def X_useEffect[A](effect: A)(implicit a: UseEffectArg[A], step: Step) = @@ -569,6 +719,35 @@ object HooksTest extends TestSuite { } } + private object UseInsertionEffect extends UseEffectTests { + override protected implicit def hooksExt1[Ctx, Step <: HooksApi.AbstractStep](api: HooksApi.Primary[Ctx, Step]): Primary[Ctx, Step] = + new Primary(api) + override protected implicit def hooksExt2[Ctx, CtxFn[_], Step <: HooksApi.SubsequentStep[Ctx, CtxFn]](api: HooksApi.Secondary[Ctx, CtxFn, Step]): Secondary[Ctx, CtxFn, Step] = + new Secondary(api) + protected class Primary[Ctx, Step <: HooksApi.AbstractStep](api: HooksApi.Primary[Ctx, Step]) extends X_UseEffect_Primary[Ctx, Step] { + override def X_useEffect[A](effect: A)(implicit a: UseEffectArg[A], step: Step) = + api.useInsertionEffect(effect) + override def X_useEffectBy[A](init: Ctx => A)(implicit a: UseEffectArg[A], step: Step) = + api.useInsertionEffectBy(init) + override def X_useEffectOnMount[A](effect: A)(implicit a: UseEffectArg[A], step: Step) = + api.useInsertionEffectOnMount(effect) + override def X_useEffectOnMountBy[A](effect: Ctx => A)(implicit a: UseEffectArg[A], step: Step) = + api.useInsertionEffectOnMountBy(effect) + override def X_useEffectWithDeps[D, A](deps: => D)(effect: D => A)(implicit a: UseEffectArg[A], r: Reusability[D], step: Step) = + api.useInsertionEffectWithDeps(deps)(effect) + override def X_useEffectWithDepsBy[D, A](deps: Ctx => D)(effect: Ctx => D => A)(implicit a: UseEffectArg[A], r: Reusability[D], step: Step) = + api.useInsertionEffectWithDepsBy(deps)(effect) + } + protected class Secondary[Ctx, CtxFn[_], Step <: HooksApi.SubsequentStep[Ctx, CtxFn]](api: HooksApi.Secondary[Ctx, CtxFn, Step]) extends Primary(api) with X_UseEffect_Secondary[Ctx, CtxFn, Step] { + override def X_useEffectBy[A](init: CtxFn[A])(implicit a: UseEffectArg[A], step: Step): step.Self = + api.useInsertionEffectBy(init) + override def X_useEffectOnMountBy[A](effect: CtxFn[A])(implicit a: UseEffectArg[A], step: Step): step.Self = + api.useInsertionEffectOnMountBy(effect) + override def X_useEffectWithDepsBy[D, A](deps: CtxFn[D])(effect: CtxFn[D => A])(implicit a: UseEffectArg[A], r: Reusability[D], step: Step): step.Self = + api.useInsertionEffectWithDepsBy(deps)(effect) + } + } + private abstract class UseEffectTests { protected implicit def hooksExt1[Ctx, Step <: HooksApi.AbstractStep](api: HooksApi.Primary[Ctx, Step]): X_UseEffect_Primary[Ctx, Step] protected implicit def hooksExt2[Ctx, CtxFn[_], Step <: HooksApi.SubsequentStep[Ctx, CtxFn]](api: HooksApi.Secondary[Ctx, CtxFn, Step]): X_UseEffect_Secondary[Ctx, CtxFn, Step] @@ -716,9 +895,135 @@ object HooksTest extends TestSuite { } assertEq(state(), "700)3540:3140") // +1000 +0 +70 } - } // UseEffectTests + private trait UseEffectHub { + protected def X_useEffect[A](effect: A)(implicit a: UseEffectArg[A]): HookResult[Unit] + protected def X_useEffectOnMount[A](effect: A)(implicit a: UseEffectArg[A]): HookResult[Unit] + protected def X_useEffectWithDeps[D, A](deps: => D)(effect: D => A)(implicit a: UseEffectArg[A], r: Reusability[D]): HookResult[Unit] + + def testSingle(): Unit = { + val counter1 = new Counter + val counter2 = new Counter + def state() = s"${counter1.value}:${counter2.value}" + + val comp = ScalaFnComponent[Unit]( _ => + for { + _ <- X_useEffect(counter1.incCB.ret(counter2.incCB)) + _ <- X_useEffect(counter1.incCB(101)) + _ <- X_useEffect(counter1.incCB.ret(counter2.incCB)) + } yield EmptyVdom + ) + + test(comp()) { _ => + assertEq(state(), "103:0") + } + assertEq(state(), "103:2") + } + + + def testConst(): Unit = { + val counter1 = new Counter + val counter2 = new Counter + def state() = s"${counter1.value}:${counter2.value}" + + val comp = ScalaFnComponent[Unit]( _ => + for { + _ <- X_useEffect(counter1.incCB.ret(counter2.incCB)) + _ <- X_useEffect(counter1.incCB(101)) + _ <- X_useEffect(counter1.incCB.ret(counter2.incCB)) + s <- useState(321) + } yield <.button(^.onClick --> s.modState(_ + 1)) + ) + + + test(comp()) { t => + assertEq(state(), "103:0") + t.clickButton(); assertEq(state(), "206:2") + } + assertEq(state(), "206:4") + } + + def testOnMount(): Unit = { + val counter1 = new Counter + val counter2 = new Counter + def state() = s"${counter1.value}:${counter2.value}" + val comp = ScalaFnComponent[Unit](_ => + for { + s <- useState(100) + _ <- X_useEffectOnMount(counter1.incCB(s.value)) + _ <- X_useEffectOnMount(counter1.incCB.ret(counter2.incCB(s.value))) + } yield <.button(^.onClick --> s.modState(_ + 1)) + ) + + test(comp()) { t => + assertEq(state(), "101:0") + t.clickButton(); assertEq(state(), "101:0") + } + assertEq(state(), "101:100") + } + + def testWithDeps(): Unit = { + var _state = -1 + val dep1 = new Counter + val dep2 = new Counter + val dep3 = new Counter + val counter1 = new Counter + val counter2 = new Counter + def state() = s"${_state})${counter1.value}:${counter2.value}" + + val comp = ScalaFnComponent[PI]( p => + for { + _ <- X_useEffectWithDeps(dep1.value)(_ => counter1.incCB(p.pi).ret(counter2.incCB(p.pi))) + s <- useState(100) + _ <- X_useEffectWithDeps(dep2.value)(_ => counter1.incCB(s.value)) + _ <- X_useEffectWithDeps(dep3.value)(_ => counter1.incCB(s.value / 10).ret(counter2.incCB(s.value / 10))) + } yield { + _state = s.value + <.button(^.onClick --> s.modState(_ + 100)) + } + ) + + test(comp(PI(1000))) { t => + assertEq(state(), "100)1110:0") + t.clickButton(); assertEq(state(), "200)1110:0") + dep2.inc(); t.clickButton(); assertEq(state(), "300)1410:0") + dep1.inc(); t.clickButton(); assertEq(state(), "400)2410:1000") + dep1.inc(); t.clickButton(); assertEq(state(), "500)3410:2000") + dep3.inc(); t.clickButton(); assertEq(state(), "600)3470:2010") // s'=100, d'=10, s=600, d=60 + dep3.inc(); t.clickButton(); assertEq(state(), "700)3540:2070") // s'=600, d'=60, s=700, d=70 + } + assertEq(state(), "700)3540:3140") // +1000 +0 +70 + } + } + + private object UseEffectMonadic extends UseEffectHub { + protected def X_useEffect[A](effect: A)(implicit a: UseEffectArg[A]): HookResult[Unit] = + useEffect(effect) + protected def X_useEffectOnMount[A](effect: A)(implicit a: UseEffectArg[A]): HookResult[Unit] = + useEffectOnMount(effect) + protected def X_useEffectWithDeps[D, A](deps: => D)(effect: D => A)(implicit a: UseEffectArg[A], r: Reusability[D]): HookResult[Unit] = + useEffectWithDeps(deps)(effect) + } + + private object UseLayoutEffectMonadic extends UseEffectHub { + protected def X_useEffect[A](effect: A)(implicit a: UseEffectArg[A]): HookResult[Unit] = + useLayoutEffect(effect) + protected def X_useEffectOnMount[A](effect: A)(implicit a: UseEffectArg[A]): HookResult[Unit] = + useLayoutEffectOnMount(effect) + protected def X_useEffectWithDeps[D, A](deps: => D)(effect: D => A)(implicit a: UseEffectArg[A], r: Reusability[D]): HookResult[Unit] = + useLayoutEffectWithDeps(deps)(effect) + } + + private object UseInsertionEffectMonadic extends UseEffectHub { + protected def X_useEffect[A](effect: A)(implicit a: UseEffectArg[A]): HookResult[Unit] = + useInsertionEffect(effect) + protected def X_useEffectOnMount[A](effect: A)(implicit a: UseEffectArg[A]): HookResult[Unit] = + useInsertionEffectOnMount(effect) + protected def X_useEffectWithDeps[D, A](deps: => D)(effect: D => A)(implicit a: UseEffectArg[A], r: Reusability[D]): HookResult[Unit] = + useInsertionEffectWithDeps(deps)(effect) + } + private def testUseForceUpdate(): Unit = { val counter = new Counter val comp = ScalaFnComponent.withHooks[Unit] @@ -737,6 +1042,24 @@ object HooksTest extends TestSuite { } } + private def testMonadicUseForceUpdate(): Unit = { + val counter = new Counter + val comp = ScalaFnComponent[Unit]{ _ => + useForceUpdate.map{ forceUpdate => + val rev = counter.inc() + <.div( + rev, ":", ReusableCallbackComponent(forceUpdate) + ) + } + } + + test(comp()) { t => + t.assertText("1:1") + t.clickButton(); t.assertText("2:2") + t.clickButton(); t.assertText("3:3") + } + } + private def testUseMemo(): Unit = { val dep1 = new Counter val dep2 = new Counter @@ -817,38 +1140,134 @@ object HooksTest extends TestSuite { } } - private def testUseRefManual(): Unit = { - val comp = ScalaFnComponent.withHooks[Unit] - .useRef(100) - .useState(0) - .render { (_, ref, s) => + private def testMonadicUseMemo(): Unit = { + val counter = new Counter + val comp = ScalaFnComponent[PI] { p => + for { + c1 <- useMemo(p.pi)(counter.incCB) + s2 <- useState(1) + c2 <- useMemo(s2.value)(counter.incCB) + s3 <- useState(5) + c3 <- useMemo(p.pi + s3.value)(counter.incCB) + } yield <.div( - ref.value, - <.button(^.onClick --> ref.mod(_ + 1)), - <.button(^.onClick --> s.modState(_ + 1)), + "S2=", s2.value, + ", S3=", s3.value, + ", C1=", ReusableCallbackComponent(c1), + ", C2=", ReusableCallbackComponent(c2), + ", C3=", ReusableCallbackComponent(c3), + <.button(^.onClick --> s2.modState(_ + 1)), + <.button(^.onClick --> s3.modState(_ + 1)), ) - } + } + + val c1 = 1 // + p + val c2 = 2 // + s2 + val c3 = 3 // + p + s3 + val s2 = 4 + val s3 = 5 + + test(comp(PI(10))) { t => + assertEq(counter.value, 0); t.assertText("S2=1, S3=5, C1=1, C2=1, C3=1") + t.clickButton(c2); assertEq(counter.value, 1); t.assertText("S2=1, S3=5, C1=1, C2=1, C3=1") + t.clickButton(c1); assertEq(counter.value, 11); t.assertText("S2=1, S3=5, C1=1, C2=1, C3=1") + t.clickButton(c3); assertEq(counter.value, 26); t.assertText("S2=1, S3=5, C1=1, C2=1, C3=1") + t.clickButton(s2); assertEq(counter.value, 26); t.assertText("S2=2, S3=5, C1=1, C2=2, C3=1") + t.clickButton(c2); assertEq(counter.value, 28); t.assertText("S2=2, S3=5, C1=1, C2=2, C3=1") + t.clickButton(s3); assertEq(counter.value, 28); t.assertText("S2=2, S3=6, C1=1, C2=2, C3=2") + t.clickButton(c3); assertEq(counter.value, 44); t.assertText("S2=2, S3=6, C1=1, C2=2, C3=2") + } + } + + private def testUseId(): Unit = { + val comp = ScalaFnComponent.withHooks[Unit] + .useId + .render((_, id) => <.div(id)) test(comp()) { t => - t.assertText("100") - t.clickButton(1); t.assertText("100") - t.clickButton(2); t.assertText("101") - t.clickButton(1); t.assertText("101") - t.clickButton(2); t.assertText("102") + assertEq(t.getText.length, 4) } } - private def testUseRefManualBy(): Unit = { - val comp = ScalaFnComponent.withHooks[PI] - .useRefBy(_.pi + 1) - .useRefBy($ => $.props.pi + $.hook1.value) - .useState(0) - .render { (_, ref1, ref2, s) => - <.div( - s"${ref1.value}:${ref2.value}", - <.button(^.onClick --> ref1.mod(_ + 1)), - <.button(^.onClick --> ref2.mod(_ + 1)), - <.button(^.onClick --> s.modState(_ + 1)), + private def testMonadicUseId(): Unit = { + val comp = ScalaFnComponent[Unit]{ _ => + useId.map(id => <.div(id)) + } + + test(comp()) { t => + assertEq(t.getText.length, 4) + } + } + + private def testUseTransition(): Unit = { + val comp = ScalaFnComponent.withHooks[Unit] + .useTransition + .useState(false) + .render { (_, transition, state) => + <.button( + ^.onClick --> transition.startTransition(state.modState(!_)), + state.value.toString() + ) + } + + test(comp()) { t => + assertEq(t.getText, "false") + t.clickButton() + assertEq(t.getText, "true") + } + } + + private def testMonadicUseTransition(): Unit = { + val comp = ScalaFnComponent[Unit]{ _ => + for { + transition <- useTransition + state <- useState(false) + } yield + <.button( + ^.onClick --> transition.startTransition(state.modState(!_)), + state.value.toString() + ) + } + + test(comp()) { t => + assertEq(t.getText, "false") + t.clickButton() + assertEq(t.getText, "true") + } + } + + private def testUseRefManual(): Unit = { + val comp = ScalaFnComponent.withHooks[Unit] + .useRef(100) + .useState(0) + .render { (_, ref, s) => + <.div( + ref.value, + <.button(^.onClick --> ref.mod(_ + 1)), + <.button(^.onClick --> s.modState(_ + 1)), + ) + } + + test(comp()) { t => + t.assertText("100") + t.clickButton(1); t.assertText("100") + t.clickButton(2); t.assertText("101") + t.clickButton(1); t.assertText("101") + t.clickButton(2); t.assertText("102") + } + } + + private def testUseRefManualBy(): Unit = { + val comp = ScalaFnComponent.withHooks[PI] + .useRefBy(_.pi + 1) + .useRefBy($ => $.props.pi + $.hook1.value) + .useState(0) + .render { (_, ref1, ref2, s) => + <.div( + s"${ref1.value}:${ref2.value}", + <.button(^.onClick --> ref1.mod(_ + 1)), + <.button(^.onClick --> ref2.mod(_ + 1)), + <.button(^.onClick --> s.modState(_ + 1)), ) } @@ -861,6 +1280,30 @@ object HooksTest extends TestSuite { } } + private def testMonadicUseRefManual(): Unit = { + val comp = ScalaFnComponent[PI]{ p => + for { + ref1 <- useRef(p.pi + 1) + ref2 <- useRef(p.pi + ref1.value) + s <- useState(0) + } yield + <.div( + s"${ref1.value}:${ref2.value}", + <.button(^.onClick --> ref1.mod(_ + 1)), + <.button(^.onClick --> ref2.mod(_ + 1)), + <.button(^.onClick --> s.modState(_ + 1)), + ) + } + + test(comp(PI(4))) { t => + t.assertText("5:9") + t.clickButton(1); t.assertText("5:9") + t.clickButton(3); t.assertText("6:9") + t.clickButton(2); t.assertText("6:9") + t.clickButton(3); t.assertText("6:10") + } + } + private def testUseRefVdom(): Unit = { var text = "uninitialised" val comp = ScalaFnComponent.withHooks[Unit] @@ -897,6 +1340,44 @@ object HooksTest extends TestSuite { } } + private def testMonadicUseRefVdom(): Unit = { + var text = "uninitialised" + val comp = ScalaFnComponent[Unit] { _ => + for { + inputRef <- useRefToVdom[Input] + s <- useState("x") + } yield { + + def onChange(e: ReactEventFromInput): Callback = + s.setState(e.target.value) + + def btn: Callback = + for { + i <- inputRef.get.asCBO + // _ <- Callback.log(s"i.value = [${i.value}]") + } yield { + text = i.value + } + + <.div( + <.input.text.withRef(inputRef)(^.value := s.value, ^.onChange ==> onChange), + <.button(^.onClick --> btn) + ) + } + } + + test(comp()) { t => + t.assertInputText("x") + t.clickButton() + assertEq(text, "x") + + t.setInputText("hehe") + t.assertInputText("hehe") + t.clickButton() + assertEq(text, "hehe") + } + } + private def testUseReducer(): Unit = { def add(n: Int): (Int, Int) => Int = _ + _ + n val comp = ScalaFnComponent.withHooks[PI] @@ -919,6 +1400,30 @@ object HooksTest extends TestSuite { } } + private def testMonadicUseReducer(): Unit = { + def add(n: Int): (Int, Int) => Int = _ + _ + n + val comp = ScalaFnComponent[PI] { p => + for { + s1 <- useReducer(add(0), 100) + s2 <- useReducer(add(s1.value), p.pi + s1.value) + s3 <- useReducer(add(s1.value), p.pi + s1.value + s2.value) + } yield + <.div( + <.div(s"P=$p, s1=${s1.value}, s2=${s2.value}, s3=${s3.value}"), + <.button(^.onClick --> s1.dispatch(1)), + <.button(^.onClick --> s2.dispatch(10)), + <.button(^.onClick --> s3.dispatch(100)), + ) + } + + test(comp(PI(666))) { t => + t.assertText("P=PI(666), s1=100, s2=766, s3=1532") + t.clickButton(1); t.assertText("P=PI(666), s1=101, s2=766, s3=1532") + t.clickButton(2); t.assertText("P=PI(666), s1=101, s2=877, s3=1532") // +101+10 + t.clickButton(3); t.assertText("P=PI(666), s1=101, s2=877, s3=1733") // +101+100 + } + } + private def testUseState(): Unit = { val comp = ScalaFnComponent.withHooks[PI] .useState(100) @@ -1002,6 +1507,91 @@ object HooksTest extends TestSuite { } } + private def testMonadicUseState(): Unit = { + val comp = ScalaFnComponent[PI] { p => + for { + s1 <- useState(100) + s2 <- useState(p.pi + s1.value) + s3 <- useState(p.pi + s1.value + s2.value) + } yield + <.div( + <.div(s"P=$p, s1=${s1.value}, s2=${s2.value}, s3=${s3.value}"), + <.button(^.onClick --> ( + s1.modState(_ + 1) >> s2.modState(-_) >> s3.modState(_ * 10) + ))) + } + + test(comp(PI(666))) { t => + t.assertText("P=PI(666), s1=100, s2=766, s3=1532") + t.clickButton(); t.assertText("P=PI(666), s1=101, s2=-766, s3=15320") + } + } + + private def testMonadicUseStateSetStateReusability(): Unit = { + val comp = ScalaFnComponent[Unit] { _ => + useState(4).map { s => + <.div( + s"S=${s.value}", + ", R1=", ReusableSetIntComponent(s.setState), + ", R2=", ReusableSetIntComponent(s.setState), + ", R3=", ReusableCallbackComponent(s.withReusableInputs.setState(Reusable.implicitly(if (s.value >= 30) -30 else 30))), + ", R4=", ReusableCallbackComponent(s.withReusableInputs.setState(Reusable.implicitly(40))), + ) + } + } + + val r1_1 = 1 + val r1_2 = 2 + val r2_1 = 3 + val r2_2 = 4 + val r3 = 5 + val r4 = 6 + + test(comp()) { t => + t.assertText("S=4, R1=1, R2=1, R3=1, R4=1") + t.clickButton(r2_1); t.assertText("S=1, R1=1, R2=1, R3=1, R4=1") + t.clickButton(r1_2); t.assertText("S=2, R1=1, R2=1, R3=1, R4=1") + t.clickButton(r1_1); t.assertText("S=1, R1=1, R2=1, R3=1, R4=1") + t.clickButton(r2_2); t.assertText("S=2, R1=1, R2=1, R3=1, R4=1") + t.clickButton(r3); t.assertText("S=30, R1=1, R2=1, R3=2, R4=1") + t.clickButton(r4); t.assertText("S=40, R1=1, R2=1, R3=2, R4=1") + t.clickButton(r3); t.assertText("S=-30, R1=1, R2=1, R3=3, R4=1") + t.clickButton(r3); t.assertText("S=30, R1=1, R2=1, R3=4, R4=1") + } + } + + private def testMonadicUseStateModStateReusability(): Unit = { + val comp = ScalaFnComponent[Unit] { _ => + useState(4).map { s => + <.div( + s"S=${s.value}", + ", R1=", ReusableModIntComponent(s.modState), + ", R2=", ReusableModIntComponent(s.modState), + ", R3=", ReusableCallbackComponent(s.withReusableInputs.modState(if (s.value >= 28) incBy5 else incBy1)), + ", R4=", ReusableCallbackComponent(s.withReusableInputs.modState(incBy1)), + ) + } + } + + val r1_1 = 1 + val r1_10 = 2 + val r2_1 = 3 + val r2_10 = 4 + val r3 = 5 + val r4 = 6 + + test(comp()) { t => + t.assertText("S=4, R1=1, R2=1, R3=1, R4=1") + t.clickButton(r2_1); t.assertText("S=5, R1=1, R2=1, R3=1, R4=1") + t.clickButton(r1_10); t.assertText("S=15, R1=1, R2=1, R3=1, R4=1") + t.clickButton(r1_1); t.assertText("S=16, R1=1, R2=1, R3=1, R4=1") + t.clickButton(r2_10); t.assertText("S=26, R1=1, R2=1, R3=1, R4=1") + t.clickButton(r3); t.assertText("S=27, R1=1, R2=1, R3=1, R4=1") + t.clickButton(r4); t.assertText("S=28, R1=1, R2=1, R3=2, R4=1") + t.clickButton(r3); t.assertText("S=33, R1=1, R2=1, R3=2, R4=1") + } + } + private def testUseStateWithReuse(): Unit = { implicit val reusability: Reusability[PI] = Reusability.by[PI, Int](_.pi >> 1) @@ -1090,6 +1680,96 @@ object HooksTest extends TestSuite { } } + private def testMonadicUseStateWithReuse(): Unit = { + implicit val reusability: Reusability[PI] = Reusability.by[PI, Int](_.pi >> 1) + + val comp = ScalaFnComponent[PI] { p => + for { + s1 <- useStateWithReuse(PI(100)) + s2 <- useStateWithReuse(p + s1.value) + s3 <- useStateWithReuse(p + s1.value + s2.value) + } yield + <.div( + <.div(s"P=$p, s1=${s1.value}, s2=${s2.value}, s3=${s3.value}"), + <.button(^.onClick --> ( + s1.modState(_ + 2) >> s2.modState(-_) >> s3.modState(_ * 10) + )), + <.button(^.onClick --> ( + s1.modState(_ + 1) >> s2.modState(-_) + )) + ) + } + + test(comp(PI(666))) { t => + t.assertText("P=PI(666), s1=PI(100), s2=PI(766), s3=PI(1532)") + t.clickButton(1); t.assertText("P=PI(666), s1=PI(102), s2=PI(-766), s3=PI(15320)") + t.clickButton(2); t.assertText("P=PI(666), s1=PI(102), s2=PI(766), s3=PI(15320)") + } + } + + private def testMonadicUseStateWithReuseSetStateReusability(): Unit = { + implicit val reusability: Reusability[PI] = Reusability[PI]((x, y) => (x.pi - y.pi).abs <= 1) + + val comp = ScalaFnComponent[Unit] { _ => + useStateWithReuse(PI(4)).map { s => + <.div( + s"S=${s.value}", + ", R1=", ReusableSetIntComponent(s.setState.map(f => (i: Int) => f(PI(i)).value)), + ", R2=", ReusableSetIntComponent(s.setState.map(f => (i: Int) => f(PI(i)).value)), + ", R3=", ReusableCallbackComponent(s.setState(PI(if (s.value.pi >= 30) -30 else 30))), + ", R4=", ReusableCallbackComponent(s.setState(PI(40))), + ) + } + } + + val r1_1 = 1 + val r1_2 = 2 + val r2_1 = 3 + val r2_2 = 4 + val r3 = 5 + val r4 = 6 + + test(comp()) { t => + t.assertText("S=PI(4), R1=1, R2=1, R3=1, R4=1") + t.clickButton(r2_1); t.assertText("S=PI(1), R1=1, R2=1, R3=1, R4=1") + t.clickButton(r1_2); t.assertText("S=PI(1), R1=1, R2=1, R3=1, R4=1") + t.clickButton(r3); t.assertText("S=PI(30), R1=1, R2=1, R3=2, R4=1") + t.clickButton(r4); t.assertText("S=PI(40), R1=1, R2=1, R3=2, R4=1") + t.clickButton(r2_2); t.assertText("S=PI(2), R1=1, R2=1, R3=3, R4=1") + t.clickButton(r1_1); t.assertText("S=PI(2), R1=1, R2=1, R3=3, R4=1") + t.clickButton(r3); t.assertText("S=PI(30), R1=1, R2=1, R3=4, R4=1") + t.clickButton(r3); t.assertText("S=PI(-30), R1=1, R2=1, R3=5, R4=1") + t.clickButton(r3); t.assertText("S=PI(30), R1=1, R2=1, R3=6, R4=1") + } + } + + private def testMonadicUseStateWithReuseModStateReusability(): Unit = { + implicit val reusability: Reusability[PI] = Reusability.by[PI, Int](_.pi >> 1) + val comp = ScalaFnComponent[Unit] { _ => + useStateWithReuse(PI(4)).map { s => + <.div( + s"S=${s.value}", + ", R0=", ReusableCallbackComponent(s.modState(_ + 1)), + ", R1=", ReusableCallbackComponent(s.modState(_ + 2)), + ", R2=", ReusableCallbackComponent(s.modState(_ + 4)), + ) + } + } + + val inc1 = 1 + val inc2 = 2 + val inc4 = 3 + + test(comp()) { t => + t.assertText("S=PI(4), R0=1, R1=1, R2=1") + t.clickButton(inc2); t.assertText("S=PI(6), R0=1, R1=1, R2=1") + t.clickButton(inc4); t.assertText("S=PI(10), R0=1, R1=1, R2=1") + t.clickButton(inc1); t.assertText("S=PI(10), R0=1, R1=1, R2=1") + t.clickButton(inc4); t.assertText("S=PI(14), R0=1, R1=1, R2=1") + t.clickButton(inc2); t.assertText("S=PI(16), R0=1, R1=1, R2=1") + } + } + private def testUseStateSnapshot(): Unit = { val counter = new Counter var latestS3 = 0 @@ -1126,6 +1806,45 @@ object HooksTest extends TestSuite { } } + private def testMonadicUseStateSnapshot(): Unit = { + val counter = new Counter + var latestS3 = 0 + val comp = ScalaFnComponent[PI]{ p => + for { + s1 <- useStateSnapshot(100) + s2 <- useStateSnapshot(p.pi + s1.value) + s3 <- useStateSnapshot(p.pi + s1.value + s2.value) + } yield { + latestS3 = s3.value + <.div( + <.button(^.onClick --> (s1.modState(_ + 1) >> s2.modState(-_) >> s3.modState(_ * 10))), + <.button(^.onClick --> s3.modState(_ + 1, CallbackTo(latestS3).flatMap(counter.incCB(_)))), + <.button(^.onClick --> s3.setStateOption(None, counter.incCB)), + s"P=$p", + s", S1=${s1.value}:", ReusableSetIntComponent(s1.underlyingSetFn.map(f => (i: Int) => f(Some(i), Callback.empty))), + s", S2=${s2.value}:", ReusableSetIntComponent(s1.underlyingSetFn.map(f => (i: Int) => f(Some(i), Callback.empty))), + s", S3=${s3.value}:", ReusableSetIntComponent(s1.underlyingSetFn.map(f => (i: Int) => f(Some(i), Callback.empty))), + ) + } + } + + + test(comp(PI(666))) { t => + t.assertText("P=PI(666), S1=100:1, S2=766:1, S3=1532:1") + t.clickButton(1); t.assertText("P=PI(666), S1=101:1, S2=-766:1, S3=15320:1") + t.clickButton(2); t.assertText("P=PI(666), S1=101:1, S2=-766:1, S3=15321:1") + assertEq(counter.value, 15321) // verify that the modState(cb) executes after the state update + t.clickButton(3); t.assertText("P=PI(666), S1=101:1, S2=-766:1, S3=15321:1") + assertEq(counter.value, 15322) // verify that the setState(None, cb) executes (and that ↖ the previous modState effect ↖ doesn't execute again) + t.clickButton(3); t.assertText("P=PI(666), S1=101:1, S2=-766:1, S3=15321:1") + assertEq(counter.value, 15323) + t.clickButton(2); t.assertText("P=PI(666), S1=101:1, S2=-766:1, S3=15322:1") + assertEq(counter.value, 15323 + 15322) + t.clickButton(4); t.assertText("P=PI(666), S1=1:1, S2=-766:1, S3=15322:1") + assertEq(counter.value, 15323 + 15322) + } + } + private def testUseStateSnapshotWithReuse(): Unit = { type I = Int {type A=1} implicit val I = Reusability.int.contramap((_: I) >> 1) @@ -1170,6 +1889,52 @@ object HooksTest extends TestSuite { } } + private def testMonadicUseStateSnapshotWithReuse(): Unit = { + type I = Int {type A=1} + implicit val I = Reusability.int.contramap((_: I) >> 1) + val counter = new Counter + var latestS3 = 0 + val comp = ScalaFnComponent[PI] { p => + for { + s1 <- useStateSnapshotWithReuse(100) + s2 <- useStateSnapshotWithReuse(p.pi + s1.value) + s3 <- useStateSnapshotWithReuse(p.pi + s1.value + s2.value) + s4 <- useStateSnapshotWithReuse(330.asInstanceOf[I]) + } yield { + latestS3 = s3.value + <.div( + <.button(^.onClick --> (s1.modState(_ + 1) >> s2.modState(-_) >> s3.modState(_ * 10))), + <.button(^.onClick --> s3.modState(_ + 1, CallbackTo(latestS3).flatMap(counter.incCB(_)))), + <.button(^.onClick --> s3.setStateOption(None, counter.incCB)), + s"P=$p", + ", S1=", ReusableStateSnapshotComponent(s1), // buttons: 4 & 5 + ", S2=", ReusableStateSnapshotComponent(s2), // buttons: 6 & 7 + ", S3=", ReusableStateSnapshotComponent(s3), // buttons: 8 & 9 + s", S4=", ReusableStateSnapshotComponent(s4.asInstanceOf[StateSnapshot[Int]]), // buttons: 10 & 11 + ) + } + } + + test(comp(PI(666))) { t => + t.assertText("P=PI(666), S1=100:1, S2=766:1, S3=1532:1, S4=330:1") + t.clickButton(1); t.assertText("P=PI(666), S1=101:2, S2=-766:2, S3=15320:2, S4=330:1") + t.clickButton(2); t.assertText("P=PI(666), S1=101:2, S2=-766:2, S3=15321:3, S4=330:1") + assertEq(counter.value, 15321) // verify that the modState(cb) executes after the state update + t.clickButton(3); t.assertText("P=PI(666), S1=101:2, S2=-766:2, S3=15321:3, S4=330:1") + assertEq(counter.value, 15322) // verify that the setState(None, cb) executes (and that ↖ the previous modState effect ↖ doesn't execute again) + t.clickButton(3); t.assertText("P=PI(666), S1=101:2, S2=-766:2, S3=15321:3, S4=330:1") + assertEq(counter.value, 15323) + t.clickButton(2); t.assertText("P=PI(666), S1=101:2, S2=-766:2, S3=15322:4, S4=330:1") + assertEq(counter.value, 15323 + 15322) + t.clickButton(4); t.assertText("P=PI(666), S1=102:3, S2=-766:2, S3=15322:4, S4=330:1") + t.clickButton(4); t.assertText("P=PI(666), S1=103:4, S2=-766:2, S3=15322:4, S4=330:1") + assertEq(counter.value, 15323 + 15322) + t.clickButton(11); t.assertText("P=PI(666), S1=103:4, S2=-766:2, S3=15322:4, S4=340:2") + t.clickButton(10); t.assertText("P=PI(666), S1=103:4, S2=-766:2, S3=15322:4, S4=340:2") // reusability blocks update + t.clickButton(11); t.assertText("P=PI(666), S1=103:4, S2=-766:2, S3=15322:4, S4=350:3") + } + } + private def testRenderWithReuse(): Unit = { implicit val reusability: Reusability[PI] = Reusability.by[PI, Int](_.pi >> 1) var renders = 0 @@ -1202,6 +1967,47 @@ object HooksTest extends TestSuite { } } + private def testMonadicRenderWithReuse(): Unit = { + implicit val reusability: Reusability[PI] = Reusability.by[PI, Int](_.pi >> 1) + var renders = 0 + var extState = 5 + + val inner = + React.memo( + ScalaFnComponent[(PI, Hooks.UseState[Int], Reusable[Callback], Reusable[Callback])] { case (p, s, incES, fu) => + renders += 1 + <.div( + s"P=$p, S=${s.value}, ES=$extState, R=$renders", + <.button(^.onClick --> s.modState(_ + 1)), + <.button(^.onClick --> (incES.value >> fu.value)), + ) + } + ) + + val comp = ScalaFnComponent[PI] { p => + for { + s <- useState(20) + incES <- useCallback(Callback(extState += 1)) + fu <- useForceUpdate + } yield + inner((p, s, incES, fu)) + } + + val wrapper = ScalaComponent.builder[PI].render_P(comp(_)).build + + withRenderedIntoBody(wrapper(PI(3))) { (m, root) => + val t = new DomTester(root) + t.assertText("P=PI(3), S=20, ES=5, R=1") + replaceProps(wrapper, m)(PI(2)); t.assertText("P=PI(3), S=20, ES=5, R=1") + t.clickButton(1); t.assertText("P=PI(2), S=21, ES=5, R=2") + replaceProps(wrapper, m)(PI(2)); t.assertText("P=PI(2), S=21, ES=5, R=2") + replaceProps(wrapper, m)(PI(3)); t.assertText("P=PI(2), S=21, ES=5, R=2") + replaceProps(wrapper, m)(PI(4)); t.assertText("P=PI(4), S=21, ES=5, R=3") + t.clickButton(2); t.assertText("P=PI(4), S=21, ES=6, R=4") + replaceProps(wrapper, m)(PI(5)); t.assertText("P=PI(4), S=21, ES=6, R=4") + } + } + // See https://github.com/japgolly/scalajs-react/issues/1027 private def testRenderWithReuseNever(): Unit = { implicit val reusability: Reusability[PI] = Reusability.never @@ -1223,8 +2029,7 @@ object HooksTest extends TestSuite { } .build - withRenderedIntoBody(wrapper(PI(3))) { (_, root) => - val t = new DomTester(root) + test(wrapper(PI(3))) { (t) => t.assertText("P=PI(3), R=1") t.clickButton(2); t.assertText("P=PI(4), R=2") t.clickButton(1); t.assertText("P=PI(4), R=3") @@ -1288,12 +2093,203 @@ object HooksTest extends TestSuite { } } + private def testUseReused(): Unit = { + implicit val reusePIByRounding: Reusability[PI] = Reusability.by(_.pi / 2) + + val comp = ScalaFnComponent[Unit] { _ => + for { + count <- useState(PI(0)) + reused <- useReused(count.value) + (stable, rev) = reused + } yield + <.div( + <.div(s"count=${count.value}, stable=$stable, rev=$rev"), + <.button(^.onClick --> count.modState(_ + 1)) + ) + } + + test(comp()) { (t) => + t.assertText("count=PI(0), stable=PI(0), rev=1") + t.clickButton(1); t.assertText("count=PI(1), stable=PI(0), rev=1") + t.clickButton(1); t.assertText("count=PI(2), stable=PI(2), rev=2") + } + } + + private def testFromFunction() = { + val jsHook1: js.Function1[Int, Int] = _ + 1 + val jsHook2: js.Function2[Int, Int, String] = (a: Int, b: Int) => (a + b).toString + + val useIncrementer = HookResult.fromFunction(jsHook1) + val useAdder = HookResult.fromFunction(jsHook2) + + assertTypeOf[Int => HookResult[Int]](useIncrementer) + assertTypeOf[(Int, Int) => HookResult[String]](useAdder) + + val comp = ScalaFnComponent[Unit] { _ => + for { + s <- useState(100) + inc <- useIncrementer(s.value) + add <- useAdder(s.value, inc) + } yield + <.div( + <.div(s"s=${s.value}, inc=$inc, add=$add"), + <.button(^.onClick --> s.modState(_ + 1)) + ) + } + + test(comp()) { t => + t.assertText("s=100, inc=101, add=201") + t.clickButton(); t.assertText("s=101, inc=102, add=203") + } + } + + object UseSyncExternalStore { + private class ExternalStore { + private val values: mutable.Map[Boolean, Int] = mutable.Map(true -> 0, false -> 0) + private val listeners: mutable.Map[Boolean, Callback] = mutable.Map.empty + + def get(which: Boolean): CallbackTo[Int] = CallbackTo(values(which)) + + def register(which: Boolean)(listener: Callback): CallbackTo[Callback] = { + Callback(this.listeners.updateWith(which)(_ => Some(listener))) + .ret(Callback(this.listeners.updateWith(which)(_ => None))) + } + + def peekListener(which: Boolean): Option[Callback] = listeners.get(which) + + def notifyListener(which: Boolean): Callback = listeners.get(which).getOrElse(Callback.empty) + + def inc(which: Boolean): Callback = Callback(values.updateWith(which)(_.map(_ + 1))) >> notifyListener(which) + } + + def testConst() = { + val store = new ExternalStore + + val comp = ScalaFnComponent + .withHooks[Unit] + .useSyncExternalStore(store.register(true), store.get(true)) + .render { (_, i) => + <.div(s"i=$i") + } + + test(comp()) { t => + t.assertText("i=0") + store.inc(true).runNow() + t.assertText("i=1") + } + assert(store.peekListener(true).isEmpty) + assert(store.peekListener(false).isEmpty) + } + + def testConstBy() = { + val store = new ExternalStore + + val comp = ScalaFnComponent + .withHooks[Boolean] + .useSyncExternalStoreBy(store.register, store.get) + .render { (_, i) => + <.div(s"i=$i") + } + + test(comp(false)) { t => + t.assertText("i=0") + store.inc(false).runNow() + t.assertText("i=1") + } + assert(store.peekListener(true).isEmpty) + assert(store.peekListener(false).isEmpty) + } + + def testMonadicConst() = { + val store = new ExternalStore + + val comp = ScalaFnComponent[Unit] { _ => + for { + i <- useSyncExternalStore(store.register(true), store.get(true)) + } yield <.div(s"i=$i") + } + + test(comp()) { t => + t.assertText("i=0") + store.inc(true).runNow() + t.assertText("i=1") + } + assert(store.peekListener(true).isEmpty) + assert(store.peekListener(false).isEmpty) + } + } + + object UseDeferred { + def testConst() = { + var renders: List[(Int, Int, Boolean)] = Nil + + val comp = ScalaFnComponent + .withHooks[Unit] + .useState(0) + .useDeferredValue((_, state) => state.value) + .render { (_, state, deferredValue) => + val isStale: Boolean = state.value != deferredValue + renders = renders :+ (state.value, deferredValue, isStale) + <.button(^.onClick --> state.modState(_ + 1)) + } + + test(comp()) { t => + t.clickButton() + } + assertEq(renders, List((0, 0, false), (1, 0, true), (1, 1, false))) + } + + // initialValue was added in React 19 - Uncomment when we upgrade to React 19 + // def testConstWithInitial() = { + // var renders: List[(Int, Int, Boolean)] = Nil + + // val comp = ScalaFnComponent + // .withHooks[Unit] + // .useState(0) + // .useDeferredValue((_, state) => state.value, (_, _) => 100) + // .render { (_, state, deferredValue) => + // val isStale: Boolean = state.value != deferredValue + // renders = renders :+ (state.value, deferredValue, isStale) + // <.div( + // deferredValue, + // <.button(^.onClick --> state.modState(_ + 1)) + // ) + // } + + // test(comp()) { t => + // t.clickButton() + // } + // assertEq(renders, List((0, 100, true), (0, 0, false), (1, 0, true), (1, 1, false))) + // } + + def testMonadicConst() = { + var renders: List[(Int, Int, Boolean)] = Nil + + val comp = ScalaFnComponent[Unit] { _ => + for { + state <- useState(0) + deferredValue <- useDeferredValue(state.value) + } yield { + val isStale: Boolean = state.value != deferredValue + renders = renders :+ (state.value, deferredValue, isStale) + <.button(^.onClick --> state.modState(_ + 1)) + } + } + + test(comp()) { t => + t.clickButton() + } + assertEq(renders, List((0, 0, false), (1, 0, true), (1, 1, false))) + } + } + // =================================================================================================================== override def tests = Tests { "custom" - { "usage" - testCustomHook() "composition" - testCustomHookComposition() + "monadic composition" - testCustomMonadicHookComposition() } "localLazyVal" - testLazyVal() "localVal" - testVal() @@ -1304,9 +2300,15 @@ object HooksTest extends TestSuite { "deps" - testUseCallbackWithDeps() "depsBy" - testUseCallbackWithDepsBy() } + "useCallback (monadic)" - { + "const" - testMonadicUseCallback() + "deps" - testMonadicUseCallbackWithDeps() + } "unchecked" - testUnchecked() "useContext" - testUseContext() + "useContext (monadic)" - testMonadicUseContext() "useDebugValue" - testUseDebugValue() + "useDebugValue (monadic)" - testMonadicUseDebugValue() "useEffect" - { import UseEffect._ "single" - testSingle() @@ -1317,7 +2319,15 @@ object HooksTest extends TestSuite { "mount" - testOnMount() "mountBy" - testOnMountBy() } + "useEffect (monadic)" - { + import UseEffectMonadic._ + "single" - testSingle() + "const" - testConst() + "deps" - testWithDeps() + "mount" - testOnMount() + } "useForceUpdate" - testUseForceUpdate() + "useForceUpdate (monadic)" - testMonadicUseForceUpdate() "useLayoutEffect" - { import UseLayoutEffect._ "single" - testSingle() @@ -1328,16 +2338,48 @@ object HooksTest extends TestSuite { "mount" - testOnMount() "mountBy" - testOnMountBy() } + "useLayoutEffect (monadic)" - { + import UseLayoutEffectMonadic._ + "single" - testSingle() + "const" - testConst() + "depsBy" - testWithDeps() + "mount" - testOnMount() + } + "useInsertionEffect" - { + import UseInsertionEffect._ + "single" - testSingle() + "const" - testConst() + "constBy" - testConstBy() + "deps" - testWithDeps() + "depsBy" - testWithDepsBy() + "mount" - testOnMount() + "mountBy" - testOnMountBy() + } + "useInsertionEffect (monadic)" - { + import UseInsertionEffectMonadic._ + "single" - testSingle() + "const" - testConst() + "depsBy" - testWithDeps() + "mount" - testOnMount() + } "useMemo" - { "deps" - testUseMemo() "depsBy" - testUseMemoBy() } + "useMemo (monadic)" - { + "deps" - testMonadicUseMemo() + } "useRef" - { "manual" - testUseRefManual() "manualBy" - testUseRefManualBy() "vdom" - testUseRefVdom() } + "useRef (monadic)" - { + "manual" - testMonadicUseRefManual() + "vdom" - testMonadicUseRefVdom() + } "useReducer" - testUseReducer() + "useReducer (monadic)" - testMonadicUseReducer() "useState" - { "state" - testUseState() "reusability" - { @@ -1345,6 +2387,13 @@ object HooksTest extends TestSuite { "mod" - testUseStateModStateReusability() } } + "useState (monadic)" - { + "state" - testMonadicUseState() + "reusability" - { + "set" - testMonadicUseStateSetStateReusability() + "mod" - testMonadicUseStateModStateReusability() + } + } "useStateWithReuse" - { "state" - testUseStateWithReuse() "reusability" - { @@ -1352,8 +2401,41 @@ object HooksTest extends TestSuite { "mod" - testUseStateWithReuseModStateReusability() } } + "useStateWithReuse (monadic)" - { + "state" - testMonadicUseStateWithReuse() + "reusability" - { + "set" - testMonadicUseStateWithReuseSetStateReusability() + "mod" - testMonadicUseStateWithReuseModStateReusability() + } + } "useStateSnapshot" - testUseStateSnapshot() + "useStateSnapshot (monadic)" - testMonadicUseStateSnapshot() "useStateSnapshotWithReuse" - testUseStateSnapshotWithReuse() + "useStateSnapshotWithReuse (monadic)" - testMonadicUseStateSnapshotWithReuse() + + "useId" - testUseId() + "useId (monadic)" - testMonadicUseId() + + "useTransition" - testUseTransition() + "useTransition (monadic)" - testMonadicUseTransition() + + "useSyncExternalStore" - { + "const" - UseSyncExternalStore.testConst() + "constBy" - UseSyncExternalStore.testConstBy() + } + "useSyncExternalStore (monadic)" - { + "const" - UseSyncExternalStore.testMonadicConst() + } + + "useDeferred" - { + "const" - UseDeferred.testConst() + // initialValue was added in React 19 - Uncomment when we upgrade to React 19 + // "constWithInitial" - UseDeferred.testConstWithInitial() + } + "useDeferred (monadic)" - { + "const" - UseDeferred.testMonadicConst() + // "constWithInitial" - UseDeferred.testMonadicConstWithInitial() + } "renderWithReuse" - { "main" - testRenderWithReuse() @@ -1361,5 +2443,10 @@ object HooksTest extends TestSuite { "useRef" - testRenderWithReuseAndUseRef() "useRefToVdom" - testRenderWithReuseAndUseRefToVdom() } + "renderWithReuse (monadic alternative using Render.memo)" - { + "main" - testMonadicRenderWithReuse() + } + "useReused" - testUseReused() + "fromFunction" - testFromFunction() } } diff --git a/library/tests/src/test/scala/japgolly/scalajs/react/core/RefTest.scala b/library/tests/src/test/scala/japgolly/scalajs/react/core/RefTest.scala index f7e4ae650..862a7ad90 100644 --- a/library/tests/src/test/scala/japgolly/scalajs/react/core/RefTest.scala +++ b/library/tests/src/test/scala/japgolly/scalajs/react/core/RefTest.scala @@ -152,9 +152,11 @@ object RefTest extends TestSuite { @js.native private object RawComp extends js.Object - private val Forwarder = JsForwardRefComponent[Null, Children.Varargs, html.Button](RawComp) + val Forwarder = JsForwardRefComponent[Null, Children.Varargs, html.Button](RawComp) - def nullary() = assertRender(Forwarder(), "
") + def nullaryExpectation = "
" + + def nullary() = assertRender(Forwarder(), nullaryExpectation) def children() = assertRender(Forwarder(<.br, <.hr), "
") @@ -180,10 +182,12 @@ object RefTest extends TestSuite { object ScalaToVdom { - private val Forwarder = React.forwardRef.justChildren[html.Button]((c, r) => + val Forwarder = React.forwardRef.justChildren[html.Button]((c, r) => <.div(<.button.withOptionalRef(r)(^.cls := "fancy", c))) - def nullary() = assertRender(Forwarder(), "
") + def nullaryExpectation = "
" + + def nullary() = assertRender(Forwarder(), nullaryExpectation) def children() = assertRender(Forwarder(<.br, <.hr), "
") diff --git a/library/tests/src/test/scala/japgolly/scalajs/react/core/RenderableTest.scala b/library/tests/src/test/scala/japgolly/scalajs/react/core/RenderableTest.scala new file mode 100644 index 000000000..87f26b33f --- /dev/null +++ b/library/tests/src/test/scala/japgolly/scalajs/react/core/RenderableTest.scala @@ -0,0 +1,88 @@ +package japgolly.scalajs.react.core + +import japgolly.scalajs.react._ +import japgolly.scalajs.react.test.ReactTestUtils2 +import japgolly.scalajs.react.vdom.VdomNode +import japgolly.scalajs.react.vdom.html_<^._ +import sourcecode.Line +import utest._ + +object RenderableTest extends TestSuite { + + private def test[A: Renderable](source: A, expectHtml: String)(implicit l: Line): Unit = + ReactTestUtils2.withRendered(source) { r => + r.outerHTML.assert(expectHtml) + } + + override def tests = Tests { + + // TODO: Add undefined + + "text" - { + test("cool", "cool") + } + + "short" - { + test(6.toShort, "6") + } + + "int" - { + test(3, "3") + } + + "double" - { + test(3.2, "3.2") + } + + "long" - { + test(500000000L, "500000000") + } + + "rawNode" - { + test(VdomNode("abc").rawNode, "abc") + } + + "rawElement" - { + test(<.div("oof").rawElement, "
oof
") + } + + "vdomNode" - { + test(VdomNode("hehe"), "hehe") + } + + "vdomElement" - { + test(<.div("ah"), "
ah
") + } + + "jsComponent" - { + import JsComponentEs6PTest._ + test(Component(JsProps("Nim")), "
Hello Nim
") + } + + "jsFnComponent" - { + import JsFnComponentTest._ + test(Component(JsProps("Aiden")), "
Hello Aiden
") + } + + "scalaComponent" - { + val ScalaComp = ScalaComponent.builder[Unit].render_(<.div("scala!")).build + test(ScalaComp(), "
scala!
") + } + + "scalaFnComponent" - { + val ScalaFnComp = ScalaFnComponent[Unit](_ => <.div("scala fn!")) + test(ScalaFnComp(), "
scala fn!
") + } + + "jsForwardRef" - { + import RefTest.TestRefForwarding.JsToVdom._ + test(Forwarder(), nullaryExpectation) + } + + "scalaForwardRef" - { + import RefTest.TestRefForwarding.ScalaToVdom._ + test(Forwarder(), nullaryExpectation) + } + } + +} diff --git a/library/tests/src/test/scala/japgolly/scalajs/react/core/ScalaComponentTest.scala b/library/tests/src/test/scala/japgolly/scalajs/react/core/ScalaComponentTest.scala index 49852cc8f..ba033caf9 100644 --- a/library/tests/src/test/scala/japgolly/scalajs/react/core/ScalaComponentTest.scala +++ b/library/tests/src/test/scala/japgolly/scalajs/react/core/ScalaComponentTest.scala @@ -169,7 +169,8 @@ object ScalaComponentPTest extends TestSuite { assertEq("willUnmountCount", willUnmountCount, 0) mounted = Comp(null).renderIntoDOM(mountNode) - assertOuterHTMLMatches(el(), "
Error: Cannot read propert(y|ies) of null.*
") + // Error message varies between development and production modes + assertOuterHTMLMatches(el(), "
(?:Error: Cannot read propert(y|ies) of null.*|Error: java\\.lang\\.NullPointerException)
") assertEq("willUnmountCount", willUnmountCount, 1) mounted.withEffectsPure.getDOMNode } diff --git a/library/tests/src/test/scala/japgolly/scalajs/react/core/ScalaFnComponentTest.scala b/library/tests/src/test/scala/japgolly/scalajs/react/core/ScalaFnComponentTest.scala index c1a8a1e89..f9fb2b631 100644 --- a/library/tests/src/test/scala/japgolly/scalajs/react/core/ScalaFnComponentTest.scala +++ b/library/tests/src/test/scala/japgolly/scalajs/react/core/ScalaFnComponentTest.scala @@ -13,7 +13,7 @@ object ScalaFnComponentTest extends TestSuite { final case class Add(x: Int, y: Int) - val CaseClassProps = ScalaFnComponent[Add] { a => + val CaseClassProps = ScalaFnComponent.withDisplayName("Add")[Add] { a => import a._ <.code(s"$x + $y = ${x + y}") } @@ -33,6 +33,11 @@ object ScalaFnComponentTest extends TestSuite { "justChild" - assertRender(JustChildren(c1), "

good

") "justChildren" - assertRender(JustChildren(c1, c2), "

good222

") + "displayName" - { + assertEq(IntProps.displayName, "ScalaFnComponentTest.IntProps (japgolly.scalajs.react.core)") + assertEq(CaseClassProps.displayName, "Add") + } + "memo" - { var rendered = 0 implicit def reusabilityAdd: Reusability[Add] = Reusability.by(_.x) diff --git a/library/tests/src/test/scala/japgolly/scalajs/react/core/vdom/PrefixedTest.scala b/library/tests/src/test/scala/japgolly/scalajs/react/core/vdom/PrefixedTest.scala index 9eb1b9615..b3e553d19 100644 --- a/library/tests/src/test/scala/japgolly/scalajs/react/core/vdom/PrefixedTest.scala +++ b/library/tests/src/test/scala/japgolly/scalajs/react/core/vdom/PrefixedTest.scala @@ -6,6 +6,7 @@ import japgolly.scalajs.react.test.TestUtil._ import japgolly.scalajs.react.vdom.html_<^._ import scala.annotation.nowarn import scala.scalajs.js +import sourcecode.Line import utest._ @nowarn("msg=Stream.+is.deprecated") @@ -24,7 +25,7 @@ object PrefixedTest extends TestSuite { def checkbox(check: Boolean) = <.input.checkbox(^.checked := check, ^.readOnly := true) - def test(subj: VdomNode, exp: String): Unit = { + def test(subj: VdomNode, exp: String)(implicit l: Line): Unit = { val comp = ScalaComponent.static("tmp")(subj) assertRender(comp(), exp) } @@ -49,7 +50,7 @@ object PrefixedTest extends TestSuite { "compJS" - test(<.div(jsComp), """
Hello yo
""") } - "checkboxT" - test(checkbox(true), """""") + "checkboxT" - test(checkbox(true), """""") "checkboxF" - test(checkbox(false), """""") "attr" - { diff --git a/library/tests/src/test/scala/japgolly/scalajs/react/core/vdom/UnprefixedTest.scala b/library/tests/src/test/scala/japgolly/scalajs/react/core/vdom/UnprefixedTest.scala index b06da1554..71e52055f 100644 --- a/library/tests/src/test/scala/japgolly/scalajs/react/core/vdom/UnprefixedTest.scala +++ b/library/tests/src/test/scala/japgolly/scalajs/react/core/vdom/UnprefixedTest.scala @@ -6,6 +6,7 @@ import japgolly.scalajs.react.test.TestUtil._ import japgolly.scalajs.react.vdom.all._ import scala.annotation.nowarn import scala.scalajs.js +import sourcecode.Line import utest._ @nowarn("msg=Stream.+is.deprecated") @@ -24,7 +25,7 @@ object UnprefixedTest extends TestSuite { def checkbox(check: Boolean) = input.checkbox(checked := check, readOnly := true) - def test(subj: VdomNode, exp: String): Unit = { + def test(subj: VdomNode, exp: String)(implicit l: Line): Unit = { val comp = ScalaComponent.static("tmp")(subj) assertRender(comp(), exp) } @@ -49,7 +50,7 @@ object UnprefixedTest extends TestSuite { "compJS" - test(div(jsComp), """
Hello yo
""") } - "checkboxT" - test(checkbox(true), """""") + "checkboxT" - test(checkbox(true), """""") "checkboxF" - test(checkbox(false), """""") "attr" - { diff --git a/library/tests/src/test/scala/japgolly/scalajs/react/test/DomTester.scala b/library/tests/src/test/scala/japgolly/scalajs/react/test/DomTester.scala index a9a417ca9..ee5711291 100644 --- a/library/tests/src/test/scala/japgolly/scalajs/react/test/DomTester.scala +++ b/library/tests/src/test/scala/japgolly/scalajs/react/test/DomTester.scala @@ -2,7 +2,7 @@ package japgolly.scalajs.react.test import japgolly.microlibs.testutil.TestUtil._ import japgolly.scalajs.react._ -import japgolly.scalajs.react.test.ReactTestUtils._ +import japgolly.scalajs.react.test.ReactTestUtils2._ import japgolly.scalajs.react.test._ import org.scalajs.dom.html.{Button, Element, Input} import sourcecode.Line @@ -39,6 +39,9 @@ class DomTester(root: Element) { act(SimEvent.Change(t).simulate(i)) } + def getText: String = + DomTester.getText(root) + private def getInputText(): Input = { val is = root.querySelectorAll("input[type=text]") val len = is.length diff --git a/library/tests/src/test/scala/japgolly/scalajs/react/test/LegacyTestTest.scala b/library/tests/src/test/scala/japgolly/scalajs/react/test/LegacyTestTest.scala new file mode 100644 index 000000000..1aa2af081 --- /dev/null +++ b/library/tests/src/test/scala/japgolly/scalajs/react/test/LegacyTestTest.scala @@ -0,0 +1,341 @@ +package japgolly.scalajs.react.test + +import japgolly.scalajs.react._ +import japgolly.scalajs.react.facade.SyntheticEvent +import japgolly.scalajs.react.test.TestUtil._ +import japgolly.scalajs.react.vdom.html_<^._ +import org.scalajs.dom +import org.scalajs.dom.{HTMLElement, HTMLInputElement, document} +import scala.annotation.nowarn +import scala.concurrent.Promise +import scala.scalajs.concurrent.JSExecutionContext.Implicits.queue +import sizzle.Sizzle +import utest._ + +object LegacyTestTest extends TestSuite { + + lazy val A = ScalaComponent.builder[Unit]("A").render_C(c => <.p(^.cls := "AA", c)).build + lazy val B = ScalaComponent.builder[Unit]("B").renderStatic(<.p(^.cls := "BB", "hehehe")).build + lazy val rab = ReactTestUtils.renderIntoDocument(A(B())) + + val inputRef = Ref[HTMLInputElement] + + lazy val IC = ScalaComponent.builder[Unit]("IC").initialState(true).renderS(($,s) => { + val ch = (_: ReactEvent) => $.modState(x => !x) + <.label( + <.input.checkbox(^.checked := s, ^.readOnly := true, ^.onClick ==> ch).withRef(inputRef), + <.span(s"s = $s") + ) + }).build + + lazy val IT = ScalaComponent.builder[Unit]("IT").initialState("NIL").renderS(($,s) => { + val ch = (e: ReactEventFromInput) => $.setState(e.target.value.toUpperCase) + <.input.text(^.value := s, ^.onChange ==> ch) + }).build + + class CP { + var prev = "none" + def render(p: String) = <.div(s"$prev → $p") + } + @nowarn("cat=deprecation") + val CP = ScalaComponent.builder[String]("asd") + .backend(_ => new CP) + .renderBackend + .componentWillReceiveProps(i => Callback(i.backend.prev = i.currentProps)) + .build + + val tests = Tests { + + "findRenderedDOMComponentWithClass" - { + val x = ReactTestUtils.findRenderedDOMComponentWithClass(rab, "BB") + val n = x.getDOMNode.asMounted().asElement() + assert(n.matchesBy[HTMLElement](_.className == "BB")) + } + + "findRenderedComponentWithType" - { + val n = ReactTestUtils.findRenderedComponentWithType(rab, B).getDOMNode.asMounted().asElement() + assert(n.matchesBy[HTMLElement](_.className == "BB")) + } + + "renderIntoDocument" - { + def test(c: GenericComponent.MountedRaw, exp: String): Unit = + assertOuterHTML(ReactDOM.findDOMNode(c.raw).get.asElement(), exp) + + "plainElement" - { + val re: VdomElement = <.div("Good") + val c = ReactTestUtils.renderIntoDocument(re) + test(c, """
Good
""") + } + + "scalaComponent" - { + val c = ReactTestUtils.renderIntoDocument(B()) + test(c, """

hehehe

""") + } + } + + "Simulate" - { + "click" - { + val c = ReactTestUtils.renderIntoDocument(IC()) + val s = ReactTestUtils.findRenderedDOMComponentWithTag(c, "span") + val a = s.getDOMNode.asMounted().asElement().innerHTML + Simulate.click(inputRef.unsafeGet()) + val b = s.getDOMNode.asMounted().asElement().innerHTML + assert(a != b) + } + + "eventTypes" - { + def test[E[+x <: dom.Node] <: SyntheticEvent[x]](eventType: VdomAttr.Event[E], simF: ReactOrDomNode => Unit) = { + val IDC = ScalaComponent.builder[Unit]("IC").initialState(true).render($ => { + @nowarn("cat=unused") val ch = (e: E[dom.Node]) => $.modState(x => !x) + <.label( + <.input.text(^.value := $.state, ^.readOnly := true, eventType ==> ch).withRef(inputRef), + <.span(s"s = ${$.state}") + ) + }).build + + val c = ReactTestUtils.renderIntoDocument(IDC()) + val s = ReactTestUtils.findRenderedDOMComponentWithTag(c, "span") + + val a = s.getDOMNode.asMounted().asElement().innerHTML + simF(inputRef.unsafeGet()) + val b = s.getDOMNode.asMounted().asElement().innerHTML + + assert(a != b) + } + + "onAuxClick" - test(^.onAuxClick, Simulate.auxClick(_)) + "onBeforeInput" - test(^.onBeforeInput, Simulate.beforeInput(_)) + "onBlur" - test(^.onBlur, Simulate.blur(_)) + "onChange" - test(^.onChange, Simulate.change(_)) + "onClick" - test(^.onClick, Simulate.click(_)) + "onCompositionEnd" - test(^.onCompositionEnd, Simulate.compositionEnd(_)) + "onCompositionStart" - test(^.onCompositionStart, Simulate.compositionStart(_)) + "onCompositionUpdate" - test(^.onCompositionUpdate, Simulate.compositionUpdate(_)) + "onContextMenu" - test(^.onContextMenu, Simulate.contextMenu(_)) + "onCopy" - test(^.onCopy, Simulate.copy(_)) + "onCut" - test(^.onCut, Simulate.cut(_)) + "onDblClick" - test(^.onDblClick, Simulate.doubleClick(_)) + "onDragEnd" - test(^.onDragEnd, Simulate.dragEnd(_)) + "onDragEnter" - test(^.onDragEnter, Simulate.dragEnter(_)) + "onDragExit" - test(^.onDragExit, Simulate.dragExit(_)) + "onDragLeave" - test(^.onDragLeave, Simulate.dragLeave(_)) + "onDragOver" - test(^.onDragOver, Simulate.dragOver(_)) + "onDragStart" - test(^.onDragStart, Simulate.dragStart(_)) + "onDrag" - test(^.onDrag, Simulate.drag(_)) + "onDrop" - test(^.onDrop, Simulate.drop(_)) + "onError" - test(^.onError, Simulate.error(_)) + "onFocus" - test(^.onFocus, Simulate.focus(_)) + "onGotPointerCapture" - test(^.onGotPointerCapture, Simulate.gotPointerCapture(_)) + "onInput" - test(^.onInput, Simulate.input(_)) + "onKeyDown" - test(^.onKeyDown, Simulate.keyDown(_)) + "onKeyPress" - test(^.onKeyPress, Simulate.keyPress(_)) + "onKeyUp" - test(^.onKeyUp, Simulate.keyUp(_)) + "onLoad" - test(^.onLoad, Simulate.load(_)) + "onLostPointerCapture" - test(^.onLostPointerCapture, Simulate.lostPointerCapture(_)) + "onMouseDown" - test(^.onMouseDown, Simulate.mouseDown(_)) + "onMouseEnter" - test(^.onMouseEnter, Simulate.mouseEnter(_)) + "onMouseLeave" - test(^.onMouseLeave, Simulate.mouseLeave(_)) + "onMouseMove" - test(^.onMouseMove, Simulate.mouseMove(_)) + "onMouseOut" - test(^.onMouseOut, Simulate.mouseOut(_)) + "onMouseOver" - test(^.onMouseOver, Simulate.mouseOver(_)) + "onMouseUp" - test(^.onMouseUp, Simulate.mouseUp(_)) + "onPaste" - test(^.onPaste, Simulate.paste(_)) + "onPointerCancel" - test(^.onPointerCancel, Simulate.pointerCancel(_)) + "onPointerDown" - test(^.onPointerDown, Simulate.pointerDown(_)) + "onPointerEnter" - test(^.onPointerEnter, Simulate.pointerEnter(_)) + "onPointerLeave" - test(^.onPointerLeave, Simulate.pointerLeave(_)) + "onPointerMove" - test(^.onPointerMove, Simulate.pointerMove(_)) + "onPointerOut" - test(^.onPointerOut, Simulate.pointerOut(_)) + "onPointerOver" - test(^.onPointerOver, Simulate.pointerOver(_)) + "onPointerUp" - test(^.onPointerUp, Simulate.pointerUp(_)) + "onReset" - test(^.onReset, Simulate.reset(_)) + "onScroll" - test(^.onScroll, Simulate.scroll(_)) + "onSelect" - test(^.onSelect, Simulate.select(_)) + "onSubmit" - test(^.onSubmit, Simulate.submit(_)) + "onTouchCancel" - test(^.onTouchCancel, Simulate.touchCancel(_)) + "onTouchEnd" - test(^.onTouchEnd, Simulate.touchEnd(_)) + "onTouchMove" - test(^.onTouchMove, Simulate.touchMove(_)) + "onTouchStart" - test(^.onTouchStart, Simulate.touchStart(_)) + "onWheel" - test(^.onWheel, Simulate.wheel(_)) + } + + "eventDefaults" - { + var ok = false + val c = ScalaComponent.builder[Unit]("").render_P { _ => + def onClick(e: ReactMouseEvent) = { + // Make sure these don't throw + e.defaultPrevented + e.isDefaultPrevented() + locally(e.screenX) + locally(e.screenY) + locally(e.clientX) + locally(e.clientY) + locally(e.pageX) + locally(e.pageY) + locally(e.altKey) + locally(e.ctrlKey) + locally(e.metaKey) + locally(e.shiftKey) + locally(e.button) + locally(e.buttons) + Callback { ok = true } + } + <.div(^.onClick ==> onClick) + }.build + ReactTestUtils.withRenderedIntoDocument(c()) { m => + Simulate.click(m) + } + assertEq(ok, true) + } + + "change" - { + val c = ReactTestUtils.renderIntoDocument(IT()) + SimEvent.Change("hehe").simulate(c) + val t = c.getDOMNode.asMounted().domCast[HTMLInputElement].value + assertEq(t, "HEHE") + } + + "focusChangeBlur" - { + var events = Vector.empty[String] + val C = ScalaComponent.builder[Unit]("C").initialState("ey").render(T => { + def e(s: String) = Callback(events :+= s) + def chg(ev: ReactEventFromInput) = + e("change") >> T.setState(ev.target.value) + <.input.text(^.value := T.state, ^.onFocus --> e("focus"), ^.onChange ==> chg, ^.onBlur --> e("blur")).withRef(inputRef) + }).build + ReactTestUtils.renderIntoDocument(C()) + Simulation.focusChangeBlur("good") run inputRef.unsafeGet() + assertEq(events, Vector("focus", "change", "blur")) + assertEq(inputRef.unsafeGet().value, "good") + } + "targetByName" - { + val c = ReactTestUtils.renderIntoDocument(IC()) + var count = 0 + def tgt = { + count += 1 + Sizzle("input", c.getDOMNode.asMounted().asElement()).sole() + } + Simulation.focusChangeBlur("-") run tgt + assert(count == 3) + } + } + + "withRenderedIntoDocument" - { + var m: ScalaComponent.MountedImpure[Unit, Boolean, Unit] = null + ReactTestUtils.withRenderedIntoDocument(IC()) { mm => + m = mm + val n = m.getDOMNode.asMounted().asElement() + assert(ReactTestUtils.removeReactInternals(n.outerHTML) startsWith "