14 minute reading time; ~2600 words
Welcome and hello!
Today, we’re going to create a template list page!
Along the way, I’m going to show how we break down this more complex card, interpreting the wireframe into pieces of work, and how we get from start to finish. We’ll also explore how to efficiently style our page, and get to an end product that will let our stakeholders start to see the shape of the feature we’re building.
If you haven’t read the previous posts, I’d suggest you start at An App From Scratch: Part 1 – What To Build. You can also find all the related documents and code here: Building Tailgunner.
What Do I See?
Today’s card seems simple at first glance, but the details link to a mockup, which suggests there’s more work expected.
Card: US1-C2
Title
Create template list page
Type
Story
Description
As a business owner, I want a page that will list all templates I own, so that I can manage and work with them.
Acceptance Criteria
- A tool exists to list all templates the business owner is allowed to see.
Details
- Create a page to list the templates: Mockup
What I Need To Do
Let’s break it down into tasks.
- Create a new route to show our template list
- Create a new controller to list the items and show an Inertia page
- Create an Inertia page using Vue.js
- Make the Inertia page work like the wireframe
- Make sure I have all the data elements loaded
- Add some buttons
- Figure out how to create relative dates
- Style the page
- Make the buttons pop up an alert for what they do
Putting It Into Action
Create a new route to show our template list
First thing’s first, we need a route to let us access the template list page. There are two methods to do this. Method 1 means you will have to explicitly specify every single route setup that you want, but it’s more flexible for uncommon routes, while Method 2 is a more elegant way for the most common REST access methods.
// Method 1
Route::get('/templates', [TemplateController::class, 'index'])->name('templates.index');
// Method 2
Route::resource('/templates', TemplateController::class)->only(['index']);
Create a new controller to list the items and show an Inertia page
Now that we have a route, we get to update our controller! Remember in the last post, we created an empty controller along with our model, and now it’s time to make it do something.
The code we’re adding here will load up a list of templates, modify it with some fields we don’t have the data for yet (using the transform()
method), and then pass the whole thing back to the user along with the Inertia page to render it.
<?php
namespace App\Http\Controllers;
use App\Models\Template;
use Illuminate\Http\Request;
use Inertia\Inertia;
class TemplateController extends Controller
{
public function index()
{
/**
* Get the template data, and modify it to include the fields that
* we're going to fetch later when we make the actual table to store
* the data entered in a template.
*/
$templates = Template::query()
->where('user_id', auth()->id())
->get()
->transform(function($template) {
return [
'id' => $template->id,
'title' => $template->title,
'description' => $template->description,
'created_at' => $template->created_at,
// Fake these fields for now
'last_used' => $template->created_at->addDay(),
'records' => rand(20, 100),
];
});
// Send back the Inertia template and template data
return Inertia::render('Templates/Index', [
'templates' => $templates
]);
}
}
Along with the controller code, we’re going to make an empty template list page, so that we can actually load the endpoint and see something. To do this, we’re just going to copy the Welcome page that the initial install provided us with, change the name and title, and leave a spot for us to add our data in later.
<script setup>
import AppLayout from '@/Layouts/AppLayout.vue';
</script>
<template>
<AppLayout title="Templates">
<template #header>
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
Templates
</h2>
</template>
<div class="py-1">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-xl sm:rounded-lg">
A list of templates
</div>
</div>
</div>
</AppLayout>
</template>
At this point, everything should be working! Here’s what the page looks like now:
Make the Inertia page work like the wireframe
Now that we have a basic working page, let’s get the data properly loaded into it, and add various UI elements and formatting.
- Make sure I have all the data elements loaded
- Add some buttons
First, we need to update our page to access the properties we’re passing back from the controller.
<script setup>
import AppLayout from '@/Layouts/AppLayout.vue';
defineProps({
templates: Object
});
</script>
Next, we’re going to replace our placeholder text “A list of templates” with an actual list of templates. We’ll add a table if there are any templates for this user (line 3), or display a message if not (line 33).
Inside the table, we’ll have to loop over the templates data structure to generate each row of the table (line 15). This simple table will give our page some basic structure, and show us the data so we can further tune the list. I’ve also added some basic buttons that we can play with as we get deeper into this card.
<template> <!-- Ignore this line, it's just so the formatter shows the right colors -->
<table v-if="templates.length">
<thead>
<tr>
<th>Template Name</th>
<th>Description</th>
<th>Created</th>
<th>Last Used</th>
<th>Records</th>
<th>Template</th>
</tr>
</thead>
<tbody>
<tr v-for="template in templates" :key="template.id">
<td>{{ template.title }}</td>
<td>{{ template.description }}</td>
<td>{{ template.created_at }}</td>
<td>{{ template.last_used }}</td>
<td>
{{ template.records }} Entries
<button>Add Record</button>
<button>View Records</button>
</td>
<td>
<button>Edit</button>
<button>Delete</button>
<button>Clone</button>
</td>
</tr>
</tbody>
</table>
<div v-else>
No templates found.
</div>
Now that we have all the elements lined up in a table, and populated with data, let’s make the user experience better!
- Figure out how to create relative dates
It’s a lot easier to read a relative date, so we’re going to add a formatter for this. I’d still like to have the date available if they hover over it as well, in case they need to know exactly when something was done.
For this formatting, we’re going to use Day.js (a modern date handling Javascript library). First we’ll install it:
npm install dayjs
Then we’ll need to add it to our setup code. Along with this, we’re going to add a few shortcut functions for use in our template.
<script setup>
import AppLayout from '@/Layouts/AppLayout.vue';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import advancedFormat from 'dayjs/plugin/advancedFormat';
dayjs.extend(relativeTime);
dayjs.extend(advancedFormat);
defineProps({
templates: Object
});
const relDate = (date) => dayjs(date).fromNow();
const formatDate = (date) => dayjs(date).format('YYYY-MM-DD HH:mm:ss Z');
</script>
Now, let’s update our display code to show these dates. We’re going to use Vue’s dynamic binding to format the date and add it as a title property on our cells, and we’re also going to format our dates as relative ones.
<template> <!-- Ignore this line, it's just so the formatter shows the right colors -->
<tr v-for="template in templates" :key="template.id">
<td>{{ template.title }}</td>
<td>{{ template.description }}</td>
<td :title="formatDate(template.created_at)">{{ relDate(template.created_at) }}</td>
<td :title="formatDate(template.last_used)">{{ relDate(template.last_used) }}</td>
With that cleaned up, let’s move on and make the UI easier to navigate!
- Style the page
Finally, we’re going to apply some styles to the page, to make it easier to read and work with, since what we have right now is a little… lacking in character.
We’re using Tailwindcss, so we have a ton of easy selectors available out of the box. This also tends to be a little verbose if we want to change multiple properties however, so we’re going to modify the app.css file that comes with Laravel, and do some styling shortcuts, instead of repeating the styles over and over in the table code itself.
I’m going to create a style called style01
, to use with my table, and write it so that I only have to add it to the table, and the elements inside will be adjusted as well.
/* Set a style for the whole table. */
table.style01{
@apply min-w-full bg-white border border-gray-300 rounded-md;
}
/* Set a style for any header cell. */
table.style01 thead th{
@apply px-4 py-2 text-left text-gray-50 bg-gray-700 border-b-4;
}
/* Set a style for alternating rows in the table. */
table.style01 tbody tr:nth-child(even){
@apply bg-gray-200;
}
/* Set a style for when someone hovers over a row. Order matters in CSS,
* if this was before the alternating row color, it wouldn't work on the
* gray rows.
*/
table.style01 tbody tr:hover{
@apply bg-green-100;
}
/* Set a style to apply to each table body cell. */
table.style01 tbody td{
@apply px-4 py-2 text-left text-gray-900 border-b;
}
/* Create a generic style for buttons inside this table. */
table.style01 button{
@apply mx-1 px-3 py-1 text-xs text-white rounded bg-slate-400;
}
/* When we hover over the button, give it a different background to distinguish it. */
table.style01 button:hover{
@apply bg-blue-800;
}
In addition to the breakdown I gave you there, let’s break down the first header cell style to give you an idea of what it does. (You can read more in the Tailwindcss docs about what these do.)
table.style01 thead th{
@apply px-4 py-2 text-left text-gray-600 font-medium border-b;
}
Class Name | Effect |
---|---|
px-4 | Adds horizontal padding to the cell (1rem / 16px). |
py-2 | Adds vertical padding to the cell (0.5rem / 8px). |
text-left | Explicitly aligns the text in the cell to the left. |
text-gray-50 | Sets the cell text color to a very light gray. |
bg-gray-700 | Sets the cell background color to dark gray. |
border-b-4 | Adds a border to the bottom of the cell (4px wide). |
Now that we have our styles, let’s format our table and elements. As you can see below, all we have to do to affect the whole table is add the one style to it (line 3). I also noticed some of the text was squashed, so we’re going to add a text-nowrap
to fix that where it matters most (lines 18, 22, 26).
<template> <!-- Ignore this line, it's just so the formatter shows the right colors -->
<table v-if="templates.length" class="style01">
<thead>
<tr>
<th>Template Name</th>
<th>Description</th>
<th>Created</th>
<th>Last Used</th>
<th>Records</th>
<th>Template</th>
</tr>
</thead>
<tbody>
<tr v-for="template in templates" :key="template.id">
<td>{{ template.title }}</td>
<td>{{ template.description }}</td>
<td class="text-nowrap"
:title="formatDate(template.created_at)">
{{ relDate(template.created_at) }}
</td>
<td class="text-nowrap"
:title="formatDate(template.last_used)">
{{ relDate(template.last_used) }}
</td>
<td class="text-nowrap">
{{ template.records }} Entries<br>
<button>Add Record</button><br>
<button>View Records</button>
</td>
<td>
<button>Edit</button>
<button>Delete</button>
<button>Clone</button>
</td>
</tr>
</tbody>
</table>
With these styles now applied, let’s have a quick look at the list of templates now. In the new screenshot, I’m hovering over both a row and specific button, so you can see that style as well.
It’s amazing what a few style tweaks can do to take something cramped and hard to read and make it easier to work with!
- Make the buttons pop up an alert for what they do
One little bit of easy interactivity, since this tool has a lot of not useful buttons right now, is to add a simple alert for them. We’re going to add a simple click event that will pop up an alert telling you what the button will do.
First, we’ll create a small generic function in our setup code, to pop up the alert.
<script setup>
const btnClick = (message) => {window.alert(message)}
Then we’ll add the click event to the button, to trigger the message.
<button @click="btnClick(`Add Record To Template ${template.id}`)">
Add Record
</button>
And then we’ll give it a click.
With this completed, we now have a list page that is easy to read, and has some interactivity to it as well, allowing our stakeholders to get an early feel for the tool.
Commit And Merge
Along the way, we’re made quite a few changes. As we talked about previously, there are multiple strategies to arranging your code, but today’s branch is named: US2-C2_create_template_list
$ git push origin US2-C2_create_template_list
Enumerating objects: 36, done.
Counting objects: 100% (36/36), done.
Delta compression using up to 32 threads
Compressing objects: 100% (19/19), done.
Writing objects: 100% (21/21), 3.34 KiB | 114.00 KiB/s, done.
Total 21 (delta 12), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (12/12), completed with 10 local objects.
remote:
remote: Create a pull request for 'US2-C2_create_template_list' on GitHub by visiting:
remote: https://github.com/bendauphinee/tailgunner/pull/new/US2-C2_create_template_list
remote:
remote: GitHub found 1 vulnerability on bendauphinee/tailgunner's default branch (1 moderate).
remote: To find out more, visit:
remote: https://github.com/bendauphinee/tailgunner/security/dependabot/1
remote:
To github.com:bendauphinee/tailgunner.git
* [new branch] US2-C2_create_template_list -> US2-C2_create_template_list
In pushing our code to GitHub, we also got back a Dependabot security warning. Looking into that, there’s a moderate security issue with one of the libraries Laravel uses, so I’m having Dependabot generate a PR for the fix while I create and review my own code.
With a few small whitespace tweaks, my PR is merged.
Reviewing the automated PR from Dependabot, it bumps the version of the vulnerable library, and related ones. I’ve merged that change as well, concluding the code for today’s post!
One other thing, since there are now changes that for sure aren’t reflected in my local code, it’s time to go back to our main branch, pull down updates from our Github repo (which now include the security change), and then update our development install.
# Switch back from our working branch to main
$ git checkout main
Updating files: 100% (6/6), done.
Switched to branch 'main'
Your branch is up to date with 'origin/main'.
# Pull the changes to main in from Github
$ git pull origin main
remote: Enumerating objects: 38, done.
remote: Counting objects: 100% (38/38), done.
remote: Compressing objects: 100% (14/14), done.
remote: Total 22 (delta 13), reused 11 (delta 6), pack-reused 0 (from 0)
Unpacking objects: 100% (22/22), 5.06 KiB | 6.00 KiB/s, done.
From github.com:bendauphinee/tailgunner
* branch main -> FETCH_HEAD
df58791..795a5c9 main -> origin/main
Updating files: 100% (7/7), done.
Updating df58791..795a5c9
Fast-forward
src/app/Http/Controllers/TemplateController.php | 31 ++++++++++-
src/composer.lock | 60 ++++++++++----------
src/package-lock.json | 6 ++
src/package.json | 1 +
src/resources/css/app.css | 28 ++++++++++
src/resources/js/Pages/Templates/Index.vue | 73 +++++++++++++++++++++++++
src/routes/web.php | 3 +
7 files changed, 171 insertions(+), 31 deletions(-)
create mode 100644 src/resources/js/Pages/Templates/Index.vue
# Run composer install to update the things Dependabot changed
$ composer install
Installing dependencies from lock file (including require-dev)
Verifying lock file contents can be installed on current platform.
Package operations: 0 installs, 2 updates, 0 removals
- Downloading symfony/translation (v7.2.2)
- Downloading nesbot/carbon (3.8.4)
- Upgrading symfony/translation (v7.2.0 => v7.2.2): Extracting archive
- Upgrading nesbot/carbon (3.8.2 => 3.8.4): Extracting archive
Generating optimized autoload files
These steps are a common task in between different pieces of work on a project. This housekeeping ensures that the next thing we do starts from whatever state the main codebase is in, which should reduce the risk of having code conflicts when we do more work.
Wrapping Up
This post took us from a tool that only had a database full of fake data, and gave us a way to actually look at the data. Building this tool first sets the stage for our other work, since it serves as the entry point into any of our other workflows, and it also gives us a way to look at our data to see that it makes sense.
From start to finish, this work would have taken an hour or three, depending on what debugging or styling a developer chooses to go with, and how many rounds of changes they make, either by themselves or after talking with stakeholders.
Hopefully you found this part of the process illuminating, and look for the next part soon: Creating new templates tool (US1-C3)
Let me know what you think of my design, or anything else from this post!
Cheers!
Leave a Reply