Coder Social home page Coder Social logo

hypercomponent's Introduction

hypercomponent

Build Status Standard - JavaScript Style Guide

โšก Fast and light component system, backed by hyperHTML

HyperComponent is an abstract component system designed to be fast, and light - weighing in at ~4kb.

const HyperComponent = require('hypercomponent')

class HelloMessage extends HyperComponent {
  renderCallback (wire) {
    return wire`
      <div>Hello ${this.props.name}</div>
    `
  }
}

const greeting = new HelloMessage({ name: 'Jane'})

greeting.render(document.body)

Install

npm

npm install hypercomponent --save

cdn

<script src="https://unpkg.com/hypercomponent@latest/dist/hypercomponent.min.js"></scrpt>

API

HyperComponent

HyperComponent is a base class for creating generic front-end components.

class Component extends HyperComponent {
  constructor (props) {
    super(props)
  }
}

Instances of HyperComponent have the following internal properties:

  • this.el: The DOM node a component has rendered to. Defaults to null.
  • this.props: The initial properties passed to the HyperComponent constructor. Defaults to Component.defaultProps or {}
  • this.state: The initial state of the HyperComponent constructor. Defaults to Component.defaultState or {}

HyperComponent.prototype.renderCallback(wire, component)

You'll always want to implement a render function. This forms the public interface for your component. Your renderCallback method should always return DOM nodes, and the output of your renderCallback is automatically assigned to this.el.

The following arguments are available:

wire

The wire argument is a tagged template literal for turning your template into DOM.

Internally your template is cached against the instance of a component, and as such additional calls to wire with different templates will result in errors.

For cases where you want sub templates, you can simply pass a optional type argument, eg.

class Component extends HyperComponent {
  constructor (props) {
    super(props)
    this.winning = true
  }
  renderCallback (wire) {
    return wire`
      <div>${this.winning
        ? wire(':winning')`<span>Winning!</span>`
        : wire(':not-winning')`<span>Not winning!</span>`
      }</div>
    `
  }

For those familiar with hyperHTML a wire in this case is literally a facade around hyperHTML.wire([obj[, type]]), and can be used in the same way, eg.

class Component extends HyperComponent {
  constructor (props) {
    super(props)
    this.items = [
      { text: 'Foo' },
      { text: 'Bar' }
    ]
  }
  renderCallback (wire) {
    return wire`
      <div>
        <ul>${this.items.map((item) => wire(item, ':unordered')`
          <li> ${item.text} </li>`
        )}</ul>
        <ol>${this.items.map((item) => wire(item, ':ordered')`
          <li> ${item.text} </li>`
        )}</ol>
      </div>`
  }
}

component(Component, props, children)

The component argument is useful for managing component composition, and the returned value of component is the result of calling Component.prototype.renderCallback(). The following arguments are available.

Component

The Component argument is a component class you wish to compose within your parent component. It's expected that Component is an instance of HyperComponent.

props

Internally this will effectively create a new instance of your child component by passing props to the constructor eg. new Component(props)

For managing multiple instances of the same component class, you can additionally assign a key property via props.key, which is used to ensure component instances are reused on subsequent calls to renderCallback().

children

It's expected that the children argument is a valid type for children within hyperHTML eg. a String of text or markup, DOM nodes, a Promise, or an Array of the previous types. Internally the children argument is assigned to this.props.children.

As an example, the following demonstrates all of the above.

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

class Child extends HyperComponent {
  renderCallback (wire) {
    return wire`<div id="${ this.props.key }">${
      this.props.children
    }</div>`
  }
}

HyperComponent.prototype.render(node)

Renders a component returning it's rendered DOM tree. When the optional argument node is provided the contents of the target DOM node will be replaced by your rendered component.

HyperComponent.prototype.handleEvent(event)

By default handleEvent is preconfigured to delegate any component method whose name matches a valid DOM event. eg. onclick.

The benefit being that instead of binding event handlers individually to your components, you can simply pass your HyperComponent instance to your event handler, and delegate all event handling logic through the handleEvent API.

As an example:

// instead of this... ๐Ÿ‘Ž
class BoundButton extends HyperComponent {
  constructor (props) {
    super(props)
    this.onclick = this.onclick.bind(this)
  }
  onclick (event) {
    console.log(event.target, " has been clicked!")
  }
  renderCallback (wire) {
    return wire`
      <button onclick="${this.onclick}">Click me</button>
    `
  }
}

// you can simply do this! ๐ŸŽ‰
class DelegatedButton extends HyperComponent {
  onclick (event) {
    console.log(event.target, " has been clicked!")
  }
  renderCallback (wire) {
    return wire`
      <button onclick="${this}">Click me</button>
    `
  }
}

Of course the HyperComponent.prototype.handleEvent method is simply a helper. You can always override it to create your own event delegation logic.

As an example, if camelCase handlers are more your style:

class Button extends HyperComponent {
  handleEvent (event) {
    const type = event.type
    this[`on${type.substr(0, 1).toUpperCase() + type.substr(1)}`](event)
  }
  onClick (event) {
    console.log(event.target, " has been clicked!")
  }
  renderCallback (wire) {
    return wire`
      <button onclick="${this}">Click me</button>
    `
  }
}

For more information on the benefits of handleEvent checkout this post by @WebReflection: DOM handleEvent: a cross-platform standard since year 2000.

HyperComponent.prototype.connectedCallback()

When assigned, the connectedCallback handler will be called once your component has been inserted into the DOM.

HyperComponent.prototype.disconnectedCallback()

When assigned, the disconnectedCallback handler will be called once your component has been removed from the DOM.

HyperComponent.prototype.setState(obj)

Sets the internal state of a component eg. this.state by extending previous state with next state. After the state is updated your components renderCallback() method will be called.

Examples

A Basic Component

const HyperComponent = require('hypercomponent')

class HelloMessage extends HyperComponent {
  renderCallback (wire) {
    return wire`
      <div>Hello ${this.props.name}</div>
    `
  }
}

const greeting = new HelloMessage({ name: 'Jane'})

greeting.render(document.body)

A Stateful Component

const HyperComponent = require('hypercomponent')

class Timer extends HyperComponent {
  constructor (props) {
    super(props)
    this.state = {
      secondsElapsed: 0
    }
  }
  tick () {
    this.setState({
      secondsElapsed: this.state.secondsElapsed + 1
    })
  }
  connectedCallback () {
    this.interval = setInterval(() => this.tick(), 1000)
  }
  disconnectedCallback () {
    clearInterval(this.interval)
  }
  renderCallback (wire) {
    return wire`
      <div>Seconds Elapsed: ${this.state.secondsElapsed}</div>
    `
  }
}

const myTimer = new Timer()

myTimer.render(document.body)

An Application

class TodoApp extends HyperComponent {
  constructor (props) {
    super(props)
    this.state = {items: [], text: ''}
  }
  renderCallback (wire, component) {
    return wire`
      <div>
        <h3>TODO</h3>${
        component(TodoList, {items: this.state.items})
        }<form onsubmit="${this}">
          <input onchange="${this}" value="${this.state.text}" />
          <button>Add #${this.state.items.length + 1}</button>
        </form>
      </div>
    `
  }
  onchange (event) {
    this.setState({text: event.target.value})
  }
  onsubmit (event) {
    event.preventDefault()
    var newItem = {
      text: this.state.text,
      id: Date.now()
    }
    this.setState({
      items: this.state.items.concat(newItem),
      text: ''
    })
  }
}

class TodoList extends HyperComponent {
  renderCallback (wire) {
    return wire`
      <ul>${this.props.items.map(item => wire(item)`
        <li>
          ${item.text}
        </li>`)
      }</ul>`
  }
}

const app = new TodoApp()

app.render(document.body)

License

MIT

hypercomponent's People

Contributors

joshgillies avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

Forkers

dredzone

hypercomponent's Issues

Provide a default `handleEvent`?

Previous to releasing v3 of HyperComponent I had an idea around providing a default for handleEvent which was preconfigured to delegate any component method that's name mached a valid DOM event. eg. onclick/click.

class Clicker extends HyperComponent {
  onclick (event) {
    console.log(event.target, " has been clicked!")
  }
}

// instead of this... ๐Ÿ‘Ž
class BoundButton extends Clicker {
  constructor (props) {
    super(props)
    this.onclick = this.onclick.bind(this)
  }
  render () {
    return this.html`<button onclick="${this.onclick}">Click me</button>`
  }
}

// you can simply do this! ๐ŸŽ‰
class DelegatedButton extends Clicker {
  render () {
    return this.html`<button onclick="${this}">Click me</button>`
  }
}

I still wonder if there's value in this.

Composability via component nesting

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?

Provide an `adopt` API?

Given hyperHTML has this feature currently, it would be interesting to see whether an adopt interface for HyperComponent makes sense.

In my mind, given a component rendered on the server and returning the following HTML:

<div id="app">Hello form the Server</div>

In our client bundle we'd be able to do the following:

class App extends HyperComponent {
  render () {
    return this.html`
      <div id="app">Hello from the ${this.props.env}</div>
    `
  }
}

const app = new App({ env: 'Browser' })

app.adopt(document.getElementById('app')).render()
// -> <div id="app">Hello from the Browser</div>

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.