Skip to content

Commit

Permalink
API: Merge EventfulNode trait into ReactiveElement
Browse files Browse the repository at this point in the history
  • Loading branch information
raquo committed Mar 14, 2020
1 parent 08eb42d commit 23db4aa
Show file tree
Hide file tree
Showing 4 changed files with 72 additions and 76 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ Breaking changes in **bold**.
* Combine `domapi.*Api` traits into a single `DomApi` object
* Use the new `DomApi` object directly instead of passing implicit `domapi.*Api` parameters.
* Move `Setter` and `EventPropSetter` into Laminar and simplify type signatures
* Merge into relevant Laminar subtypes: `Node` -> `ReactiveNode` (add Ref type param), `Comment` -> `ReactiveComment`, `Text` -> `TextNode`, `ParentNode` -> `ParentNode`, `ChildNode` -> `ChildNode`, `EventfulNode` -> `EventfulNode`, `Root` -> `RootNode`, `TagSyntax` -> `HtmlTag` and `SvgTag`
* Merge into relevant Laminar subtypes: `Node` -> `ReactiveNode` (add Ref type param), `Comment` -> `ReactiveComment`, `Text` -> `TextNode`, `ParentNode` -> `ParentNode`, `ChildNode` -> `ChildNode`, `Root` -> `RootNode`, `TagSyntax` -> `HtmlTag` and `SvgTag`
* Merge `EventfulNode` trait into `ReactiveElement` (split members between the trait and the object)
* Change type of `maybeEventListeners` to have `List` instead of `mutable.Buffer`
* **Migration:** If you reference any of the affected types directly, you will need to import them from Laminar, or use their corresponding Laminar replacements listed above. Other than that, everything should just work.
* API: `ReactiveElement` and other node types that take type params now have `type Base` defined on their companion objects containing the most generic version of that type, e.g. `ReactiveElement[dom.Element]` for `ReactiveElement`.

Expand Down
69 changes: 0 additions & 69 deletions src/main/scala/com/raquo/laminar/nodes/EventfulNode.scala

This file was deleted.

67 changes: 65 additions & 2 deletions src/main/scala/com/raquo/laminar/nodes/ReactiveElement.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,28 @@ import com.raquo.airstream.ownership.{Owned, Owner}
import com.raquo.airstream.signal.Signal
import com.raquo.domtypes
import com.raquo.domtypes.generic.keys.EventProp
import com.raquo.laminar.DomApi
import com.raquo.laminar.emitter.EventPropEmitter
import com.raquo.laminar.lifecycle.{MountEvent, NodeDidMount, NodeWasDiscarded, NodeWillUnmount, ParentChangeEvent}
import com.raquo.laminar.nodes.ChildNode.isParentMounted
import com.raquo.laminar.nodes.ReactiveElement.noMountEvents
import com.raquo.laminar.receivers.{ChildReceiver, ChildrenCommandReceiver, ChildrenReceiver, MaybeChildReceiver, TextChildReceiver}
import com.raquo.laminar.setters.EventPropSetter
import org.scalajs.dom

import scala.collection.mutable

trait ReactiveElement[+Ref <: dom.Element]
extends EventfulNode[Ref]
with ChildNode[Ref]
extends ChildNode[Ref]
with ParentNode[Ref]
with domtypes.generic.nodes.Element
with Owner {

// @TODO[Naming] We reuse EventPropSetter to represent an active event listener. Makes for a bit confusing naming.
private[ReactiveElement] var _maybeEventListeners: Option[mutable.Buffer[EventPropSetter[_]]] = None

@inline def maybeEventListeners: Option[List[EventPropSetter[_]]] = _maybeEventListeners.map(_.toList)

/** Event bus that emits parent change events.
* For efficiency, it is only populated when someone accesses [[parentChangeEvents]]
*/
Expand Down Expand Up @@ -201,4 +209,59 @@ object ReactiveElement {
type Base = ReactiveElement[dom.Element]

private val noMountEvents: EventStream[MountEvent] = EventStream.fromSeq(Nil, emitOnce = true)

/** @return Whether listener was added (false if such a listener has already been present) */
def addEventListener[Ev <: dom.Event](element: ReactiveElement.Base, listener: EventPropSetter[Ev]): Boolean = {
val shouldAddListener = indexOfEventListener(element, listener) == -1
if (shouldAddListener) {
// 1. Update this node
if (element._maybeEventListeners.isEmpty) {
element._maybeEventListeners = Some(mutable.Buffer(listener))
} else {
element._maybeEventListeners.foreach { eventListeners =>
eventListeners += listener
}
}
// 2. Update the DOM
DomApi.addEventListener(element, listener)
}
shouldAddListener
}

/** @return Whether listener was removed (false if such a listener was not found) */
def removeEventListener[Ev <: dom.Event](element: ReactiveElement.Base, listener: EventPropSetter[Ev]): Boolean = {
val index = indexOfEventListener(element, listener)
val shouldRemoveListener = index != -1
if (shouldRemoveListener) {
// 1. Update this node
element._maybeEventListeners.foreach(eventListeners => eventListeners.remove(index))
// 2. Update the DOM
DomApi.removeEventListener(element, listener)
}
shouldRemoveListener
}

/** @return -1 if not found */
def indexOfEventListener[Ev <: dom.Event](element: ReactiveElement.Base, listener: EventPropSetter[Ev]): Int = {
// Note: Ugly for performance.
// - We want to reduce usage of Scala's collections and anonymous functions
// - js.Array is unaware of Scala's `equals` method
val notFoundIndex = -1
if (element._maybeEventListeners.isEmpty) {
notFoundIndex
} else {
var found = false
var index = 0
element._maybeEventListeners.foreach { listeners =>
while (!found && index < listeners.length) {
if (listener equals listeners(index)) {
found = true
} else {
index += 1
}
}
}
if (found) index else notFoundIndex
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.raquo.laminar.setters

import com.raquo.domtypes.generic.Modifier
import com.raquo.domtypes.generic.keys.EventProp
import com.raquo.laminar.nodes.EventfulNode
import com.raquo.laminar.nodes.ReactiveElement
import org.scalajs.dom

import scala.scalajs.js
Expand All @@ -13,7 +13,7 @@ class EventPropSetter[Ev <: dom.Event](
val key: EventProp[Ev],
val value: Ev => Unit,
val useCapture: Boolean
) extends Modifier[EventfulNode[dom.Element]] {
) extends Modifier[ReactiveElement.Base] {

/** To make sure that you remove the event listener successfully in JS DOM, you need to
* provide the same Javascript callback function that was originally added as a listener.
Expand All @@ -24,8 +24,8 @@ class EventPropSetter[Ev <: dom.Event](
*/
val domValue: js.Function1[Ev, Unit] = value

override def apply(node: EventfulNode[dom.Element]): Unit = {
node.addEventListener(this)
override def apply(node: ReactiveElement.Base): Unit = {
ReactiveElement.addEventListener(node, this)
}

override def equals(that: Any): Boolean = {
Expand Down

0 comments on commit 23db4aa

Please sign in to comment.