[![tests](https://img.shields.io/github/actions/workflow/status/substrate-system/tonic/nodejs.yml?style=flat-square)](https://github.com/substrate-system/tonic/actions/workflows/nodejs.yml) [![GZip size](https://img.shields.io/bundlephobia/minzip/@substrate-system/tonic?style=flat-square&label=GZip%20size)](https://bundlephobia.com/result?p=@substrate-system/tonic) [![install size](https://flat.badgen.net/packagephobia/install/@substrate-system/tonic?)](https://packagephobia.com/result?p=@substrate-system/tonic) [![module](https://img.shields.io/badge/module-ESM%2FCJS-blue?style=flat-square)](README.md) [![semantic versioning](https://img.shields.io/badge/semver-2.0.0-blue?logo=semver&style=flat-square)](https://semver.org/) [![Common Changelog](https://nichoth.github.io/badge/common-changelog.svg)](./CHANGELOG.md) [![license](https://img.shields.io/badge/license-Big_Time-blue?style=flat-square)](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`

Loading...

` const res = await fetch('https://api.github.com/') const urls = await res.json() return this.html`
        ${JSON.stringify(urls, 2, 2)}
      
` } } ``` ### 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) // => '
Hello, World!
' ``` #### Nested Components + Hydration ```js class InnerComponent extends Tonic { render () { return this.html`${this.props.text}` } } class OuterComponent extends Tonic { render () { return this.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}

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