crysalead-js / dom-layer

Virtual DOM implementation.
MIT License
30 stars 1 forks source link

Sub-views? #69

Closed sprat closed 8 years ago

sprat commented 8 years ago

I discovered this project recently and I quite like the API. However, there's something I would like to do and I can't find how to do it with the API.

I would like to do something like this

function main() {
  return new Tag('section', {}, [
    header,
    content
  ]
}

function header() {
  return new Tag('header', {}, []);
}

function content() {
  return [
    new Tag('p', {}, []),
    new Tag('p', {}, [])
  ];
}

But render does not accept a list of functions as children, or a list with tags and functions: it only accepts a single function or a list of tags.

In my example, I guess you would just call the header and content function in main. But in the "thing" I would like to build, the header and content are more involved than that: they are callable View objects, with an API (e.g. event emitter, ...), and I would call them to render.

I've seen in your "input" example that you use sub-views/sub-components. But due to the restriction I just talked about, you are forced to wrap your view.render/component.render in a div, which is a bit annoying.

Is there a way to do what I want to do with the existing API? Or can you please consider extending the API? I can't probably send a merge request if you want.

jails commented 8 years ago

All componentizations related features must be managed in a higher level of abstration imo. Creating views where components are rendered like this will be really time consuming for big views since you will need to rewrite your whole vdom on updates. Each components should be autonomous and only rendered when dirty (like in the deku implementation).

In regards to the mandatory extra "root div", a lot of vdom implementations have this limitation. I'm not saying it's not possible to acheive but the "extra complexity behind" versus "the gain" doesn't worth the effort imo.

Anyway I'm not sure get why it's not possible to deal with complex callable View objects by juste calling header.render() instead of just using the header reference.

sprat commented 8 years ago

Thanks for your answer.

As I see it, the last parameter of Tag is a list of children with a special case: when there's a single child, you can pass the child itself and avoid creating a list with a single child. In this respect, if you accept a single callable child, I think you should also accept a list of eventually callable children for consistency. Or maybe remove the special "callable" case on a single child... (my confusion came from that).

I am not sure if it'll have a significant impact on the performance, but in that case, the "callable" check with a single child also has an impact. People can also call the single child explicitly, like in the multiple children case.

For sure, I can deal with that in a higher-level abstraction: in fact, that's exactly what I do now. But the problem in that, since I do that in my hyperscript function, I'm a bit worried that people would return a list of Views (i.e. functions) without calling the hyperscript function itself.

What I am talking about is probably a little bit abstract, so here is an example of what I'm trying to implement as higher-level API:

Application.prototype.render = function (h) {
    return h('.application', [
        h('h1.title', [this.title]),
        h.view(this.menu).on('itemSelected', function(item) {
          /* assign this.content depending on the selected item... */
        }),
        h.view(this.content)
    ]);
};

h.view create an View object representing a view of a sub-component. This object would be called automatically to render the sub-component, when inserted in a tag or returned from the render function. The view's function does some boilerplate things around the call to the sub-component's render function. And as you see, the view object expose helper functions too: here, I attach an event handler that will be triggered when the sub-component emit the itemSelected event. A view object may also stored in the parent component (as an attribute for example) in order to remember the event handlers attached to a specific sub-component.

In what I am trying to do, I think having to call render on the view would cause confusion with the component's render function and it will also prevent chaining helpers functions calls on the view. That's why I would like dom-layer to deal with lists of callables.

As for you remark about re-rendering everything, I can probably address the problem of detecting changes in the view's function: either re-render the underlying component wrapped by my view object or return the previous rendering result... but detecting changes on mutable objects is hard, so I'm not sure I will address the problem anytime soon ;) But I'm thinking about it, for sure !

jails commented 8 years ago

The behavior of the rendering function is the following one https://github.com/crysalead-js/dom-layer/blob/master/src/tree/render.js#L3-L16 nodes needs to be an array or a callable which generates an array. Indeed, if nodes can be an array of callable it'll end up to a nested array like the following [ [child1,child2,child3], [child4,[child5,child6]], etc. ] and this format is not supported by this low level library.

I know that some vdom implementations flatten that kind of nested children array in their core. But personnaly I thought that it's the responsability of the hyperscript function to do this kind of pre-processing since it can vary from an implementation from another.

For example in your example you have the h() hyperscript function. This function take the array of children as second parametter. So you can do the preprocessing you talked about on the children array inside your hyperscript function then flattening it with this kind of function https://github.com/crysalead-js/lead-ui/blob/master/src/renderer.js#L230-245 before creating the Tag instance.

That way you will be able to deal with your callable the way you need for your abstraction.

This is how I managed this need in my high level of abstration.

sprat commented 8 years ago

Fair enough. I'm trying to implement the flattening in my hyperscript function and on the output of the component's rendering function like you describe and I didn't see the flattening problem. You are right, it would probably have an impact on performance. So, I close this issue.