Docs Examples

Intro

Minimalist, Stable, Audit Friendly.

Features

  • Only 250 lines of code.
  • One-way data binding.
  • Mix in your own Routers, Reducers, Validators, etc.
  • Composition & component oriented.
  • Plain old HTML, no template languages.
  • Event delegation by default.

1. Getting Started

Building a component with Tonic starts by creating a Javascript Class. The class should have at least one method named render which usually returns a string of HTML.

class MyGreeting extends Tonic {
  //
  // The render function can return a template-literal of HTML,
  // it can also include other components.
  //
  render () {
    return `<div>Hello, World.</div>`
  }
}

The name of your class will determine the html tag name for your component. A Camel cased class names will create hyphenated tag names, ie MyGreeting will become <my-greeting></my-greeting>. Web components require that you have two part names.


Next, register your component with Tonic.add(ClassName).

Tonic.add(MyGreeting)

After adding your Javascript to your HTML, you can start to use your component.

<html>
  <head>
    <script src="index.js"></script>
  </head>

  <body>
    <my-greeting></my-greeting>
  </body>
</html>

Note: Unrelated to Tonic, custom tags (in all browsers) require a closing tag (even if they have no children).


When the component is rendered by the browser, the result of your render function will be inserted into the component tag.

<html>
  <head>
    <script src="index.js"></script>
  </head>

  <body>
    <my-greeting>
      <div>Hello, World.</div>
    </my-greeting>
  </body>
</html>

2. Properties

Properties are used by a component to help it decide how it should appear or how it should behave. Properties are read only. In this case, message is our property and Hello, World is our property value.

class MyApp extends Tonic {
  render () {
    return `
      <my-greeting message='Hello, World'></my-greeting>
    `
  }
}

Properties added to a component appear on the this.props object.

class MyGreeting extends Tonic {
  render () {
    return `
      <h1>${this.props.message}</h1>
    `
  }
}

Tonic has no templating language, it uses HTML! Remember, HTML only understands string values. If we want to pass more complex values to a component, prefix the string returned by the render function with this.html.

const data = { greeting: 'hello, world' }

class MyApp extends Tonic {
  render () {
    return this.html`
      <my-component title=${data}></my-component>
    `
  }
}

Now this.props has a reference to the data object.

class MyComponent extends Tonic {
  render () {
    return `
      <h1>${this.props.data.greeting}</h1>
    `
  }
}

Note: A property named fooBar='30' will become lowercased (as per the HTML spec). If you want the property name to be camel cased when added to the props object, use foo-bar='30' to get this.props.fooBar.


You can use the "spread" operator to expand object literals into html properties.

class FooBar extends Tonic {
  render () {
    const o = {
      a: 'testing',
      b: 2.2,
      fooBar: '"ok"'
    }

    return this.html`
      <spread-component ...${o}>
      </spread-component>

      <div ...${o}>
      </div>
    `
  }
}

The above compoent renders the following output.

<foo-bar>
  <spread-component a="testing" b="2.2" foo-bar="&quot;ok&quot;">
    <div a="testing" b="2.2" foo-bar="&amp;quot;ok&amp;quot;">
    </div>
  </spread-component>

  <div a="testing" b="2.2" foo-bar="&quot;ok&quot;">
  </div>
</foo-bar>

Updating properties

If you want to manually update a component, you should think of your document's hierarchy and where in it the update should take place. It's better to update a component higher up in the hierarchy and let the data cascade downward to child components.


To manually update a component you can use the .reRender() method. This method receives either an object or a function. For example...

// Update a component's properties
this.reRender(props => ({
  ...props,
  color: 'red'
}))

// Reset a component's properties
this.reRender({ color: 'red' })

// Re-render a component with its existing properties
this.reRender()

The .reRender() method can also be called directly on a component.

document.getElementById('parent').reRender({ data: [1,2,3, ...9999] })

3. Methods

A method is a function of a component. It can help to organize the internal logic of a component.

The constructor is a special method that is called once each time an instance of your component is created.

class MyComponent extends Tonic {
  constructor () {
    super()
    // ...
  }

  myMethod (n) {
    this.innerHTML = `The number is ${n}`
  }
}

After the component is created, the method myMethod method can be called.

document.getElementById('foo').myMethod(42)

4. Styling

Components should ship with as little CSS as possible and try to inherit whenever possible from the document's stylesheets. Tonic supports two approaches to styling components.

Approach 1. Inline styles

It is a security risk to add inline styles from html. A CSP policy will usually prevent this. Use the styles() method to inline styles safely. Tonic will apply the style properties when the render() method is called.

class MyGreeting extends Tonic {
  styles () {
    return {
      fontStyle: {
        color: this.props.fg,
        fontSize: '30px'
      },
      background: {
        backgroundColor: this.props.bg,
        padding: '10px'
      }
    }
  }

  render () {
    return `<div styles="fontStyle background">${this.children}</div>`
  }
}
<my-greeting fg="white" bg="red">Hello, World</my-greeting>

Approach 2. Dynamic Stylesheets

Use the stylesheet() function to inline a stylesheet into the document where the component is rendered. Since the value is css, you can use any css-in-js library.

class MyGreeting extends Tonic {
  stylesheet () {
    return `

      my-greeting div {
        display: inline-block;
        border: 1px dotted #666;
        line-height: 90px;
      }

      my-greeting .tonic--my-greeting--show {
        display: flex;
      }
    `
  }

  render () {
    return `<div></div>`
  }
}

5. Events

Tonic helps you capture events that happen when someone interacts with your component. It also helps you organize that code.

class Example extends Tonic {
  //
  // You can listen to any DOM event that happens in your component
  // by creating a method with the corresponding name. The method will
  // receive the plain old Javascript event object.
  //
  mouseover (e) {
    // ...
  }

  change (e) {
    // ...
  }

  click (e) {
    //
    // You may want to check which element in the component was actually
    // clicked. You can also check the `e.path` attribute to see what was
    // clicked (helpful when handling clicks on top of SVGs).
    //
    if (!e.target.matches('.parent')) return

    // ...
  }

  render () {
    return `<div></div>`
  }
}

The convention of most frameworks is to attach individual event listeners, such as onClick={myHandler()} or click=myHandler. In the case where you have a table with 2000 rows, this would create 2000 individual listeners.

Tonic prefers the event delegation pattern. With event delegation, we attach a single event listener and watch for interactions on the child elements of a component. With this approach, fewer listeners are created and we do not need to rebind them when the DOM is re-created.

Each event handler method will receive the plain old Javascript event object. This object contains a target property, the exact element that was clicked. The path property is an array of elements containing the exact hierarchy.

Some helpful native DOM APIs for testing properties of an element:

Tonic also provides a helper function which checks if the element matches the selector, and if not, tries to find the closest match.

Tonic.match(el, 'selector')

Here, when a particular element inside a child component is clicked, we intercept the click event and pass along some data to the parent component.

Example

class Child extends Tonic {
  click (e) {
    e.detail.bar = true
  }
  render () {
    return `<div class="foo">Click Me</div>`
  }
}

class Parent extends Tonic {
  click (e) {
    if (e.target.matches('.foo')) {
      console.log(e.detail.bar)
    }
  }
  render () {
    return `<child></child>`
  }
}

The event object has a Event.stopPropagation() method that is useful for preventing an event from bubbling up to parent components. You may also be interested in the Event.preventDefault() method.

6. State

Props are received by the parent and should never be changed by the component that receives them. A component can however change its state. Each instance of a component has state object, this.state. This is just a plain-old javascript object. this.setState() can receive a value or a function.

// Update a component's state
this.setState(state => ({
  ...state,
  color: 'red'
}))

// Reset a component's state
this.setState({ color: 'red' })

.setState() will not cause a component to re-render. The reasoning behind this is that the state can be updated independently, as needed — rendering happens only when changes to the representation of the component are required.

7. Composition

You may want to move the children of a component inside some additional layout when the render() function is executed. The this.children property is helpful for this. This is not a "special" member of the props object like React (which is unintuitive), it's a member of the class instance.

class Parent extends Tonic {
  render () {
    return `
      <div class="parent">
        <another-component>
          ${this.children}
        </another-component>
      </div>
    `
  }
}

Tonic.add(Parent)

class Child extends Tonic {
  render () {
    return `
      <div class="child">
        ${this.props.value}
      </div>
    `
  }
}

Tonic.add(Child)

Input HTML

<parent>
  <child value="hello world"></child>
</parent>

Output HTML

<parent>
  <div class="parent">
    <another-component>
      <child>
        <div class="child">hello world</div>
      </child>
    </another-component>
  </div>
</parent>

8. Performance

If you have lots of structure, but minimal changes, you could pre-render your layout to create a reusable node and pass it to the render method. This structure could also come from a <template> tag which my also improve performance.

class AnotherThing extends Tonic {
  constructor (node) {
    super(node)

    const template = document.createElement('template')
    template.appendChild(document.createElement('span'))  

    this.template = template.content
  }

  //
  // Render will automatically deep-clone this node for you.
  //
  render () {
    return this.template
  }
}

9. CSP

CSP stands for Content Security Policy. It's important to add one of these to your app or website if you do anything beyond pure html. This is a good introduction to CSPs if you're not already familiar with how they work.

This is an example policy, it's quite liberal, in a real app you would want these rules to be more specific.

<meta
  http-equiv="Content-Security-Policy"
  content="
    default-src 'self';
    font-src 'self' https:;
    img-src 'self' https: data:;
    style-src 'self' 'nonce-123' https:;
    script-src 'self' 'nonce-123';
    connect-src 'self' https:;">

In order to allow Tonic to execute properly when using a CSP, you might need to set the Tonic.nonce property. For example, given the above policy you would add the following to your javascript...

Tonic.nonce = '123'

Note that 123 is a placeholder, this should be an actual nonce.

BUILT IN DEV MODE