Skip to content

Commit ef31959

Browse files
committed
Handle useStateBy and withPropsChildren
1 parent d3b8a09 commit ef31959

File tree

18 files changed

+449
-65
lines changed

18 files changed

+449
-65
lines changed

library/coreGeneric/src/main/scala-2/japgolly/scalajs/react/hooks/HookMacros.scala

Lines changed: 145 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -27,23 +27,74 @@ class HookMacros(val c: Context) extends MacroUtils {
2727

2828
private implicit def autoTagToType[A](t: c.WeakTypeTag[A]): Type = t.tpe
2929

30-
private def Box : Tree = q"_root_.japgolly.scalajs.react.internal.Box"
31-
private def Box(t: Type): Type = appliedType(c.typeOf[Box[_]], t)
32-
private def Hooks : Tree = q"_root_.japgolly.scalajs.react.hooks.Hooks"
33-
private def JsFn : Tree = q"_root_.japgolly.scalajs.react.component.JsFn"
34-
private def React : Tree = q"_root_.japgolly.scalajs.react.facade.React"
35-
private def ScalaFn : Tree = q"_root_.japgolly.scalajs.react.component.ScalaFn"
36-
private def withHooks = "withHooks"
30+
private def Box : Tree = q"_root_.japgolly.scalajs.react.internal.Box"
31+
private def Box(t: Type) : Type = appliedType(c.typeOf[Box[_]], t)
32+
private def HookCtx : Tree = q"_root_.japgolly.scalajs.react.hooks.HookCtx"
33+
private def Hooks : Tree = q"_root_.japgolly.scalajs.react.hooks.Hooks"
34+
private def JsFn : Tree = q"_root_.japgolly.scalajs.react.component.JsFn"
35+
private def PropsChildren: Tree = q"_root_.japgolly.scalajs.react.PropsChildren"
36+
private def React : Tree = q"_root_.japgolly.scalajs.react.facade.React"
37+
private def ScalaFn : Tree = q"_root_.japgolly.scalajs.react.component.ScalaFn"
38+
private def withHooks = "withHooks"
3739

38-
case class HookDefn(steps: List[HookStep])
39-
case class HookStep(name: String, targs: List[Tree], args: List[List[Tree]])
40+
private case class HookDefn(steps: List[HookStep])
41+
42+
private case class HookStep(name: String, targs: List[Tree], args: List[List[Tree]])
43+
44+
private class HookRewriter(props: Tree, initChildren: Tree, propsChildren: Tree) {
45+
private var stmts = Vector.empty[Tree]
46+
private var hooks = List.empty[Ident]
47+
private var _hookCount = 0
48+
private var _usesChildren = false
49+
50+
def usesChildren() =
51+
_usesChildren
52+
53+
def useChildren(): Unit = {
54+
_usesChildren = true
55+
this += initChildren
56+
}
57+
58+
def +=(stmt: Tree): Unit =
59+
stmts :+= stmt
60+
61+
def hookCount(): Int =
62+
_hookCount
63+
64+
def nextHookName(suffix: String = ""): TermName =
65+
TermName("hook" + (hookCount() + 1) + suffix)
66+
67+
def registerHook(h: TermName): Unit = {
68+
hooks :+= Ident(h)
69+
_hookCount += 1
70+
}
71+
72+
def args(): List[Tree] =
73+
if (usesChildren())
74+
props :: propsChildren :: hooks
75+
else
76+
props :: hooks
77+
78+
def ctxArg(): Tree = {
79+
val hookCtxObj = if (usesChildren()) q"$HookCtx.withChildren" else HookCtx
80+
val create = Apply(hookCtxObj, args())
81+
val name = nextHookName("_ctx")
82+
this += q"val $name = $create"
83+
Ident(name)
84+
}
85+
86+
def wrap(body: Tree): Tree =
87+
q"..$stmts; $body"
88+
}
89+
90+
// -------------------------------------------------------------------------------------------------------------------
4091

4192
def render[P, C <: Children, Ctx, CtxFn[_], Step <: SubsequentStep[Ctx, CtxFn]]
4293
(f: c.Tree)(step: c.Tree, s: c.Tree)
4394
(implicit P: c.WeakTypeTag[P], C: c.WeakTypeTag[C]): c.Tree = {
4495

4596
implicit val log = MacroLogger()
46-
// log.enabled = showCode(c.macroApplication).contains("counter.value")
97+
log.enabled = showCode(c.macroApplication).contains("DEBUG") // TODO: DELETE
4798
log.header()
4899
log("macroApplication", showRaw(c.macroApplication))
49100

@@ -103,72 +154,90 @@ class HookMacros(val c: Context) extends MacroUtils {
103154
Left(() => "Don't know how to parse " + showRaw(tree))
104155
}
105156

106-
private type RenderInliner = (Tree, Init) => Tree
107-
108-
private def inlineHookDefn(h: HookDefn)(implicit log: MacroLogger): Either[() => String, RenderInliner] = {
109-
val init = new Init("hook" + _, lazyVals = false)
157+
private def inlineHookDefn(h: HookDefn)(implicit log: MacroLogger): Either[() => String, HookRewriter => Tree] = {
110158
val it = h.steps.iterator
111-
var stepId = 0
112159
var renderStep: HookStep = null
113-
var hooks = List.empty[TermName]
160+
var hooks = Vector.empty[HookRewriter => TermName]
161+
var withPropsChildren = false
114162
while (it.hasNext) {
115163
val step = it.next()
116164
if (it.hasNext) {
117-
stepId += 1
118-
inlineHookStep(stepId, step, init) match {
119-
case Right(termName) => hooks ::= termName
120-
case Left(e) => return Left(e)
121-
}
165+
if (hooks.isEmpty && step.name == "withPropsChildren")
166+
withPropsChildren = true
167+
else
168+
inlineHookStep(step) match {
169+
case Right(h) => hooks :+= h
170+
case Left(e) => return Left(e)
171+
}
122172
} else
123173
renderStep = step
124174
}
125-
hooks = hooks.reverse
126175

127-
hookRenderInliner(renderStep, hooks.map(Ident(_))).map { f =>
128-
(props, init2) => {
129-
init2 ++= init.stmts
130-
f(props, init2)
131-
}
176+
hookRenderInliner(renderStep).map { buildRender => b =>
177+
if (withPropsChildren)
178+
b.useChildren()
179+
for (h <- hooks)
180+
b registerHook h(b)
181+
buildRender(b)
132182
}
133183
}
134184

135-
private def inlineHookStep(stepId: Int, step: HookStep, init: Init)(implicit log: MacroLogger): Either[() => String, TermName] = {
185+
private def inlineHookStep(step: HookStep)(implicit log: MacroLogger): Either[() => String, HookRewriter => TermName] = {
136186
log("inlineHookStep." + step.name, step)
187+
188+
def useState(b: HookRewriter, tpe: Tree, body: Tree) = {
189+
val rawName = b.nextHookName("_raw")
190+
val name = b.nextHookName()
191+
b += q"val $rawName = $React.useStateFn(() => $Box[$tpe]($body))"
192+
b += q"val $name = $Hooks.UseState.fromJsBoxed[$tpe]($rawName)"
193+
name
194+
}
195+
137196
step.name match {
197+
138198
case "useState" =>
139-
val stateType = step.targs.head
140-
val arg = step.args.head.head
141-
val rawName = TermName("hook" + stepId + "_raw")
142-
val name = TermName("hook" + stepId)
143-
init += q"val $rawName = $React.useStateFn(() => $Box[$stateType]($arg))"
144-
init += q"val $name = $Hooks.UseState.fromJsBoxed[$stateType]($rawName)"
145-
Right(name)
199+
val targ = step.targs.head
200+
val arg = step.args.head.head
201+
Right(useState(_, targ, arg))
202+
203+
case "useStateBy" =>
204+
val targ = step.targs.head
205+
val arg = step.args.head.head
206+
arg match {
207+
case f@ Function(params, _) =>
208+
if (params.sizeIs == 1)
209+
Right { b =>
210+
val ctxArg = b.ctxArg()
211+
useState(b, targ, call(f, ctxArg :: Nil))
212+
}
213+
else
214+
Right(b => useState(b, targ, call(f, b.args())))
215+
216+
case _ =>
217+
Left(() => s"Expected a function, found: ${showRaw(arg)}")
218+
}
146219

147220
case _ =>
148221
Left(() => s"Inlining of hook method '${step.name}' not yet supported.")
149222
}
150223
}
151224

152-
private def hookRenderInliner(step: HookStep, hooks: List[Tree])(implicit log: MacroLogger): Either[() => String, RenderInliner] = {
225+
private def hookRenderInliner(step: HookStep)(implicit log: MacroLogger): Either[() => String, HookRewriter => Tree] = {
153226
log("inlineHookRender." + step.name, step)
154227
step.name match {
155228
case "render" =>
156229
@nowarn("msg=exhaustive") val List(List(renderFn), _) = step.args
157-
Right { (props, _) =>
158-
val args = props :: hooks
159-
Apply(Select(renderFn, TermName("apply")), args)
160-
}
230+
Right(b => call(renderFn, b.args()))
161231

162232
case _ =>
163233
Left(() => s"Inlining of hook render method '${step.name}' not yet supported.")
164234
}
165235
}
166236

167-
private def inlineHookRawComponent[P](renderInliner: RenderInliner)(implicit P: c.WeakTypeTag[P]): Tree = {
168-
val props_unbox = q"props.unbox"
169-
val init = new Init("_i" + _)
170-
val render1 = renderInliner(props_unbox, init)
171-
val render2 = init.wrap(q"$render1.rawNode")
237+
private def inlineHookRawComponent[P](rewrite: HookRewriter => Tree)(implicit P: c.WeakTypeTag[P]): Tree = {
238+
val b = new HookRewriter(q"props.unbox", q"val children = $PropsChildren.fromRawProps(props)", q"children")
239+
val render1 = rewrite(b)
240+
val render2 = b.wrap(q"$render1.rawNode")
172241
q"(props => $render2): $JsFn.RawComponent[${Box(P)}]"
173242
}
174243

@@ -178,4 +247,35 @@ class HookMacros(val c: Context) extends MacroUtils {
178247
$ScalaFn.fromBoxed($JsFn.fromJsFn[${Box(P)}, $C](rawComponent)($summoner))
179248
""")
180249
}
250+
251+
// -------------------------------------------------------------------------------------------------------------------
252+
253+
private def call(function: Tree, args: List[Tree]): Tree = {
254+
import internal._
255+
256+
function match {
257+
case Function(params, body) =>
258+
259+
// From scala/test/files/run/macro-range/Common_1.scala
260+
class TreeSubstituter(from: List[Symbol], to: List[Tree]) extends Transformer {
261+
override def transform(tree: Tree): Tree = tree match {
262+
case Ident(_) =>
263+
def subst(from: List[Symbol], to: List[Tree]): Tree =
264+
if (from.isEmpty) tree
265+
else if (tree.symbol == from.head) to.head.duplicate
266+
else subst(from.tail, to.tail);
267+
subst(from, to)
268+
case _ =>
269+
val tree1 = super.transform(tree)
270+
if (tree1 ne tree) setType(tree1, null)
271+
tree1
272+
}
273+
}
274+
val t = new TreeSubstituter(params.map(_.symbol), args)
275+
t.transform(body)
276+
277+
case _ =>
278+
Apply(Select(function, TermName("apply")), args)
279+
}
280+
}
181281
}

library/coreGeneric/src/main/scala/japgolly/scalajs/react/internal/Box.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ trait Box[+A] extends js.Object {
99
}
1010

1111
object Box {
12-
@inline def apply[A](value: A): Box[A] =
12+
def apply[A](value: A): Box[A] =
1313
js.Dynamic.literal(a = value.asInstanceOf[js.Any]).asInstanceOf[Box[A]]
1414

1515
val Unit: Box[Unit] =

library/coreGeneric/src/main/scala/japgolly/scalajs/react/internal/MacroLogger.scala

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,15 @@ class MacroLogger {
6969

7070
def apply(name: => Any, value: => Any)(implicit l: Line): Unit =
7171
apply(s"$YELLOW$name:$RESET $value")
72+
73+
def all(name: => Any, values: => Iterable[Any])(implicit l: Line): Unit =
74+
if (enabled) {
75+
val vs = values
76+
val total = vs.size
77+
var i = 0
78+
for (v <- vs) {
79+
i += 1
80+
apply(s"$name [$i/$total]", v)
81+
}
82+
}
7283
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package japgolly.scalajs.react.test.emissions
2+
3+
import japgolly.scalajs.react._
4+
import japgolly.scalajs.react.vdom.html_<^._
5+
6+
object HooksWithChildren {
7+
8+
val Component = ScalaFnComponent.withHooks[Int]
9+
.withPropsChildren
10+
.useState(123)
11+
.render { (p, c, s1) =>
12+
val sum = p + s1.value + c.count
13+
<.button(
14+
"Sum = ", sum,
15+
^.onClick --> s1.modState(_ + 1),
16+
c
17+
)
18+
}
19+
}

library/testEmissions/js/src/main/scala/japgolly/scalajs/react/test/emissions/Main.scala

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ object Main {
1414

1515
private val Component = ScalaFnComponent[Unit] { _ =>
1616
<.div(
17-
UseState.Component(),
17+
HooksWithChildren.Component(0)(<.div),
18+
NoHooksWithChildren.Component(0)(<.div),
19+
UseState.Component(0),
1820
)
1921
}
2022
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package japgolly.scalajs.react.test.emissions
2+
3+
import japgolly.scalajs.react._
4+
import japgolly.scalajs.react.vdom.html_<^._
5+
6+
object NoHooksWithChildren {
7+
8+
val Component = ScalaFnComponent.withHooks[Int]
9+
.withPropsChildren
10+
.render { (p, c) =>
11+
val sum = p + c.count
12+
<.div("DEBUG = ", sum)
13+
}
14+
}

library/testEmissions/js/src/main/scala/japgolly/scalajs/react/test/emissions/UseState.scala

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,15 @@ import japgolly.scalajs.react.vdom.html_<^._
55

66
object UseState {
77

8-
val Component = ScalaFnComponent.withHooks[Unit]
8+
val Component = ScalaFnComponent.withHooks[Int]
99
.useState(123)
10-
.render { (_, s) =>
10+
.useStateBy((p, s1) => p + s1.value)
11+
.useStateBy($ => $.props + $.hook1.value + $.hook2.value)
12+
.render { (_, s1, s2, s3) =>
13+
val sum = s1.value + s2.value + s3.value
1114
<.button(
12-
"Count is ", s.value,
13-
^.onClick --> s.modState(_ + 1),
15+
"Sum = ", sum,
16+
^.onClick --> s1.modState(_ + 1),
1417
)
1518
}
1619
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import React from 'react'
2+
3+
function App() {
4+
return React.createElement("div", {}, "count is: 0");
5+
}
6+
7+
export default App
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import React from 'react';
2+
3+
function App() {
4+
return React.createElement("div", {}, "count is: 0");
5+
}
6+
7+
_c = App;
8+
export default App;
9+
10+
var _c;
11+
12+
$RefreshReg$(_c, "App");
File renamed without changes.

0 commit comments

Comments
 (0)