ThoughtWorksInc / Binding.scala

Reactive data-binding for Scala
https://javadoc.io/page/com.thoughtworks.binding/binding_2.13/latest/com/thoughtworks/binding/index.html
MIT License
1.59k stars 108 forks source link

Limitations of the component model #128

Open glmars opened 5 years ago

glmars commented 5 years ago

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:
```scala
<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:
```scala
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:
```scala
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_:exclamation:

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

    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 :disappointed:

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:
```scala
<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 :heavy_check_mark: :heavy_check_mark:
component attribute :heavy_check_mark: :heavy_check_mark:
bindable attribute :heavy_check_mark: :heavy_check_mark:
containment :no_entry: :heavy_check_mark:
multiple “holes” :no_entry: :no_entry:
painless HTML creation :heavy_check_mark: :no_entry:

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 :wink:
  3. ... etc.

List of references

  1. https://github.com/ThoughtWorksInc/Binding.scala/pull/110
  2. https://github.com/ThoughtWorksInc/Binding.scala/issues/43
  3. https://scalafiddle.io/sf/gII6UlB/0
  4. https://github.com/ThoughtWorksInc/Binding.scala/issues/42
  5. https://github.com/ThoughtWorksInc/Binding.scala/issues/4
  6. https://reactjs.org/docs/composition-vs-inheritance.html#containment
skaz1970 commented 5 years ago

Agree!

Atry commented 5 years ago

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

Would it be better if we introduce some type classes to convert things to BindingSeq?

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

The above code have been improved in Binding.scala for FXML, because the type of an HTML literal is Node, while the type of an FXML literal is Binding[Node] or BindingSeq[Node]. See https://github.com/ThoughtWorksInc/Binding.scala/wiki/FXML#whats-different-from-dom

Atry commented 5 years ago

There was a proposal to implement a new annotation @html , which behaves like @fxml.

glmars commented 5 years ago

Would it be better if we introduce some type classes to convert things to BindingSeq?

Yes, I think it's a good idea anyway. When I implemented Option[Node] support, I had an idea to implement Option[String] support also, but It's not possible without extracting domBindingSeq (from dom.scala) to some type classes.

glmars commented 5 years ago

The above code have been improved in Binding.scala for FXML

I have to admit, I do not fully understand the benefits of @html in general and in this case in particular. I should try make this composition with @fxml :wink:

Atry commented 5 years ago

@glmars https://github.com/ThoughtWorksInc/Binding.scala/pull/129/files#diff-f8a68b011e13edbb825af507a6d28f58R38

glmars commented 5 years ago

Good point! If I understand correctly, you introduced new extension point for Binding.scala to manage of children mounting. This is a useful feature, but it does not relate directly to the user defined tags.

We need ability to decompose and organize our code with something like "dom templates": https://github.com/ThoughtWorksInc/Binding.scala/pull/132/files#diff-83b72dae9d603e2ad2d906c1dc0621e7R88

lxohi commented 5 years ago

User defined tags are really cool! And I think this is the missing part when comparing with ReasonML. I will definitely use a lot when it becomes available 😆.


In React there's some components that passing properties all the way through it children.

For example, in this code for Antd menu:

Currently there are two ways to archive similar behavior in Binding.scala:

Is there a way that I can passing there kind of "internal" properties around when using user defined tags?

lxohi commented 5 years ago

Never mind my last comment. I'm now consider what they do in Antd for complicated components as not a good practice. And I will not implement my component in that structure. I'm now more preferring the approaches for complicated components in material-UI.

glmars commented 5 years ago

@lxohi could you describe here the material-UI approaches?

lxohi commented 5 years ago

@glmars Yes.

Let's say that we want some components to build a navigation side panel. This panel have some states in it. (e.g. expand/collapse state of item group OR selected highlights for menu item)

Antd's approach

Material-UI's approach


Although I was decided to not make my Binding.scala components works like Antd does. (which means passing a lot of things under the water and/or modify the other component from outside of it.)

The Antd components are really easy to use and users will love it A LOT.

So then... My decision

Refs:

Atry commented 5 years ago

I prefer the material-ui's approach as well, except I thought clickHandler should be encapsulated in another small "component" of mutator. For example, in https://scalafiddle.io/sf/y0ixoUP/0, check or choice are mutators to change the Var state.

lxohi commented 5 years ago

This example is genius! And I believe it can loops again and again without having Maximum call stack size exceeded error in the browser which is more interesting :) Your suggestion is totally correct. That clickHandler thing wasn't powerful enough. So I've modified navigation recently adding a function param to deal with it

Atry commented 5 years ago

Hi, @glmars, recently I created bindable.scala, a library that provides type classes to convert other types to Binding and BindingSeq automatically.

I think bindable.scala solves the “containment” and “multiple holes” problem.

How do you think of it?

glmars commented 5 years ago

Hi, @Atry, I like it a lot! @lxohi will be happy too, I think :)

Only some questions:

1) Do we need Null (Empty) type (for ex. to implement typeclass for Option)? (I suppose, com.thoughtworks.binding.Binding.Empty is a good candidate) 2) I do not have much experience with functional Scala, but we really need such a complex implementation with Lt,Aux and etc.? 3) I worry about the public API :), Bindable (without .Lt postfix) is more clear name to use in a many places of user defined code.

Atry commented 5 years ago

Hi, @glmars ,

  1. How about Constants.empty?
  2. Unfortunately, this is the simplest implementation to create a dependent type class so far. Try search Aux pattern on Google. You can also have a look at https://github.com/mpilquist/simulacrum/pull/74 for discussion.
  3. Lt is a naming convention stolen from https://github.com/milessabin/shapeless/blob/906875e9eaedbcdedb3a7b63e5d34bd2b70f2def/core/src/main/scala/shapeless/singletons.scala#L38 . You can renaming-import it as you wish.
glmars commented 5 years ago
  1. I like Constants.empty (it's similar to Vars.empty) :+1:
  2. Thanks, Aux pattern is clear for me now :wink:
  3. moving forward to the Lt understanding... :smile:
Atry commented 5 years ago

Free free to create a PR for Constants.empty