Get Your Front-end Rolling with Vue and InertiaJS

What to expect

  • An overview of cbInertia and Inertia.js
  • Why you would use this library
  • Example app to explore performance
  • Live coding some cbInertia views!

It will help if you are familiar with...

  • ColdBox Handlers and Routing
  • Vue Components and Templates

Who Am I?

Utah

Ortus Solutions

Prolific Module Author

1 wife, 3 kids, 1 dog

Type 1 Diabetic

Theatre Nerd

What is cbInertia?

Why am I learning about another JavaScript framework?

Today's Web Apps

  • Fast
  • Lightweight
  • Inertactive

Option #1

Multi-page Server-rendered App

Multi-page Server-rendered App

Cons

  • Full page refresh for each request
  • JavaScript feels bolted on

Pros

  • Codebase you are familiar with
  • Only one repository to configure routing, security, etc.

Option #2

Single Page App (SPA)

Single Page App (SPA)

Cons

  • Have to build an API
  • Client Side Auth and Routing
  • CORS

Pros

  • Fast (No page refreshes)
  • Interactive
  • Lightweight, Cacheable Payloads

Option #3

cbInertia & Inertia.js

cbInertia & Inertia.js

Pros

  • Single source of truth for routing, security, etc.
  • SPA-like Performance
  • No need for an API
  • Framework-agnostic

No, really, what is cbInertia?

  • Not a new framework, but the glue between frameworks
  • Build a SPA without the API
  • Use React, Vue, or Svelte as your View layer
  • cbInertia is the ColdBox server-side adapter for Inertia.js
  • Inertia.js was developed by Jonathan Reinink
  • There are official server-side adapters for Laravel and Rails
  • There are lots of community adapters (like ColdBox!)
  • There are official client-side adapters for React, Vue, and Svelte

Background

Demo App

CFCasts

Getting Started

Client-side Setup

Setup

npm install @inertiajs/vue3 vue
npm install --save-dev vite coldbox-vite-plugin @vitejs/plugin-vue

Setup

// vite.config.js

import { defineConfig } from "vite";
import coldbox from "coldbox-vite-plugin";
import vue from "@vitejs/plugin-vue";

export default () => {
    return defineConfig({
        plugins: [
            vue(),
            coldbox({
                input: ["resources/assets/js/app.js"],
                refresh: true
            })
        ]
    });
};

Setup

// resources/assets/js/app.js

import { createApp, h } from "vue";
import { createInertiaApp } from "@inertiajs/vue3";
import { resolvePageComponent } from "coldbox-vite-plugin/inertia-helpers";

createInertiaApp({
    id: "app",
    title: title => `${title} | Quick Tailwind Inertia Template`,
    resolve: name =>
        resolvePageComponent(
            `./Pages/${name}.vue`,
            import.meta.glob("./Pages/**/*.vue")
        ),
    setup({ el, App, props, plugin }) {
        createApp({ render: () => h(App, props) })
            .use(plugin)
            .mount(el)
    }
});

Folder Structure

Server-side Setup

Installation

box install cbInertia

Usage

Simple Render

// handlers/Dashboard.cfc
component {

    property name="cbInertia" inject="Inertia@cbInertia";

    function index( event, rc, prc ) {
        variables.cbInertia.render( "Dashboard/Index" );
    }

}

Vue Template

<!-- resources/assets/js/Pages/Dashboard/Index.vue -->

<template>
  <div>
    <h1>Dashboard</h1>
    <p>Hey there! Welcome to my Dashboard!</p>
    <div>
      <Link href="/">Home Page</Link>
    </div>
  </div>
</template>

<script setup>
import { Link } from "@inertiajs/vue3";
</script>

Passing Props

// handlers/Users.cfc
component {

    property name="cbInertia" inject="Inertia@cbInertia";
    property name="userService" inject="quickService:User";

    function index( event, rc, prc ) {
        variables.cbInertia.render( "Users/Index", {
            "users": variables.userService.asMemento().get()
        } );
    }

}

Accepting Props

<!-- resources/assets/js/Pages/Users/Index.vue -->

<template>
  <div>
    <h1>Users</h1>
    <div>
      <Link href="/users/new">
        Create User
      </Link>
    </div>
    <div>
      <table>
        <thead>
          <tr>
            <th>Name</th>
            <th>Email</th>
            <th>Role</th>
          </tr>
        </thead>
        <tbody>
          <tr v-for="user in users" :key="user.id">
            <td>
              <Link :href="`/users/${user.id}/edit`">
                <img v-if="user.photo" :src="user.photo">
                {{ user.name }}
              </Link>
            </td>
            <td>
              <Link :href="`/users/${user.id}/edit`" tabindex="-1">
                {{ user.email }}
              </Link>
            </td>
            <td>
              <Link :href="`/users/${user.id}/edit`" tabindex="-1">
                {{ user.owner ? 'Owner' : 'User' }}
              </Link>
            </td>
          </tr>          
          <tr v-if="users.length === 0">
            <td class="border-t px-6 py-4" colspan="3">No users found.</td>
          </tr>
        </tbody>
      </table>
    </div>
  </div>
</template>

<script setup>
import { Link } from "@inertiajs/vue3";
defineProps({ users: Array });
</script>

inertia() helper

// handlers/Users.cfc
component {

    property name="userService" inject="quickService:User";

    function index( event, rc, prc ) {
        inertia().render( "Users/Index", {
            "users": variables.userService.asMemento().get()
        } );
    }

}

inertia() render shorthand

// handlers/Users.cfc
component {

    property name="userService" inject="quickService:User";

    function index( event, rc, prc ) {
        inertia( "Users/Index", {
            "users": variables.userService.asMemento().all()
        } );
    }

}

Submitting Form Requests

<!-- resources/assets/js/Pages/Users/Create.vue -->

<template>
  <div>
    <h1>
      <Link href="/users">Users</Link>
      <span>/</span> Create
    </h1>
    <div>
      <form @submit.prevent="form.post('/users')">
        <div>
          <TextInput v-model="form.firstName" label="First name" />
          <TextInput v-model="form.lastName" label="Last name" />
          <TextInput v-model="form.email" label="Email" />
          <TextInput v-model="form.password" type="password" label="Password" />
          <SelectInput v-model="form.owner" label="Owner">
            <option :value="true">Yes</option>
            <option :value="false">No</option>
          </SelectInput>
        </div>
        <div>
          <LoadingButton :loading="form.processing" type="submit">
              Create User
          </LoadingButton>
        </div>
      </form>
    </div>
  </div>
</template>

<script>
import Layout from "@/Shared/Layout.vue";

export default {
    layout: Layout
}
</script>

<script setup>
import { Link } from "@inertiajs/vue3";
import LoadingButton from ""@/Shared/LoadingButton";
import SelectInput from "@/Shared/SelectInput";
import TextInput from "@/Shared/TextInput";
import { useForm } from ""@inertiajs/vue3";

const form = useForm({
    firstName: null,
    lastName: null,
    email: null,
    password: null,
    owner: false,
    photo: null,
});
</script>

Handling Form Requests

// handlers/Users.cfc
component {

    function new( event, rc, prc ) {
        inertia( "Users/Create" );
    }
    
    function create( event, rc, prc ) {
        var data = validateOrFail( target = rc, constraints = {
            "firstName": { "required": true, "size": "1..50" },
            "lastName": { "required": true, "size": "1..50" },
            "email": { "required": true, "size": "1..50", "type": "email" },
            "password": { "required": false },
            "owner": { "required": true, "type": "boolean" }
        } );

        auth().user().getAccount().users().create( {
            "firstName" = data.firstName,
            "lastName" = data.lastName,
            "email" = data.email,
            "password" = event.getValue( "password", "" ),
            "owner" = data.owner
        } );

        relocate( "users" );
    }

}

Sharing Data

// interceptors/InertiaShareInterceptor.cfc
component {

    property name="cbInertia" inject="provider:Inertia@cbInertia";
    property name="authService" inject="provider:AuthenticationService@cbauth";

    function preProcess() {
        variables.cbInertia.share( "auth", {
            "user": function() {
                return variables.authService.check() ?
                    variables.authService.user().getMemento() :
                    javacast( "null", "" );
            }
        } );
        
        variables.cbInertia.share( "errors", function() {
            return flash.get( "errors", {} );
        } );
    }

}

Layouts

<!-- resources/assets/js/Pages/Dashboard/Index.vue -->

<template>
    <div>
        <Head title="Dashboard" />
        <h1>Dashboard</h1>
        <p>Hey there! Welcome to the Dashboard.</p>
    </div>
</template>

<script>
import Layout from "@/Shared/Layout";

export default {
  layout: Layout
}
</script>

<script setup>
import { Head } from "@inertiajs/vue3";
</script>

Layouts

<!-- resources/assets/js/Shared/Layout.vue -->
<template>
  <div>
    <div>
      <div>
        <Link href="/">
          <Logo width="120" height="28" />
        </Link>
      </div>
      <div>
        <div>{{ user.account.name }}</div>
      </div>
    </div>
    <div>
      <div>
        <slot />
      </div>
    </div>
  </div>
</template>

<script setup>
import Logo from "@/Shared/Logo";
import { computed } from "vue";
import { usePage } from "@inertiajs/vue3";

const page = usePage();
const user = computed(() => page.props.auth.user);
</script>

And More!

  • Programmatic Navigation
  • Asset Version Change Detection
  • Scroll Regions
  • Partial Reloads
  • Nested Layouts
  • Local State Cache
  • Prop Transformation

Get Started

box coldbox create app skeleton=cbTemplate-quick-tailwind-inertia

Live Coding

Links

  • https://inertiajs.com/
  • https://forgebox.io/view/cbinertia
  • https://forgebox.io/view/cbtemplate-quick-tailwind-inertia

CFCamp 2023: Get Your Front-end Moving with cbInertia

By Eric Peterson

CFCamp 2023: Get Your Front-end Moving with cbInertia

  • 349