WebReflection / hyperHTML

A Fast & Light Virtual DOM Alternative
ISC License
3.07k stars 112 forks source link

How to use nested hyperHTML Component? #189

Closed liming closed 6 years ago

liming commented 6 years ago

For example:

class FirstComponent extends hyper.Component {}

class SecondComponent extends hyper.Component {}

class CombinedComponent extends hyper.Component {
  render(){
    this.html`<p>how to render FirstComponent and SecondComponent here?<p>`
  }
}

The project hypercomponent can do this but I prefer using hyperHTML.Component.

Thank you!

WebReflection commented 6 years ago

would this example explain ?

const {hyper} = hyperHTML;

class FirstComponent extends hyper.Component {
  // it's important to always return the result
  render(){ return this.html
    `<span>first ${Math.random()}</span>`
  }
}

class SecondComponent extends hyper.Component {
  render(){ return this.html
    `<span>second ${Math.random()}</span>`
  }
}

class CombinedComponent extends hyper.Component {
  constructor() {
    super();
    this.first = new FirstComponent;
    this.second = new SecondComponent;
  }
  render(){ return this.html
    `<p>hi, this is ${this.first} and ${this.second}<p>`
  }
}

hyper(document.body)
  `${new CombinedComponent}`;

it's live on codepen

liming commented 6 years ago

Thank you for the explain. But how to manage the state of child component?

Usually you would like to set parents state and update children’s state as well.

WebReflection commented 6 years ago
const {hyper} = hyperHTML;

class FirstComponent extends hyper.Component {
  update(state) {
    this.setState(state);
    return this.render();
  }
  render(){ return this.html
    `<span>first ${this.state.i}</span>`
  }
}

class SecondComponent extends hyper.Component {
  update(state) {
    this.setState(state);
    return this.render();
  }
  render(){ return this.html
    `<span>second ${this.state.i}</span>`
  }
}

class CombinedComponent extends hyper.Component {
  constructor() {
    super();
    this.first = new FirstComponent;
    this.second = new SecondComponent;
  }
  render(){ return this.html
    `<p>hi, this is ${this.first.update({i: 1})}
        and ${this.second.update({i: 2})}<p>`
  }
}

hyper(document.body)
  `${new CombinedComponent}`;
liming commented 6 years ago

It's not working well.

Say if we have a component named as MenuItem, another component named Menu. Normally we would like to create MenuItem at runtime.

class Menu extends BaseComponent {
  constructor(props) {
    super(props);

    this.menuItem = new MenuItem();
  }

  render() {
    return this.html`
      <ul>
       ${this.state.items.map(item => this.menuItem.update(item))}
      </ul>
    `;
  }
}

But it's not working. We can only create the item in constructor and that's only one item. This way is not going to manage complex nested components.

liming commented 6 years ago

By the way, the question should not be closed until the solutions are accepted. Someone else might have good solution but they are not be able to see a closed question.

WebReflection commented 6 years ago

Normally we would like to create MenuItem at runtime.

See this demo.

It creates a list of nodes and it append it later on. You don't need to necessarily create within the template, you can have a method and keep the template clean.

class Menu extends BaseComponent {
  constructor(props) {
    super(props);
    this._menuItems = new WeakMap;
  }

  get items() {
    return this.state.items.map(item => {
      let view = this._menuItems.get(item);
      if (!view) {
        view = new MenuItem(item);
        this._menuItems.set(item, view);
      }
      return view;
    });
  }

  render() {
    return this.html`
      <ul>
       ${this.items}
      </ul>
    `;
  }
}

Of course things work more seamlessly with Custom Elements and HyperHTMLElement and while there is an attempt to automate that logic via JSX, currently you need to compose your component the way you want.

Other simple examples in here: https://viperhtml.js.org/hyperhtml/examples/#!fw=React&example=Combined%20Components

the question should not be closed until the solutions are accepted

that's Stack Overflow :smile: ... here I've closed the ticket because it's not a bug or anything I can work on. I didn't close the conversation though, and I'm sill here to help/suggest, if needed.

liming commented 6 years ago

Thank you for the idea.

I tried to wrap hyper.Component to make it easier to use. The main problem of hyper.Component is the child components are not easy managed. Another problem is, this.state can be fragile so it would keep rerender the DOM if developer broke the state. I can see the Component is trying to avoid breaking this.state here but it's not a compete solution.

To solve the problem, I introduced a base component inherited from hyper.Component. This base component would keep an "virtual state" to patch this.state. Patching data is far more easier than patching the DOM.

import {Component} from 'hyperhtml/esm';
import diff from 'deep-diff';

class BaseComponent extends Component {
  constructor(props) {
    super(props);

    this.innerState = {};

    this.children = new WeakMap();

    this.apply(props);
  }

  apply(props) {
    // convert the props to states
    const state = this.toState(props);

    // diff the original state and the new state
    const changes = diff.diff(this.innerState, state);

    if (changes) {
      // apply the change to the state object. This would keep the state as the same
      changes.forEach(c => diff.applyChange(this.innerState, state, c));
    }

    this.setState(this.innerState);
  }

  /**
   * toState is the method you can map the properties to inner state
   */
  toState(props) {
    return Object.assign({}, props);
  }

  child(ChildComponent, props) {
    let childComponent = this.children.get(props);

    if (!childComponent) {
      childComponent = new ChildComponent(props);

      this.children.set(props, childComponent);
    } else {
      childComponent.update(props);
    }

    return childComponent;
  }

  /**
   * update component properties. It would trigger render method
   */
  update(props) {
    this.apply(props);
  }
};

With this BaseComponent, I can use it like this:

class MenuItem extends BaseComponent {
  constructor(props) {
    super(props);
  }

  render() {
    return this.html`
      <li>${this.state.name}</li>
    `;
  }
}

class Menu extends BaseComponent {
  constructor(props) {
    super(props);
  }

  render() {
    return this.html`
      <div>A simple menu</div>
      <ul>
       ${this.state.items.map(item => this.child(MenuItem, item))}
      </ul>
    `;
  }
}

Do you think it's a good idea to incorporate the idea into hyper.Component?