Skip to content

Limitations of the component model #128

Open
@glmars

Description

@glmars

Introduction

In our company we have started using Binding.scala in several projects. The library is wonderful, especially when you write code like web designer does. But when we started decomposing our code, we faced the limitations of the component model, especially when processing child elements.

This issue is a meta-task to discuss this limitations

There are two component models in Binding.scala

Let's extract internal div in this example as a component:

<div><div class="dialog"/></div>

Please, see full source code here

@dom functions (official)

Official way is using a regular function with @dom annotation:

@dom
def dialog: Binding[Div] = <div class="dialog"/>

Which can be used in other @dom functions:

<div>{dialog.bind}</div>

User defined tags (unofficial)

This is undocumented, but it is possible to write composable user defined tags. The same component as above is:

implicit final class UserTags(x: TagsAndTags2.type) {
  object dialog {
    def render: Div = {
        val div = x.div.render
        div.className = "dialog"
        div
    }
  }
}

Which can be used in @dom functions:

<div><dialog/></div>

Features of the component model

Using attributes of an underline html element

It's easy to use attributes of an underline html element.

  1. We should slightly change the implementation of our @dom component

    @dom
    def dialog(id: String): Binding[Div] = <div id={id} class="dialog"/>

    and using:

    <div>{dialog("message").bind}</div>
  2. Nothing changed in the implementation of user defined tag.
    Use it like this:

    <div><dialog id="message"/></div>

Please, see full source code here

Creating a component attribute

In this chapter we'll create a caption attribute for our dialog.
And second goal is check ability of using bindable attributes.

  1. It's just an additional parameter of @dom component function:

    @dom
    def dialog(id: String, caption: Binding[String]): Binding[Div] = <div id={id} class="dialog" data:dialog-caption={caption.bind}/>

    and using:

    val caption = Var("Caption")
    <div>{dialog("message", caption).bind}</div>
  2. It's quite hard to implement the same for user defined tag. But easy to use.

    <div><dialog id="message" caption={caption.bind}/></div>

Please, see full source code here

Containment

Some components don’t know their children ahead of time. This is especially common for components like Sidebar or Dialog that represent generic “boxes”.
(source)

Children can be absent, can contain single node, list of nodes, option node, text node etc. and their combinations.

  1. A common type for all of this is BindingSeq[Node], you can easily add such parameter to @dom component function:

    @dom
    def dialog(children: BindingSeq[Node]): Binding[Div] = <div class="dialog">{children}</div>

    but it's inconvenient to use:

    val warning = Some(<div>warning</div>)
    <div>{dialog(Constants(<div>Some text</div> +: warning.bind.toSeq:_*)).bind}</div>

    converting types to BindingSeq[Node] makes code less readable and kills a partial update

  2. Nothing changed in the implementation of user defined tag. And any kind of children can be used like a charm 😄:

    val warning = Some(<div>warning</div>)
    <div><dialog><div>Some text</div>{warning.bind}</dialog></div>

Please, see full source code here

Multiple “holes” in a component

While this is less common, sometimes you might need multiple “holes” in a component
(the same source)

In both component models you can add BindingSeq[Node] parameter and have hard way on using it 😞

Painless HTML creation

  1. As you can see in previous examples, HTML creation is easy in @dom component (because of @dom magic):

    @dom
    def dialog(id: String, caption: Binding[String]): Binding[Div] = <div id={id} class="dialog" data:dialog-caption={caption.bind}/>
  2. Unfortunately, it isn't possible to use @dom for implementing user defined tag. It would be great if we could write something like this:

    object Dialog {
      @dom
      def apply(id: String, caption: Binding[String]): Binding[Div] = <div id={id} class="dialog" data:dialog-caption={caption.bind}/>
    }
    
    implicit final class UserTags(x: TagsAndTags2.type) {
      val dialog = Dialog
    }

    when using stays the same as before:

    <div><dialog id="message" caption={caption.bind}/></div>

Please, see full source code here

Ops, there is some ugly workaround

Conclusion

Current state

@dom tag
html element attribute ✔️ ✔️
component attribute ✔️ ✔️
bindable attribute ✔️ ✔️
containment ✔️
multiple “holes”
painless HTML creation ✔️

Possible improvements

  1. Painless conversion of any types of children to BindingSeq[Node] (or to any other more appropriate type)
  2. I think, user defined tags is powerful and elegant concept and it should get an official support 😉
  3. ... etc.

List of references

  1. Special syntax to disable tag name checking #110
  2. Allow usage of custom tags defined in native js libraries #43
  3. https://scalafiddle.io/sf/gII6UlB/0
  4. Customize tags with inner html #42
  5. Custom tags (ie: SVG) #4
  6. https://reactjs.org/docs/composition-vs-inheritance.html#containment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions