[](https://github.com/substrate-system/tonic/actions/workflows/nodejs.yml)
[](https://bundlephobia.com/result?p=@substrate-system/tonic)
[](https://packagephobia.com/result?p=@substrate-system/tonic)
[](README.md)
[](https://semver.org/)
[](./CHANGELOG.md)
[](LICENSE)
# Tonic
Tonic is a low profile component framework for the web.
It's designed to be used with contemporary Javascript and is compatible
with all modern browsers. It's built on top of
[Web Components](https://developer.mozilla.org/en-US/docs/Web/API/Web_components).
[See the API docs](https://substrate-system.github.io/tonic/index.html)
Contents
- [Install](#install)
- [tl;dr](#tldr)
- [Use](#use)
* [Bundler](#bundler)
* [Pre-bundled](#pre-bundled)
+ [Copy](#copy)
+ [HTML](#html)
- [Get Started](#get-started)
* [Register](#register)
* [HTML](#html-1)
* [Render](#render)
* [Rerender](#rerender)
* [Events](#events)
+ [Events Example](#events-example)
* [State](#state)
- [Server-Side Rendering](#server-side-rendering)
* [SSR Example](#ssr-example)
+ [The Component](#the-component)
+ [Render](#render-1)
+ [Nested Components + Hydration](#nested-components--hydration)
* [Async Components + Hydration](#async-components--hydration)
* [Hydration](#hydration)
+ [Start a local example](#start-a-local-example)
+ [Build the hydration example](#build-the-hydration-example)
+ [Server](#server)
+ [Client](#client)
- [Docs](#docs)
- [API](#api)
* [Event listeners](#event-listeners)
+ [Event Listener Example](#event-listener-example)
* [`tag`](#tag)
* [`emit`](#emit)
+ [emit example](#emit-example)
* [`static event`](#static-event)
+ [example](#example)
* [`dispatch`](#dispatch)
+ [`dispatch` example](#dispatch-example)
- [Develop](#develop)
* [build ESM](#build-esm)
* [build Common JS](#build-common-js)
* [build UMD modules](#build-umd-modules)
- [Useful links](#useful-links)
## Install
```sh
npm i -S @substrate-system/tonic
```
## tl;dr
This is a front-end view library, like React, but using web components.
> [!TIP]
> DOM state, such as element focus and input values, is preserved
> across multiple calls to `reRender`.
-------
## Use
### Bundler
```js
import Tonic from '@substrate-system/tonic'
```
### Pre-bundled
This package exposes minified JS files too. Copy them so they are accessible
to your web server, then link to them in HTML.
#### Copy
```sh
cp ./node_modules/@substrate-system/tonic/dist/index.min.js ./public/tonic.min.js
```
#### HTML
```html
```
-----------------------------------
## Get Started
Building a component with Tonic starts by creating a function or a
[class](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes).
The class should have at least one method named *render* which returns
a [template literal](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals)
of HTML.
```js
import Tonic from '@substrate-system/tonic'
class MyGreeting extends Tonic {
render () {
return this.html`
Hello, World.
`
}
}
```
or
```js
function MyGreeting () {
return this.html`
Hello, World.
`
}
```
---
The HTML tag for your component will match the class or function name.
> [!NOTE]
> Tonic is a thin wrapper around `web components`. Web
> components require a name with two or more parts. So your class name should
> be `CamelCased` (starting with an uppercase letter). For example, `MyGreeting`
> becomes ``.
>
---
### Register
Next, register your component with `Tonic.add(ClassName)`.
```js
Tonic.add(MyGreeting)
```
---
### HTML
After adding your Javascript to your HTML, you can use your component anywhere.
```html
```
>
> [!NOTE]
> Custom tags (in all browsers) require a closing tag even if
> they have no children. Tonic doesn't add any "magic" to change how this works.
>
---
### Render
When the component is rendered by the browser, the result of your render
function will be inserted into the component tag.
```html
Hello, World.
```
A component (or its render function) may be an `async` or an `async generator`.
```js
class GithubUrls extends Tonic {
async * render () {
yield this.html`
`
}
}
```
### Rerender
Call `tonicInstance.reRender()` to render your component again with updated
state. This is totally decoupled from any kind of state machine, so you can
choose how to batch state updates, and just re-render when necessary.
> [!TIP]
> DOM state, such as focus and input values, is preserved
> across multiple calls to `reRender`.
### Events
There is a convention for event handler method names. Name a method like
`handle_example`, and the method will be called with any `example` type
event.
#### Events Example
```js
import { Tonic } from '@substrate-system/tonic'
class ButtonExample extends Tonic {
handle_click (ev) {
ev.preventDefault()
if (Tonic.match(ev.target as HTMLButtonElement, 'button')) {
// button clicks only
this.increment()
}
this.props.onbtnclick('hello')
}
render () {
return this.html`
example
`
}
}
```
### State
`this.state` is a plain-old javascript object. Its value will be persisted if
the component is re-rendered. **Any component with state must have an**
**id property**.
Setting the state will not cause a component to re-render. This way you can
make incremental updates. Components can be updated independently, and
rendering only happens only when necessary.
Remember to clean up! States are just a set of key-value pairs on the Tonic
object. So if you create temporary components that use state,
clean up their state after you delete them. For example,
if I have a component with thousands of temporary child elements that
all use state, I should delete their state after they get destroyed.
Delete `Tonic._states[someRandomId]`
## Server-Side Rendering
Tonic includes a `renderToString` function that converts component instances
to static HTML strings, making it easy to implement server-side rendering.
The `renderToString` function will process nested Tonic components recursively.
### SSR Example
#### The Component
```js
// my-component.js
import Tonic from '@substrate-system/tonic'
export class MyComponent extends Tonic {
render () {
return this.html`
Hello, ${this.props.name}.
`
}
}
```
#### Render
> [!IMPORTANT]
> Need to import `Tonic` after `render-to-string`,
> because it will polyfill some globals.
```js
// this runs in node
import {
render as renderToString
} from '@substrate-system/tonic/render-to-string'
// Import Tonic after render-to-string
import { Tonic } from '@substrate-system/tonic'
import { MyComponent } from './my-component.js'
// Create a component instance
const component = new MyComponent()
component.props = { name: 'World' }
// Render to HTML string
const html = await renderToString(component)
console.log(html)
// => '
`
}
}
Tonic.add(InnerComponent)
Tonic.add(OuterComponent)
const component = new OuterComponent()
const html = await renderToString(component)
// Nested components are rendered correctly
```
### Async Components + Hydration
The `renderToString` function works with async component render methods:
```js
class AsyncComponent extends Tonic {
async render () {
const data = await fetchData()
return this.html`
${data}
`
}
}
Tonic.add(AsyncComponent)
const component = new AsyncComponent()
const html = await renderToString(component)
// Waits for async render to complete
```
### Hydration
Add interactivity to server-rendered HTML without re-rendering.
The server attaches serialized state to the page,
and the client initializes components with that state.
#### Start a local example
```sh
npm run start:hydration
```
#### Build the hydration example
```sh
npm run build:hydration
```
#### Server
`render` returns the inner HTML of a component. `toHtml`
wraps that in the custom element tag (e.g.
`...content...`), with props encoded as
attributes. Pass a `state` option to embed complex props
as JSON.
```js
import {
render,
toHtml
} from '@substrate-system/tonic/render-to-string'
class MyApp extends Tonic {
render () {
return this.html`
${this.props.title}
${this.props.items.map(item =>
this.html`
${item}
`
)}
`
}
}
Tonic.add(MyApp)
const props = { title: 'Hello', items: ['a', 'b', 'c'] }
const app = new MyApp()
app.props = props
const content = await render(app)
// Wrap in component tag + embed state for hydration
const html = toHtml(app, content, {
id: 'app',
state: { app: props }
})
// html =>
//
//
Hello
...
//
//
```
Simple props (strings, numbers, booleans, null) are encoded
as HTML attributes automatically. Complex props (objects,
arrays) are transferred via the JSON `