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:
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.