Templating with TinyBase, Tailwind, and RippleUI
Tags: webdev, javascript, tutorial, programming
Published: 2023-02-20 22:23:17

In the last section, [First Steps With TinyBase](https://dev.to/parenttobias/first-steps-with-tinybase-1ola), we built the data models we would need to support the front end of a Todo app. We will come back around to those. At this point, it might make sense to set up a "template view", one that sets up our sections/containers.

For ease of use, we'll continue with Vite and TinyBase, but we'll also add in two more packages: Tailwind CSS and RippleUI. For those who haven't used it yet, Tailwind CSS is a great utility package that will allow us to style directly in our HTML, by applying classes to elements. And RippleUI is a component framework that builds on Tailwind. So together, they're a powerful team.

In addition to building the basic site itself, we'll also work with some features of TinyBase that will be very useful both here and going forward. We will set up some *values* in TinyBase, rather than the *tables* we used in the last section. We will set up *persistence*, allowing the TinyBase store to save/load `localStorage` as needed. 
And finally, we'll explore *listeners and reactivity* in TinyBase, allowing us to listen to change events on a store and respond accordingly.

## Some Setup Stuff

First, we'll add in the packages we'll need. Let's begin with an entirely new Vite project:

```bash
npm create vite@latest app-skeleton --template vanilla 
// OR
yarn create vite app-skeleton --template vanilla
```

Going forward, I'll be using `yarn`, but take as given that you can use `npm` if you prefer. Next, we'll install the packages so far and add in TinyBase:

```bash
cd app-skeleton
npm install tinybase
```

That installs the existing packages, as well as adding TinyBase. Next, we will get Tailwind CSS and its dependencies (taken from https://tailwindcss.com/docs/guides/vite):

```bash
yarn add tailwindcss postcss autoprefixer -D
```

WIth the modules added, we can create the `tailwind.config.cjs` and `postcss.config.cjs`:

```bash
npx tailwindcss init -p
```

That will set up two files for us in the root of our Vite project. We'll need to tell Tailwind's JIT parser where to work, so we'll edit the `tailwind.config.cjs` first:

```js
// tailwind.config.cjs

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    // add in these two lines
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ], 
  theme: {
    extend: {},
  }, 
  plugins: [], 
}
```

We don't *need* to add the `ts,jsx,tsx` options at this point, but if some of you are working in Typescript, you might want them.

Next, we need to edit the `postcss.config.cjs`, just to be sure it's set up properly. If you left the `-p` option off the `npx tailwindcss init`, you won't *have* that file, so you'd just need to create it yourself. Either way, make sure it contains:

```js
// postcss.config.js

module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  }
}
```

Finally, we need to tell Vite to go ahead and bring in the Tailwind classes. In our `style.css`, we place this at the top:

```css
@tailwind base;
@tailwind components;
@tailwind utilities;
```

At that point, we have tailwind all set up and ready to go. Let's also add in [RippleUI](https://www.ripple-ui.com/) at this point:

```bash
yarn add rippleui
```

With the package installed, we need Tailwind to be made aware of it. So we go *back* to the `tailwind.config.cjs` and add a plugin:

```js
// tailwind.config.cjs

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  // EDIT THIS LINE
  plugins: [require('rippleui')],
}
```

With that done, we should be good to go. At this point, we can start the dev server and begin playing!

```bash
npm run dev
// OR
yarn dev
```

One more thing to include, simply to create icons quickly and easily: open up the `index.html`, and we'll add a link to Bootstrap Icons via CDN:

```html
<link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.3/font/bootstrap-icons.css'>
```

We could as easily have added this via `npm` or `yarn`, do what works for you.

## A Handy Utility Snippet

We'll need an easy way of converting strings to DOM trees, as we will be wanting to construct and style some pretty complex DOM nodes. David Walsh writes about a very useful method for just this (https://davidwalsh.name/convert-html-stings-dom-nodes), which we'll leverage into a `toHtml` method:

```js
// src/util/toHtml.js

const toHtml = (str)=>document.createRange()
  .createContextualFragment(
    str.trim()
  ).firstChild;

export default toHtml;
```

This will take in an HTML string and convert it into a DOM tree for us. This can become powerful, as we aren't obligated to provide *static strings* - we can use *template literals* as we like, and dynamically create our DOM!

```js
const getTimeEl = ()=> toHtml(`
<span class='time'>${new Date().toTimeString()}</span>
`)
```

That function will dynamically update the template literal, and create a new `<span>` containing the current time for us.

I'll be using the `toHtml` function quite a lot, for building and populating templates.

## A Header Template

```js
// src/templates/header.js
import toHtml from "../util/toHtml";

const Header = toHtml(`
<header >
  <h1>App Title</h1>
  <div class='header-controls'>
    <div class='light-switch'>
      <i class="bi-brightness-high"></i>
    </div>
  </div>
</header>`);

export default Header;
```

So the HTML here is pretty straightforward, and at this point, we have three classes included: `header-controls`, `light-switch` and `bi-brightness-hight`. The last one is a Bootstrap Icons class, showing a circle with rays. The first two are ones we will hook into later - the `.light-switch` will be used for toggling between light and dark mode.

But we'll want to add in some Tailwind classes to pretty things up:

```js
import toHtml from "../util/toHtml";

const Header = toHtml(`
<header class='flex justify-between px-3 py-2'>
  <h1 class='inline-block text-3xl font-black text-indigo-800 p-6'>App Title</h1>
  <div class='header-controls'>
    <div class='light-switch text-3xl font-black text-secondary'>
      <i class="bi-brightness-high"></i>
    </div>
  </div>
</header>`);

export default Header;
```

And finally, I would like that `.light-switch` to display as a button. Things like this are why I've added RippleUI:

```js
    <div class='light-switch btn btn-outline-secondary text-3xl font-black text-secondary'>
      <i class="bi-brightness-high"></i>
    </div>
```

So I added the `btn btn-outline-secondary` classes to the `.light-switch`, and the RippleUI button styles will be applied.

## ... And a Layout Template

For the layout, I would like to have a sidebar and a main content area. Also, simply because we *can*, we'll make that sidebar collapsible.

```js
import toHtml from "../util/toHtml"

const Layout = toHtml(`
<main class='container flex'>
  <div>
    <input type='checkbox' id='drawer-left' class='drawer-toggle' />
    <label for='drawer-left' class='btn btn-secondary'>
      <span class='bi bi-list font-black text-xl'></span>
    </label>
    <label class='overlay' for='drawer-left'></label>
    <div class='drawer'>
      <div class='drawer-content'>
        <label class='absolute top-4 right-4' for='drawer-left'>
          <span class='bi bi-x-circle text-secondary font-black text-xl'></span>
        </label>
        <p>Sidebar Content</p>
      </div>
    </div>
  </div>
  <div class='main-content'></div>
</main>`);

export default Layout;
```

In the `<main>`, we have two divs: one for the sidebar, and one for the remaining content. The sidebar is a bit more complex, as it does have some moving parts - all of which are being *managed and defined* from RippleUI. The `class="drawer-toggle"` applied to the input hides it, but also sets up the toggle functionality for the sidebar as a drawer component.

Now, if we revisit the `index.html`, we have something like this:

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>TobyPlaysTheUke :: Template App</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.3/font/bootstrap-icons.css">
  </head>
  <body>
    <div id="app" class="w-full h-full"></div>
    <script type="module" src="/main.js"></script>
  </body>
</html>
```

I've added the `class="w-full h-full"` to the app container, simply so that it is forced to the full screen size. 

## Composing the View

In our `main.js`, we will import those templates, and we'll drop them in:

```js
// main.js

import "./style.css"

import Header from './src/templates/header';
import Layout from './src/templates/layer';

document.querySelector("#app").append(Header, Layout);

```

And, at this point, the page should be set. The hamburger menu opens an empty sidebar, the header content displays as we like, but we haven't set up the responsivity yet - we can't click the light switch. Let's work on that next!

## Getting Back to TinyBase

There may be other site configuration we want to do later, so we will set up a *second* TinyBase data store, leaving the one from the last lesson alone. It will still be set up as a service, like so:

```js
// src/services/config.js
import { createStore } from 'tinybase';

const configStore = createStore();

export default configStore;
```

Now, we want to not only switch between dark mode and light, but we want that choice to *persist*. Fortunately, TinyBase comes with a great `localStorage` interface that we'll consume, `createLocalPersister`:

```js
// src/services/config.js
import { createStore, createLocalPersister } from 'tinybase';

const configStore = createStore();
const configPersister = createLocalPersister(configStore, 'siteConfig')

export default configStore;
```

`createLocalPersister` takes two parameters: the store we want to persist, and a key for `localStorage` . With that, our `configPersister` is ready to run.

As I mentioned, later we might want to add more user-configurable values (color preferences, position of components, whatever). At this point, we only have one property we'll want to deal with, which I'll call `colorMode`. In order to make this updateable later, we can create a `defaultConfig.json` file. Doing that, we can have a set of defaults *if the user hasn't specified one*:

```json
// src/defaultConfig.json
{
  "colorMode": "light"
}
```

We'll default to light mode for now. But how do we tell our `configStore` to use that? Further, we don't *always* want to use that - we only want to use that if there isn't a value in the `localStorage` yet!

Fortunately, TinyBase has us covered. We can tell the `configPersister` to start saving/loading automatically any time a value is changed, and we can provide a default for the autoload! Let's see how that works:

```js
import {createStore, createLocalPersister} from 'tinybase';
import * as initialConfig from '../defaultConfig.json';

const configStore = createStore();
const configPersister = createLocalPersister(configStore, 'siteConfig');

configPersister.startAutoLoad( {}, initialConfig );
configPersister.startAutoSave();

export default configStore;
```

So we call `configPersister.startAutoLoad()`, passing in two parameters. The first would define the *tables* we'd want to default to, and the second is the *values* object we want as our defaults. We aren't using tables here at all, so I've passed in an empty object, but for our values, I pulled the `defaultConfig.json` into a variable, `initialConfig`.

That will check localStorage for the `"siteConfig"` key and, if it exists, populate the `configStore` from that. If `localStorage` doesn't have that key, it will use the `initialConfig` to set the store's values for us.

And then we call `configPersister.startAutoSave()` - and this will automatically write our store back into `localStorage` any time anything changes. That's it for the `configStore`, let's jump back to `main.js` and see how we might consume this!

## Updating the Color Theme

The way Tailwind and RippleUI handle dark mode, we want to set a `data-theme` attribute and `color-scheme` style on the `<html>` tag. But what do we want to set them *to*? Well, we have the `configStore` defined for exactly this purpose:

```js
// main.js
import "./style.css";
import configStore from './src/services/config';

import Header from './src/templates/header';
import Layout from './src/templates/layout';

const setColor = () =>{
  document.documentElement.dataset.theme = configStore.getValue('colorMode');
  document.documentElement.style.colorScheme = configStore.getValue('colorMode');
}

setColor();

document.querySelector('#app').append(Header, Layout);
```

So we're setting the `documentElement.dataset.theme`, which is the `<html>` tag's `"data-theme"` attribute, to the current value of `configStore.getValue('colorMode')` - which by default is `"light"`. And we do the same to the inline style element. If you examine the `<html>` element in the Dev Tools > Element Inspector, you'll see those attributes now set.

## Wiring the Light Switch

Now, in the `Header`, we included a `".light-switch"` element. When that is clicked, we want to update the `configStore`, flipping it from "light" to "dark" or vice versa.

```js
// main.js

const setColor = () =>{
  document.documentElement.dataset.theme = configStore.getValue('colorMode');
  document.documentElement.style.colorScheme = configStore.getValue('colorMode');
}

setColor();

document.querySelector('#app').append(Header, Layout);

Header.querySelector(".light-switch").addEventListener("click", (e)=>{
  configStore.setValue(
    'colorMode', 
    configStore.getValue('colorMode')==='dark' ?
      'light' :
      'dark'
  );
  e.currentTarget.querySelector("i").classList.toggle("bi-brightness-high");
  e.currentTarget.querySelector("i").classList.toggle("bi-moon-stars");
})
```

so the `configStore.setValue()` is being changed *based on its current value*. If that value is `"dark"`, we set it to `"light"`. Otherwise, we set it to `"dark"`. At the same we can toggle the light mode and dark mode icons, flipping the icon to match the theme.

But we've flipped the `configStore`, and we've switched the button - we haven't yet changed the page itself. This is where we explore *reactivity* in TinyBase.

## Responding to Changes

We've got everything set, and we *could* have simply called `setColor` from within the event listener for the switch - but what if we wanted to add other functionality, other *side effects*, when the `colorMode` changes? We would either have to go back to the lightswitch method and keep adding more mess there, or we need to be able to observe and respond to changes.

Fortunately (again), TinyBase comes to the rescue with a full suite of responsivity methods! Here, we only need one: we're listening to one particular value, `"colorMode"`, and we want to respond any time that one value is altered.

Looking at https://tinybase.org/guides/the-basics/listening-to-stores/, we can see that there are many ways we could listen to a store. We can listen for changes to any one or all values or value `id` (think the "key" for that value), as well as any or all tables, table rows, or cells, or any of their `id` values. If any of that changes, we can listen to whatever level of granularity we might need.

For this, we can get away with *simplicity*: we are only responding based on the change of a particular `value`: `"colorMode"`. So to listen to that, we add this last line:

```js
configStore.addValueListener("colorMode", setColor)
```

That will call the `setColor` method each time the `colorMode` value changes in the store, toggling the page itself between light mode and dark.

## To Recap

Again, this is a simple *reactive* use, we could do considerably more with it (and in the next lesson, we will  get reactive with tabular data). But we've set up a pretty solid skeleton project here, and one that I have published as my repo template. In this, TinyBase may seem like a small part, but the significance of not having to deal with `localStorage`, with having default values, and with reactive listeners is significant.

The repo, which is also my template repo, is available: https://codeberg.org/tobyPlaysTheUke/vite-tailwindcss-rippleui-vanilla-template/src/branch/main