Server side render for Custom Elements.
Enhance enables a web standards based workflow that embraces the platform by supporting Custom Elements and slot syntax.
npm i @enhance/ssr
import HelloWorld from './path/to/elements/hello-world.mjs'
import enhance from '@enhance/ssr'
const html = enhance({
elements: {
'hello-world': HelloWorld
}
})
console.log(html`<hello-world greeting="Well hi!"></hello-world>`)
Elements are pure functions that are passed an object containing an html
function used to expand custom elements and a state object comprised of attrs
which are the attributes set on the custom element and a store
object that contains application state.
export default function HelloWorld({ html, state }) {
const { attrs } = state
const { greeting='Hello World' } = attrs
return html`
<style scope="global">
h1 {
color: red;
}
</style>
<h1>${greeting}</h1>
`
}
The rendered output
<head>
<style scope="global">
h1 {
color: red;
}
</style>
</head>
<body>
<hello-world>
<h1>Hello World</h1>
</hello-world>
</body>
You can also use an object that exposes a render
function as your template. The render function will be passed the same arguments { html:function, state:object }
.
{
attrs: [ 'label' ],
init(el) {
el.addEventListener('click', el.click)
},
render({ html, state }) {
const { attrs={} } = state
const { label='Nope' } = attrs
return html`
<pre>
${JSON.stringify(state)}
</pre>
<button>${ label }</button>
`
},
click(e) {
console.log('CLICKED')
},
adopted() {
console.log('ADOPTED')
},
connected() {
console.log('CONNECTED')
},
disconnected() {
console.log('DISCONNECTED')
}
}
Use these options objects with the enhance custom element factory
Supply initital state to enhance and it will be passed along in a store
object nested inside the state object.
import MyStoreData from './path/to/elements/my-store-data.mjs'
import enhance from '@enhance/ssr'
const html = enhance({
elements: {
'my-store-data': MyStoreData
},
initialState: { apps: [ { users: [ { name: 'tim', id: 001 }, { name: 'kim', id: 002 } ] } ] }
})
console.log(html`<my-store-data app-index="0" user-index="1"></my-store-data>`)
export default function MyStoreData({ html, state }) {
const { attrs, store } = state
const appIndex = attrs['app-index']
const userIndex = attrs['user-index']
const { id='', name='' } = store?.apps?.[appIndex]?.users?.[userIndex] || {}
return `
<div>
<h1>${name}</h1>
<h1>${id}</h1>
</div>
`
}
The store is used to pass state to all components in the tree.
Enhance supports the use of slots
in your custom element templates.
export default function MyParagraph({ html }) {
return html`
<p>
<slot name="my-text">
My default text
</slot>
</p>
`
}
You can override the default text by adding a slot attribute with a value that matches the slot name you want to replace.
<my-paragraph>
<span slot="my-text">Let's have some different text!</span>
</my-paragraph>
Enhance supports unnamed slots for when you want to create a container element for all non-slotted child nodes.
export default function MyButton({ html }) {
return html`
<button>
<slot>Submit</slot>
</button>
`
}
<my-button></my-button>
<my-button>Save</my-button>
Enhance supports the inclusion of script and style transform functions. You add a function to the array of scriptTransforms
and/or styleTransforms
and are able to transform the contents however you wish, just return your desired output.
import enhance from '@enhance/ssr'
const html = enhance({
elements: {
'my-transform-script': MyTransformScript
},
scriptTransforms: [
function({ attrs, raw }) {
// raw is the raw text from inside the script tag
// attrs are the attributes from the script tag
return raw + ' yolo'
}
],
styleTransforms: [
function({ attrs, raw }) {
// raw is the raw text from inside the style tag
// attrs are the attributes from the style tag
const { scope } = attrs
return `
/* Scope: ${ scope } */
${ raw }
`
}
]
})
function MyTransformScript({ html }) {
return html`
<style scope="component">
:host {
display: block;
}
</style>
<h1>My Transform Script</h1>
<script type=module>
class MyTransformScript extends HTMLElement {
constructor() {
super()
}
}
customElements.define('my-transform-script', MyTransformScript)
</script>
`
}
console.log(html`<my-transform-script></my-transform-script>`)
There are times you will need to pass state to nested child custom elements. To avoid the tedium of passing attributes through multiple levels of nested elements Enhance SSR supplies a context
object to add state to.
Parent sets context
export default function MyContextParent({ html, state }) {
const { attrs, context } = state
const { message } = attrs
context.message = message
return html`
<slot></slot>
`
}
Child retrieves state from parent supplied context
export default function MyContextChild({ html, state }) {
const { context } = state
const { message } = context
return html`
<span>${ message }</span>
`
}
Authoring
<my-context-parent message="hmmm">
<div>
<span>
<my-context-child></my-context-child>
</span>
</div>
</my-context-parent>
When rendering custom elements from a single template there are times where you may need to target a specific instance. The instanceID
is passed in the state
object.
export default function MyInstanceID({ html, state }) {
const { instanceID='' } = state
return html`
<p>${instanceID}</p>
`
}
Enhance SSR outputs an entire valid HTML page but you can pass bodyContent: true
to get just the content of the <body>
element. This can be useful for when you want to isolate the HTML output to just the Custom Element(s) you are authoring.
const html = enhance({
bodyContent: true,
elements: {
'my-paragraph': MyParagraph,
}
})
const output = html`
<my-paragraph></my-paragraph>
`
Enhance SSR will intelligently merge its rendered <html>
, <head>
, and <body>
elements with any that you provide to it (unless you choose to use the bodyContent: true
option).
For example:
const html = enhance({
elements: {
'my-content': MyContent,
}
})
html`
<!DOCTYPE html>
<html lang="en">
<head>
<title>My Website</title>
</head>
<body class="foo bar">
<my-content></my-content>
</body>
</html>
`
P.S. Enhance works really well with Begin.