Tokamak icon indicating copy to clipboard operation
Tokamak copied to clipboard

Easily access DOM from Views

Open carson-katri opened this issue 3 years ago • 11 comments

The following additions can be made to TokamakDOM:

  1. @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)
  }
}
  1. 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"])
  1. Everything else can be done directly with JavaScriptKit

carson-katri avatar Jul 31 '20 23:07 carson-katri

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)
  }
}

j-f1 avatar Aug 01 '20 02:08 j-f1

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?

carson-katri avatar Aug 01 '20 12:08 carson-katri

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" }
  }
}

MaxDesiatov avatar Aug 01 '20 15:08 MaxDesiatov

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:

  1. @Ref property wrapper for accessing DOM nodes inside Tokamak
  2. domAttributes modifier for setting arbitrary values on DOM nodes
  3. Everything else can be done directly with JavaScriptKit

carson-katri avatar Aug 01 '20 15:08 carson-katri

If we had some sort of typed DOM access, it could be implemented with KeyPaths.

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.

j-f1 avatar Aug 01 '20 17:08 j-f1

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.

MaxDesiatov avatar Aug 01 '20 17:08 MaxDesiatov

Is there any current workaround for the second point except using HTML?

revolter avatar Mar 07 '21 12:03 revolter

Can you elaborate please? What's your use case?

MaxDesiatov avatar Mar 07 '21 12:03 MaxDesiatov

I just want to create a Button with a custom id attribute.

revolter avatar Mar 07 '21 12:03 revolter

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.

MaxDesiatov avatar Mar 07 '21 12:03 MaxDesiatov

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"])
    }])
}

agg23 avatar Sep 18 '21 23:09 agg23