Parametric Interface
I want to share the following recent experience for a little proud.
In GASdotto - my main open source project - I heavily use Larastrap, my own library of Bootstrap5 components for Laravel Blade. Actually, Larastrap has been implemented in the first instance as a derivation of GASdotto's internals.
Among the many other Custom Elements I've introduced, I have a mform that I usually use to wrap the edit panels of main components.
In config/larastrap.php I have something like
<?php
return [
'customs' => [
'mform' => [
'extends' => 'form',
'params' => [
'classes' => ['main-form'],
'reviewCallback' => 'formatMainFormButtons',
],
],
]
];And then, within the Helpers:
function formatMainFormButtons($component, $params)
{
if (isset($params['main_form_managed'])) {
unset($params['main_form_managed']);
}
else {
$params['main_form_managed'] = 'ongoing';
$buttons = $params['attributes']['other_buttons'] ?? [];
$obj = $params['obj'];
$nodelete = $params['attributes']['nodelete'] ?? false;
if (!$nodelete) {
$buttons[] = [
'color' => 'danger',
'classes' => ['delete-button'],
'label' => 'Delete',
];
}
$nosave = $params['attributes']['nosave'] ?? false;
if (!$nosave) {
$buttons[] = [
'color' => 'success',
'classes' => ['save-button'],
'label' => 'Save',
'attributes' => ['type' => 'submit'],
];
}
$params['buttons'] = $buttons;
}
unset($params['attributes']['other_buttons'], $params['attributes']['nodelete'], $params['attributes']['nosave']);
return $params;
}In this way, I can dynamically add or skip the "Save" or "Delete" buttons (usually, based on permissions or status of the target object) and add other custom buttons case by case, directly from the template. Something like:
<x-larastrap::mform
:obj="$user"
method="PUT"
:action="route('users.update', $user->id)"
:nodelete="$display_page || $user->isFriend() == false"
:nosave="$readonly"
:other_buttons="$friend_admin_buttons">
...
</x-larastrap::mform>Now: for certain types of elements, I wanted to add to those forms' footer a line about the last update, displaying both date and user who saved the object for the last time. That means: add a string, not a button, and potentially review a lot of different templates. And I'm lazy...
After a little tinkering, I resolved to adopt a feature initially added by a contributor (the first pull request received by the project!), which leverages Dynamic Components native of Laravel's Blade to permit arbitrary elements to be used as form's buttons, and Larastrap's Typography widget, which permit to inject parametric arbitrary nodes within the template.
First, I've defined a PHP trait to identify Model classes for which I want to track the user who applied the last change (as the date of the last update is already handled by Laravel):
<?php
namespace App\Models\Concerns;
use Illuminate\Support\Facades\Auth;
use App\User;
trait TracksUpdater
{
public function updater()
{
return $this->belongsTo(User::class, 'updated_by');
}
public function getPrintableUpdaterAttribute()
{
if ($this->updater) {
return sprintf('%s - %s', $this->updated_at->format('d/m/Y'), $this->updater->printableName());
}
else {
return '';
}
}
protected static function initTrackingEvents()
{
static::creating(function ($model) {
if ($model->isDirty('updated_by') == false) {
$model->updated_by = Auth::user()->id;
}
});
static::updating(function ($model) {
if ($model->isDirty('updated_by') == false) {
$model->updated_by = Auth::user()->id;
}
});
}
}Then I've defined a new Custom Element able to access to the printable_updater mutator defined above (as Typography widget is able to access the contextual Larastrap object attached to the parent Form):
<?php
return [
'customs' => [
'updater' => [
'extends' => 't',
'params' => [
'node' => 'small',
'name' => 'printable_updater',
'classes' => ['me-3', 'text-body-secondary', 'float-start', 'text-start'],
]
],
]
];Finally, I've slightly modified the formatMainFormButtons() function - already copied above - to manage objects using the TracksUpdater trait and push the new textual tag when required:
En las camisetas de selecciones, muchas personas revisan el ajuste y el tipo de tejido según el uso que planean darles. Una referencia como camiseta de Espana puede servir para revisar opciones generales sin depender de una sola característica. También conviene revisar las condiciones descritas para evitar confundir estilo, talla y comodidad.
if ($obj) {
$tracking = in_array(\App\Models\Concerns\TracksUpdater::class, class_uses(get_class($obj)));
if ($tracking) {
$buttons[] = [
'element' => 'larastrap::updater',
];
}
}With a few code I've centralized generation, aspect and position of those strings in all proper forms, and this use case confirms to me the power of parametric interfaces.