Single Page Apps

This page focuses on the fundamentals needed to develop and build SPA apps with Tohama.

Install

Since Tohama packages are private GitHub packages, add a .npmrc file to the root of your app.

// .npmrc

@dija:registry=https://npm.pkg.github.com/
//npm.pkg.github.com/:_authToken=[YOUR_AUTH_TOKEN]
{
  "name": "my-app",
  "version": "1.0.0",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "serve": "vite preview",
    "help": "vite --help"
  },
  "dependencies": {
    "@dija/tohama-assets": "^2.0.0",
    "@dija/tohama-components": "^2.0.0",
    "@dija/tohama-plugins": "^2.0.0",
    "@dija/tohama-shared": "^2.0.0",
    "@dija/tohama-spa": "^2.0.0",
    "@dija/tohama-state": "^2.0.0",
    "@dija/tohama-use": "^2.0.0",
    "@dija/tohama-validation": "^2.0.0",
    "@dija/tohama-vite": "^2.0.0",
    "vue": "^3",
    "vite": "^4"
  },
  "devDependencies": {
    "@dija/eslint-config": "^2.1.0"
  }
}

Structure

This is the recommended structure for a production-ready app with Tohama. See the @dija/tohama-starter repository for a full example.

my-app/
  src/
    app/
      component.tsx
      index.scss
      index.ts
    components/
    locales/
      en.json
    pages/
      home-page.tsx
    views/
      home-view
        component.tsx
        index.ts
    main.ts
  dist/
  .eslintrc
  .npmrc
  index.html
  package.json
  tsconfig.eslint.json
  tsconfig.json
  tohama.config.ts
  css.config.ts
  vite.config.ts

vite.config.ts

This is vite configurations file. Use it to add custom plugins, customize dev server, and more. Make sure to add VitePluginTohama to the list of plugins.

import { VitePluginTohama } from "@dija/tohama-vite";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [VitePluginTohama()],
});

tohama.config.ts

Use this file to override Tohama build-time default configurations.

import { defineConfig } from "@dija/tohama-vite";

export default defineConfig({
  /**
   * Components libraries you want to generate CSS for, On-demand.
   */
  cssTranspile: [
    {
      name: "@dija/tohama-components",
    },
    {
      name: "@dija/tohama-plugins",
    },
  ],
  /**
   * Autogenerate head meta attributes in output index.html and manifest.webmanifest.
   */
  meta: {
    host: "https://tohama.io",
    title: "Tohama Starter",
    titleFull: "Tohama Starter",
    description: "This is a starter template to quickly start developing web apps with tohama.",
    includeCards: true,
    includeFavicons: true,
    includeManifest: true,
    social: {
      twitterHandle: "@dijasols",
      twitter: "https://twitter.com/dijasols",
    },
  },
});

css.config.ts

Use this file to override Tohama default configurations for UnoCSS.

import { createUnocssConfig, colors } from "@dija/tohama-vite";

export default createUnocssConfig({
  /**
   * Pre-generate CSS helper classes.
   */
  generator: {
    /**
     * Autogenerate flex classes.
     */
    flex: {
      direction: ["row", "col"],
      justify: ["center", "between", "end"],
      align: ["start", "center", "stretch"],
      breakpoints: ["re"],
    },
    /**
     * Autogenerate grid classes.
     */
    grid: {
      cols: ["6"],
      gap: ["2"],
      breakpoints: ["re"],
    },
  },
  /**
   * Customize UnoCSS theme.
   */
  theme: {
    colors: {
      primary: colors.indigo,
      secondary: colors.slate,
    },
  },
});

index.html

This is vite entry file. Use it to directly add external dependencies.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>

main.ts

This is your app's entry file. Use it to define your app component, routes, locales, and setup plugins.

import { createDefaults } from "@dija/tohama-components";
import { runApp } from "@dija/tohama-spa";

/**
 * Tohama css output is injected to this virtual module
 */
import "virtual:tohama.css";

/**
 * Your css output is injected to this virtual module
 */
import "uno.css";

/**
 * Create App
 */
export default runApp(
  {
    app: () => import("@/app"),
    initialLocale: "en",
    locales: [
      {
        code: "en",
        direction: "ltr",
        messages: () => import("@/locales/en.json"),
      },
      {
        code: "ar",
        direction: "rtl",
        messages: () => import("@/locales/ar.json"),
      },
    ],
    routes: [
      {
        path: "/",
        name: "home",
        component: () => import("@/pages/home-page"),
      },
      {
        path: "/about",
        name: "about",
        component: () => import("@/pages/about-page"),
      },
      {
        path: "/contact",
        name: "contact",
        component: () => import("@/pages/contact-page"),
      },
    ],
  },
  /**
   * Hook to configure plugins before the app starts
   */
  async ({ app }) => {
    /**
     * Set components defaults
     */
    const defaults = createDefaults({
      title: {
        color: "primary",
        weight: "semibold",
      },
    });
    app.use(defaults);
  }
);

app

This directory contains the first component in your app. Use it to define global attributes and style. It should return a t-app component in order to enable many of the required features such as CSS resets, fonts, light/dark theme support.

// app/index.ts

import component from "./component";

export default component;
// app/component.tsx

import { TApp, TCell, TDivider, TRouterView } from "@dija/tohama-components";
import { useGlobalTranslate, useHeadMeta } from "@dija/tohama-plugins";
import { usePwaSnackbar } from "@dija/tohama-spa";
import { computed, defineComponent } from "vue";

import "./index.scss";

export default defineComponent({
  setup() {
    usePwaSnackbar();

    const translate = useGlobalTranslate();

    const name = computed(() => {
      return translate.t("page-title");
    });

    useHeadMeta({ name });

    return () => (
      <TApp class="app">
        <header>My header</header>
        <TCell fill>
          <TRouterView transition="slide-y" fill />
        </TCell>
      </TApp>
    );
  },
});
// app/index.scss

.app {
  @apply transition-colors light:bg-secondary-50 dark:bg-secondary-900;
}

locales

This directory contains global locales files. Create a json file per locale containing its messages.

// locales/en.json

{
  "title": "Tohama",
  "description": "This library includes components and utils to make building apps easier."
}

pages

This directory contains route pages' components. A page component is used to read the route's parameters and pass their values to a view. You can also use it to define the page head meta.

// pages/home-page.tsx

import { HomeView } from "@/views";
import { useHeadMeta } from "@dija/tohama-plugins";
import { computed, defineComponent } from "vue";
import { useRouter } from "vue-router";

export default defineComponent({
  setup() {
    const router = useRouter();

    const id = computed<string | undefined>(() => {
      return router.currentRoute.value.params.id?.toString();
    });

    useHeadMeta({
      title: "Home",
    });

    return () => <HomeView id={id.value} />;
  },
});

views

This directory contains components that are reusable across pages and popups. Separating the view layer gives you the most consistency, where your components can be reused easily.

// views/home-view/component.tsx

import { TText } from "@dija/tohama-components";
import { defineComponent } from "vue";

export default defineComponent({
  props: {
    id: String,
  },
  setup(props) {
    return () => <TText>{props.id}</TText>;
  },
});