Skip to content

Commit

Permalink
Add Static HTML Renderer and Documentation (#204)
Browse files Browse the repository at this point in the history
  • Loading branch information
carson-katri authored Aug 1, 2020
1 parent fbb8937 commit 4c654da
Show file tree
Hide file tree
Showing 47 changed files with 848 additions and 94 deletions.
26 changes: 23 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ let package = Package(
name: "TokamakShim",
targets: ["TokamakShim"]
),
.library(
name: "TokamakStaticHTML",
targets: ["TokamakStaticHTML"]
),
.executable(
name: "TokamakStaticDemo",
targets: ["TokamakStaticDemo"]
),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
Expand All @@ -51,17 +59,29 @@ let package = Package(
dependencies: ["CombineShim", "Runtime"]
),
.target(
name: "TokamakDemo",
dependencies: ["JavaScriptKit", "TokamakShim"]
name: "TokamakStaticHTML",
dependencies: [
"TokamakCore",
]
),
.target(
name: "TokamakDOM",
dependencies: ["CombineShim", "JavaScriptKit", "TokamakCore"]
dependencies: ["CombineShim", "JavaScriptKit", "TokamakCore", "TokamakStaticHTML"]
),
.target(
name: "TokamakShim",
dependencies: [.target(name: "TokamakDOM", condition: .when(platforms: [.wasi]))]
),
.target(
name: "TokamakDemo",
dependencies: ["JavaScriptKit", "TokamakShim"]
),
.target(
name: "TokamakStaticDemo",
dependencies: [
"TokamakStaticHTML",
]
),
.target(
name: "TokamakTestRenderer",
dependencies: ["TokamakCore"]
Expand Down
1 change: 1 addition & 0 deletions Sources/TokamakDOM/App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import CombineShim
import JavaScriptKit
import TokamakCore
import TokamakStaticHTML

private enum ScenePhaseObserver {
static var publisher = CurrentValueSubject<ScenePhase, Never>(.active)
Expand Down
22 changes: 22 additions & 0 deletions Sources/TokamakDOM/DOMNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,28 @@

import JavaScriptKit
import TokamakCore
import TokamakStaticHTML

extension AnyHTML {
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] = .string(value)
}

if let dynamicSelf = self as? AnyDynamicHTML {
dom.reinstall(dynamicSelf.listeners)
}

guard let innerHTML = innerHTML else { return }
dom.ref.innerHTML = .string(innerHTML)
}
}

final class DOMNode: Target {
let ref: JSObjectRef
Expand Down
15 changes: 10 additions & 5 deletions Sources/TokamakDOM/DOMRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import JavaScriptKit
import TokamakCore
import TokamakStaticHTML

extension EnvironmentValues {
/// Returns default settings for the DOM environment
Expand Down Expand Up @@ -91,10 +92,10 @@ final class DOMRenderer: Renderer {
)
}

func mountTarget(to parent: DOMNode, with host: MountedHost) -> DOMNode? {
guard let (outerHTML, listeners) = mapAnyView(
public func mountTarget(to parent: DOMNode, with host: MountedHost) -> DOMNode? {
guard let anyHTML = mapAnyView(
host.view,
transform: { (html: AnyHTML) in (html.outerHTML, html.listeners) }
transform: { (html: AnyHTML) in html }
) else {
// handle cases like `TupleView`
if mapAnyView(host.view, transform: { (view: ParentView) in view }) != nil {
Expand All @@ -104,7 +105,7 @@ final class DOMRenderer: Renderer {
return nil
}

_ = parent.ref.insertAdjacentHTML!("beforeend", JSValue(stringLiteral: outerHTML))
_ = parent.ref.insertAdjacentHTML!("beforeend", JSValue(stringLiteral: anyHTML.outerHTML))

guard
let children = parent.ref.childNodes.object,
Expand All @@ -121,7 +122,11 @@ final class DOMRenderer: Renderer {
lastChild.style.object!.height = "100%"
}

return DOMNode(host.view, lastChild, listeners)
if let dynamicHTML = anyHTML as? AnyDynamicHTML {
return DOMNode(host.view, lastChild, dynamicHTML.listeners)
} else {
return DOMNode(host.view, lastChild, [:])
}
}

func update(target: DOMNode, with host: MountedHost) {
Expand Down
5 changes: 4 additions & 1 deletion Sources/TokamakDOM/Styles/ToggleStyle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,14 @@
//

import TokamakCore
import TokamakStaticHTML

public struct DefaultToggleStyle: ToggleStyle {
public func makeBody(configuration: Configuration) -> some View {
CheckboxToggleStyle().makeBody(configuration: configuration)
}

public init() {}
}

public struct CheckboxToggleStyle: ToggleStyle {
Expand All @@ -30,7 +33,7 @@ public struct CheckboxToggleStyle: ToggleStyle {
attrs["checked"] = "checked"
}
return HTML("label") {
HTML("input", attrs, listeners: [
DynamicHTML("input", attrs, listeners: [
"change": { event in
let checked = event.target.object?.checked.boolean ?? false
configuration.isOn = checked
Expand Down
2 changes: 1 addition & 1 deletion Sources/TokamakDOM/Views/Buttons/Button.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ extension _Button: ViewDeferredToRenderer where Label == Text {
attributes = ["class": "_tokamak-buttonstyle-reset"]
}

return AnyView(HTML("button", attributes, listeners: [
return AnyView(DynamicHTML("button", attributes, listeners: [
"click": { _ in action() },
"pointerdown": { _ in isPressed = true },
"pointerup": { _ in isPressed = false },
Expand Down
17 changes: 10 additions & 7 deletions Sources/TokamakDOM/Views/Containers/DisclosureGroup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,19 @@
//

import TokamakCore
import TokamakStaticHTML

extension DisclosureGroup: ViewDeferredToRenderer {
var chevron: some View {
HTML("div",
["class": "_tokamak-disclosuregroup-chevron-container"],
listeners: [
"click": { _ in
_DisclosureGroupProxy(self).toggleIsExpanded()
},
]) {
DynamicHTML(
"div",
["class": "_tokamak-disclosuregroup-chevron-container"],
listeners: [
"click": { _ in
_DisclosureGroupProxy(self).toggleIsExpanded()
},
]
) {
HTML("div", ["class": "_tokamak-disclosuregroup-chevron"])
.rotationEffect(_DisclosureGroupProxy(self).isExpanded ?
.degrees(90) :
Expand Down
67 changes: 67 additions & 0 deletions Sources/TokamakDOM/Views/DynamicHTML.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// 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.
//
// Created by Carson Katri on 7/31/20.
//

import JavaScriptKit
import TokamakCore
import TokamakStaticHTML

public typealias Listener = (JSObjectRef) -> ()

protocol AnyDynamicHTML: AnyHTML {
var listeners: [String: Listener] { get }
}

public struct DynamicHTML<Content>: View, AnyDynamicHTML where Content: View {
public let tag: String
public let attributes: [String: String]
public 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 innerHTML: String? { nil }

public var body: Never {
neverBody("HTML")
}
}

extension DynamicHTML where Content == EmptyView {
public init(
_ tag: String,
_ attributes: [String: String] = [:],
listeners: [String: Listener] = [:]
) {
self = DynamicHTML(tag, attributes, listeners: listeners) { EmptyView() }
}
}

extension DynamicHTML: ParentView {
public var children: [AnyView] {
[AnyView(content)]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ extension NavigationLink: ViewDeferredToRenderer {
public var deferredBody: AnyView {
let proxy = _NavigationLinkProxy(self)
return AnyView(
HTML("a", [
DynamicHTML("a", [
"href": "javascript:void%200",
], listeners: [
// FIXME: Focus destination or something so assistive
Expand Down
3 changes: 2 additions & 1 deletion Sources/TokamakDOM/Views/Selectors/Picker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@

import JavaScriptKit
import TokamakCore
import TokamakStaticHTML

extension _PickerContainer: ViewDeferredToRenderer {
public var deferredBody: AnyView {
AnyView(HTML("label") {
label
Text(" ")
HTML("select", listeners: ["change": {
DynamicHTML("select", listeners: ["change": {
guard
let valueString = $0.target.object!.value.string,
let value = Int(valueString) as? SelectionValue
Expand Down
2 changes: 1 addition & 1 deletion Sources/TokamakDOM/Views/Text/SecureField.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import TokamakCore
extension SecureField: ViewDeferredToRenderer where Label == Text {
public var deferredBody: AnyView {
let proxy = _SecureFieldProxy(self)
return AnyView(HTML("input", [
return AnyView(DynamicHTML("input", [
"type": "password",
"value": proxy.textBinding.wrappedValue,
"placeholder": proxy.label.rawText,
Expand Down
2 changes: 1 addition & 1 deletion Sources/TokamakDOM/Views/Text/TextField.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ extension TextField: ViewDeferredToRenderer where Label == Text {
public var deferredBody: AnyView {
let proxy = _TextFieldProxy(self)

return AnyView(HTML("input", [
return AnyView(DynamicHTML("input", [
"type": proxy.textFieldStyle is RoundedBorderTextFieldStyle ? "search" : "text",
"value": proxy.textBinding.wrappedValue,
"placeholder": proxy.label.rawText,
Expand Down
6 changes: 1 addition & 5 deletions Sources/TokamakDemo/AppStorageDemo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,7 @@
// Created by Carson Katri on 7/17/20.
//

#if canImport(SwiftUI)
import SwiftUI
#else
import TokamakDOM
#endif
import TokamakShim

@available(OSX 11.0, iOS 14.0, *)
struct AppStorageButtons: View {
Expand Down
7 changes: 1 addition & 6 deletions Sources/TokamakDemo/ToggleDemo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

#if canImport(SwiftUI)
import SwiftUI
#else
import TokamakCore
import TokamakDOM
#endif
import TokamakShim

public struct ToggleDemo: View {
@State var checked = false
Expand Down
24 changes: 24 additions & 0 deletions Sources/TokamakStaticDemo/ContentView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// 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.
//
// Created by Carson Katri on 7/31/20.
//

import TokamakStaticHTML

struct ContentView: View {
var body: some View {
Text("Hello, world!")
}
}
28 changes: 28 additions & 0 deletions Sources/TokamakStaticDemo/main.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// 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.
//
// Created by Carson Katri on 7/20/20.
//

import TokamakStaticHTML

struct TestApp: App {
var body: some Scene {
WindowGroup("TokamakStaticHTML Demo") {
ContentView()
}
}
}

print(StaticHTMLRenderer(TestApp()).html)
Loading

0 comments on commit 4c654da

Please sign in to comment.