Skip to content

Commit 83c2b94

Browse files
authored
improved DOM attribute and event handler handling (#24)
* wip * testing wasm crashers * remove some deinit tracing * more efficient event handlers * removed view context from patch calls * changed _ViewContext to borrowing * better text patching * element clean up * todo grooming * fixme grooming * stack size for full swift debug
1 parent 4f16c10 commit 83c2b94

34 files changed

+625
-582
lines changed

Examples/Basic/Sources/App/Views.swift

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ struct App {
99
@State var counters: [Int] = [1]
1010
@State var nextCounterName = 1
1111
@State var data = SomeData()
12+
@State var bindingViewCount = 1
1213

1314
var content: some View {
1415
div {
@@ -23,7 +24,20 @@ struct App {
2324
.environment(data)
2425
}
2526
hr()
26-
BindingsView()
27+
div {
28+
for _ in 0..<bindingViewCount {
29+
BindingsView()
30+
}
31+
button { "Add bindings view" }
32+
.onClick { _ in
33+
bindingViewCount += 1
34+
}
35+
button { "Remove bindings view" }
36+
.onClick { _ in
37+
guard bindingViewCount > 0 else { return }
38+
bindingViewCount -= 1
39+
}
40+
}
2741
hr()
2842
// TODE: replaceChildren does not keep animations and similar going....
2943
// if counters.count > 1 {
@@ -126,8 +140,7 @@ struct TestObjectView {
126140

127141
var content: some View {
128142
span { "Via environment object: \(data.name)" }
129-
// TODO: figure out how to make optional environment object work in embedded
130143
br()
131-
//span { "Via optional environment object: \(optionalData?.name ?? "")" }
144+
span { "Via optional environment object: \(optionalData?.name ?? "")" }
132145
}
133146
}

Examples/Swiftle/Package.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ let package = Package(
1515
dependencies: [
1616
.product(name: "ElementaryDOM", package: "elementary-dom"),
1717
.product(name: "ElementaryCSS", package: "elementary-css"),
18+
],
19+
linkerSettings: [
20+
.unsafeFlags(["-Xlinker", "-z", "-Xlinker", "stack-size=1048576"], .when(platforms: [.wasi], configuration: .debug))
1821
]
1922
)
2023
],

Examples/Swiftle/Sources/Swiftle/main.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ struct App {
77
}
88
}
99

10+
print("Mounting app")
1011
App().mount(in: .body)

Examples/Swiftle/build-dev.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@ swift package \
55
--swift-sdk "$(swiftc -print-target-info | jq -r '.swiftCompilerTag')_wasm" \
66
--enable-experimental-prebuilts \
77
--allow-writing-to-package-directory \
8-
js -c debug --output $OUTDIR --use-cdn
8+
js -c debug --output $OUTDIR --use-cdn --debug-info-format dwarf

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ let package = Package(
1010
],
1111
dependencies: [
1212
.package(url: "https://github.com/swiftwasm/JavaScriptKit", from: "0.33.1"),
13-
.package(url: "https://github.com/sliemeobn/elementary", from: "0.5.1"),
13+
.package(url: "https://github.com/sliemeobn/elementary", from: "0.5.4"),
1414
.package(url: "https://github.com/swiftlang/swift-syntax", "600.0.0"..<"602.0.0-prerelease"),
1515
],
1616
targets: [

Sources/ElementaryDOM/Data/Environment/View+Envionment.swift

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,10 @@ public struct _EnvironmentView<V, Wrapped: View>: View {
3838

3939
public static func _makeNode(
4040
_ view: consuming Self,
41-
context: consuming _ViewContext,
41+
context: borrowing _ViewContext,
4242
reconciler: inout _RenderContext
4343
) -> _MountedNode {
44-
44+
var context = copy context
4545
let box = EnvironmentValues._Box<V>(view.value)
4646
context.environment.boxes[view.key.propertyID] = box
4747

@@ -50,7 +50,6 @@ public struct _EnvironmentView<V, Wrapped: View>: View {
5050

5151
public static func _patchNode(
5252
_ view: consuming Self,
53-
context: consuming _ViewContext,
5453
node: inout _MountedNode,
5554
reconciler: inout _RenderContext
5655
) {
@@ -67,7 +66,6 @@ public struct _EnvironmentView<V, Wrapped: View>: View {
6766
)
6867
}
6968

70-
context.environment.boxes[view.key.propertyID] = node.state
71-
Wrapped._patchNode(view.wrapped, context: context, node: &node.child, reconciler: &reconciler)
69+
Wrapped._patchNode(view.wrapped, node: &node.child, reconciler: &reconciler)
7270
}
7371
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import Elementary
2+
3+
public final class _AttributeModifier: DOMElementModifier, Invalidateable {
4+
typealias Value = _AttributeStorage
5+
6+
let upstream: _AttributeModifier?
7+
var tracker: DependencyTracker = .init()
8+
9+
private var lastValue: Value
10+
11+
var value: Value {
12+
var combined = lastValue
13+
combined.append(upstream?.value ?? .none)
14+
return combined
15+
}
16+
17+
init(value: consuming Value, upstream: borrowing DOMElementModifiers, _ context: inout _RenderContext) {
18+
self.lastValue = value
19+
self.upstream = upstream[_AttributeModifier.key]
20+
self.upstream?.tracker.addDependency(self)
21+
22+
#if hasFeature(Embedded)
23+
if __omg_this_was_annoying_I_am_false {
24+
// this is to force inclusion of types
25+
_ = p {}.attributes(.class([""]), .style(["": ""]))
26+
}
27+
#endif
28+
}
29+
30+
func updateValue(_ value: consuming Value, _ context: inout _RenderContext) {
31+
if value != lastValue {
32+
lastValue = value
33+
tracker.invalidateAll(&context)
34+
}
35+
}
36+
37+
func mount(_ node: DOM.Node, _ context: inout _CommitContext) -> AnyUnmountable {
38+
logTrace("mounting attribute modifier")
39+
return AnyUnmountable(MountedInstance(node, self, &context))
40+
}
41+
42+
func invalidate(_ context: inout _RenderContext) {
43+
self.tracker.invalidateAll(&context)
44+
}
45+
}
46+
47+
extension _AttributeModifier {
48+
final class MountedInstance: Unmountable, Invalidateable {
49+
let modifier: _AttributeModifier
50+
let node: DOM.Node
51+
52+
var isDirty: Bool = false
53+
var previousValue: _AttributeStorage = .none
54+
55+
init(_ node: DOM.Node, _ modifier: _AttributeModifier, _ context: inout _CommitContext) {
56+
self.node = node
57+
self.modifier = modifier
58+
self.modifier.tracker.addDependency(self)
59+
updateDOMNode(&context)
60+
}
61+
62+
func invalidate(_ context: inout _RenderContext) {
63+
guard !isDirty else { return }
64+
logTrace("invalidating attribute modifier")
65+
isDirty = true
66+
context.commitPlan.addNodeAction(CommitAction(run: updateDOMNode(_:)))
67+
}
68+
69+
func updateDOMNode(_ context: inout _CommitContext) {
70+
logTrace("updating attribute modifier")
71+
let newValue = modifier.value
72+
context.dom.patchElementAttributes(node, with: newValue, replacing: previousValue)
73+
isDirty = false
74+
previousValue = newValue
75+
}
76+
77+
func unmount(_ context: inout _CommitContext) {
78+
logTrace("unmounting attribute modifier")
79+
self.modifier.tracker.removeDependency(self)
80+
}
81+
}
82+
}

Sources/ElementaryDOM/DOMElementModifiers.swift renamed to Sources/ElementaryDOM/ElementModifiers/BindingModifiers.swift

Lines changed: 3 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,3 @@
1-
protocol DOMElementModifier: AnyObject {
2-
associatedtype Value
3-
4-
static var key: DOMElementModifiers.Key<Self> { get }
5-
6-
init(value: consuming Value, upstream: Self?, _ context: inout _RenderContext)
7-
func updateValue(_ value: consuming Value, _ context: inout _RenderContext)
8-
9-
func mount(_ node: DOM.Node, _ context: inout _CommitContext) -> AnyUnmountable
10-
}
11-
12-
extension DOMElementModifier {
13-
static var key: DOMElementModifiers.Key<Self> {
14-
DOMElementModifiers.Key(Self.self)
15-
}
16-
}
17-
18-
protocol Unmountable: AnyObject {
19-
func unmount(_ context: inout _CommitContext)
20-
}
21-
22-
struct DOMElementModifiers {
23-
struct Key<Directive: DOMElementModifier> {
24-
let typeID: ObjectIdentifier
25-
26-
init(_: Directive.Type) {
27-
typeID = ObjectIdentifier(Directive.self)
28-
}
29-
}
30-
31-
private var storage: [ObjectIdentifier: any DOMElementModifier] = [:]
32-
33-
subscript<Directive: DOMElementModifier>(_ key: Key<Directive>) -> Directive? {
34-
get {
35-
storage[key.typeID] as? Directive
36-
}
37-
set {
38-
if let newValue = newValue {
39-
storage[key.typeID] = newValue
40-
} else {
41-
storage.removeValue(forKey: key.typeID)
42-
}
43-
}
44-
}
45-
46-
consuming func takeModifiers() -> [any DOMElementModifier] {
47-
let directives = Array(storage.values)
48-
storage.removeAll()
49-
return directives
50-
}
51-
}
52-
53-
struct AnyUnmountable {
54-
private let _unmount: (inout _CommitContext) -> Void
55-
56-
init(_ unmountable: some Unmountable) {
57-
self._unmount = unmountable.unmount(_:)
58-
}
59-
60-
func unmount(_ context: inout _CommitContext) {
61-
_unmount(&context)
62-
}
63-
}
64-
651
final class BindingModifier<Configuration>: DOMElementModifier, Unmountable where Configuration: BindingConfiguration {
662
typealias Value = Binding<Configuration.Value>
673

@@ -73,7 +9,7 @@ final class BindingModifier<Configuration>: DOMElementModifier, Unmountable wher
739
var accessor: DOM.PropertyAccessor?
7410
var isDirty: Bool = false
7511

76-
init(value: consuming Value, upstream: BindingModifier?, _ context: inout _RenderContext) {
12+
init(value: consuming Value, upstream: borrowing DOMElementModifiers, _ context: inout _RenderContext) {
7713
self.lastValue = value.wrappedValue
7814
self.binding = value
7915
}
@@ -88,6 +24,7 @@ final class BindingModifier<Configuration>: DOMElementModifier, Unmountable wher
8824
}
8925

9026
private func markDirty(_ context: inout _RenderContext) {
27+
precondition(mountedNode != nil, "Binding effect can only be marked dirty on a mounted element")
9128
guard !isDirty else { return }
9229
isDirty = true
9330

@@ -138,7 +75,7 @@ final class BindingModifier<Configuration>: DOMElementModifier, Unmountable wher
13875

13976
func unmount(_ context: inout _CommitContext) {
14077
guard let sink = self.sink, let node = self.mountedNode else {
141-
assertionFailure("Binding effect can only be unmounted on a mounted element")
78+
// NOTE: since this object is used for both state and mounted effect, it will be unmounted twice
14279
return
14380
}
14481

@@ -231,17 +168,3 @@ struct CheckboxBindingConfiguration: BindingConfiguration {
231168
.boolean(value)
232169
}
233170
}
234-
235-
struct DependencyList: ~Copyable {
236-
private var downstreams: [() -> Void] = []
237-
238-
mutating func add(_ invalidate: @escaping () -> Void) {
239-
downstreams.append(invalidate)
240-
}
241-
242-
func invalidateAll() {
243-
for downstream in downstreams {
244-
downstream()
245-
}
246-
}
247-
}

0 commit comments

Comments
 (0)