Twill, Inertia, Vue and Tailwind improvements
We will use our previous PageContent module to make some improvements.
Props optimization
When Inertia renders a Page, all the props are JSON encoded in a data-page
attribute of the root div
.
So, when we add the $item
to Inertia in our App Controller,
return Inertia::render('Page/Content', [
'item' => $item,
]);
all the attributes of the Model are serialized, as you can see in the rendered HTML:
If you use Vue.js devtools, you can inspect the props:
As you can see, there are a lot of data we don't need, which slows the page load and we don't want to be visible to our visitors.
To improve this, we will use the only()
method of the Eloquent Model to return a subset of attributes. You can directly use this in the App Controller:
/app/Http/Controllers/App/PageContentController.php
<?php
class PageContentController extends Controller
{
public function show(string $slug): InertiaResponse
{
...
return Inertia::render('Page/Content', [
'item' => $item->only([
'title',
'meta_title',
'meta_description',
]),
]);
}
}
We prefer to create a $publicAttributes
array attribute in our Model (like existing $translatedAttributes
, $slugAttributes
, ...) to declare these attributes:
/app/Models/PageContent.php
<?php
class PageContent extends Model implements Sortable
{
...
public array $publicAttributes = [
'title',
'meta_title',
'meta_description',
];
}
And use it in the App Controller:
/app/Http/Controllers/App/PageContentController.php
<?php
class PageContentController extends Controller
{
public function show(string $slug): InertiaResponse
{
...
return Inertia::render('Page/Content', [
'item' => $item->only($item->publicAttributes),
]);
}
}
If we inspect the props, we see there are just the needed attributes:
Cache
Our content will not change often and loading all the needed data implies multiple queries that necessarily takes some time:
Search for a published Model with the given slug
Load Model relations: translations, medias, files, blocks
and we will see in a future article, we will rework some data (mainly for the blocks).
In our projects, we generally try to improve performance by caching database retrieved and reworked data. In the Laravel / Twill context, it can be done like that:
/app/Http/Controllers/App/PageContentController.php
<?php
namespace App\Http\Controllers\App;
use App\Models\PageContent;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Cache;
use Inertia\Inertia;
use Inertia\Response as InertiaResponse;
class PageContentController extends Controller
{
public function show(string $slug): InertiaResponse
{
$item = Cache::rememberForever(
'page.content.' . app()->getLocale() . '.' . $slug,
function () use ($slug) {
$item = PageContent::publishedInListings()
->forSlug($slug)
->first();
if ($item !== null) {
$item->load('translations', 'medias', 'blocks');
}
return $item;
}
);
...
}
}
To clear the cache when the content is modified, it is handled in the Repository where Twill provides an afterSave()
method we can override:
/app/Repositories/PageContentRepository.php
<?php
namespace App\Repositories;
...
use Illuminate\Support\Facades\Cache;
class PageContentRepository extends ModuleRepository
{
...
public function afterSave($object, $fields): void
{
// Cache clearing.
foreach (optional($object)->slugs as $slug) {
Cache::forget('page.content.' . $slug->locale . '.' . $slug->slug);
}
parent::afterSave($object, $fields);
}
}
Vue props type alias
As we use TypeScript, we can benefit from static type checking and many IDE support this language providing auto-complete and warnings. It really improves your DX and we will see how.
Defining types
We create a models.d.ts
file in /resources/js/types
to define the Eloquent-based objects used in Inertia pages in a Model
namespace. For our module, we will create a Page
type:
/resources/js/types/models.d.ts
declare namespace Model {
export type Page = {
title: string
meta_title?: string
meta_description?: string
}
}
Using types
In the previous article, we were using object
for our item
type, in our IDE (VS Code), that triggers TypeScript warnings:
/resources/views/Pages/Page/Content.vue
Now we can change the type of our item
prop from object
to Model.Page
:
<script setup lang="ts">
interface Props {
item: Model.Page
}
defineProps<Props>()
</script>
<template>
<div class="bg-gray-200 w-full h-screen flex flex-col justify-center items-center">
<h1 class="text-center text-4xl font-semibold text-gray-900">
{{ item.title }}
</h1>
</div>
</template>
No more warnings, and we can have information in our IDE about the properties of the object and their types:
Vite / TypeScript alias
As our application grows, we will have more and more components, split into different directories to keep a clean architecture (and maybe at some point we will create a new version of the theme, components, ...).
To simplify how we import components into other components and avoid relative paths like ../../Theme/Head.vue
, we can benefit from alias
feature to map an alias to a path and make it available in code and IDE.
We generally define 3 default aliases on a new project:
@Composable
: stores our Vue Composition API composables@Form
: stores our form components (input, select, switch, ...)@Theme
: stores our theme components (head, blocks, ...)
Feel free to create yours according to your project structure.
Configuration
The aliases have to be known by Vite (Rollup) and the TypeScript compiler.
/vite.config.js
import { defineConfig } from 'vite';
export default defineConfig({
resolve: {
alias: [
{
find: '@Composable',
replacement: '/resources/js/Composables'
},
{
find: "@Form",
replacement: `/resources/views/Components/Form`,
},
{
find: '@Theme',
replacement: '/resources/views/Components/Theme'
}
]
},
...
});
/tsconfig.json
{
"compilerOptions": {
...
"paths": {
"@/*": ["resources/*"],
"@Composable/*": ["resources/js/Composables/*"],
"@Form/*": ["resources/views/Components/Form/*"],
"@Theme/*": ["resources/views/Components/Theme/*"]
},
...
}
}
Usage
In your component, you can now import other components more easily.
In the example, the Head
component is resolved from @Theme/Head.vue
to /resources/views/Components/Theme/Head.vue
<script setup lang="ts">
import Head from '@Theme/Head.vue'
</script>
<template>
<Head></Head>
</template>
Vue plugins
Vue provides many plugins that can improve DX.
AutoImport
When we use functions of libraries in our components, we need to import them in <script setup>
. For vue functions for example, it may seem like an unnecessary waste of time:
import { computed, ref } from 'vue'
const title = ref('My Title')
const meta_title = computed(() => `${title.value} | My Application`)
The AutoImport plugin allows you to skip the first line by automatically generating an auto-imported declaration file ๐
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by unplugin-auto-import
export {}
declare global {
...
const computed: typeof import('vue')['computed']
const ref: typeof import('vue')['ref']
...
}
// for type re-export
declare global {
// @ts-ignore
export type { Component, ComponentPublicInstance, ComputedRef, InjectionKey, PropType, Ref, VNode } from 'vue'
}
You can use this plugin for many external libraries like
vueuse
,vue-router
, ...Plugin documentation: https://github.com/antfu/unplugin-auto-import#configuration
Installation
yarn add unplugin-auto-import --dev
Configuration of the plugin in /vite.config.js
import AutoImport from 'unplugin-auto-import/vite';
export default defineConfig({
plugins: [
...
AutoImport({
// targets to transform
include: [
/\.[tj]sx?$/, // .ts, .tsx, .js, .jsx
/\.vue$/, /\.vue\?vue/, // .vue
/\.md$/, // .md
],
imports: [
'vue',
],
}),
],
});
DefineOptions
From 3.3 version of Vue, this macro has been integrated into the Vue core package: https://vuejs.org/api/sfc-script-setup.html#defineoptions
As we use Vue Composition API, some features are not available in <script setup>
like defining component name and other properties, ... and that are available in Vue Options API.
There is a plugin that provides a defineOptions
macro that can be used in <script setup>
Installation
yarn add unplugin-vue-define-options --dev
Configuration of the plugin in /vite.config.js
import DefineOptions from 'unplugin-vue-define-options/vite';
export default defineConfig({
plugins: [
...
DefineOptions(),
],
});
Usage
<script setup lang="ts">
defineOptions({
name: 'PageContent',
layoutName: 'FullPage',
})
</script>
Tailwind Automatic Class Sorting plugin
Tailwind provides a Prettier plugin to automatically sort the utility classes according to the official recommended class order, freeing the heads of you and your team from this purpose.
Installation
yarn add prettier prettier-plugin-tailwindcss --dev
We'll do our best to provide source code of the serie on GitHub
Subscribe to my newsletter
Read articles from Codivores directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by