Add basic DOM renderer
This commit is contained in:
parent
5e85356a1c
commit
426bb999c5
|
@ -0,0 +1 @@
|
|||
wasm-DEVELOPMENT-SNAPSHOT-2020-06-07-a
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"editor.formatOnSave": true,
|
||||
"licenser.author": "Tokamak contributors"
|
||||
}
|
|
@ -12,6 +12,11 @@
|
|||
"label": "swift test",
|
||||
"type": "shell",
|
||||
"command": "swift test"
|
||||
},
|
||||
{
|
||||
"label": "carton dev",
|
||||
"type": "shell",
|
||||
"command": "carton dev --product TokamakDemo"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,12 +1,21 @@
|
|||
{
|
||||
"object": {
|
||||
"pins": [
|
||||
{
|
||||
"package": "JavaScriptKit",
|
||||
"repositoryURL": "https://github.com/MaxDesiatov/JavaScriptKit.git",
|
||||
"state": {
|
||||
"branch": "1edcf70",
|
||||
"revision": "1edcf707dcb06f50e980c6cac542f226361dc124",
|
||||
"version": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "Runtime",
|
||||
"repositoryURL": "https://github.com/MaxDesiatov/Runtime.git",
|
||||
"state": {
|
||||
"branch": "wasi-build",
|
||||
"revision": "4ac000b019460c5eb5b9d7a8d7aaa1cb24076bf6",
|
||||
"revision": "a617ead8a125a97e69d6100e4d27922006e82e0a",
|
||||
"version": null
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,9 +12,13 @@ let package = Package(
|
|||
products: [
|
||||
// Products define the executables and libraries produced by a package,
|
||||
// and make them visible to other packages.
|
||||
.executable(
|
||||
name: "TokamakDemo",
|
||||
targets: ["TokamakDemo"]
|
||||
),
|
||||
.library(
|
||||
name: "Tokamak",
|
||||
targets: ["Tokamak"]
|
||||
name: "TokamakDOM",
|
||||
targets: ["TokamakDOM"]
|
||||
),
|
||||
.library(
|
||||
name: "TokamakTestRenderer",
|
||||
|
@ -24,6 +28,7 @@ let package = Package(
|
|||
dependencies: [
|
||||
// Dependencies declare other packages that this package depends on.
|
||||
// .package(url: /* package url */, from: "1.0.0"),
|
||||
.package(url: "https://github.com/MaxDesiatov/JavaScriptKit.git", .revision("1edcf70")),
|
||||
.package(url: "https://github.com/MaxDesiatov/Runtime.git", .branch("wasi-build")),
|
||||
],
|
||||
targets: [
|
||||
|
@ -37,11 +42,11 @@ let package = Package(
|
|||
),
|
||||
.target(
|
||||
name: "TokamakDemo",
|
||||
dependencies: ["Tokamak"]
|
||||
dependencies: ["JavaScriptKit", "Tokamak", "TokamakDOM"]
|
||||
),
|
||||
.target(
|
||||
name: "TokamakDOM",
|
||||
dependencies: ["Tokamak"]
|
||||
dependencies: ["JavaScriptKit", "Tokamak"]
|
||||
),
|
||||
.target(
|
||||
name: "TokamakTestRenderer",
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
// Created by Max Desiatov on 03/12/2018.
|
||||
//
|
||||
|
||||
import Dispatch
|
||||
import Runtime
|
||||
|
||||
final class MountedCompositeView<R: Renderer>: MountedView<R>, Hashable {
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
// Created by Max Desiatov on 28/11/2018.
|
||||
//
|
||||
|
||||
import Dispatch
|
||||
import Runtime
|
||||
|
||||
public final class StackReconciler<R: Renderer> {
|
||||
|
@ -14,9 +13,16 @@ public final class StackReconciler<R: Renderer> {
|
|||
public let rootTarget: R.TargetType
|
||||
private let rootView: MountedView<R>
|
||||
private(set) weak var renderer: R?
|
||||
private let scheduler: (@escaping () -> ()) -> ()
|
||||
|
||||
public init<V: View>(view: V, target: R.TargetType, renderer: R) {
|
||||
public init<V: View>(
|
||||
view: V,
|
||||
target: R.TargetType,
|
||||
renderer: R,
|
||||
scheduler: @escaping (@escaping () -> ()) -> ()
|
||||
) {
|
||||
self.renderer = renderer
|
||||
self.scheduler = scheduler
|
||||
rootTarget = target
|
||||
|
||||
rootView = view.makeMountedView(target)
|
||||
|
@ -36,9 +42,7 @@ public final class StackReconciler<R: Renderer> {
|
|||
|
||||
guard scheduleReconcile else { return }
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.updateStateAndReconcile()
|
||||
}
|
||||
scheduler { [weak self] in self?.updateStateAndReconcile() }
|
||||
}
|
||||
|
||||
private func updateStateAndReconcile() {
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
open class Target {
|
||||
public internal(set) var view: AnyView
|
||||
|
||||
public init<V: View>(view: V) {
|
||||
public init<V: View>(_ view: V) {
|
||||
self.view = AnyView(view)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,8 +44,14 @@ public struct AnyView: View {
|
|||
}
|
||||
}
|
||||
|
||||
public func mapAnyView<T, V>(_ anyView: AnyView, transform: (V) -> T) -> T? {
|
||||
guard let view = anyView.view as? V else { return nil }
|
||||
|
||||
return transform(view)
|
||||
}
|
||||
|
||||
extension AnyView: ParentView {
|
||||
var children: [AnyView] {
|
||||
public var children: [AnyView] {
|
||||
(view as? ParentView)?.children ?? []
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,9 @@
|
|||
|
||||
public struct Button<Label>: View where Label: View {
|
||||
let label: Label
|
||||
let action: () -> ()
|
||||
|
||||
// FIXME: this should be internal
|
||||
public let action: () -> ()
|
||||
|
||||
public init(action: @escaping () -> (), @ViewBuilder label: () -> Label) {
|
||||
self.label = label()
|
||||
|
@ -19,3 +21,13 @@ extension Button where Label == Text {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Button: ParentView {
|
||||
public var children: [AnyView] {
|
||||
(label as? GroupView)?.children ?? [AnyView(label)]
|
||||
}
|
||||
}
|
||||
|
||||
public func buttonLabel(_ button: Button<Text>) -> String {
|
||||
button.label.content
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ public struct HStack<Content>: View where Content: View {
|
|||
}
|
||||
|
||||
extension HStack: ParentView {
|
||||
var children: [AnyView] {
|
||||
public var children: [AnyView] {
|
||||
(content as? GroupView)?.children ?? [AnyView(content)]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,8 +3,7 @@
|
|||
//
|
||||
|
||||
public struct Text: View {
|
||||
// FIXME: should be internal
|
||||
public let content: String
|
||||
let content: String
|
||||
|
||||
public init(verbatim content: String) {
|
||||
self.content = content
|
||||
|
@ -14,3 +13,7 @@ public struct Text: View {
|
|||
self.content = String(content)
|
||||
}
|
||||
}
|
||||
|
||||
public func textContent(_ text: Text) -> String {
|
||||
text.content
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
public struct TupleView<T>: View {
|
||||
public let value: T
|
||||
|
||||
let children: [AnyView]
|
||||
public let children: [AnyView]
|
||||
|
||||
public init(_ value: T) {
|
||||
self.value = value
|
||||
|
|
|
@ -17,7 +17,7 @@ public extension View where Body == Never {
|
|||
}
|
||||
|
||||
/// A `View` type that renders with subviews, usually specified in the `Content` type argument
|
||||
protocol ParentView {
|
||||
public protocol ParentView {
|
||||
var children: [AnyView] { get }
|
||||
}
|
||||
|
||||
|
|
|
@ -4,8 +4,13 @@
|
|||
|
||||
import Tokamak
|
||||
|
||||
extension Button: ViewDeferredToRenderer {
|
||||
public typealias Button = Tokamak.Button
|
||||
|
||||
extension Button: ViewDeferredToRenderer where Label == Text {
|
||||
public var deferredBody: AnyView {
|
||||
AnyView(HTML(tag: "button"))
|
||||
AnyView(HTML(
|
||||
tag: "button",
|
||||
listeners: ["click": { _ in action() }]
|
||||
) { Text(buttonLabel(self)) })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,18 +2,63 @@
|
|||
// Created by Max Desiatov on 11/04/2020.
|
||||
//
|
||||
|
||||
import JavaScriptKit
|
||||
import Tokamak
|
||||
|
||||
public final class DOMNode: Target {}
|
||||
public final class DOMNode: Target {
|
||||
let ref: JSObjectRef
|
||||
|
||||
init<V: View>(_ view: V, _ ref: JSObjectRef) {
|
||||
self.ref = ref
|
||||
super.init(view)
|
||||
}
|
||||
}
|
||||
|
||||
public final class DOMRenderer: Renderer {
|
||||
public private(set) var reconciler: StackReconciler<DOMRenderer>?
|
||||
|
||||
public func mountTarget(to parent: DOMNode, with view: MountedHost) -> DOMNode? {
|
||||
nil
|
||||
public init<V: View>(_ view: V, _ ref: JSObjectRef) {
|
||||
reconciler = StackReconciler(view: view, target: DOMNode(view, ref), renderer: self) { closure in
|
||||
let fn = JSFunctionRef.from { _ in
|
||||
closure()
|
||||
return .undefined
|
||||
}
|
||||
_ = JSObjectRef.global.setTimeout!(fn, 0)
|
||||
}
|
||||
}
|
||||
|
||||
public func mountTarget(to parent: DOMNode, with host: MountedHost) -> DOMNode? {
|
||||
guard let (html, listeners) = mapAnyView(
|
||||
host.view,
|
||||
transform: { (html: AnyHTML) in (html.description, html.listeners) }
|
||||
)
|
||||
else { return nil }
|
||||
|
||||
parent.ref.innerHTML = JSValue(stringLiteral: html)
|
||||
|
||||
guard
|
||||
let children = parent.ref.childNodes.object,
|
||||
let length = children.length.number,
|
||||
length > 0,
|
||||
let firstChild = children[0].object
|
||||
else { return nil }
|
||||
|
||||
for (event, listener) in listeners {
|
||||
_ = firstChild.addEventListener!(event, JSFunctionRef.from {
|
||||
listener($0[0].object!)
|
||||
return .undefined
|
||||
})
|
||||
}
|
||||
|
||||
return DOMNode(host.view, firstChild)
|
||||
}
|
||||
|
||||
public func update(target: DOMNode, with view: MountedHost) {}
|
||||
|
||||
public func unmount(target: DOMNode, from parent: DOMNode, with view: MountedHost, completion: @escaping () -> ()) {}
|
||||
public func unmount(
|
||||
target: DOMNode,
|
||||
from parent: DOMNode,
|
||||
with view: MountedHost,
|
||||
completion: @escaping () -> ()
|
||||
) {}
|
||||
}
|
||||
|
|
|
@ -2,31 +2,53 @@
|
|||
// Created by Max Desiatov on 11/04/2020.
|
||||
//
|
||||
|
||||
import JavaScriptKit
|
||||
import Tokamak
|
||||
|
||||
public struct HTML<Content>: View where Content: View {
|
||||
public typealias Listener = (JSObjectRef) -> ()
|
||||
|
||||
protocol AnyHTML: CustomStringConvertible {
|
||||
var listeners: [String: Listener] { get }
|
||||
}
|
||||
|
||||
public struct HTML<Content>: View, AnyHTML where Content: View {
|
||||
let tag: String
|
||||
let attributes: [String: String]
|
||||
let listeners: [String: Listener]
|
||||
let content: Content
|
||||
|
||||
public init(
|
||||
tag: String,
|
||||
attributes: [String: String] = [:],
|
||||
listeners: [String: Listener] = [:],
|
||||
@ViewBuilder content: () -> Content
|
||||
) {
|
||||
self.tag = tag
|
||||
self.attributes = attributes
|
||||
self.listeners = listeners
|
||||
self.content = content()
|
||||
}
|
||||
|
||||
public var description: String {
|
||||
"<\(tag) \(attributes.map { #"\#($0)="\#($1)""# }.joined(separator: " "))></\(tag)>"
|
||||
}
|
||||
}
|
||||
|
||||
extension HTML where Content == EmptyView {
|
||||
public init(
|
||||
tag: String,
|
||||
attributes: [String: String] = [:]
|
||||
attributes: [String: String] = [:],
|
||||
listeners: [String: Listener] = [:]
|
||||
) {
|
||||
self.tag = tag
|
||||
self.attributes = attributes
|
||||
self.listeners = listeners
|
||||
content = EmptyView()
|
||||
}
|
||||
}
|
||||
|
||||
extension HTML: ParentView {
|
||||
public var children: [AnyView] {
|
||||
[AnyView(content)]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
// Copyright 2020 Tokamak contributors
|
||||
//
|
||||
// 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.
|
||||
|
||||
import Tokamak
|
||||
|
||||
public typealias Text = Tokamak.Text
|
||||
|
||||
extension Text: AnyHTML {
|
||||
public var description: String { textContent(self) }
|
||||
|
||||
var listeners: [String: Listener] { [:] }
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import JavaScriptKit
|
||||
import TokamakDOM
|
||||
|
||||
let document = JSObjectRef.global.document.object!
|
||||
|
||||
let divElement = document.createElement!("div").object!
|
||||
let renderer = DOMRenderer(Button("button", action: { print("hello") }), divElement)
|
||||
|
||||
let body = document.body.object!
|
||||
_ = body.appendChild!(divElement)
|
|
@ -2,6 +2,7 @@
|
|||
// Created by Max Desiatov on 21/12/2018.
|
||||
//
|
||||
|
||||
import Dispatch
|
||||
import Tokamak
|
||||
|
||||
public final class TestRenderer: Renderer {
|
||||
|
@ -13,7 +14,9 @@ public final class TestRenderer: Renderer {
|
|||
|
||||
public init<V: View>(_ view: V) {
|
||||
// FIXME: the root target shouldn't be `EmptyView`, but something more sensible, maybe Group?
|
||||
reconciler = StackReconciler(view: view, target: TestView(EmptyView()), renderer: self)
|
||||
reconciler = StackReconciler(view: view, target: TestView(EmptyView()), renderer: self) {
|
||||
DispatchQueue.main.async(execute: $0)
|
||||
}
|
||||
}
|
||||
|
||||
public func mountTarget(
|
||||
|
|
|
@ -21,7 +21,7 @@ public final class TestView: Target {
|
|||
init<V: View>(_ view: V,
|
||||
_ subviews: [TestView] = []) {
|
||||
self.subviews = subviews
|
||||
super.init(view: view)
|
||||
super.init(view)
|
||||
}
|
||||
|
||||
/** Add a subview to this test view.
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
import XCTest
|
||||
|
||||
import TokamakTests
|
||||
|
||||
var tests = [XCTestCaseEntry]()
|
||||
tests += TokamakTests.__allTests()
|
||||
|
||||
XCTMain(tests)
|
|
@ -0,0 +1,31 @@
|
|||
#if !canImport(ObjectiveC)
|
||||
import XCTest
|
||||
|
||||
extension ColorTests {
|
||||
// DO NOT MODIFY: This is autogenerated, use:
|
||||
// `swift test --generate-linuxmain`
|
||||
// to regenerate.
|
||||
static let __allTests__ColorTests = [
|
||||
("testHexColors", testHexColors),
|
||||
]
|
||||
}
|
||||
|
||||
extension ReconcilerTests {
|
||||
// DO NOT MODIFY: This is autogenerated, use:
|
||||
// `swift test --generate-linuxmain`
|
||||
// to regenerate.
|
||||
static let __allTests__ReconcilerTests = [
|
||||
("testDoubleUpdate", testDoubleUpdate),
|
||||
("testMount", testMount),
|
||||
("testUnmount", testUnmount),
|
||||
("testUpdate", testUpdate),
|
||||
]
|
||||
}
|
||||
|
||||
public func __allTests() -> [XCTestCaseEntry] {
|
||||
return [
|
||||
testCase(ColorTests.__allTests__ColorTests),
|
||||
testCase(ReconcilerTests.__allTests__ReconcilerTests),
|
||||
]
|
||||
}
|
||||
#endif
|
Loading…
Reference in New Issue