hyper-element

0.11.1 • Public • Published

hyper-element

Combining the best of hyperHTML and Custom Elements!

npm version CDN link gzip size

Your new custom-element will be rendered with the super fast hyperHTML and will react to tag attribute and store changes.

why hyper-element

  • hyper-element is fast & small
  • With a completely stateless approach, setting and reseting the view is trivial
  • Simple yet powerful Api
  • Built in template system to customise the rendered output
  • Inline style objects supported (similar to React)
  • First class support for data stores
  • Pass function to other custom hyper-elements via there tag attribute

Live Demo

If you like it, please ★ it on github



Define a custom-element

document.registerElement("my-elem", class extends hyperElement{
 
  render(Html){
    Html`hello ${this.attrs.who}`
  }// END render
 
})// END my-elem

If using webpack

const hyperElement from "hyper-element"

To use your element in brower

<!DOCTYPE html>
<html>
<head>
  <script src="https://cdn.jsdelivr.net/npm/webcomponentsjs@latest/lite.min.js"></script> 
  <script src="https://cdn.jsdelivr.net/npm/hyperhtml@latest/index.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

Html

The primary operation is to describe the complete inner content of the element.

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

The Html has a primary operation and two utilities: .wire & .lite


Html.wire

The .wire is for creating reusable sub-element

The wire can take two arguments Html.wire(obj,id)

  1. a refrive object to match with the create node. Allowing for reuse of the exiting node.
  2. a string to identify the markup used. Allowing the markup template to be generated only once.

Example of displaying a list of users from an array

    Html`
      <ul>
          ${users.map(user => Html.wire(user,":user_list_item")`<li>${user.name}</li>`)}
      </ul>
    `

An anti-pattern is to inline the markup as a string

BAD example: ✗

    Html`
      <ul>
          ${users.map(user => `<li>${user.name}</li>`)}
      </ul>
    `

This will create a new node for every element on every render. The is have a Negative impact on performance and output will Not be sanitized. So DONT do this!


Html.lite

The .lite is for creating once off sub-element

Example of wrapping the jQuary date picker

 
onSelect(dateText, inst){
  console.log("selected time "+dateText)
} // END onSelect
 
Date(lite){
  const inputElem = lite`<input type="text"/>`
  $(inputElem).datepicker({onSelect:this.onSelect});
  return {
    any: inputElem,
    once:true
  }
} // END Date
 
render(Html){
  Html` Pick a date ${{Date:Html.lite}} `
} // END render
 

setup

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

Connect a data source

Example of re-rendering when the mouse moves. Will pass mouse values to render

// getMouseValues(){ ... }
 
setup(attachStore){
 
    // the getMouseValues function will be call before each render and pass to render
    const onStoreChange = attachStore(getMouseValues)
 
    // call next on every mouse event
    onMouseMove(onStoreChange)
 
    // cleanup logic
    return ()=> console.warn("On remove, do component cleanup here")
}// END setup

re-rendering without a data source

Example of re-rendering every second

setup(attachStore){
    setInterval(attachStore(), 1000);
}// END setup

Set initial values to pass to every render

Example of hard coding an object that will be used on every render

setup(attachStore){
    attachStore({max_levels:3})
}// END setup

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(attachStore){
 
  let newSocketValue;
  const onStoreChange = attachStore(()=> newSocketValue);
  const ws = new WebSocket("ws://127.0.0.1/data");
 
  ws.onmessage = ({data}) => {
    newSocketValue = JSON.parse(data);
    onStoreChange()
  }
 
  // Return way to unsubscribe
  return ws.close.bind(ws)
}// END setup
 
render(Html,incomingMessage){
  // ...
}// END render

Returning a "teardown function" from setup address's the problem of needing a reference to the resource you want to release.

If the "teardown function" was a public function. We would need to store the reference to the resource somewhere. So the teardown can access it when needed.

With this approach there is no leaking of references.

✎ To subscribe to 2 events

setup(attachStore){
 
  const onStoreChange = attachStore(user);
 
  mobx.autorun(onStoreChange);       // update when changed (real-time feedback)
  setInterval(onStoreChange, 1000);  // update every second (update "the time is now ...")
 
}// END setup
 

this

  • this.attrs : the attributes on the tag <my-elem min="0" max="10" /> = { min:0, max:10 }
    • Casting types supported: Number
  • this.store : the value returned from the store function. !only updated before each render
  • this.wrappedContent : the text content embedded between your tag <my-elem>Hi!</my-elem> = "Hi!"
  • this.element : a reference to your created element
  • 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 an attribute you must use assignment on the dataset
      • Bad: this.dataset.user.name = ""
      • Good: this.dataset.user = {name:""}

Advanced attributes

Dynamic attributes with custom-element children

Being able to set attributes at run-time should be the same for dealing with a native element and ones defined by hyper-element.

⚠ To support dynamic attributes on custom elements YOU MUST USE customElements.define which requires native ES6 support! Use the native source /source/hyperElement.js NOT /source/bundle.js

This is what allows for the passing any dynamic attributes from parent to child custom element! You can also pass a function to a child element(that extends hyperElement).

Example:

In you document:

<script src="https://cdn.jsdelivr.net/npm/hyper-element@latest/source/hyperElement.js"></script>
<users-elem />

Implementation:

window.customElements.define("a-user",class extends hyperElement{
  render(Html){
    const onClick = () => this.attrs.hi("Hello from "+this.attrs.name);
    Html`${this.attrs.name} <button onclick=${onClick}>Say hi!</button>`
  }
})
 
window.customElements.define("users-elem",class extends hyperElement{
  onHi(val){
    console.log("hi was clicked",val)
  }
  render(Html){
    Html`<a-user hi=${this.onHi} name="Beckett" />`
  }
})

Output:

<users-elem>
  <a-user update="fn-bgzvylhphgvpwtv" name="Beckett">
     Beckett <button>Say hi!</button>
  </a-user>
</users-elem>

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

Example:

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))}
        `
      }// END render
 })// END my-list

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 within your renderer. Pass an object with a property matching the fragment name and any values 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, (Not sanitised)
  • template: A template string to use, (Is sanitised)
    • values: A set of values to be used in the template

Example:

Implementation:

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 => `you have ${friends.count} friends`)
              .catch(err => "problem loading friends")
        }// END return
      }// END FriendCount
 
      render(Html){
        const userId = this.attrs.myId
        Html`<h2> ${{FriendCount:userId}} </h2>`
      }// END render
 })// END my-friends

Output:

<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>

fragment templates

You can use the template syntax with in a fragment

  • The template will use the values pass to it from the render or using a "values" property to match the template string

e.g. assigning values to template from with in the fragment function

  • Foo(values){ return{ template:"<p>{txt}</p>", values:{txt:"Ipsum"} }}
  • with Html`${{Foo:{}}}`

or assigning values to template from with in the render function

  • Foo(values){ return{ template:"<p>{txt}</p>" }}
  • with Html`${{Foo:{txt:"Ipsum"}}}`

Note: the different is whether or not a "values" is returned from the fragment function

output

<p>Ipsum</p>

Example:

Implementation:

document.registerElement("click-me",class extends hyperElement{
      Button(){
        return {
          template:`<button type="button" class="btn"
                        onclick={onclick}>{text}</button>`
        }// END return
      }// END Button
      render(Html){
        Html`Try ${{Button:{
                  text:"Click Me",
                  onclick:()=>alert("Hello!")
              }}}`
      }// END render
 })// END click-me

Output:

<click-me>
  Try <button type="button" class="btn">Click Me</button>
</click-me>

Asynchronous fragment templates

You can also return a promise as your template property.

Rewritting the my-friends example

Example:

Implementation:

document.registerElement("my-friends",class extends hyperElement{
 
      FriendCount(user){
 
        const templatePromise = fetch("/user/"+user.userId+"/friends")
                                  .then(b => b.json())
                                  .then(friends => ({
                                          template:`you have {count} friends`,
                                          values:{count:friends.count}
                                        })
                                  }) // END .then
                                  .catch(err=>({ template:`problem loading friends` })
 
        return {
          once: true,
          placeholder: "loading your number of friends",
          template: templatePromise
        } // END return
      }// END FriendCount
 
      render(Html){
        const userId = this.attrs.myId
        Html`<h2> ${{FriendCount:userId}} </h2>`
      }// END render
 }) //END my-friends

In this example, the values returned from the promise are used. As the "values" from a fragment function(if provided) takes priority over values passed in from render.

Output:

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

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%)"
    }//END style
    Html`<div style=${style}> center </div>`
  }//END render
 

Example of connecting to a data store

backbone

var user = new (Backbone.Model.extend({
    defaults: {
        name: 'Guest User',
    }
}));//END Backbone.Model.extend
 
 
document.registerElement("my-profile", class extends hyperElement{
 
  setup(attachStore){
    user.on("change",attachStore(user.toJSON.bind(user)));
    // OR user.on("change",attachStore(()=>user.toJSON()));
  }//END setup
 
  render(Html,{name}){
    Html`Profile: ${name}`
  }//END render
})//END my-profile

mobx

const user = observable({
  name: 'Guest User'
})//END observable
 
 
document.registerElement("my-profile", class extends hyperElement{
 
  setup(attachStore){
    mobx.autorun(attachStore(user));
  }// END setup
 
  render(Html,{name}){
    Html`Profile: ${name}`
  }// END render
})//END my-profile

redux

document.registerElement("my-profile", class extends hyperElement{
 
  setup(attachStore){
    store.subcribe(attachStore(store.getState)
  }// END setup
 
  render(Html,{user}){
    Html`Profile: ${user.name}`
  }// END render
})// END my-profile

Package Sidebar

Install

npm i hyper-element

Weekly Downloads

3

Version

0.11.1

License

MIT

Unpacked Size

68.1 kB

Total Files

11

Last publish

Collaborators

  • codemeasandwich