JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 1
  • Score
    100M100P100Q27735F
  • License ISC

hyperHTML + WebComponents

Package Exports

  • hyper-element

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (hyper-element) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

Readme

hyper-element

A combining the best of hyperHTML and Custom Elements!

npm version CDN link

Your new custom-elements are built with hyperHTML and will be re-rendered on attribute and store change.

why hyper-element

  • hyper-element is fast & small: under 5k (minify + gzip)
    • With only 1 dependency: The super fast renderer hyperHTML
  • With a completely stateless approach, setting and reseting the view is trivial
  • Simple yet powerful Api
  • Built in template system to define markup fragments
  • Inline style objects supported (similar to React)
  • First class support for data stores

Live Demo



Define a Custom Element

document.registerElement("my-elem", class extends hyperElement{

  render(Html){
    Html`hello ${this.attrs.who}`
  }

})

To use your element

<!DOCTYPE html>
<html>
<head>
  <script src="https://cdn.jsdelivr.net/npm/hyperhtml@latest/min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/hyper-element@latest/source/bundle.min.js"></script>
</head>
<body>
  <my-elem who="world"></my-elem>
</body>
<html>

Output

<my-elem who="world">
    hello world
</my-elem>

Api

Define your element

There are 2 functions. render is required and setup is optional

render

This is what will be displayed with in your element. Use the Html to define your content

render(Html,store){

    Html`
      <h1>
          Lasted updated at ${new Date().toLocaleTimeString()}
      </h1>
    `
}

setup

The setup function wires up an external data-source. This is done with the onNext argument that binds a data source to your renderer.

Connent a data source

Example to re-rendering when the mouse moves and pass current mouse values to render

// getMouseValues(){ ... }

setup(onNext){

    // the getMouseValues function will be call before each render and pass to render
    const next = onNext(getMouseValues)

    // call next on every mouse event
    onMouseMove(next)

    // cleanup logic
    return ()=>{ console.warn("On remove, do component cleanup here") }
}

re-rendering with out a data source

Example of re-rendering every second

setup(onNext){
    setInterval(onNext(), 1000);
}

Set initial values to pass to every render

Example of attaching an object, that will be used on every render

setup(onNext){
    onNext({max_levels:3})
}

How to cleanup

Any logic you wish to run when the element is removed from the page should be returned as a function from the setup function

// listen to a WebSocket
setup(onNext){

  const next = onNext();
  const ws = new WebSocket("ws://127.0.0.1/data");

  ws.onmessage = ({data}) => next(JSON.parse(data))

  // Return way to unsubscribe
  const teardown = ws.close.bind(ws);
  return teardown
}

render(Html,incomingMessage){
  // ...
}

Returning a "teardown function" from setup address the problem of needing a reference to the resource you what to release. If the "teardown function" was a public function. We would need to store the reference to the resource some, that the teardown can access it when call.

With this approach there is no leaking of references.

✎ To subscribe to 2 events

setup(onNext){

  const next = onNext(user);

  mobx.autorun(next);       // update when changed (real-time feedback)
  setInterval(next, 1000);  // update every second (update "the time is now ...")

}

this

  • this.attrs : the attributes on the tage <my-elem min="0" max="10" /> = { min:0, max:10 }
    • Casting types supported: Number
  • this.store : the value returned from the store function. !only update before each render
  • this.wrappedContent : the text content embedded between your tag <my-elem>Hi!</my-elem> = "Hi!"
  • this.dataset: This allows reading and writing to all the custom data attributes data-* set on the element.
    • Data will be parsed to try and cast them to Javascript types
    • Casting types supported: Object, Array, Number & Boolean
    • dataset is a live reflection. Changes on this object will update matching data attribute on its element.
    • e.g. <my-elem data-users='["ann","bob"]'></my-elem> to this.dataset.users // ["ann","bob"]
    • ⚠ For performance! The dataset works by reference. To update the attribute you must use assignment this.dataset.user = {name:""}

Templates

You can declare markup to be used as a template within the custom element

To enable templates:

  1. Add an attribute "templates" to your custom element
  2. Define the template markup within your element

In you document:

<my-list template data-json='[{"name":"ann","url":""},{"name":"bob","url":""}]' >
  <div>
    <a href="{url}">{name}</a>
  </div>
</my-list>

Implementation:

document.registerElement("my-list",class extends hyperElement{

      render(Html){
        Html`
        ${this.dataset.json.map(user => Html.template(user))}
        `
      }
 })

Output:

<my-list template data-json='[{"name":"ann","url":""},{"name":"bob","url":""}]' >
    <div>
      <a href="">ann</a>
    </div>
    <div>
      <a href="">bob</a>
    </div>
</my-list>

Fragments

Fragments are pieces of content that can be loaded asynchronously.

You define one with a class property starting with a capital letter.

To use one with in you render. Pass an object with a property matching the fragment name and any value needed.

The fragment function should return an object with the following properties

  • placeholder: the placeholder to show while resolving the fragment
  • once: Only generate the fragment once.
    • Default: false. The fragment function will be run on every render!

and one of the following as the fragment's result:

  • text: An escaped string to output
  • any: An type of content
  • html: A html string to output,
document.registerElement("my-friends",class extends hyperElement{

      FriendCount(user){
        return {

          once:true,

          placeholder: "loading your number of friends",

          text:fetch("/user/"+user.userId+"/friends")
              .then(b => b.json())
              .then(friends => {
                if (friends) return `you have ${friends.count} friends`;
                else return "problem loading friends";
              })
        }
      }

      render(Html){
        const userId = this.attrs.myId
        Html`<h2> ${{FriendCount:{userId}}} </h2>`
      }
 })

Ouput:

<my-friends myId="1234">
  <h2> loading your number of friends </h2>
</my-friends>

then

<my-friends myId="1234">
  <h2> you have 635 friends </h2>
</my-friends>

Inline fragments

The fragment function will be run on every render!

Example of dynamically loading markup

document.registerElement("profile-widget",class extends hyperElement{

      render(Html){
        Html`${{
            html: fetch('/widgets/profile/'+this.attrs.profileId)
                  .then(recivedMarkup => recivedMarkup.text())
            placeholder: 'Loading your profile ...'
          }}`
      }
 })

Ouput:

<profile-widget profileId="1234">
  Loading your profile ...
</profile-widget>

then

<profile-widget profileId="1234">
  <div>
    <img>...</img>
    ...
  </div>
</profile-widget>

Styling

Supports an object as the style attribute. Compatible with React's implementation.

Example of centering an element

  render(Html){
    const style= {
      position: "absolute",
      top: "50%", left: "50%",  
      marginRight: "-50%",  
      transform: "translate(-50%, -50%)"
    }
    Html`<div style=${style}> center </div>`
  }

Example of connecting to a data store

backbone

var user = new (Backbone.Model.extend({
    defaults: {
        name: 'Guest User',
    }
}));


document.registerElement("my-profile", class extends hyperElement{

  setup(onNext){
    user.on("change",onNext(user.toJSON.bind(user)));
    // OR user.on("change",onNext(()=>user.toJSON()));
  }

  render(Html,{name}){
    Html`Profile: ${name}`
  }
})

mobx

const user = observable({
  name: 'Guest User'
})


document.registerElement("my-profile", class extends hyperElement{

  setup(onNext){
    mobx.autorun(onNext(user));
  }

  render(Html,{name}){
    Html`Profile: ${name}`
  }
})

redux

document.registerElement("my-profile", class extends hyperElement{

  setup(onNext){
    store.subcribe(onNext(store.getState)
  }

  render(Html,{user}){
    Html`Profile: ${user.name}`
  }
})