Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Easily access DOM from Views #231

Open
carson-katri opened this issue Jul 31, 2020 · 11 comments
Open

Easily access DOM from Views #231

carson-katri opened this issue Jul 31, 2020 · 11 comments
Labels
API design API design and prototyping is needed

Comments

@carson-katri
Copy link
Member

carson-katri commented Jul 31, 2020

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 carson-katri added the API design API design and prototyping is needed label Jul 31, 2020
@carson-katri carson-katri changed the title Easily access DOM/JS from Views Easily access DOM from Views Jul 31, 2020
@j-f1
Copy link
Member

j-f1 commented Aug 1, 2020

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

@carson-katri
Copy link
Member Author

carson-katri commented Aug 1, 2020

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?

@MaxDesiatov
Copy link
Collaborator

MaxDesiatov commented Aug 1, 2020

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

@carson-katri
Copy link
Member Author

carson-katri commented Aug 1, 2020

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

@j-f1
Copy link
Member

j-f1 commented Aug 1, 2020

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.

@MaxDesiatov
Copy link
Collaborator

MaxDesiatov commented Aug 1, 2020

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 added a commit that referenced this issue Aug 2, 2020
Resolves partially #231. `_targetRef` is a modifier that can be used by any renderer, while `_domRef` is an adaptation of that for `DOMRenderer`. Both are underscored as they are not available in SwiftUI, and also their stability is currently not so well known to us, we may consider changing this API in the future.

Example use:

```swift
struct DOMRefDemo: View {
  @State var button: JSObjectRef?

  var body: some View {
    Button("Click me") {
      button?.innerHTML = "This text was set directly through a DOM reference"
    }._domRef($button)
  }
}
```

I've also fixed all known line length warnings in this PR.
MaxDesiatov added a commit that referenced this issue Aug 11, 2020
This is just an empty API at the moment. I hope it can be implemented purely in the `deferredBody` of `GeometryReader` with [the ResizeObserver API](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver) without requiring any tweaks in the `Renderer` protocol or the reconciler.

Seems like I need the `domRef` modifier that writes `JSObjectRef` to a given binding working first, as discussed in #231.
@MaxDesiatov MaxDesiatov pinned this issue Aug 20, 2020
@revolter
Copy link

revolter commented Mar 7, 2021

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

@MaxDesiatov
Copy link
Collaborator

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

@revolter
Copy link

revolter commented Mar 7, 2021

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

@MaxDesiatov
Copy link
Collaborator

MaxDesiatov commented Mar 7, 2021

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.

@agg23
Copy link
Contributor

agg23 commented Sep 18, 2021

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
API design API design and prototyping is needed
Development

No branches or pull requests

5 participants