Skip to content

Commit 9374772

Browse files
committed
Merge multiple event handlers
Closes #767
1 parent 31294dc commit 9374772

File tree

4 files changed

+68
-11
lines changed

4 files changed

+68
-11
lines changed

core/src/main/scala/japgolly/scalajs/react/vdom/Attr.scala

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,15 +67,18 @@ object Attr {
6767
}
6868

6969
final class Event[E[+x <: dom.Node] <: raw.SyntheticEvent[x]](name: String)
70-
extends Generic[js.Function1[E[Nothing], Any]](name) {
70+
extends Attr[js.Function1[E[Nothing], Unit]](name) {
7171

7272
type Event = E[Nothing]
7373

74+
override def :=[A](a: A)(implicit t: ValueType[A, js.Function1[E[Nothing], Unit]]): TagMod =
75+
TagMod.fn(b => t.fn(f => b.addEventHandler(name, f.asInstanceOf[js.Function1[js.Any, Unit]]), a))
76+
7477
def -->[A: DomCallbackResult](callback: => CallbackTo[A]): TagMod =
7578
==>(_ => callback)
7679

7780
def ==>[A: DomCallbackResult](eventHandler: Event => CallbackTo[A]): TagMod =
78-
:=(((e: Event) => eventHandler(e).runNow()): js.Function1[E[Nothing], Any])(ValueType.direct)
81+
:=(((e: Event) => eventHandler(e).runNow()): js.Function1[E[Nothing], Unit])(ValueType.direct)
7982

8083
def -->?[O[_]](callback: => O[Callback])(implicit o: OptionLike[O]): TagMod =
8184
this --> Callback(o.foreach(callback)(_.runNow()))

core/src/main/scala/japgolly/scalajs/react/vdom/Builder.scala

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@ import scala.scalajs.js
88
/** Mutable target for immutable VDOM constituents to compose.
99
*/
1010
trait Builder {
11-
val addAttr : (String, js.Any) => Unit
12-
val addClassName : js.Any => Unit
13-
val addStyle : (String, js.Any) => Unit
14-
val addStylesObject: js.Object => Unit
15-
val appendChild : RawChild => Unit
16-
val setKey : js.Any => Unit
11+
val addAttr : (String, js.Any) => Unit
12+
val addClassName : js.Any => Unit
13+
val addEventHandler: (String, js.Function1[js.Any, Unit]) => Unit
14+
val addStyle : (String, js.Any) => Unit
15+
val addStylesObject: js.Object => Unit
16+
val appendChild : RawChild => Unit
17+
val setKey : js.Any => Unit
1718

1819
final def addStyles(j: js.Any): Unit = {
1920
// Hack because Attr.ValueType.Fn takes a js.Any => Unit.
@@ -28,13 +29,18 @@ trait Builder {
2829
object Builder {
2930
type RawChild = raw.React.Node
3031

31-
def setObjectKeyValue(o: js.Object, k: String, v: js.Any): Unit =
32+
@inline def setObjectKeyValue(o: js.Object, k: String, v: js.Any): Unit =
3233
o.asInstanceOf[js.Dynamic].updateDynamic(k)(v)
3334

34-
def nonEmptyObject[O <: js.Object](o: O): js.UndefOr[O] =
35+
@inline def modObjectKeyValue[A <: js.Any](o: js.Object, k: String, v: js.UndefOr[A] => A): Unit = {
36+
val cur = o.asInstanceOf[js.Dynamic].selectDynamic(k).asInstanceOf[js.UndefOr[A]]
37+
o.asInstanceOf[js.Dynamic].updateDynamic(k)(v(cur))
38+
}
39+
40+
@inline def nonEmptyObject[O <: js.Object](o: O): js.UndefOr[O] =
3541
if (js.Object.keys(o).length == 0) js.undefined else o
3642

37-
def nonEmptyJsArray[A](as: js.Array[A]): js.UndefOr[js.Array[A]] =
43+
@inline def nonEmptyJsArray[A](as: js.Array[A]): js.UndefOr[js.Array[A]] =
3844
if (as.length == 0) js.undefined else as
3945

4046
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -73,6 +79,12 @@ object Builder {
7379
override val addClassName: js.Any => Unit =
7480
n => nonEmptyClassName = nonEmptyClassName.fold(n)(_.toString + " " + n)
7581

82+
override val addEventHandler =
83+
(k, g) => modObjectKeyValue[js.Function1[js.Any, Unit]](
84+
props,
85+
k,
86+
c => if (c.isEmpty) g else {val f = c.get; e => {f(e); g(e)}})
87+
7688
override val addStyle: (String, js.Any) => Unit =
7789
setObjectKeyValue(styles, _, _)
7890

doc/changelog/1.7.5.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# 1.7.5
2+
3+
* Multiple event handlers are now run in sequence, rather than the latest overriding the previous.
4+
5+
Example:
6+
7+
```scala
8+
<.input(
9+
^.onKeyDown ==> handler1,
10+
^.onKeyDown ==> handler2,
11+
```
12+

test/src/test/scala/japgolly/scalajs/react/core/vdom/VdomTest.scala

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import japgolly.scalajs.react._
44
import japgolly.scalajs.react.test.TestUtil._
55
import japgolly.scalajs.react.test._
66
import japgolly.scalajs.react.vdom.html_<^._
7+
import org.scalajs.dom.ext.KeyCode
8+
import org.scalajs.dom.html
79
import scala.annotation.nowarn
810
import utest._
911

@@ -79,5 +81,33 @@ object VdomTest extends TestSuite {
7981
}
8082
}
8183

84+
"multipleEventHandlers" - {
85+
val c =
86+
ScalaComponent.builder[Unit]
87+
.initialState("init")
88+
.noBackend
89+
.renderS { ($, s) =>
90+
91+
val eh1: ReactKeyboardEventFromInput => Callback =
92+
e => $.setState("enter!").when_(e.keyCode == KeyCode.Enter)
93+
94+
val eh2: ReactKeyboardEventFromInput => Callback =
95+
e => $.setState("SPACE!").when_(e.keyCode == KeyCode.Space)
96+
97+
<.input(
98+
^.onKeyDown ==> eh1,
99+
^.onKeyDown ==> eh2,
100+
^.readOnly := true,
101+
^.value := s)
102+
}
103+
.build
104+
ReactTestUtils.withRenderedIntoBody(c()) { m =>
105+
def txt() = m.getDOMNode.asMounted().domCast[html.Input].value
106+
SimEvent.Keyboard.Enter.simulateKeyDown(m)
107+
assertEq(txt(), "enter!")
108+
SimEvent.Keyboard.Space.simulateKeyDown(m)
109+
assertEq(txt(), "SPACE!")
110+
}
111+
}
82112
}
83113
}

0 commit comments

Comments
 (0)