Is there a particular reason why `ElementScope<...
# compose-web
b
Is there a particular reason why
ElementScope<T>
is an interface, but
AttrsBuilder<T>
is a class? It'd be great if you could make
AttrsBuilder<T>
an interface as well as this would allow injecting some custom DSL for attrs builder (without polluting global namespace as those DSLs are usually only meant for that particular component based on HTMLDivElement and not all HTMLDivElements) when using compose-web to wrap custom components. Here's how I'm utilising the fact that
ElementScope<T>
is an interface already (although it only works for lower-level components and is kinda hacky).
👀 1
Main benefit of these being interfaces is that you can extend a particular instance by wrapping it into a class and having that class implement the same interface by delegating the implementation to the instance. e.g.
Copy code
class MyCustomBuilder(base: Builder): Builder by base {
// My custom scoped extensions
}
Happy to raise an issue if you think this has something to it
o
AttrsBuilder is an open class. So it can be inherited and if I understand correctly your case with custom attributes should be achievable by extending AttrsBuilder class. Or I miss something?
b
I tried that, but just extending an open class is not enough, because I also need to proxy all attrs builder DSLs from my consumer calls into the original attrs instance. I've noticed that AttrsBuilder has an internal copy method, which would solve my case as I could just apply consumer builder on my extended instance of AttrsBuilder and then just copy its state into original attrs instance
Maybe this will help in visualising my case better:
Copy code
class MyCustomAttrsBuilder: AttrsBuilder<HTMLButton>() {
  fun onCustomEvent(listener: () -> Unit) {
    addEventListener("MyCustomEventName") { listener() }
  }
}

@Composable
fun MyCustomComponent(attrs: MyCustomAttrsBuilder.() -> Unit) {
  Button(
    attrs = {
      val custom = MyCustomAttrsBuilder().apply(attrs)
      this.copyFrom(custom) // <------- my missing piece at the moment
    }
  )
}
Since ElementScope is an interface, i can solve the same issue with delegate implementation:
Copy code
class MyCustomElementScope(originalScope: ElementScope<HTMLButton>): ElementScope<HTMLButton> by originalScope {
  @Composable
  fun CustomSubComponent() {
    Div()
  }
}

@Composable
fun MyCustomComponent(content: @Composable MyCustomElementScope.() -> Unit) {
  Button(
    content = {
      MyCustomElementScope(this).content() // Works just fine, because all base methods will be delegated to `this`
    }
  )
}
I think it would be good for consistency as well. attrs builder should be exposed to public APIs as an interface, same as content builder.
o
I see. That makes sense. So the question for us to decide is what's better in current state: make "copy" function public or try to make interface AttrsBuilder (without or only little breaking changes), or maybe some third alternative Could you please file an issue on our github? I think it's worth to dig into such use cases which needed more possibilities for extending/inheritance.
I'm happy to contribute a fix once you guys agree on the solution. I left some initial thoughts on the ticket, so let me know if you're happy for me to proceed with that approach or go some different direction entirely.
🙏 1