From 9bed8e0cb8d7e60a79f8b4dde6db1e88d13a7578 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Mon, 29 Jun 2020 00:09:28 +0100 Subject: [PATCH] Update DOM node properties and listeners on renderer update (#117) * Update DOM properties and listeners on renderer update * Fix listeners not passed in EmptyView init --- Sources/TokamakDOM/DOMRenderer.swift | 40 ++++++++++++++++++++-------- Sources/TokamakDOM/Views/HTML.swift | 22 ++++++++++----- 2 files changed, 44 insertions(+), 18 deletions(-) diff --git a/Sources/TokamakDOM/DOMRenderer.swift b/Sources/TokamakDOM/DOMRenderer.swift index 730c6c47..3a3bce51 100644 --- a/Sources/TokamakDOM/DOMRenderer.swift +++ b/Sources/TokamakDOM/DOMRenderer.swift @@ -20,10 +20,31 @@ import TokamakCore public final class DOMNode: Target { let ref: JSObjectRef + private var listeners: [String: JSClosure] - init(_ view: V, _ ref: JSObjectRef) { + init(_ view: V, _ ref: JSObjectRef, _ listeners: [String: Listener] = [:]) { self.ref = ref + self.listeners = [:] super.init(view) + reinstall(listeners) + } + + /// Removes all existing event listeners on this DOM node and install new ones from + /// the `listeners` argument + func reinstall(_ listeners: [String: Listener]) { + for (event, jsClosure) in self.listeners { + _ = ref.removeEventListener!(event, jsClosure) + } + self.listeners = [:] + + for (event, listener) in listeners { + let jsClosure = JSClosure { + listener($0[0].object!) + return .undefined + } + _ = ref.addEventListener!(event, jsClosure) + self.listeners[event] = jsClosure + } } } @@ -36,7 +57,11 @@ public final class DOMRenderer: Renderer { public init(_ view: V, _ ref: JSObjectRef) { rootRef = ref - reconciler = StackReconciler(view: view, target: DOMNode(view, ref), renderer: self) { closure in + reconciler = StackReconciler( + view: view, + target: DOMNode(view, ref), + renderer: self + ) { closure in let fn = JSClosure { _ in closure() return .undefined @@ -67,14 +92,7 @@ public final class DOMRenderer: Renderer { let lastChild = children[Int(length) - 1].object else { return nil } - for (event, listener) in listeners { - _ = lastChild.addEventListener!(event, JSClosure { - listener($0[0].object!) - return .undefined - }) - } - - return DOMNode(host.view, lastChild) + return DOMNode(host.view, lastChild, listeners) } public func update(target: DOMNode, with host: MountedHost) { @@ -83,7 +101,7 @@ public final class DOMRenderer: Renderer { transform: { (html: AnyHTML) in html } ) else { return } - html.update(dom: target.ref) + html.update(dom: target) } public func unmount( diff --git a/Sources/TokamakDOM/Views/HTML.swift b/Sources/TokamakDOM/Views/HTML.swift index 482fe021..0c0e1d68 100644 --- a/Sources/TokamakDOM/Views/HTML.swift +++ b/Sources/TokamakDOM/Views/HTML.swift @@ -42,10 +42,21 @@ extension AnyHTML { """ } - func update(dom: JSObjectRef) { - // FIXME: handle attributes and listeners here + func update(dom: DOMNode) { + // FIXME: is there a sensible way to diff attributes and listeners to avoid + // crossing the JavaScript bridge and touching DOM if not needed? + + // @carson-katri: For diffing, could you build a Set from the keys and values of the dictionary, + // then use the standard lib to get the difference? + + for (attribute, value) in attributes { + _ = dom.ref[dynamicMember: attribute] = JSValue(stringLiteral: value) + } + + dom.reinstall(listeners) + guard let innerHTML = innerHTML else { return } - dom.innerHTML = .string(innerHTML) + dom.ref.innerHTML = .string(innerHTML) } } @@ -80,10 +91,7 @@ extension HTML where Content == EmptyView { _ attributes: [String: String] = [:], listeners: [String: Listener] = [:] ) { - self.tag = tag - self.attributes = attributes - self.listeners = listeners - content = EmptyView() + self = HTML(tag, attributes, listeners: listeners) { EmptyView() } } }