@st-lib/render
Version:
Lightweight HTML rendering library.
521 lines (410 loc) • 13.9 kB
Markdown
# Lightweight HTML rendering library.
> ESNext module, please use [webpack](https://webpack.js.org/) / [rollup](https://rollupjs.org/guide/en/) / [parcel](https://parceljs.org/) / etc.
## Additional materials
1. [@st-lib/render-html](https://www.npmjs.com/package/@st-lib/render-html),
2. [@st-lib/render-svg](https://www.npmjs.com/package/@st-lib/render-svg),
3. [@st-lib/render-mathml](https://www.npmjs.com/package/@st-lib/render-mathml),
4. [@st-lib/render-events](https://www.npmjs.com/package/@st-lib/render-events),
4. [@st-lib/render-with-state](https://www.npmjs.com/package/@st-lib/render-with-state).
## Concepts.
1. The library uses a context-oriented approach to describing the structure of HTML.
2. The purpose of the library is NOT to work with the state of the application, use third-party libraries for this.
3. The library does not create intermediate representations of the document structure (Virtual DOM), but instead updates the document directly.
## Basic usage.
Use `render` function for render to document.
```ts
export interface Renderer {
createElement(tagName: string, options?: ElementCreationOptions): Element;
createElementNS(namespaceURI: string | null, tagName: string, options?: ElementCreationOptions): Element;
createTextNode(data: string): Text;
createComment(data: string): Comment;
createAttribute(localName: string): Attr;
createAttributeNS(namespace: string | null, qualifiedName: string): Attr;
}
export declare function render<T extends Element>(target: T | null, content: (ref: T) => void, renderer?: Renderer): void;
```
Use `rerender` function for render element with last used content callback and renderer
```ts
render(document.body, () => {
let i = 0
element(null, 'button', ref => {
// see https://www.npmjs.com/package/@st-lib/render-events
onClick(() => {
i++
rerender(ref)
})
text(null, i)
})
})
```
### Element
Create element node with specified key, tag name, namespace URI, creation options and content rendering function or raw HTML string
> UPDATE: added customized built-in elements support.
```ts
export declare function element<T extends Element>(key: any, tag: string | [string, (string | null)?, (ElementCreationOptions | null)?], content?: ElementContentDescriptor<T> | false | null | undefined): T | null;
```
___Key___: accepts any type: `number`, `string`, `symbol` or `object`. Passing `null` or `undefined` as key will be replaced with current key order.
___Tag___: accepts `tagName: string` or turple` [tagName: string, namespaceURI?: string | null, options?: ElementCreationOptions | null] `
___Content___: content rendering function `(ref: T extends Element) => void` or raw HTML string
Returns the created element node or `null` if the node is not created.
#### Example:
```ts
import { render, element } from '@st-lib/render'
window.onload = () => {
render(document.body, () => {
element(null, 'header')
element(null, 'main', () => {
element(null, 'article')
element(null, 'article')
element(null, 'article')
element(null, 'article')
// svg support
element(null, ['svg', 'http://www.w3.org/2000/svg'], () => {
element(null, ['a', 'http://www.w3.org/2000/svg'])
})
// custom element
element(null, 'custom-element')
// cusomized built-in
element(null, ['form', null, { is: 'custom-form' }])
})
element(null, 'footer')
//
})
}
/*
<body>
<header></header>
<main>
<article></article>
<article></article>
<article></article>
<article></article>
<svg>
<a />
</svg>
<custom-element></custom-element>
<form is='custom-form'></form>
</main>
<footer></footer>
</body>
*/
```
### Attributes
Update current rendering element attributes
```ts
// Setting single attribute
export declare function attr(name: string, value: OptionalAttrValue, namespaceURI?: string | null): void | null;
```
```ts
// Setting several attributes in one operation
export declare type AttrsMapEntry = AttrValue | [OptionalAttrValue, (string | null)?];
export declare type OptionalAttrMapEntry = AttrsMapEntry | false | null | undefined;
export declare type AttrsMap = Record<string, OptionalAttrMapEntry>;
export declare function attrs<T extends AttrsMap>(inp: T): void;
```
___name___: `string`, the name of attribute .
___value___: `string | number | false | null | undefined`, the value of attribute
* if `null | undefined`: do nothing
* else if `false`: delete attribute
* else: set attribute to `String(value)`
___namespaceURI___: `string | null | undefined`
> Setting several attributes in one operation has corresponding optimizations.
> Note: unused attributes will __NOT__ be removed.
#### Example:
```ts
import { render, element, text, attr, attrs } from '@st-lib/render'
window.onload = () => {
render(document.body, () => {
element(null, 'form', () => {
// set all attributes with one operation
attrs({
action: '/some/url',
method: 'post',
enctype: 'multipart/form-data',
'delete-attribute': false,
'skipped-attribute-1': null,
'skipped-attribute-2': undefined,
})
element(null, 'input', () => {
// make input required
attr('required', '') // only string, number and "falselike types" allowed
})
element(null, 'button', () => {
text(null, 'submit')
})
})
})
}
/*
<body>
<form action="/some/url" method="post" enctype="multipart/form-data">
<input required />
<button>submit</button>
</form>
</body>
*/
```
### Text
Create text node with specified key and string or number value.
```ts
export declare function text(key: any, value: OptionalTextValue): Text | null;
```
___Key___: accepts any type: `number`, `string`, `symbol` or `object`. Passing `null` or `undefined` as key will be replaced with current call order.
___Value___: accepts `string` or `number`. Passing `false`, `null` or `undefined` does not emit text node.
Returns the created text node or `null` if the node is not created.
> Passing `NaN` as value will emit console warning.
#### Example:
```ts
import { render, element, text } from '@st-lib/render'
window.onload = () => {
render(document.body, () => {
element(null, 'header')
element(null, 'main', () => {
element(null, 'article', () => {
text(null, 'Lorem ipsum dolor sit amet.')
})
element(null, 'article', () => {
text(null, 'Lorem ipsum dolor sit amet.')
})
element(null, 'article', () => {
text(null, 'Lorem ipsum dolor sit amet.')
})
element(null, 'article', () => {
text(null, 'Lorem ipsum dolor sit amet.')
})
})
element(null, 'footer')
})
}
/*
<body>
<header></header>
<main>
<article>Lorem ipsum dolor sit amet.</article>
<article>Lorem ipsum dolor sit amet.</article>
<article>Lorem ipsum dolor sit amet.</article>
<article>Lorem ipsum dolor sit amet.</article>
</main>
<footer></footer>
</body>
*/
```
> Normalization of text nodes is not supported! Use string interpolation.
```ts
import { render, text } from '@st-lib/render'
window.onload = () => {
render(document.body, () => {
// SSR invalid
const t1 = text(null, 'Lorem ipsum dolor sit amet,')
const t2 = text(null, ' consectetur adipiscing elit. Vivamus ac.')
console.log(t1 !== t2) // true
console.log(t1.textContent === 'Lorem ipsum dolor sit amet,') // true
console.log(t2.textContent === ' consectetur adipiscing elit. Vivamus ac.') // true
})
}
/*
<body>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus ac.</body>
*/
```
### Comment
Create comment node with specified key and string or number value.
```ts
export declare function comment(key: any, value: OptionalCommentValue): Comment | null | undefined;
```
___Key___: accepts any type: `number`, `string`, `symbol` or `object`. Passing `null` or `undefined` as key will be replaced with current call order.
___Value___: accepts `string` or `number`. Passing `false`, `null` or `undefined` does not emit comment node.
Returns the created comment node or `null` if the node is not created.
> Passing `NaN` as value will emit console warning.
#### Example:
```ts
import { render, comment } from '@st-lib/render'
window.onload = () => {
render(document.body, () => {
// <!-- creates comment node --!>
comment(null, 'creates comment node')
})
}
```
## Lifecycle hooks
> All lifecycle hooks work in client side rendering, after render stage.
### Created
Called after new element created or used existing sililar element (with same tag name and namespase URI).
```ts
export declare function created<T extends Element>(elementCreatedCallback: ElementCreatedCallback<T>): void;
```
Returned function used as `elementRemovedCallback`.
#### Example:
```ts
import { render, element, created } from '@st-lib/render'
window.onload = () => {
render(document.body, () => {
element(null, 'div', divElement => {
created(createdElement => {
console.log('element "div" created', createdElement)
return removedElement => {
console.log('element "div" removed', removedElement, divElement === createdElement && divElement === removedElement /* true */)
}
})
})
})
}
```
### Updated
Called when an element is rendered again.
```ts
export declare function updated<T extends Element>(elementUpdatedCallback: ElementUpdatedCallback<T>): void;
```
Returned function used as `elementCleanupCallback`
#### Example:
```ts
import { render, element, updated } from '@st-lib/render'
function App() {
element(null, 'div', divElement => {
updated(updatedElement => {
console.log('element has beed updated', updatedElement)
return cleanupElement => {
console.log('cleanup', cleanupElement, divElement === updatedElement && divElement === cleanupElement /* true */)
}
})
})
}
window.onload = () => {
// no console output
render(document.body, App)
setTimeout(() => {
// emits console output
render(document.body, App)
})
}
```
### Removed
Called before an element will be removed from document. See `created`
```ts
export declare function removed<T extends Element>(elementRemovedCallback: ElementRemovedCallback<T>): void;
```
#### Example:
```ts
import { render, element, removed } from '@st-lib/render'
function elementRemovedCallback() {
console.log('removed')
}
window.onload = () => {
render(document.body, () => {
element(0, 'div', divElement => {
removed(elementRemovedCallback) // same as created(() => elementRemovedCallback)
})
element(0, 'span') // replace element <0.div> with <0.span>
})
}
```
```ts
import { render, element, created, removed } from '@st-lib/render'
function elementRemovedCallback() {
console.log('will be called once')
}
window.onload = () => {
render(document.body, () => {
element(null, 'div', divElement => {
created(() => elementRemovedCallback)
removed(elementRemovedCallback)
})
})
}
```
## Render stage hooks
### Linking
Called instantly during rendering.
Returned function used as `elementCleanupCallback`.
```ts
export declare function linking<T extends Element>(elementReferenceCallback: ElementLinkCallback<T>): void;
```
#### Example:
```ts
import { render, element, linking } from '@st-lib/render'
window.onload = () => {
render(document.body, () => {
element(null, 'button', () => {
linking(buttonElement => {
function onClickListener(e) {
console.log('click', e)
}
buttonElement.addEventListener('click', onClickListener, true)
return () => {
buttonElement.removeEventListener('click', onClickListener, true)
}
})
})
})
}
```
### Cleanup
Called every time before rendering. See `linking` or `update`.
```ts
export declare function cleanup<T extends Element>(elementCleanupCallback: ElementCleanupCallback<T>): void;
```
## SSR (Server Side Rendering)
Use `write` function for render to string.
```ts
export declare function write(content: (ref: null) => void): string;
```
#### Example:
```ts
// App.js
import { element, text } from '@st-lib/render'
function onClickListener(e) {
console.log('click', e)
}
export default function App() {
element(null, 'header')
element(null, 'main', () => {
element(null, 'article', () => {
text(null, 'Lorem ipsum dolor sit amet.')
})
element(null, 'article', () => {
text(null, 'Lorem ipsum dolor sit amet.')
})
element(null, 'article', () => {
text(null, 'Lorem ipsum dolor sit amet.')
})
element(null, 'article', () => {
text(null, 'Lorem ipsum dolor sit amet.')
})
element(null, 'button', () => {
linking(btn => {
btn.addEventListener('click', onClickListener, true)
return () => btn.removeEventListener('click', onClickListener, true)
})
})
})
element(null, 'footer')
}
```
```ts
// client.js
import { render } from '@st-lib/render'
import App from './App'
window.onload = () => {
render(document.body, App)
}
```
```ts
// server.js
import { write } from '@st-lib/render'
import { createServer } from 'http'
import App from './App'
const server = createServer((_, res) => {
res.setHeader('Content-Type', 'text/html')
res.write(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>@st-lib/render SSR example</title>
</head>
<body>${write(App)}<script src="/client.js"></script></body>
</html>`)
res.end()
})
server.listen(3000)
```