Add Static HTML Renderer and Documentation (#204)
This commit is contained in:
parent
fbb893739b
commit
4c654da456
|
@ -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.
|
||||
|
@ -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"]
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
import CombineShim
|
||||
import JavaScriptKit
|
||||
import TokamakCore
|
||||
import TokamakStaticHTML
|
||||
|
||||
private enum ScenePhaseObserver {
|
||||
static var publisher = CurrentValueSubject<ScenePhase, Never>(.active)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
import JavaScriptKit
|
||||
import TokamakCore
|
||||
import TokamakStaticHTML
|
||||
|
||||
extension EnvironmentValues {
|
||||
/// Returns default settings for the DOM environment
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
@ -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
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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) :
|
||||
|
|
|
@ -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)]
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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!")
|
||||
}
|
||||
}
|
|
@ -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)
|
|
@ -0,0 +1,33 @@
|
|||
// 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 CombineShim
|
||||
import TokamakCore
|
||||
|
||||
extension App {
|
||||
public static func _launch(_ app: Self, _ rootEnvironment: EnvironmentValues) {
|
||||
fatalError("TokamakStaticHTML does not support default `App._launch`")
|
||||
}
|
||||
|
||||
public static func _setTitle(_ title: String) {
|
||||
StaticHTMLRenderer.title = title
|
||||
}
|
||||
|
||||
public var _phasePublisher: CurrentValueSubject<ScenePhase, Never> {
|
||||
CurrentValueSubject<ScenePhase, Never>(.active)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
// 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 TokamakCore
|
||||
|
||||
// MARK: Environment & State
|
||||
|
||||
public typealias Environment = TokamakCore.Environment
|
||||
|
||||
// MARK: Modifiers & Styles
|
||||
|
||||
public typealias ViewModifier = TokamakCore.ViewModifier
|
||||
public typealias ModifiedContent = TokamakCore.ModifiedContent
|
||||
|
||||
public typealias DefaultListStyle = TokamakCore.DefaultListStyle
|
||||
public typealias PlainListStyle = TokamakCore.PlainListStyle
|
||||
public typealias InsetListStyle = TokamakCore.InsetListStyle
|
||||
public typealias GroupedListStyle = TokamakCore.GroupedListStyle
|
||||
public typealias InsetGroupedListStyle = TokamakCore.InsetGroupedListStyle
|
||||
|
||||
// MARK: Shapes
|
||||
|
||||
public typealias Shape = TokamakCore.Shape
|
||||
|
||||
public typealias Capsule = TokamakCore.Capsule
|
||||
public typealias Circle = TokamakCore.Circle
|
||||
public typealias Ellipse = TokamakCore.Ellipse
|
||||
public typealias Path = TokamakCore.Path
|
||||
public typealias Rectangle = TokamakCore.Rectangle
|
||||
public typealias RoundedRectangle = TokamakCore.RoundedRectangle
|
||||
|
||||
// MARK: Primitive values
|
||||
|
||||
public typealias Color = TokamakCore.Color
|
||||
public typealias Font = TokamakCore.Font
|
||||
|
||||
public typealias CGAffineTransform = TokamakCore.CGAffineTransform
|
||||
public typealias CGPoint = TokamakCore.CGPoint
|
||||
public typealias CGRect = TokamakCore.CGRect
|
||||
public typealias CGSize = TokamakCore.CGSize
|
||||
|
||||
// MARK: Views
|
||||
|
||||
public typealias Divider = TokamakCore.Divider
|
||||
public typealias ForEach = TokamakCore.ForEach
|
||||
public typealias GridItem = TokamakCore.GridItem
|
||||
public typealias Group = TokamakCore.Group
|
||||
public typealias HStack = TokamakCore.HStack
|
||||
public typealias LazyHGrid = TokamakCore.LazyHGrid
|
||||
public typealias LazyVGrid = TokamakCore.LazyVGrid
|
||||
public typealias List = TokamakCore.List
|
||||
public typealias ScrollView = TokamakCore.ScrollView
|
||||
public typealias Section = TokamakCore.Section
|
||||
public typealias Spacer = TokamakCore.Spacer
|
||||
public typealias Text = TokamakCore.Text
|
||||
public typealias VStack = TokamakCore.VStack
|
||||
public typealias ZStack = TokamakCore.ZStack
|
||||
|
||||
// MARK: Special Views
|
||||
|
||||
public typealias View = TokamakCore.View
|
||||
public typealias AnyView = TokamakCore.AnyView
|
||||
public typealias EmptyView = TokamakCore.EmptyView
|
||||
|
||||
// MARK: App & Scene
|
||||
|
||||
public typealias App = TokamakCore.App
|
||||
public typealias Scene = TokamakCore.Scene
|
||||
public typealias WindowGroup = TokamakCore.WindowGroup
|
||||
public typealias ScenePhase = TokamakCore.ScenePhase
|
||||
public typealias AppStorage = TokamakCore.AppStorage
|
||||
public typealias SceneStorage = TokamakCore.SceneStorage
|
||||
|
||||
// MARK: Misc
|
||||
|
||||
// FIXME: I would put this inside TokamakCore, but for
|
||||
// some reason it doesn't get exported with the typealias
|
||||
extension Text {
|
||||
public static func + (lhs: Self, rhs: Self) -> Self {
|
||||
_concatenating(lhs: lhs, rhs: rhs)
|
||||
}
|
||||
}
|
|
@ -12,7 +12,7 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
let tokamakStyles = """
|
||||
public let tokamakStyles = """
|
||||
._tokamak-stack > * {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
@ -65,7 +65,7 @@ let tokamakStyles = """
|
|||
}
|
||||
"""
|
||||
|
||||
let rootNodeStyles = """
|
||||
public let rootNodeStyles = """
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
|
@ -0,0 +1,135 @@
|
|||
// 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 TokamakCore
|
||||
|
||||
public final class HTMLTarget: Target {
|
||||
var html: AnyHTML
|
||||
var children: [HTMLTarget] = []
|
||||
|
||||
public var view: AnyView
|
||||
|
||||
init<V: View>(_ view: V, _ html: AnyHTML) {
|
||||
self.html = html
|
||||
self.view = AnyView(view)
|
||||
}
|
||||
|
||||
init(_ html: AnyHTML) {
|
||||
self.html = html
|
||||
view = AnyView(EmptyView())
|
||||
}
|
||||
}
|
||||
|
||||
extension HTMLTarget {
|
||||
var outerHTML: String {
|
||||
"""
|
||||
<\(html.tag)\(html.attributes.isEmpty ? "" : " ")\
|
||||
\(html.attributes.map { #"\#($0)="\#($1)""# }.joined(separator: " "))>\
|
||||
\(html.innerHTML ?? "")\
|
||||
\(children.map(\.outerHTML).joined(separator: "\n"))\
|
||||
</\(html.tag)>
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
struct HTMLBody: AnyHTML {
|
||||
let tag: String = "body"
|
||||
let innerHTML: String? = nil
|
||||
let attributes: [String: String] = [
|
||||
"style": "margin: 0;" + rootNodeStyles,
|
||||
]
|
||||
}
|
||||
|
||||
public final class StaticHTMLRenderer: Renderer {
|
||||
public private(set) var reconciler: StackReconciler<StaticHTMLRenderer>?
|
||||
|
||||
var rootTarget: HTMLTarget
|
||||
|
||||
static var title: String = ""
|
||||
public var html: String {
|
||||
"""
|
||||
<html>
|
||||
<head>
|
||||
<title>\(Self.title)</title>
|
||||
<style>
|
||||
\(tokamakStyles)
|
||||
</style>
|
||||
</head>
|
||||
\(rootTarget.outerHTML)
|
||||
</html>
|
||||
"""
|
||||
}
|
||||
|
||||
public init<V: View>(_ view: V, _ rootEnvironment: EnvironmentValues? = nil) {
|
||||
rootTarget = HTMLTarget(view, HTMLBody())
|
||||
|
||||
reconciler = StackReconciler(
|
||||
view: view,
|
||||
target: rootTarget,
|
||||
environment: EnvironmentValues(),
|
||||
renderer: self,
|
||||
scheduler: { _ in
|
||||
fatalError("Stateful apps cannot be created with TokamakStaticHTML")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
public init<A: App>(_ app: A, _ rootEnvironment: EnvironmentValues? = nil) {
|
||||
rootTarget = HTMLTarget(HTMLBody())
|
||||
|
||||
reconciler = StackReconciler(
|
||||
app: app,
|
||||
target: rootTarget,
|
||||
environment: EnvironmentValues(),
|
||||
renderer: self,
|
||||
scheduler: { _ in
|
||||
fatalError("Stateful apps cannot be created with TokamakStaticHTML")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
public func mountTarget(to parent: HTMLTarget, with host: MountedHost) -> HTMLTarget? {
|
||||
guard let html = mapAnyView(
|
||||
host.view,
|
||||
transform: { (html: AnyHTML) in html }
|
||||
) else {
|
||||
// handle cases like `TupleView`
|
||||
if mapAnyView(host.view, transform: { (view: ParentView) in view }) != nil {
|
||||
return parent
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
let node = HTMLTarget(host.view, html)
|
||||
parent.children.append(node)
|
||||
return node
|
||||
}
|
||||
|
||||
public func update(target: HTMLTarget, with host: MountedHost) {
|
||||
fatalError("Stateful apps cannot be created with TokamakStaticHTML")
|
||||
}
|
||||
|
||||
public func unmount(
|
||||
target: HTMLTarget,
|
||||
from parent: HTMLTarget,
|
||||
with host: MountedHost,
|
||||
completion: @escaping () -> ()
|
||||
) {
|
||||
fatalError("Stateful apps cannot be created with TokamakStaticHTML")
|
||||
}
|
||||
}
|
|
@ -15,16 +15,12 @@
|
|||
// Created by Max Desiatov on 11/04/2020.
|
||||
//
|
||||
|
||||
import JavaScriptKit
|
||||
import TokamakCore
|
||||
|
||||
public typealias Listener = (JSObjectRef) -> ()
|
||||
|
||||
protocol AnyHTML {
|
||||
public protocol AnyHTML {
|
||||
var innerHTML: String? { get }
|
||||
var tag: String { get }
|
||||
var attributes: [String: String] { get }
|
||||
var listeners: [String: Listener] { get }
|
||||
}
|
||||
|
||||
extension AnyHTML {
|
||||
|
@ -36,44 +32,24 @@ extension AnyHTML {
|
|||
</\(tag)>
|
||||
"""
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
dom.reinstall(listeners)
|
||||
|
||||
guard let innerHTML = innerHTML else { return }
|
||||
dom.ref.innerHTML = .string(innerHTML)
|
||||
}
|
||||
}
|
||||
|
||||
public struct HTML<Content>: View, AnyHTML where Content: View {
|
||||
let tag: String
|
||||
let attributes: [String: String]
|
||||
let listeners: [String: Listener]
|
||||
public let tag: String
|
||||
public let attributes: [String: String]
|
||||
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()
|
||||
}
|
||||
|
||||
var innerHTML: String? { nil }
|
||||
public var innerHTML: String? { nil }
|
||||
|
||||
public var body: Never {
|
||||
neverBody("HTML")
|
||||
|
@ -83,10 +59,9 @@ public struct HTML<Content>: View, AnyHTML where Content: View {
|
|||
extension HTML where Content == EmptyView {
|
||||
public init(
|
||||
_ tag: String,
|
||||
_ attributes: [String: String] = [:],
|
||||
listeners: [String: Listener] = [:]
|
||||
_ attributes: [String: String] = [:]
|
||||
) {
|
||||
self = HTML(tag, attributes, listeners: listeners) { EmptyView() }
|
||||
self = HTML(tag, attributes) { EmptyView() }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -96,12 +71,12 @@ extension HTML: ParentView {
|
|||
}
|
||||
}
|
||||
|
||||
protocol StylesConvertible {
|
||||
public protocol StylesConvertible {
|
||||
var styles: [String: String] { get }
|
||||
}
|
||||
|
||||
extension Dictionary {
|
||||
var inlineStyles: String {
|
||||
public var inlineStyles: String {
|
||||
map { "\($0.0): \($0.1);" }
|
||||
.joined(separator: " ")
|
||||
}
|
|
@ -28,7 +28,7 @@ extension VerticalAlignment {
|
|||
}
|
||||
|
||||
extension HStack: ViewDeferredToRenderer, SpacerContainer {
|
||||
var axis: SpacerContainerAxis { .horizontal }
|
||||
public var axis: SpacerContainerAxis { .horizontal }
|
||||
|
||||
public var deferredBody: AnyView {
|
||||
AnyView(HTML("div", [
|
|
@ -18,9 +18,9 @@
|
|||
import TokamakCore
|
||||
|
||||
extension LazyHGrid: SpacerContainer {
|
||||
var axis: SpacerContainerAxis { .horizontal }
|
||||
var hasSpacer: Bool { false }
|
||||
var fillCrossAxis: Bool {
|
||||
public var axis: SpacerContainerAxis { .horizontal }
|
||||
public var hasSpacer: Bool { false }
|
||||
public var fillCrossAxis: Bool {
|
||||
_LazyHGridProxy(self).rows.contains {
|
||||
if case .adaptive(minimum: _, maximum: _) = $0.size {
|
||||
return true
|
||||
|
@ -32,7 +32,7 @@ extension LazyHGrid: SpacerContainer {
|
|||
}
|
||||
|
||||
extension LazyHGrid: ViewDeferredToRenderer {
|
||||
var lastRow: GridItem? {
|
||||
public var lastRow: GridItem? {
|
||||
_LazyHGridProxy(self).rows.last
|
||||
}
|
||||
|
|
@ -18,9 +18,9 @@
|
|||
import TokamakCore
|
||||
|
||||
extension LazyVGrid: SpacerContainer {
|
||||
var axis: SpacerContainerAxis { .vertical }
|
||||
var hasSpacer: Bool { false }
|
||||
var fillCrossAxis: Bool {
|
||||
public var axis: SpacerContainerAxis { .vertical }
|
||||
public var hasSpacer: Bool { false }
|
||||
public var fillCrossAxis: Bool {
|
||||
_LazyVGridProxy(self).columns.contains {
|
||||
if case .adaptive(minimum: _, maximum: _) = $0.size {
|
||||
return true
|
||||
|
@ -32,7 +32,7 @@ extension LazyVGrid: SpacerContainer {
|
|||
}
|
||||
|
||||
extension LazyVGrid: ViewDeferredToRenderer {
|
||||
var lastColumn: GridItem? {
|
||||
public var lastColumn: GridItem? {
|
||||
_LazyVGridProxy(self).columns.last
|
||||
}
|
||||
|
|
@ -18,7 +18,7 @@
|
|||
import TokamakCore
|
||||
|
||||
extension ScrollView: ViewDeferredToRenderer, SpacerContainer {
|
||||
var axis: SpacerContainerAxis {
|
||||
public var axis: SpacerContainerAxis {
|
||||
if axes.contains(.horizontal) {
|
||||
return .horizontal
|
||||
} else {
|
|
@ -28,7 +28,7 @@ extension HorizontalAlignment {
|
|||
}
|
||||
|
||||
extension VStack: ViewDeferredToRenderer, SpacerContainer {
|
||||
var axis: SpacerContainerAxis { .vertical }
|
||||
public var axis: SpacerContainerAxis { .vertical }
|
||||
|
||||
public var deferredBody: AnyView {
|
||||
AnyView(HTML("div", [
|
|
@ -12,7 +12,6 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import JavaScriptKit
|
||||
import TokamakCore
|
||||
|
||||
extension NavigationView: ViewDeferredToRenderer {
|
|
@ -15,9 +15,9 @@
|
|||
import TokamakCore
|
||||
|
||||
extension Divider: AnyHTML {
|
||||
var innerHTML: String? { nil }
|
||||
var tag: String { "hr" }
|
||||
var attributes: [String: String] {
|
||||
public var innerHTML: String? { nil }
|
||||
public var tag: String { "hr" }
|
||||
public var attributes: [String: String] {
|
||||
[
|
||||
"style": """
|
||||
width: 100%; height: 0; margin: 0;
|
||||
|
@ -28,6 +28,4 @@ extension Divider: AnyHTML {
|
|||
""",
|
||||
]
|
||||
}
|
||||
|
||||
var listeners: [String: Listener] { [:] }
|
||||
}
|
|
@ -14,18 +14,18 @@
|
|||
|
||||
import TokamakCore
|
||||
|
||||
enum SpacerContainerAxis {
|
||||
public enum SpacerContainerAxis {
|
||||
case horizontal, vertical
|
||||
}
|
||||
|
||||
protocol SpacerContainer {
|
||||
public protocol SpacerContainer {
|
||||
var hasSpacer: Bool { get }
|
||||
var axis: SpacerContainerAxis { get }
|
||||
var fillCrossAxis: Bool { get }
|
||||
}
|
||||
|
||||
extension SpacerContainer where Self: ParentView {
|
||||
var hasSpacer: Bool {
|
||||
public var hasSpacer: Bool {
|
||||
children
|
||||
.compactMap {
|
||||
mapAnyView($0) { (v: Spacer) in
|
||||
|
@ -45,7 +45,7 @@ extension SpacerContainer where Self: ParentView {
|
|||
// Does a child SpacerContainer along the opposite axis have a spacer?
|
||||
// (e.g., an HStack with a child VStack which contains a spacer)
|
||||
// If so, we need to fill the cross-axis so the child can show the correct layout.
|
||||
var fillCrossAxis: Bool {
|
||||
public var fillCrossAxis: Bool {
|
||||
children
|
||||
.compactMap {
|
||||
mapAnyView($0) { (v: SpacerContainer) in v }
|
|
@ -12,7 +12,6 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import JavaScriptKit
|
||||
import TokamakCore
|
||||
|
||||
extension Font.Design: CustomStringConvertible {
|
||||
|
@ -78,7 +77,7 @@ extension Font.Leading: CustomStringConvertible {
|
|||
}
|
||||
|
||||
extension Font: StylesConvertible {
|
||||
var styles: [String: String] {
|
||||
public var styles: [String: String] {
|
||||
[
|
||||
"font-family": _name == _FontNames.system.rawValue ? _design.description : _name,
|
||||
"font-weight": "\(_bold ? Font.Weight.bold.value : _weight.value)",
|
||||
|
@ -102,8 +101,8 @@ extension Text: AnyHTML {
|
|||
}
|
||||
}
|
||||
|
||||
var tag: String { "span" }
|
||||
var attributes: [String: String] {
|
||||
public var tag: String { "span" }
|
||||
public var attributes: [String: String] {
|
||||
var font: Font?
|
||||
var color: Color?
|
||||
var italic: Bool = false
|
||||
|
@ -159,6 +158,4 @@ extension Text: AnyHTML {
|
|||
""",
|
||||
]
|
||||
}
|
||||
|
||||
var listeners: [String: Listener] { [:] }
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
# `Renderers` in Tokamak
|
||||
|
||||
Tokamak is a flexible library. `TokamakCore` provides the SwiftUI-API, which your `Renderer` can use
|
||||
to construct a representation of `Views` that your platform understands.
|
||||
|
||||
To explain the creation of `Renderers`, we’ll be creating a simple one: `TokamakStaticHTML` (which
|
||||
you can find in the `Tokamak` repository).
|
||||
|
||||
Before we create the `Renderer`, we need to understand the requirements of our platform:
|
||||
|
||||
1. Stateful apps cannot be created This simplifies the scope of our project, as we only have to
|
||||
render once. However, if you are building a `Renderer` that supports state changes, the process
|
||||
is largely the same. `TokamakCore`’s `StackReconciler` will let your `Renderer` know when a
|
||||
`View` has to be redrawn.
|
||||
2. HTML should be rendered `TokamakDOM` provides HTML representations of many `Views`, so we can
|
||||
utilize it. However, we will cover how to provide custom `View` bodies your `Renderer` can
|
||||
understand, and when you are required to do so.
|
||||
|
||||
And that’s it! In the next part we’ll go more in depth on `Renderers`.
|
|
@ -0,0 +1,17 @@
|
|||
# Understanding `Renderers`
|
||||
|
||||
So, what goes into a `Renderer`?
|
||||
|
||||
1. A `Target` - Targets are the destination for rendered `Views`. For instance, on iOS this is
|
||||
`UIView`, on macOS an `NSView`, and on the web we render to DOM nodes.
|
||||
2. A `StackReconciler` - The reconciler does all the heavy lifting to understand the view tree. It
|
||||
notifies your `Renderer` of what views need to be mounted/unmounted.
|
||||
3. `func mountTarget`- This function is called when a new target instance should be created and
|
||||
added to the parent (either as a subview or some other way, e.g. installed if it’s a layout
|
||||
constraint).
|
||||
4. `func update` - This function is called when an existing target instance should be updated (e.g.
|
||||
when `State` changes).
|
||||
5. `func unmount` - This function is called when an existing target instance should be unmounted:
|
||||
removed from the parent and most likely destroyed.
|
||||
|
||||
That’s it! Let’s get our project setup.
|
|
@ -0,0 +1,80 @@
|
|||
# `TokamakStaticHTML` Setup
|
||||
|
||||
Every `Renderer` can choose what `Views`, `ViewModifiers`, property wrappers, etc. are available to
|
||||
use. A `Core.swift` file is used to reexport these symbols. For `TokamakStaticHTML`, we’ll use the
|
||||
following `Core.swift` file:
|
||||
|
||||
```swift
|
||||
import TokamakCore
|
||||
|
||||
// MARK: Environment & State
|
||||
|
||||
public typealias Environment = TokamakCore.Environment
|
||||
|
||||
// MARK: Modifiers & Styles
|
||||
|
||||
public typealias ViewModifier = TokamakCore.ViewModifier
|
||||
public typealias ModifiedContent = TokamakCore.ModifiedContent
|
||||
|
||||
public typealias DefaultListStyle = TokamakCore.DefaultListStyle
|
||||
public typealias PlainListStyle = TokamakCore.PlainListStyle
|
||||
public typealias InsetListStyle = TokamakCore.InsetListStyle
|
||||
public typealias GroupedListStyle = TokamakCore.GroupedListStyle
|
||||
public typealias InsetGroupedListStyle = TokamakCore.InsetGroupedListStyle
|
||||
|
||||
// MARK: Shapes
|
||||
|
||||
public typealias Shape = TokamakCore.Shape
|
||||
|
||||
public typealias Capsule = TokamakCore.Capsule
|
||||
public typealias Circle = TokamakCore.Circle
|
||||
public typealias Ellipse = TokamakCore.Ellipse
|
||||
public typealias Path = TokamakCore.Path
|
||||
public typealias Rectangle = TokamakCore.Rectangle
|
||||
public typealias RoundedRectangle = TokamakCore.RoundedRectangle
|
||||
|
||||
// MARK: Primitive values
|
||||
|
||||
public typealias Color = TokamakCore.Color
|
||||
public typealias Font = TokamakCore.Font
|
||||
|
||||
public typealias CGAffineTransform = TokamakCore.CGAffineTransform
|
||||
public typealias CGPoint = TokamakCore.CGPoint
|
||||
public typealias CGRect = TokamakCore.CGRect
|
||||
public typealias CGSize = TokamakCore.CGSize
|
||||
|
||||
// MARK: Views
|
||||
|
||||
public typealias Divider = TokamakCore.Divider
|
||||
public typealias ForEach = TokamakCore.ForEach
|
||||
public typealias GridItem = TokamakCore.GridItem
|
||||
public typealias Group = TokamakCore.Group
|
||||
public typealias HStack = TokamakCore.HStack
|
||||
public typealias LazyHGrid = TokamakCore.LazyHGrid
|
||||
public typealias LazyVGrid = TokamakCore.LazyVGrid
|
||||
public typealias List = TokamakCore.List
|
||||
public typealias ScrollView = TokamakCore.ScrollView
|
||||
public typealias Section = TokamakCore.Section
|
||||
public typealias Spacer = TokamakCore.Spacer
|
||||
public typealias Text = TokamakCore.Text
|
||||
public typealias VStack = TokamakCore.VStack
|
||||
public typealias ZStack = TokamakCore.ZStack
|
||||
|
||||
// MARK: Special Views
|
||||
|
||||
public typealias View = TokamakCore.View
|
||||
public typealias AnyView = TokamakCore.AnyView
|
||||
public typealias EmptyView = TokamakCore.EmptyView
|
||||
|
||||
// MARK: Misc
|
||||
|
||||
// Note: This extension is required to support concatenation of `Text`.
|
||||
extension Text {
|
||||
public static func + (lhs: Self, rhs: Self) -> Self {
|
||||
_concatenating(lhs: lhs, rhs: rhs)
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
We’ve omitted any stateful `Views`, as well as property wrappers used to modify state.
|
|
@ -0,0 +1,45 @@
|
|||
# Building the `Target`
|
||||
|
||||
If you recall, we defined a `Target` as:
|
||||
|
||||
> the destination for rendered `Views`
|
||||
|
||||
In `TokamakStaticHTML`, this would be a tag in an `HTML` file. A tag has several properties,
|
||||
although we don’t need to worry about all of them. For now, we can consider a tag to have:
|
||||
|
||||
- The HTML for the tag itself (outer HTML)
|
||||
- Child tags (inner HTML)
|
||||
|
||||
We can describe our target simply:
|
||||
|
||||
```swift
|
||||
public final class HTMLTarget: Target {
|
||||
var html: AnyHTML
|
||||
var children: [HTMLTarget] = []
|
||||
|
||||
init<V: View>(_ view: V,
|
||||
_ html: AnyHTML) {
|
||||
self.html = html
|
||||
super.init(view)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`AnyHTML` is from `TokamakDOM`, which you can declare as a dependency. The target stores the `View`
|
||||
it hosts, the `HTML` that represents it, and its child elements.
|
||||
|
||||
Lastly, we can also provide an HTML string representation of the target:
|
||||
|
||||
```swift
|
||||
extension HTMLTarget {
|
||||
var outerHTML: String {
|
||||
"""
|
||||
<\(html.tag)\(html.attributes.isEmpty ? "" : " ")\
|
||||
\(html.attributes.map { #"\#($0)="\#($1)""# }.joined(separator: " "))>\
|
||||
\(html.innerHTML ?? "")\
|
||||
\(children.map(\.outerHTML).joined(separator: "\n"))\
|
||||
</\(html.tag)>
|
||||
"""
|
||||
}
|
||||
}
|
||||
```
|
|
@ -0,0 +1,134 @@
|
|||
# Building the `Renderer`
|
||||
|
||||
Now that we have a `Target`, we can start the `Renderer`:
|
||||
|
||||
```swift
|
||||
public final class StaticHTMLRenderer: Renderer {
|
||||
public private(set) var reconciler: StackReconciler<StaticHTMLRenderer>?
|
||||
var rootTarget: HTMLTarget
|
||||
|
||||
public var html: String {
|
||||
"""
|
||||
<html>
|
||||
\(rootTarget.outerHTML)
|
||||
</html>
|
||||
"""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
We start by declaring the `StackReconciler`. It will handle the app, while our `Renderer` can focus
|
||||
on mounting and un-mounting `Views`.
|
||||
|
||||
```swift
|
||||
...
|
||||
public init<V: View>(_ view: V) {
|
||||
rootTarget = HTMLTarget(view, HTMLBody())
|
||||
reconciler = StackReconciler(
|
||||
view: view,
|
||||
target: rootTarget,
|
||||
renderer: self,
|
||||
environment: EnvironmentValues()
|
||||
) { closure in
|
||||
fatalError("Stateful apps cannot be created with TokamakStaticHTML")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Next we declare an initializer that takes a `View` and builds a reconciler. The reconciler takes the
|
||||
`View`, our root `Target` (in this case, `HTMLBody`), the renderer (`self`), and any default
|
||||
`EnvironmentValues` we may need to setup. The closure at the end is the scheduler. It tells the
|
||||
reconciler when it can update. In this case, we won’t need to update, so we can crash.
|
||||
|
||||
`HTMLBody` is declared like so:
|
||||
|
||||
```swift
|
||||
struct HTMLBody: AnyHTML {
|
||||
let tag: String = "body"
|
||||
let innerHTML: String? = nil
|
||||
let attributes: [String : String] = [:]
|
||||
let listeners: [String : Listener] = [:]
|
||||
}
|
||||
```
|
||||
|
||||
## Mounting
|
||||
|
||||
Now that we have a reconciler, we need to be able to mount the `HTMLTargets` it asks for.
|
||||
|
||||
```swift
|
||||
public func mountTarget(to parent: HTMLTarget, with host: MountedHost) -> HTMLTarget? {
|
||||
// 1.
|
||||
guard let html = mapAnyView(
|
||||
host.view,
|
||||
transform: { (html: AnyHTML) in html }
|
||||
) else {
|
||||
// 2.
|
||||
if mapAnyView(host.view, transform: { (view: ParentView) in view }) != nil {
|
||||
return parent
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 3.
|
||||
let node = HTMLTarget(host.view, html)
|
||||
parent.children.append(node)
|
||||
return node
|
||||
}}
|
||||
```
|
||||
|
||||
1. We use the `mapAnyView` function to convert the `AnyView` passed in to `AnyHTML`, which can be
|
||||
used with our `HTMLTarget`.
|
||||
2. `ParentView` is a special type of `View` in Tokamak. It indicates that the view has no
|
||||
representation itself, and is purely a container for children (e.g. `ForEach` or `Group`).
|
||||
3. We create a new `HTMLTarget` for the view, assign it as a child of the parent, and return it.
|
||||
|
||||
The other two functions required by the `Renderer` protocol can crash, as `TokamakStaticHTML`
|
||||
doesn’t support state changes:
|
||||
|
||||
```swift
|
||||
public func update(target: HTMLTarget, with host: MountedHost) {
|
||||
fatalError("Stateful apps cannot be created with TokamakStaticHTML")
|
||||
}
|
||||
|
||||
public func unmount(
|
||||
target: HTMLTarget,
|
||||
from parent: HTMLTarget,
|
||||
with host: MountedHost,
|
||||
completion: @escaping () -> ()
|
||||
) {
|
||||
fatalError("Stateful apps cannot be created with TokamakStaticHTML")
|
||||
}
|
||||
```
|
||||
|
||||
If you are creating a `Renderer` that supports state changes, here’s a quick synopsis:
|
||||
|
||||
- `func update` - Mutate the `target` to match the `host`.
|
||||
- `func unmount` - Remove the `target` from the `parent`, and call `completion` once it has been
|
||||
removed.
|
||||
|
||||
Now that we can mount, let’s give it a try:
|
||||
|
||||
```swift
|
||||
struct ContentView : View {
|
||||
var body: some View {
|
||||
Text("Hello, world!")
|
||||
}
|
||||
}
|
||||
|
||||
let renderer = StaticHTMLRenderer(ContentView())
|
||||
print(renderer.html)
|
||||
```
|
||||
|
||||
This spits out:
|
||||
|
||||
```html
|
||||
<html>
|
||||
<body>
|
||||
<span style="...">Hello, world!</span>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Congratulations 🎉 You successfully wrote a `Renderer`. We can’t wait to see what platforms you’ll
|
||||
bring Tokamak to.
|
|
@ -0,0 +1,61 @@
|
|||
# Providing platform-specific primitives
|
||||
|
||||
Primitive `Views`, such as `Text`, `Button`, `HStack`, etc. have a body type of `Never`. When the
|
||||
`StackReconciler` goes to render these `Views`, it expects your `Renderer` to provide a body.
|
||||
|
||||
This is done via the `ViewDeferredToRenderer` protocol. There we can provide a `View` that our
|
||||
`Renderer` understands. For instance, `TokamakDOM` (and `TokamakStaticHTML` by extension) use the
|
||||
`HTML` view. Let’s look at a simpler version of this view:
|
||||
|
||||
```swift
|
||||
protocol AnyHTML {
|
||||
let tag: String
|
||||
let attributes: [String:String]
|
||||
let innerHTML: String
|
||||
}
|
||||
|
||||
struct HTML: View, AnyHTML {
|
||||
let tag: String
|
||||
let attributes: [String:String]
|
||||
let innerHTML: String
|
||||
var body: Never {
|
||||
neverBody("HTML")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Here we define an `HTML` view to have a body type of `Never`, like other primitive `Views`. It also
|
||||
conforms to `AnyHTML`, which allows our `Renderer` to access the attributes of the `HTML` without
|
||||
worrying about the `associatedtypes` involved with `View`.
|
||||
|
||||
## `ViewDeferredToRenderer`
|
||||
|
||||
Now we can use `HTML` to override the body of the primitive `Views` provided by `TokamakCore`:
|
||||
|
||||
```swift
|
||||
extension Text: ViewDeferredToRenderer {
|
||||
var deferredBody: AnyView {
|
||||
AnyView(HTML("span", [:], _TextProxy(self).rawText))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If you recall, our `Renderer` mapped the `AnyView` received from the reconciler to `AnyHTML`:
|
||||
|
||||
```swift
|
||||
// 1.
|
||||
guard let html = mapAnyView(
|
||||
host.view,
|
||||
transform: { (html: AnyHTML) in html }
|
||||
) else { ... }
|
||||
```
|
||||
|
||||
Then we were able to access the properties of the HTML.
|
||||
|
||||
## Proxies
|
||||
|
||||
Proxies allow access to internal properties of views implemented by `TokamakCore`. For instance, to
|
||||
access the storage of the `Text` view, we were required to use a `_TextProxy`.
|
||||
|
||||
Proxies contain all of the properties of the primitive necessary to build your platform-specific
|
||||
implementation.
|
Loading…
Reference in New Issue