back to Writing Forms

Why Form Classes are Awesome

Imagine having one place to add a field, and poof it's elegantly added to your forms.

Sep 28, 2020

Context

Back in 2016 I started tinkering with a package that could help me automatically generate HTML forms. I had written it using Laravel Collective's HTML package and it mostly examined a config file for forms and then took each "field" and created a snippet of HTML for it. I used it for numerous projects and it was incredibly handy. It helped get basic forms in place which I'd later replace occasionally with more advanced forms if need be.

However, it was never really well structured, in the sense that it always felt weird to look at a giant config file to determine the forms. When Taylor Otwell released Nova, I got inspired by an idea they had in their system. They had Fields! These were PHP classes placed into a Resource in a fields method. This to me was a game changing idea. I went back to my Forms package and started refactoring things for version 2 (currently on version 4). I transformed the config into fields, created Form classes, and keep adding new features as I go.

What Makes Fields Special

To me the beauty of having Field classes is that I can easily add new ones in a single file and have it propagate through my whole application. I can also inject JavaScript into fields, as well as CSS if need be. These assets are then minified and injected into the page view. It's also easy to create new Field types altogether should the technology or need arise.

Why A Form Class

In general we often write HTML forms in plain HTML with conditionals for error reporting along with passing values in from the Controller. I thought it would be pretty slick that if I load the lets say User into the Controller method then what if I could place that User into a Form and send that Form output to the View. This way, I can adjust the form elements through my Form Class with Field Classes and thusly have elegant rendering that the view just integrates in, this means I can move conditionals to Form classes vs having numerous conditionals inside HTML templates. Similar to fields the biggest benefit of form classes is the fact that they live in a single space and any adjustments to them can propagate through a whole application, rather than having to adjust several pages when one Model gets a small change, or a business rule gets an update.

So How Does It All Come Together?

Let's use a ModelForm as an example, though there are BaseForm's as well which are intended for Forms which have no Models. So let's say I have a User settings page, where I may want to edit my User settings. Pretty standard stuff.

In my UserController I have the following lines:

$user = $request->user();
$form = app(UserForm::class)->edit($user);
return view('user.settings')->withForm($form);

And then in my view I have one of the following:

{!! $form !!}

Or

<x-f :content="$form"></x-f>

As we can see all I had to do was use the edit method on the Form Object to create an edit form. If I needed to create or delete I'd use those respectively. In the case of delete I actually end up with a form with a single button, which includes a confirm JavaScript method on the button click.

<php

namespace App\Http\Forms;

use App\Models\User;
use Grafite\Forms\Html\HrTag;
use Grafite\Forms\Fields\Text;
use Grafite\Forms\Fields\Email;
use Grafite\Forms\Html\Heading;
use Grafite\Forms\Forms\ModelForm;
use Grafite\Forms\Fields\FileWithPreview;
use Grafite\Forms\Fields\Bootstrap\Toggle;

class UserForm extends ModelForm
{
    public $model = User::class;

    public $routePrefix = 'user';

    public $withJsValidation = true;

    public $buttons = [
        'submit' => 'Save',
        'delete' => '<span class="fas fa-fw fa-trash"></span> Delete'
    ];

    public $columns = 1;

    public $orientation = 'horizontal';

    public $hasFiles = true;

    public function fields()
    {
        return array_merge([
            Text::make('name', [
                'required' => true,
            ]),
            Email::make('email', [
                'required' => true,
            ]),
            Toggle::make('dark_mode', [
                'legend' => 'Dark Mode',
                'theme' => (auth()->user()->dark_mode) ? 'dark' : 'light',
            ]),
            Toggle::make('allow_email_based_notifications', [
                'legend' => 'Email Contact',
            ]),
            FileWithPreview::make('avatar', [
                'preview_identifier' => '.avatar',
                'preview_as_background_image' => true,
            ]),
        ], $this->billingColumns());
    }

    public function billingColumns()
    {
        return [
            Heading::make([
                'class' => 'mt-4 mb-1',
                'content' => 'Billing Details',
                'level' => 4,
            ]),
            HrTag::make(),
            Email::make('billing_email', [
                'label' => 'Email',
                'required' => true,
            ]),
            Text::make('state', [
                'label' => 'State',
                'required' => auth()->user()->hasActiveSubscription(),
            ]),
            Text::make('country', [
                'label' => 'Country',
                'required' => auth()->user()->hasActiveSubscription(),
            ]),
        ];
    }
}

This UserForm Class follows standard conventions of Laravel resource route endpoints. So by adding the prefix of user we now have the following route integrations: user.store, user.update, user.destroy. user.edit. $withJSValidation injects a basic form event listener which removes error highlighting on keyup.

Though it is certainly not the best solution for all form use cases, using the Form Class concept (specifically the Grafite Forms) package, you can build up various forms across a site quickly with significantly less HTML writing and handling of form validation elements. Some forms will not work with this pattern, and require Vue components or other JavaScript tools. But for me, this is an incredibly powerful tool for any project from an MVP to a full featured SaaS app.