Tokamak
Tokamak copied to clipboard
Easily access DOM from Views
The following additions can be made to TokamakDOM:
-
@Ref
property wrapper for accessing DOM nodes inside Tokamak
struct ContentView: View {
@Ref var button: JSObjectRef!
var body: some View {
Button("Hello, world!") {
self.button.textContent.string! = "Clicked!"
}.ref($button)
}
}
-
domAttributes
modifier for setting arbitrary values on DOM nodes
domAttributes(_ attributes: [ElementAttribute: String])
enum ElementAttribute: String, ExpressibleByStringLiteral {
case custom(String)
case id, `class`, ...
}
Button("Hello, world!") { ... }
.domAttributes([.id: "myButton", "data-info": "some data"])
- Everything else can be done directly with JavaScriptKit
What about a React-style API where you have an @State
(or maybe a custom @Ref
) binding which you add to a view then pass to .ref()
?
struct ContentView: View {
@Ref var button: JSObjectRef!
var body: some View {
Button("Hello, world!") {
self.button.textContent.string! = "Clicked!"
}.ref($button)
}
}
That is good for accessing elements inside Tokamak, but you may also want to access outside elements too. Maybe @Ref
could also accept a selector String?
If have something like ElementAttribute
, it won't work for custom attributes if there are no associated values. Maybe it would have a separate case custom(String)
to support those? As for DOM access outside of the Tokamak element tree, would that be substantially different from this?
struct ContentView: View {
var body: some View {
Button("Click me!") { ... }
.domId("myButton")
.onAppear { document.querySelector!("#myButton").type = "submit" }
}
}
And then, would we want to have specialized dom...
modifier for every attribute, or just a plain domAttributes
to cover all possible attributes with a single modifier?
struct ContentView: View {
var body: some View {
Button("Click me!") { ... }
.domAttributes(["id": "myButton"])
.onAppear { document.querySelector!("#myButton").type = "submit" }
}
}
I'd say the domAttributes
, definitely gives more control, but is also prone to spelling errors, as opposed to something statically typed. Maybe we'd do domAttributes(_ attributes: [ElementAttribute: String])
, with a custom
case, and maybe even make ElementAttribute
conform to ExpressibleByStringLiteral
.
So maybe we'd have:
-
@Ref
property wrapper for accessing DOM nodes inside Tokamak -
domAttributes
modifier for setting arbitrary values on DOM nodes - Everything else can be done directly with JavaScriptKit
If we had some sort of typed DOM access, it could be implemented with KeyPath
s.
It would probably be good to have different handling of attributes (changed with setAttribute
) and properties (changed by directly accessing object keys). This is important for, for example, forms, where the value
attribute and property have different functionality.
For clarity and to avoid any possibility of conflict, how about tagging DOM-only APIs by prefixing them with dom_
? As far as I can tell, underscores aren’t used at all in SwiftUI names, so that syntax shows the user that something special is happening.
Something like dom_
would be basically switching these names to snake_case, which is not common in Swift, which is predominantly (if not completely) camelCase. I do think something like _dom
prefix would make sense thought.
Is there any current workaround for the second point except using HTML
?
Can you elaborate please? What's your use case?
I just want to create a Button
with a custom id
attribute.
I think the safest way is to use DynamicHTML
to create a new button from scratch with a correct id
. What HTML elements and attributes Button
uses under the hood is an implementation detail and may change in a future version of Tokamak. The hiearchy of underlying elements can even inadvertently be changed by a user, where adding modifiers adds wrapping div
elements on top of the underlying button
element.
This reasoning is similar to what SwiftUI does on Apple platforms: you can access underlying UIButton
in a Button
through some introspection hacks, but it isn't officially supported.
What HTML elements and attributes
Button
uses under the hood is an implementation detail and may change in a future version of Tokamak
While true, considering the non-Apple renderers have non-standardized appearance, it seems reasonable that there would be a simple system in place to apply view modifications per system/OS, such as:
func platform() -> Platform {
#if os(WASM)
return .wasm
#endif
#if os(macOS)
return .mac
#endif
// ...
}
public extension View {
func customPlatform(_ map: [Platform: (Self) -> AnyView]) -> some View {
let test = map[platform()]?(self)
return test ?? AnyView(self)
}
}
This allows inline custom styling or even custom components to be rendered without having to clutter user code with preprocessor directives, and allows for providing View
subtypes for individual renderers (to introduce the aforementioned domAttributes
modifier).
var body: some View {
Button("Button")
.customPlatform([.mac: { view in
view.padding()
},
.wasm: { view in
view.domAttributes(["id": "foo"])
}])
}