Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/codemeasandwich/hyper-element
Combining the best of hyperHTML and Custom Elements!
https://github.com/codemeasandwich/hyper-element
hyperhtml react web-components
Last synced: 3 months ago
JSON representation
Combining the best of hyperHTML and Custom Elements!
- Host: GitHub
- URL: https://github.com/codemeasandwich/hyper-element
- Owner: codemeasandwich
- Created: 2017-12-18T17:09:52.000Z (about 7 years ago)
- Default Branch: master
- Last Pushed: 2023-04-19T19:13:32.000Z (over 1 year ago)
- Last Synced: 2024-10-17T16:21:43.346Z (3 months ago)
- Topics: hyperhtml, react, web-components
- Language: JavaScript
- Size: 206 KB
- Stars: 5
- Watchers: 2
- Forks: 4
- Open Issues: 11
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# hyper-element
Combining the best of [hyperHTML] and [Custom Elements]!
[![npm version](https://badge.fury.io/js/hyper-element.svg)](https://www.npmjs.com/package/hyper-element)
[![CDN link](https://img.shields.io/badge/CDN_link-hyper--element-red.svg)](https://cdn.jsdelivr.net/npm/hyper-element@latest/source/bundle.min.js)
[![gzip size](http://img.badgesize.io/https://cdn.jsdelivr.net/npm/hyper-element@latest/source/bundle.min.js?compression=gzip)](https://cdn.jsdelivr.net/npm/hyper-element@latest/source/bundle.min.js)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](#api)
* Built in [template](#templates) system to customise the rendered output
* Inline style objects supported (similar to React)
* First class support for [data stores](#example-of-connecting-to-a-data-store)
* Pass `function` to other custom hyper-elements via there tag attribute# [Live Demo](https://jsfiddle.net/codemeasandwich/k25e6ufv/)
### If you like it, please [★ it on github](https://github.com/codemeasandwich/hyper-element)
---
- [Define a Custom Element](#define-a-custom-element)
- [Api](#api)
* [Define your element](#define-your-element)
+ [render](#render)
+ [Html](#html)
+ [Html.wire](#htmlwire)
+ [Html.lite](#htmllite)
+ [setup](#setup)
+ [this](#this)
* [Advanced attributes](#advanced-attributes)
* [Templates](#templates)
* [Fragments](#fragments)
+ [fragment templates](#fragment-templates)
+ [Async fragment templates](#asynchronous-fragment-templates)
* [Styling](#styling)
- [Connecting to a data store](#example-of-connecting-to-a-data-store)
* [backbone](#backbone)
* [mobx](#mobx)
* [redux](#redux)---
# Define a custom-element
```js
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
```html
```
Output
```html
hello world
```
**Live Example of [helloworld](https://codepen.io/codemeasandwich/pen/VOQpqz)**
# 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.
```js
render(Html,store){Html`
Lasted updated at ${new Date().toLocaleTimeString()}
`
}// 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
```js
Html`
- ${user.name} `)}
${users.map(user => Html.wire(user,":user_list_item")`
`
```
An **anti-pattern** is to inline the markup as a string
BAD example: ✗
```js
Html`
- ${user.name} `)}
${users.map(user => `
`
```
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](https://jqueryui.com/datepicker/)
```js
onSelect(dateText, inst){
console.log("selected time "+dateText)
} // END onSelect
Date(lite){
const inputElem = lite``
$(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
```js
// 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
```
**Live Example of [attach a store](https://codepen.io/codemeasandwich/pen/VOQWeN)**
#### re-rendering without a data source
Example of re-rendering every second
```js
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
```js
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
```js
// 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
```js
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 `` = `{ 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 `Hi!` = `"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. `` 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:
```html
```
Implementation:
```js
window.customElements.define("a-user",class extends hyperElement{
render(Html){
const onClick = () => this.attrs.hi("Hello from "+this.attrs.name);
Html`${this.attrs.name} Say hi!`
}
})
window.customElements.define("users-elem",class extends hyperElement{
onHi(val){
console.log("hi was clicked",val)
}
render(Html){
Html``
}
})
```
Output:
```html
Beckett Say hi!
```
**Live Example of passing an [onclick to a child element](https://codepen.io/codemeasandwich/pen/rgdvPX)**
## 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:
```Html
```
Implementation:
```js
document.registerElement("my-list",class extends hyperElement{
render(Html){
Html`
${this.dataset.json.map(user => Html.template(user))}
`
}// END render
})// END my-list
```
Output:
```Html
```
**Live Example of using a [templatets](https://codepen.io/codemeasandwich/pen/LoQLrK)**
## 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](#fragment-templates) string to use, **(Is sanitised)**
* **values:** A set of values to be used in the **template**
**Example:**
Implementation:
```js
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`
${{FriendCount:userId}}
`}// END render
})// END my-friends
```
Output:
```html
loading your number of friends
```
then
```html
you have 635 friends
```
### fragment templates
You can use the [template](#templates) 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:"
{txt}
", values:{txt:"Ipsum"} }}`* with `` Html`${{Foo:{}}}` ``
**or** assigning values to template from with in the **render function**
* `Foo(values){ return{ template:"
{txt}
" }}`* with `` Html`${{Foo:{txt:"Ipsum"}}}` ``
*Note: the different is whether or not a "values" is returned from the fragment function*
**output**
`
Ipsum
`**Example:**
Implementation:
```js
document.registerElement("click-me",class extends hyperElement{
Button(){
return {
template:`{text}`
}// END return
}// END Button
render(Html){
Html`Try ${{Button:{
text:"Click Me",
onclick:()=>alert("Hello!")
}}}`
}// END render
})// END click-me
```
Output:
```html
Try Click Me
```
#### Asynchronous fragment templates
You can also return a [promise] as your `template` property.
Rewritting the *my-friends* example
**Example:**
Implementation:
```js
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`
${{FriendCount:userId}}
`}// 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:
```html
you have 635 friends
```
**Live Example of using a [asynchronous fragment](https://codepen.io/codemeasandwich/pen/MdQrVd)**
## Styling
Supports an object as the style attribute. Compatible with React's implementation.
**Example:** of centering an element
```js
render(Html){
const style= {
position: "absolute",
top: "50%", left: "50%",
marginRight: "-50%",
transform: "translate(-50%, -50%)"
}//END style
Html`
}//END render
```
**Live Example of [styling](https://codepen.io/codemeasandwich/pen/RmQVKY)**
# Example of connecting to a data store
## backbone
```js
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
```js
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
```js
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
```
[shadow-dom]:https://developers.google.com/web/fundamentals/web-components/shadowdom
[innerHTML]:https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML
[hyperHTML]:https://viperhtml.js.org/hyper.html
[Custom Elements]:https://developer.mozilla.org/en-US/docs/Web/Web_Components/Custom_Elements
[Test system]:https://jsfiddle.net/codemeasandwich/k25e6ufv/36/
[promise]:https://scotch.io/tutorials/javascript-promises-for-dummies#understanding-promises