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

Limitations of the component model #128

Open
glmars opened this issue Dec 14, 2018 · 19 comments
Open

Limitations of the component model #128

glmars opened this issue Dec 14, 2018 · 19 comments

Comments

@glmars
Copy link
Contributor

glmars commented Dec 14, 2018

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
@skaz1970
Copy link

Agree!

@Atry
Copy link
Collaborator

Atry commented Dec 14, 2018

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
Copy link
Collaborator

Atry commented Dec 14, 2018

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
Copy link
Collaborator

Atry commented Dec 14, 2018

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

@glmars
Copy link
Contributor Author

glmars commented Dec 14, 2018

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
Copy link
Contributor Author

glmars commented Dec 14, 2018

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 😉

@glmars
Copy link
Contributor Author

glmars commented Dec 17, 2018

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
Copy link

lxohi commented Jan 4, 2019

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:

  • The Menu was passing a function as property onSelect to it's children(here).
  • onSelect was then passed through SubMenu(here) or MenuItemGroup(here) to their children and finally used in Menu.Item(here).
  • These kind of properties was provided by some component and used by some of their children without any needs of user's attention. They are the "internal" properties between components.

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

  • Defines Menu as a function accepts an ADT which contains MenuItemGroup / SubMenu / MenuItem. And there will be only one Menu component exists. No children components.
  • Using HList (like shapeless's labelled generic) as parameter for these kind of properties. Then concat reduce element or new element by it's parent components. This will make the definitions of @dom functions a little complicated but in a flexible & type safe way.

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

@lxohi
Copy link

lxohi commented Jan 13, 2019

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
Copy link
Contributor Author

glmars commented Jan 18, 2019

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

@lxohi
Copy link

lxohi commented Jan 18, 2019

@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

  • What Antd does is provide components that using some nasty dynamic language features.
  • This makes components really easy to use and makes user code very clean.
  • The user of these components has no needs to deal with the states.
  • Code in Antd will looks like:
<Menu ...>
  <MenuItem ... />
  <MenuItem ... />
  <MenuItem ... />
</Menu>
  • User needs to use same kind of hacks that Antd does when they have some custom features requirements. Or they may have to copy & modify that very massy code pieces of Menu / MenuItem in Antd.
  • To achieve similar behavior in Scala will result in some really complicated and/or meta-programming code. Which may not be easy & clean anymore.

Material-UI's approach

  • material-ui just provide the basic component with style and limited logic in it.
  • When user needs components like this, they provide the code piece that handles the states.
  • This makes user code more complicated compared to Antd. While still far more simple than the implementation of Antd component itself.
  • Code in material-ui will looks like:
function foo() {
    const open = ...;
    function clickHandler() {
        ... modify open ...
    }

    return (
        <List ...>
          <ListItem ...>
            {open ? <ExpandLess /> : <ExpandMore />}
          </ListItem>
          <Collapse in={open} ...>
          </Collapse>
    )
}
  • User can simply do something similar to this code piece when they have some custom features requirements.
  • We can simply do the same thing in Scala.

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
Copy link
Collaborator

Atry commented Mar 7, 2019

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
Copy link

lxohi commented Mar 7, 2019

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
Copy link
Collaborator

Atry commented Mar 8, 2019

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
Copy link
Contributor Author

glmars commented Mar 12, 2019

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
Copy link
Collaborator

Atry commented Mar 13, 2019

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 Add support for dependent types to ops and summoner typelevel/simulacrum#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
Copy link
Contributor Author

glmars commented Mar 13, 2019

  1. I like Constants.empty (it's similar to Vars.empty) 👍
  2. Thanks, Aux pattern is clear for me now 😉
  3. moving forward to the Lt understanding... 😄

@Atry
Copy link
Collaborator

Atry commented Mar 13, 2019

Free free to create a PR for Constants.empty

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants