hyper-element
v0.11.1
Published
hyperHTML + WebComponents
Downloads
9
Maintainers
Readme
hyper-element
Combining the best of hyperHTML and Custom Elements!
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 only 1 dependency: hyperHTML
- 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)
- a refrive object to match with the create node. Allowing for reuse of the exiting node.
- 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
- Casting types supported:
- 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>
tothis.dataset.users // ["ann","bob"]
- e.g.
- ⚠ For performance! The
dataset
works by reference. To update an attribute you must use assignment on thedataset
- Bad:
this.dataset.user.name = ""
✗ - Good:
this.dataset.user = {name:""}
✓
- Bad:
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:
- Add an attribute "templates" to your custom element
- 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!
- Default:
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