Stidges' Blog

Easy Bootstrap Forms in Laravel

Posted on in Development

When you are working with Bootstrap in Laravel, the extra div's that are required to format the form properly can really get in the way of the readability of your template. In this post we take a look at how to make it easier to manage your Bootstrap-based forms.

The Goal

When working with Bootstrap, you are required to wrap your label and input elements in a div with a class of form-group. On top of that, all your input elements require a form-control class to be applied. Then, when you want to show errors in your forms, you have to check if the form field has any errors, add a class to the form-group if it does and finally add the error to the end of the form-group. Your forms may look something like this:

{{ Form::open([ 'route' => 'posts.store' ]) }}

    <div class="form-group{{ $errors->has('title') ? ' has-error' : '' }}">
        {{ Form::label('title', 'Title') }}
        {{ Form::text('title', null, ['class' => 'form-control']) }}
        {{ $errors->first('title', '<p class="help-block">:message</p>') }}
    </div>

    <div class="form-group{{ $errors->has('status') ? ' has-error' : '' }}">
        {{ Form::label('status', 'Status') }}
        {{ Form::select('status', $statusOptions, null, ['class' => 'form-control']) }}
        {{ $errors->first('status', '<p class="help-block">:message</p>') }}
    </div>

{{ Form::close() }}

While this may not be many lines of code, you will have to copy this for every form field you would like to add. On top of that it is not very readable either. It would be nice to clean this all up and make the above code look like this:

{{ Form::open([ 'route' => 'posts.store' ]) }}

    {{ Form::openGroup('title', 'Title') }}
        {{ Form::text('title') }}
    {{ Form::closeGroup() }}

    {{ Form::openGroup('status', 'Status') }}
        {{ Form::select('status', $statusOptions) }}
    {{ Form::closeGroup() }}

{{ Form::close() }}

That looks much cleaner! No more handling of the form errors in the template, no more labels, and no more tedious form-control classes! Let's take a look at how we might accomplish this.

Preparations

Before we get started, I think it's good to set everything up before digging in. On that note, I will be making the assumption that you have pulled in both Laravel and Bootstrap, and that you have a Blade template that has the Bootstrap CSS included.

To take full control, instead of using Form macro's, we will be extending and customizing Laravel's FormBuilder class. Let's get started by setting up our namespace. In your app folder, create a new src directory (or place it directly in the root directory if your prefer). Then, open up your composer.json and add the following to the autoload section:

"autoload": {
    // ...
    "psr-4": {
        "Acme\\": "app/src/"
    }
}

After you have updated your composer.json, don't forget to run a composer dump-autoload for these changes to take effect.

First off, we will set up our custom FormBuilder class. In the src directory, create a new directory called Html. Then, create a new file called FormBuilder.php in this directory. For now, we will set up an empty class that we will be using later:

<?php
// File: app/src/Html/FormBuilder.php

namespace Acme\Html;

use Illuminate\Html\FormBuilder as IlluminateFormBuilder;

class FormBuilder extends IlluminateFormBuilder {

}

To be able to use our custom FormBuilder in our templates, we will create a new service provider that overrides Laravel's FormBuilder. You can either place this file within the same Html directory, or you can create a new Providers directory in your src directory that will house all your custom service providers. I will be using the latter. Create a new HtmlServiceProvider.php file in the Providers directory. This service provider will extend the HtmlServiceProvider which is provided by Laravel. That way we can change only what is necessary and leave the rest intact:

<?php
// File: app/src/Providers/HtmlServiceProvider

namespace Acme\Providers;

use Acme\Html\FormBuilder;
use Illuminate\Html\HtmlServiceProvider as IlluminateHtmlServiceProvider;

class HtmlServiceProvider extends IlluminateHtmlServiceProvider {

    /**
     * Register the form builder instance.
     *
     * @return void
     */
    protected function registerFormBuilder()
    {
        $this->app->bindShared('form', function($app)
        {
            $form = new FormBuilder($app['html'], $app['url'], $app['session.store']->getToken());

            return $form->setSessionStore($app['session.store']);
        });
    }

}

The above service provider may look a bit intimidating, so let me explain it a bit. In Laravel's HtmlServiceProvider, the register method will call the registerFormBuilder method. The above method is actually a direct copy of the original method. The only thing we change is that we import our custom FormBuilder class at the top. This causes Laravel's FormBuilder to be overridden by our custom one. This way, we don't have to change anything else (like creating a custom Facade), it will just work.

The last thing we need to do is change our app config file to use our own HtmlServiceProvider instead of Laravel's service provider. You can do this by going into the app/config/app.php and changing Illuminate\Html\HtmlServiceProvider in the providers array to Acme\Providers\HtmlServiceProvider:

<?php
// File: app/config/app.php

return array(
    // ...
    'providers' => array(
        // ...
        // 'Illuminate\Html\HtmlServiceProvider',
        'Acme\Providers\HtmlServiceProvider',
        // ...
    ),
    // ...
);

With the initial set up out of the way, let's dig into the actual code!

Getting Rid of the 'form-control' Classes

To automatically add the form-control classes to our inputs, we will override the input method of Laravel's FormBuilder class. In the background, the input method is called every time that you use things like Form::text(), Form::email(), etc. The only exception to this is the select input, so we will override this method as well.

<?php
// File: app/src/Html/FormBuilder.php
// ...
class FormBuilder extends IlluminateFormBuilder {

    /**
     * Create a form input field.
     *
     * @param  string  $type
     * @param  string  $name
     * @param  string  $value
     * @param  array   $options
     * @return string
     */
    public function input($type, $name, $value = null, $options = array())
    {
        $options = $this->appendClassToOptions('form-control', $options);

        // Call the parent input method so that Laravel can handle
        // the rest of the input set up.
        return parent::input($type, $name, $value, $options);
    }

    /**
     * Create a select box field.
     *
     * @param  string  $name
     * @param  array   $list
     * @param  string  $selected
     * @param  array   $options
     * @return string
     */
    public function select($name, $list = array(), $selected = null, $options = array())
    {
        $options = $this->appendClassToOptions('form-control', $options);

        // Call the parent select method so that Laravel can handle
        // the rest of the select set up.
        return parent::select($name, $list, $selected, $options);
    }

    /**
     * Append the given class to the given options array.
     * 
     * @param  string  $class
     * @param  array   $options
     * @return array
     */
    private function appendClassToOptions($class, array $options = array())
    {
        // If a 'class' is already specified, append the 'form-control'
        // class to it. Otherwise, set the 'class' to 'form-control'.
        $options['class'] = isset($options['class']) ? $options['class'].' ' : '';
        $options['class'] .= $class;

        return $options;
    }

}

The above code applies the 'form-control' class to every input and select element. This works fine, however, the 'radio' and 'checkbox' input will also have a 'form-control' class applied. Bootstrap requires a different markup for these elements. The code for these is not included in this post, but you can find it in the Gist which is mentioned at the end of the post.

As a final tweak, we will create a plainInput and plainSelect method, so that you can create a plain input / select element without the class automatically applied. This is useful for when you use a plugin that controls the markup of the elements (for example Selectize.js):

<?php
// File: app/src/Html/FormBuilder.php
// ...
class FormBuilder extends IlluminateFormBuilder {

    // ...

    /**
     * Create a plain form input field.
     *
     * @param  string  $type
     * @param  string  $name
     * @param  string  $value
     * @param  array   $options
     * @return string
     */
    public function plainInput($type, $name, $value = null, $options = array())
    {
        return parent::input($type, $name, $value, $options);
    }

    /**
     * Create a plain select box field.
     *
     * @param  string  $name
     * @param  array   $list
     * @param  string  $selected
     * @param  array   $options
     * @return string
     */
    public function plainSelect($name, $list = array(), $selected = null, $options = array())
    {
        return parent::select($name, $list, $selected, $options);
    }

    // ...

}

Form Groups, Labels and Errors

When you redirect back with errors in Laravel (e.g. Redirect::back()->withInput()->withErrors($errors);), Laravel will store our validation errors in the Session, which enables us to fetch them. As an added bonus, the Session class is already injected in the FormBuilder class that we extend, so we can leverage it out of the box! To get started with error handling, we will create two internal functions that will help us with this:

<?php
// File: app/src/Html/FormBuilder.php
// ...
class FormBuilder extends IlluminateFormBuilder {

    // ...

    /**
     * Determine whether the form element with the given name
     * has any validation errors.
     * 
     * @param  string  $name
     * @return bool
     */
    private function hasErrors($name)
    {
        if (is_null($session) || ! $this->session->has('errors'))
        {
            // If the session is not set, or the session doesn't contain
            // any errors, the form element does not have any errors
            // applied to it.
            return false;
        }

        // Get the errors from the session.
        $errors = $this->session->get('errors');

        // Check if the errors contain the form element with the given name.
        // This leverages Laravel's transformKey method to handle the
        // formatting of the form element's name.
        return $errors->has($this->transformKey($name));
    }

    /**
     * Get the formatted errors for the form element with the given name.
     * 
     * @param  string  $name
     * @return string
     */
    private function getFormattedErrors($name)
    {
        if ( ! $this->hasErrors($name))
        {
            // If the form element does not have any errors, return
            // an emptry string.
            return '';
        }

        // Get the errors from the session.
        $errors = $this->session->get('errors');

        // Return the formatted error message, if the form element has any.
        return $errors->first($this->transformKey($name), '<p class="help-block">:message</p>');
    }

    // ...

}

These methods will allow us to check if the form element has any errors and append a formatted error message if necessary. These will come in handy when handling the form groups.

Whenever a form element has any errors, Bootstrap requires us to set a 'has-error' class to the form group to style the containing form element. This will also style the 'help-block' for us that we have created in the getFormattedErrors method. When closing out a form group, we would like the errors to be appended to the end of the form group, so that they are nicely displayed below the form element in question. Let's implement this.

<?php
// File: app/src/Html/FormBuilder.php
// ...
class FormBuilder extends IlluminateFormBuilder {

    /**
     * An array containing the currently opened form groups.
     * 
     * @var array
     */
    protected $groupStack = array();

    // ...

    /**
     * Open a new form group.
     * 
     * @param  string  $name
     * @param  mixed   $label
     * @param  array   $options
     * @return string
     */
    public function openGroup($name, $label = null, $options = array())
    {
        $options = $this->appendClassToOptions('form-group', $options);

        // Append the name of the group to the groupStack.
        $this->groupStack[] = $name;

        if ($this->hasErrors($name))
        {
            // If the form element with the given name has any errors,
            // apply the 'has-error' class to the group.
            $options = $this->appendClassToOptions('has-error', $options);
        }

        // If a label is given, we set it up here. Otherwise, we will just
        // set it to an empty string.
        $label = $label ? $this->label($name, $label) : '';

        return '<div'.$this->html->attributes($options).'>'.$label;
    }

    /**
     * Close out the last opened form group.
     * 
     * @return string
     */
    public function closeGroup()
    {
        // Get the last added name from the groupStack and 
        // remove it from the array.
        $name = array_pop($this->groupStack);

        // Get the formatted errors for this form group.
        $errors = $this->getFormattedErrors($name);

        // Append the errors to the group and close it out.
        return $errors.'</div>';
    }

    // ...

}

Conclusion

There it is! It took a bit of code to accomplish this, but I think it is worth the effort. This will keep your Bootstrap based forms easily manageable and much more readable! All the code from this post can be found on GitHub. I hope this helps you out. If you have any questions, please do not hesitate to place a comment below!