Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

rx-html (breaking): RxElement.onMount(...) requires a node argument #3806

Merged
merged 5 commits into from
Feb 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion airframe-log/src/main/scala/wvlet/log/Logger.scala
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ object Logger {
}.foreach { level =>
l.setLogLevel(level)
}
l
()
}

def getDefaultLogLevel: LogLevel = rootLogger.getLogLevel
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
*/
package wvlet.airframe.rx.html
import org.scalajs.dom
import org.scalajs.dom.{MutationObserver, MutationObserverInit}
import wvlet.airframe.rx.{Cancelable, OnError, OnNext, Rx, RxOps, RxRunner}
import wvlet.log.LogSupport

Expand Down Expand Up @@ -80,7 +81,7 @@ object DOMRenderer extends LogSupport {
case Success(r) =>
traverse(r)
case Failure(e) =>
warn(s"Failed to render ${rx}", e)
warn(s"Failed to render ${rx}: ${e.getMessage}", e)
// Embed an empty node
(dom.document.createElement("span"), Cancelable.empty)
}
Expand All @@ -98,7 +99,7 @@ object DOMRenderer extends LogSupport {
case r: RxElement =>
r.beforeRender
val (n, c) = render(r)
r.onMount
r.onMount(n)
(n, Cancelable.merge(Cancelable(() => r.beforeUnmount), c))
case d: dom.Node =>
(d, Cancelable.empty)
Expand Down Expand Up @@ -166,7 +167,7 @@ object DOMRenderer extends LogSupport {
c1 = traverse(value, Some(start), ctx)
ctx.onFinish()
case OnError(e) =>
warn(s"An unhandled error occurred while rendering ${rx}:\n${e.getMessage}", e)
warn(s"An unhandled error occurred while rendering ${rx}: ${e.getMessage}", e)
c1 = Cancelable.empty
case other =>
c1 = Cancelable.empty
Expand All @@ -187,11 +188,27 @@ object DOMRenderer extends LogSupport {
val c1 = renderToInternal(localContext, node, r)
val elem = node.lastChild
val c2 = rx.traverseModifiers(m => renderToInternal(localContext, elem, m))
if ((rx.onMount _) ne RxElement.NoOp) {
val observer: MutationObserver = new MutationObserver({ (mut, obs) =>
mut.foreach { m =>
m.addedNodes.find(_ eq elem).foreach { n =>
rx.onMount(elem)
}
}
obs.disconnect()
})
observer.observe(
node,
new MutationObserverInit {
attributes = node.nodeType == dom.Node.ATTRIBUTE_NODE
childList = node.nodeType != dom.Node.ATTRIBUTE_NODE
}
)
}
node.mountHere(elem, anchor)
localContext.addOnRenderHook(() => rx.onMount)
Cancelable.merge(Cancelable(() => rx.beforeUnmount), Cancelable.merge(c1, c2))
case Failure(e) =>
warn(s"Failed to render ${rx}:\n${e.getMessage}", e)
warn(s"Failed to render ${rx}: ${e.getMessage}", e)
Cancelable(() => rx.beforeUnmount)
}
case s: String =>
Expand Down Expand Up @@ -310,7 +327,7 @@ object DOMRenderer extends LogSupport {
case OnNext(value) =>
c1 = traverse(value)
case OnError(e) =>
warn(s"An unhandled error occurred while rendering ${rx}", e)
warn(s"An unhandled error occurred while rendering ${rx}: ${e.getMessage}", e)
c1 = Cancelable.empty
case other =>
c1 = Cancelable.empty
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package wvlet.airframe.http.rx.html

import org.scalajs.dom
import org.scalajs.dom.HTMLElement
import wvlet.airframe.rx.{Cancelable, Rx}
import wvlet.airframe.rx.html.{DOMRenderer, RxElement}
import wvlet.airframe.rx.html.all.*
import wvlet.airspec.AirSpec

class RxOnMountTest extends AirSpec {

private def render(v: Any): (HTMLElement, Cancelable) = {
val (n, c) = v match {
case rx: Rx[RxElement] @unchecked =>
DOMRenderer.createNode(div(rx))
case other: RxElement =>
DOMRenderer.createNode(other)
}
(n.asInstanceOf[HTMLElement], c)
}

test("beforeRender/beforeUnmount") {
var a = 0
var b = 0
var afterRenderCount = 0

val v = Rx.variable(1)
val r = new RxElement {
override def beforeRender: Unit = {
a += 1
}

override def onMount(n: Any) = {
afterRenderCount += 1
}

override def beforeUnmount: Unit = {
b += 1
afterRenderCount shouldBe 1
}

override def render: RxElement = span(v.map { x => s"hello ${x}" })
}

a shouldBe 0
afterRenderCount shouldBe 0
val (n, c) = render(r)
n.outerHTML shouldBe "<span>hello 1</span>"
a shouldBe 1
b shouldBe 0

// Updating inner element should not trigger on render
v := 2
a shouldBe 1
afterRenderCount shouldBe 1
b shouldBe 0
n.outerHTML shouldBe "<span>hello 2</span>"

// unmounting
c.cancel
a shouldBe 1
b shouldBe 1
}

test("nested beforeRender/beforeUnmount") {
var a = false
var b = false
var afterRenderFlag = false

var a1 = false
var b1 = false
var afterRenderFlag1 = false

var rendered = Rx.variable(false)

val nested = new RxElement {
override def beforeRender: Unit = {
a1 = true
}

override def onMount(n: Any) = {
debug("afterMount: nested")
afterRenderFlag = true
rendered := true
rendered.stop()
}

override def beforeUnmount: Unit = {
debug(s"beforeUnmount: nested")
b1 = true
}

override def render: RxElement = span("initial")
}

val r = new RxElement {
override def beforeRender: Unit = {
a = true
}

override def onMount(n: Any) = {
debug(s"afterMount: r")
afterRenderFlag1 = true
}

override def beforeUnmount: Unit = {
debug(s"beforeUnmount: r")
b = true
}

override def render: RxElement = span(nested)
}

afterRenderFlag shouldBe false
afterRenderFlag1 shouldBe false
val (n, c) = render(r)

rendered.lastOption.map { isFullyRendered =>
isFullyRendered shouldBe true
afterRenderFlag shouldBe true
afterRenderFlag1 shouldBe true
a shouldBe true
b shouldBe false
a1 shouldBe true
b1 shouldBe false
c.cancel
b shouldBe true
b1 shouldBe true
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
*/
package wvlet.airframe.http.rx.html

import org.scalajs.dom
import org.scalajs.dom.{HTMLElement, document}
import wvlet.airframe.rx.{Cancelable, Rx, html}
import wvlet.airframe.rx.html.{DOMRenderer, Embedded, RxElement}
Expand Down Expand Up @@ -78,7 +79,7 @@ object RxRenderingTest extends AirSpec {
a += 1
}

override def onMount: Unit = {
override def onMount(node: Any): Unit = {
afterRenderCount += 1
}
override def beforeUnmount: Unit = {
Expand Down Expand Up @@ -120,7 +121,7 @@ object RxRenderingTest extends AirSpec {
a += 1
}

override def onMount: Unit = {
override def onMount(n: Any): Unit = {
afterRenderCount += 1
}
override def beforeUnmount: Unit = {
Expand Down Expand Up @@ -153,56 +154,6 @@ object RxRenderingTest extends AirSpec {
b shouldBe 1
}

test("nested beforeRender/beforeUnmount") {
var a = false
var b = false
var afterRenderFlag = false

var a1 = false
var b1 = false
var afterRenderFlag1 = false

val nested = new RxElement {
override def beforeRender: Unit = {
a1 = true
}
override def onMount: Unit = {
afterRenderFlag = true
}
override def beforeUnmount: Unit = {
b1 = true
}
override def render: RxElement = span("nested")
}

val r = new RxElement {
override def beforeRender: Unit = {
a = true
}
override def onMount: Unit = {
afterRenderFlag1 = true
}
override def beforeUnmount: Unit = {
b = true
}
override def render: RxElement = span(nested)
}

val (n, c) = render(r)
a shouldBe true
b shouldBe false
afterRenderFlag shouldBe true
a1 shouldBe true
b1 shouldBe false
afterRenderFlag shouldBe true

c.cancel
b shouldBe true
b1 shouldBe true
afterRenderFlag shouldBe true
afterRenderFlag1 shouldBe true
}

test("rendering attributes with Rx") {
val a = Rx.variable("primary")
val e = new RxElement {
Expand Down Expand Up @@ -274,16 +225,17 @@ object RxRenderingTest extends AirSpec {
}

test("render attributes with onMount hook") {
var updated = false
val updated = Rx.variable(false)

def findSpan000 = Option(document.getElementById("span000"))

val label = new RxElement() {
override def onMount: Unit = {
override def onMount(n: Any): Unit = {
logger.debug(s"onRender span: ${findSpan000}")
findSpan000.foreach { e =>
e.setAttribute("class", "active")
updated = true
updated := true
updated.stop()
}
}
override def render: RxElement = {
Expand All @@ -293,9 +245,10 @@ object RxRenderingTest extends AirSpec {
}

val main = new RxElement {
override def onMount: Unit = {
override def onMount(n: Any): Unit = {
logger.debug("onRender main")
}

override def render: RxElement = {
logger.debug(s"render main: ${findSpan000}")
div(
Expand All @@ -304,28 +257,32 @@ object RxRenderingTest extends AirSpec {
}
}
val c = main.renderTo("main")
updated shouldBe true

updated.lastOption.map { f =>
f shouldBe true
}
}

test("call onMount hook in nested RxElements") {
val page = Rx.variable("main")

var topLevelOnMountCallCount = 0
var nestedOnMountCallCount = 0
var foundElement = false
val foundElement = Rx.variable(false)

object infoPage extends RxElement {
override def onMount: Unit = {
override def onMount(n: Any): Unit = {
nestedOnMountCallCount += 1
Option(org.scalajs.dom.document.getElementById("id001")).collect { case e: HTMLElement =>
foundElement = true
foundElement := true
foundElement.stop()
}
}
override def render: RxElement = div(id -> "id001", "render: info")
}

object nestedPage extends RxElement() {
override def onMount: Unit = {
override def onMount(n: Any): Unit = {
topLevelOnMountCallCount += 1
}

Expand All @@ -342,9 +299,12 @@ object RxRenderingTest extends AirSpec {
org.scalajs.dom.document.getElementById("id001") shouldMatch { case e: HTMLElement =>
e.innerHTML shouldContain "render: info"
}
topLevelOnMountCallCount shouldBe 1
nestedOnMountCallCount shouldBe 1
foundElement shouldBe true

foundElement.lastOption.map { flag =>
flag shouldBe true
topLevelOnMountCallCount shouldBe 1
nestedOnMountCallCount shouldBe 1
}
}

test("refresh attribute with RxVar") {
Expand Down
Loading
Loading