Get Your Front-end Moving with cbInertia

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

What is cbInertia?

Why am I learning about another JavaScript framework?

Today's Web Apps

  • Fast
  • Small
  • 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
  • Small, 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 server-side adapters for Laravel and Rails
  • There are client-side adapters for React, Vue, and Svelte

Background

Demo App

Getting Started

Client-side Setup

Setup

npm install @inertiajs/inertia @inertiajs/inertia-vue vue
npm install --save-dev coldbox-elixir @babel/plugin-syntax-dynamic-import

Setup

// webpack.config.js

const elixir = require("coldbox-elixir");

// used for code splitting
elixir.config.mergeConfig({
  optimization: {
    splitChunks: {
      cacheGroups: {
        shared: {
          chunks: "async",
          minChunks: 2,
          name: "includes/js/pages/shared"
        }
      }
    }
  }
});

module.exports = elixir(mix => {
    mix.vue("app.js");
});

Setup

// resources/assets/js/app.js

import Vue from "vue";
import { InertiaApp } from "@inertiajs/inertia-vue";

Vue.use(InertiaApp);

let app = document.getElementById("app");

new Vue({
  render: h =>
    h(InertiaApp, {
      props: {
        initialPage: JSON.parse(app.dataset.page),
        resolveComponent: name =>
          import(
            /* webpackChunkName: "includes/js/pages/[request]" */ `@/Pages/${name}`
          ).then(module => module.default)
      }
    })
}).$mount(app);

Folder Structure

Server-side Setup

Installation

box install cbInertia

Usage

Simple Render

// handlers/Dashboard.cfc
component {

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

    function index( event, rc, prc ) {
        inertia.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>
      <inertia-link href="/">Home Page</inertia-link>
    </div>
  </div>
</template>

<script>
export default {}
</script>

Passing Props

// handlers/Users.cfc
component {

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

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

}

Accepting Props

// resources/assets/js/Pages/Users/Index.vue

<template>
  <div>
    <h1>Users</h1>
    <div>
      <inertia-link href="/users/new">
        Create User
      </inertia-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>
              <inertia-link :href="`/users/${user.id}/edit`">
                <img v-if="user.photo" :src="user.photo">
                {{ user.name }}
              </inertia-link>
            </td>
            <td>
              <inertia-link :href="`/users/${user.id}/edit`" tabindex="-1">
                {{ user.email }}
              </inertia-link>
            </td>
            <td>
              <inertia-link :href="`/users/${user.id}/edit`" tabindex="-1">
                {{ user.owner ? 'Owner' : 'User' }}
              </inertia-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>
export default {
  props: {
    users: {
      type: Array,
      required: true
    },
  }
}
</script>

inertia() helper

// handlers/Users.cfc
component {

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

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

}

inertia() render shorthand

// handlers/Users.cfc
component {

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

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

}

Submitting Form Requests

// resources/assets/js/Pages/Users/Create.vue

<template>
  <div>
    <h1>
      <inertia-link href="/users">Users</inertia-link>
      <span>/</span> Create
    </h1>
    <div>
      <form @submit.prevent="submit">
        <div>
          <TextInput v-model="form.first_name" label="First name" />
          <TextInput v-model="form.last_name" 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="sending" type="submit">Create User</loading-button>
        </div>
      </form>
    </div>
  </div>
</template>

<script>
import Layout from '@/Shared/Layout'
import LoadingButton from '@/Shared/LoadingButton'
import SelectInput from '@/Shared/SelectInput'
import TextInput from '@/Shared/TextInput'

export default {
  components: {
    LoadingButton,
    SelectInput,
    TextInput,
  },
  data() {
    return {
      sending: false,
      form: {
        first_name: null,
        last_name: null,
        email: null,
        password: null,
        owner: false,
        photo: null,
      },
    }
  },
  methods: {
    submit() {
      this.sending = true

      var data = new FormData()
      data.append('first_name', this.form.first_name || '')
      data.append('last_name', this.form.last_name || '')
      data.append('email', this.form.email || '')
      data.append('password', this.form.password || '')
      data.append('owner', this.form.owner ? '1' : '0')

      this.$inertia.post("/users", data, {
        onFinish: () => {
          this.sending = false;
        }
      });
    },
  },
}
</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 = {
            "first_name": { "required": true, "size": "1..50" },
            "last_name": { "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( {
            "first_name" = data.first_name,
            "last_name" = data.last_name,
            "email" = data.email,
            "password" = event.getValue( "password", "" ),
            "owner" = data.owner
        } );

        relocate( "users" );
    }

}

Sharing Data

// interceptors/InertiaShareInterceptor.cfc
component {

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

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

}

Layouts

// resources/assets/js/Pages/Dashboard/Index.vue
<template>
  <div>
    <h1>Dashboard</h1>
    <p>Hey there! Welcome to the Dashboard.</p>
  </div>
</template>

<script>
import Layout from '@/Shared/Layout'

export default {
  metaInfo: { title: 'Dashboard' },
  layout: Layout,
}
</script>

Layouts

// resources/assets/js/Shared/Layout.vue
<template>
  <div>
    <div>
      <div>
        <inertia-link href="/">
          <Logo width="120" height="28" />
        </inertia-link>
      </div>
      <div>
        <div>{{ $page.props.auth.user.account.name }}</div>
      </div>
    </div>
    <div>
      <div scroll-region>
        <slot />
      </div>
    </div>
  </div>
</template>

<script>
import Logo from '@/Shared/Logo'

export default {
  components: {
    Logo
  }
}
</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