Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

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!

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`


    ${users.map(user => Html.wire(user,":user_list_item")`
  • ${user.name}
  • `)}

`
```

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

BAD example: ✗

```js
Html`


    ${users.map(user => `
  • ${user.name}
  • `)}

`
```

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


{name}

```

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


ann


bob

```

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

center
`
}//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