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

Composability via component nesting #8

Closed
nodefish opened this issue Jul 6, 2017 · 13 comments
Closed

Composability via component nesting #8

nodefish opened this issue Jul 6, 2017 · 13 comments

Comments

@nodefish
Copy link

nodefish commented Jul 6, 2017

I was working on a class-based library for hyperHTML but this project beat me to the punch, so I figure I may drop my work and just share notes with you.

The main difficulty I've encountered in experimenting with hyperHTML is composability, which is the entire purpose of the component based approach.

There needs to be an easy way to declare nested components. It's trivial if the nested components are already instantiated; you'd be able to do the following in the parent's render function:

function render() {
    return this.html`
        <div id="parent">
            ${childComponentInstance.render()}
        </div>
    `
}

The above is only possible if the child element is already declared and instantiated. In React, this is not an issue because child elements are automatically identified and rendered via React.createElement (JSX transpiles to createElement), which instantiates and then tracks the child elements in its VDOM tree upon rendering. But if there's no VDOM and no createElement, then something is certainly missing!

Proposal:

I think the leanest approach here is a hypercomponent.prototype.child function that wraps the render function provided by hyperHTML.wire and augments it in the following way:

  1. Instantiates any nested classes and places them in parent.children under a specific key
  2. Transparently returns the render output of a child component

Hypothetical example of what this would look like:

/* Parent class
...
...
*/
function render() {
    return this.html`
        <div id="parent">
            ${this.child(ChildClass, props)}
        </div>
    `
}

hypercomponent.prototype.child aka this.child would basically create an instance of type ChildClass and return its render output directly in the parent's render template.

Starting from the first render pass, this.children would contain references to any nested components.

Does this make sense?

@joshgillies
Copy link
Owner

joshgillies commented Jul 6, 2017

@nodefish thanks so much for the info in this post. It's great to see a few of us are coming to the same conclusions here, and again I really appreciate you sharing your experience.

As luck would have it, I've been thinking on this same problem recently also, and basically have the same as you've proposed unstaged locally.

As I've been toying with the notion of taking inspiration from CustomElements, where you have HyperComponent.prototype.child I've arrived at HyperComponent.prototype.slot.

The way I envisage this working is effectively this:

HyperComponent.prototype.slot(Component, name?) returns a ComponentProxy that's effectively a factory function, exposing an interface to both constructor(...args) and render(...args) functions. eg:

class Component extends HyperComponent {
  render () {
    return this.html`<div>${[
      this.slot(ChildComponent),
      this.slot(ChildComponent, 'named-slot'),
      this.slot(ChildComponent, 'call-render').render(...args)
      this.slot(ChildComponent, 'call-constructor')(...args),
      this.slot(ChildComponent, 'call-constructor-and-render')(...args).render(...args)
    ]}</div>`
  }
}

The nice thing about an interface like this, is we'll be able to abstract caching component instances with basically a WeakMap, similar to how hyperHTML.wire works. 👍

Would love to hear you thoughts on the above. cc/ @WebReflection

@nodefish
Copy link
Author

nodefish commented Jul 6, 2017

Sounds promising. One of my concerns (in general, not specific to your approach) is adding overhead to nesting child elements in the parent template because we should be able to have thousands of child elements (e.g. in a dynamic list), but your approach seems lean enough.

What's the rationale behind exposing the nested component's constructor? Perhaps standardizing the constructor is the way to go, in which case we don't need to expose it, i.e. it just needs props. I'm drawing heavily from React here, where React.createElement has the signature (ClassName, props, children). I think JSX is the perfect amount of verbosity, and the user works as if the custom component is an HTML tag, meaning just ClassName and props (HTML attributes) are specified and the rest is taken care of automatically.

Another random thing that comes to mind which may or may not be helpful: in other frameworks, the slot or yield keyword is for transclusion, as in, defining dynamic child content within a child tag, from the parent scope. Here's how I view the terminology:

Nested tag

function render() {
    return this.html`
        <div id="parent">
            ${this.child(ChildClass, props, null)}
        </div>
    `
}

Nested tag with transclusion

function render() {
    return this.html`
        <div id="parent">
            ${this.child(ChildClass, props, ['<div>', this.child(AnotherChildClass, OtherProps, null), '</div>])}
        </div>
    `
}

As in, in the parent scope (depth 0), we are adding a nested element (depth 1) with its own nested element inside it (depth 2), all defined from the parent scope. We could somewhat equivalently have defined the depth 2 component (nested within the nested component) in the definition of the depth 1 component - but then we lose the ability to dynamically set this from the parent scope (useful sometimes). This is equivalent to the innerHTML property but for custom tags, e.g. [slot / yield]

Essentially, this is heavily drawing from React internals and the React.createElement concept, where the 3rd parameter allows the equivalent of other frameworks' yield or slot.

@joshgillies
Copy link
Owner

joshgillies commented Jul 6, 2017

That's a really good point which I hadn't considered fully slot/yield != children.

I think in order to adopt a React.createElement style interface, my only comment would be I'd expect the children argument to mirror types as supported by hyperHTML: https://github.com/WebReflection/hyperHTML/blob/master/DEEPDIVE.md#good

Which given your above examples could effectively result in the following API:

class Parent extends HyperComponent {
  function render () {
    return this.html`<div id="parent">${
      this.child(Child, { id: 'child1' }, this.wire(':child')`<div>${[
        this.child(Child, { id: 'subchild1' }, `<div>woah!</div>`),
        this.child(Child, { id: 'subchild2' }, `<div>dude!</div>`)
      ]}</div>`)
    }</div>`
  }
}

class Child extends HyperComponent {
  function render () {
    if (typeof this.props.children === 'string') {
      return this.html`<div id="${ this.props.id }"> ${this.props.children} </div>`
    }
    return this.html`<div id="${ this.props.id }">${
      this.props.children
    }</div>`
  }
}

The challenge with the above is to manage calls to this.wire() (which just proxies hyperHTML.wire) as we really want to ensure we're reusing wires with every subsequent render. - Not an issue, see edit. 😄

Edit:

Updated example to illustrate passing different types as children.

Edit Edit:

Updated the this.wire call to illustrate assigning an id to the wire. Basically this.wire(':child') is a facade over hyperHTML.wire(this, ':child'). 💥

@nodefish
Copy link
Author

nodefish commented Jul 6, 2017

I still have a couple of blind spots in my understanding of wires, but this looks pretty good to me! I have a suspicion some quirks are going to appear at some point during actual usage, but nonetheless this looks like the right direction.

@joshgillies joshgillies mentioned this issue Jul 6, 2017
Merged
7 tasks
@joshgillies
Copy link
Owner

joshgillies commented Jul 6, 2017

@nodefish ah-ha! I just realised managing wires with this.wire is actually dead easy - thanks to the following: https://github.com/WebReflection/hyperHTML/#new-in-011.

Because wires can now be bound to the same object via an id we can simply have this.wire(this, ':child') and it will be automagically managed internally by hyperHTML. Is there anything @WebReflection hasn't thought of here? ;)

@nodefish
Copy link
Author

nodefish commented Jul 7, 2017

Awesome!

One thing to note is that :type notation for wire seems to work on html tags that are part of the final output e.g li or div but our child item would not have a <child> tag in the markup, as it would be replaced by its render output.

I wasn't sure what was the best thing to be doing with the wires here. They're essentially persistent document fragments, and there are a lot of options for what to do with them. I was thinking since we want this.child to instantiate the child element upon the first render call, and add it to a keyed list that the parent can track (parent.children), we have different things that can be done, that are not mutually exclusive:

  1. Encapsulate the whole wire functionality in each component instance so when we call this.child, (childInstance).render is called and it would transparently render to its own wire contained in the instance - meaning all components render to a wire unless if they're bound directly to a DOM element (usually an app container component would need to be bound directly).

  2. Have a wire containing multiple wires in order to handle lists (no idea if this works, the hyperHTML docs are sparse here, probably needs some experiments)

  3. Have the 3rd argument of this.child automatically call wire on the array elements

Really happy to share ideas on this, as this is quite challenging to navigate but the possibilities are pretty exciting.

@joshgillies
Copy link
Owner

Hrmmm, I'm not entirely sure the type argument in wire(obj, type) has to be anything at all related to the final output. Looking over the code I suspect it's more of a convention to have :li for a wire managing a list items than anything else. Happy to be told otherwise /ping @WebReflection

@nodefish did you happen to see the latest in #7, I've just pushed a working draft of what we've discussed above. Would love feedback if you're able.

@nodefish
Copy link
Author

nodefish commented Jul 7, 2017

I just gave my feedback on the draft. It's looking good and I can't think of much to say. It's pretty much what I think it should be at this stage. Need to test it to be able to say more. Is it stable enough to give it a try?

Re: wire: you could be right! I didn't realize it was just an arbitrary id.

@WebReflection
Copy link

type can be 'html' or 'svg' or 'async' , by default is 'html'. Type can contain any ':id' which is used to weakly relate a wired content to the same context. 'html:any-id' is the same as ':any-id', the colon is there just to delimit type from I'd.

Type is needed mostly to figure out how the wire will be used: some HTML content, some svg which require different handling, some async content which will be resolved at distance and it will figure out if where it's resolved should be HTML or svg.

@nodefish
Copy link
Author

nodefish commented Jul 7, 2017

Understood. I've read the hyperHTML docs but I need to get my hands dirty to truly understand it, as it's a lot to take in (and the writing style is a bit erratic :) )

@WebReflection
Copy link

I just need time to write a better documentation. Right now the situation is too poor

@nodefish
Copy link
Author

nodefish commented Jul 7, 2017

No worries, it's a huge amount of stuff and I'm surprised you did it all on your own in the first place.

joshgillies added a commit that referenced this issue Aug 6, 2017
* v3 Refactor

* Update docs

* Add browser alias viperhtml->hyperhtml

* Add API for managing child components #8

* Add setState API

* Update wire interface to allow shorthand named wire creation

* Remove WIP adopt API

* Update examples in readme

* Remove svg method

* Update documentaion

Closes: #5

* Add build step

* add package-lock.json

* v3 re-refactor

* Update docs
@joshgillies
Copy link
Owner

I'm going to close this one off, as the API discussed here made it's way into v3 via component(Component, props, children). Feel free discuss further if required, or open another issue entirely.

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

3 participants