Is it beneficial to add Laravel Collective HTML package into your Laravel project?

|12 min read|
Teklog
Teklog


Introduction

Laravel Collective is a set of components that have been removed from Laravel's core framework and are no longer maintained by Taylor Otwell. Instead there is group of volunteers who decided to maintain following once famous packages:

In this tutorial, we are going to examine laravelcollective/html, trying to verify its usefulness in your future projects. Let's start then!

Assumptions

  • php and composer are installed
  • laravel command is available

Solution

Create Laravel project from scratch

laravel new testing-laravel-collective

As you probably know, Laravel base project comes with three packages

"require": {
    "php": ">=5.6.4",
    "laravel/framework": "5.4.*",
    "laravel/tinker": "~1.0"
}

In order to add Laravel Collective Html package, update composer dependency as shown below

composer require "laravelcollective/html":"^5.4.0"

Please note that Laravel Collective Html is not standalone package, it requires five other illuminate packages such as

"require": {
  "php": ">=5.6.4",
  "illuminate/http": "5.4.*",
  "illuminate/routing": "5.4.*",
  "illuminate/session": "5.4.*",
  "illuminate/support": "5.4.*",
  "illuminate/view": "5.4.*"
}

Typically, when you create new Laravel project, you are actually pulling laravel/framework package which replaces all illuminate packages required by Laravel Collective Html.

"replace": {
  "illuminate/http": "self.version",
  "illuminate/routing": "self.version",
  "illuminate/session": "self.version",
  "illuminate/support": "self.version",
  "illuminate/view": "self.version"
}

As a result, you only see laravelcollective/html package being added to your composer.json.

If this part is clear, let's move on to adding Laravel Collective Html provider to the providers array of config/app.php

 'providers' => [
     // ...
     Collective\Html\HtmlServiceProvider::class,
     // ...
 ],

If you enjoy Laravel Facades, don't forget to add two class aliases to the aliases array of config/app.php

 'aliases' => [
     // ...
     'Form' => Collective\Html\FormFacade::class,
     'Html' => Collective\Html\HtmlFacade::class,
     // ...
 ],

Installation part is over, so let's try to explore a few features from Laravel Collective Html package.

Features

General Form creation

Laravel Collective Html Laravel
{!! Form::open(['url' => '/']) !!}
{!! Form::close() !!}
<form method="post" action="{{ url('/') }}" accept-charset="utf-8">
</form>

If you care about HTML naming conventions, please keep in mind that Laravel Collective Html produces form with uppercased method and accept-charset as shown below.

<form method="POST" action="http://blogging.app" accept-charset="UTF-8">
  <input name="_token" type="hidden" value="XnZwoGRTD4GszGv1kgTuFO9U8rlmMCX4HH7evCdM">
</form>

Sophisticated Form creation

Laravel Collective Html Laravel
{!! Form::open(['route' => 'home', 'method' => 'put', 'files' => 'yes']) !!}
{!! Form::close() !!}
<form method="post" action="{{ route('home') }}" accept-charset="utf-8" enctype="multipart/form-data">
{{ method_field('PUT') }}
{{ csrf_field() }}
</form>

Form Model Bindings

Having following user

{
  "id":1,
  "name":"Miss Dianna Funk",
  "email":"isadore.farrell@example.com",
  "active":0,
  "created_at":"2017-07-08 16:30:36",
  "updated_at":"2017-07-08 16:30:36"
}

passed to the Blade template, you can easily bind it to the form, using Form::model method, instead of Form::open.

{!! Form::model($user, ['action' => 'HomeController@user']) !!}

{!! Form::label('email', 'E-mail Address') !!}
{!! Form::email('email') !!}

{!! Form::label('active', 'Is Active?') !!}
{!! Form::checkbox('active', 'Yes') !!}

{!! Form::label('updatedAt', 'Updated At') !!}
{!! Form::date('updatedAt') !!}

{!! Form::label('created_at', '<strong>Created At</strong>', ['class' => 'btn btn-success', 'id' => 'created_at_id'], false) !!}
{!! Form::datetime('created_at', null, ['class' => 'btn']) !!}

{!! Form::submit('Edit') !!}

{!! Form::close() !!}

However there are some caveats.

  • if you want to use Form::date, make sure your model formats Carbon object properly. In order to do it, you could use Eloquent Mutators or benefit from trait Collective\Html\Eloquent\FormAccessible
  • you must name your inputs exactly the same as properties in your Eloquent model. You will face the wall if you intend to construct the input names as arrays such as user[email], user[updatedAt].

If you wanted to achieve the same with plain Laravel, here is the snippet

<form action="{{ action('HomeController@user') }}" method="post" accept-charset="utf-8">
  {{ csrf_field() }}

  <label for="email">Email</label>
  <input type="email" id="email" name="email" value="{{ old('email', request('email') ?? $user->email ?? null) }}" />

  <label for="active">Is Active?</label>
  <input type="checkbox" id="active" name="active" value="Yes" @if(old('active', request('active') ?? $user->active ?? null)) checked @endif />

  <label for="updatedAt">Updated At</label>
  <input name="updatedAt" type="date" value="{{ old('updatedAt', request('updatedAt') ?? $user->updated_at ?? null) }}" id="updatedAt">

  <label for="created_at" class="btn btn-success" id="created_at_id"><strong>Created At</strong></label>
  <input class="btn" name="created_at" type="datetime" value="{{ old('createdAt', request('createdAt') ?? $user->created_at ?? null) }}" id="created_at">

  <input type="submit" value="Edit" />
</form>

Macros and Components

In order to create a Macro or Component, head over to your AppServiceProvider and inside boot method place the following

FormBuilder::macro('tekmi', function () {
  return '<input type="text" value="tekmi">';
});

FormBuilder::component(
  'bsTekmi', 
  'components.form.text', 
  ['name', 'value' => null, 'attributes' => []]
);

Additionally, Components use Blade templates, so don't forget to create one in resources/views/components/form/text.blade.php with following snippet

<div class="form-group">
  {{ Form::label($name, null, ['class' => 'control-label tekmi-label']) }}
  {{ Form::text($name, $value, array_merge(['class' => 'form-control tekmi-text'], $attributes)) }}
</div>

Having Macro and Component ready, here is how this can be invoked inside your Blade templates

{!! Form::tekmi() !!}
{!! Form::bsTekmi('first_name') !!}
{!! Form::bsTekmi('last_name', 'tekmi', ['class' => 'form-control tekmi-text']) !!}

If you wanted to achieve something similar in pure Laravel, it should be pretty straightforward. Just create a custom class which:

  • will use trait Illuminate\Support\Traits\Macroable from Laravel Illuminate Support
  • will reuse the logic from trait  Collective\Html\Componentable from Laravel Collective Html

Other FormBuilder and HtmlBuilder conveniences

I went carefully through all the public methods from Collective\Html\FormBuilder and Collective\Html\HtmlBuilder classes, using the most complicated combination of parameters possible, as presented below.

{{ Form::token() }}

{!! Form::textarea('description', 'Text', ['size' => '20x5', 'id' => 'description', 'name' => 'description', 'class' => 'text text-long']) !!}
{!! Form::hidden('hidden', 'Hidden', ['id' => 'hidden', 'name' => 'hidden', 'class' => 'text text-hidden']) !!}
{!! Form::password('password', ['id' => 'password', 'name' => 'password', 'class' => 'text text-center']) !!}

{!! Form::search('search', 'Search', ['id' => 'search', 'name' => 'search', 'class' => 'text text-left']) !!}
{!! Form::tel('tel', '031647555333', ['id' => 'tel', 'name' => 'tel', 'class' => 'text text-left']) !!}
{!! Form::number('number', '0.05', ['min' => 0, 'step' => 0.01, 'id' => 'number', 'name' => 'number', 'class' => 'text text-left']) !!}

{!! Form::datetimeLocal('localDateTime', '2017-10-10T21:10:10', ['id' => 'localDateTime', 'name' => 'localDateTime', 'class' => 'text text-left']) !!}
{!! Form::time('time', '10:30:55', ['id' => 'time', 'name' => 'time', 'class' => 'text text-left']) !!}
{!! Form::url('url', 'http://tekmi.nl', ['id' => 'url', 'name' => 'url', 'class' => 'text text-left']) !!}

{!! Form::select('size', ['L' => 'Large', 'S' => 'Small'], null, ['placeholder' => 'Pick a size...', 'id' => 'size', 'class' => 'select select-big'], ['L' => ['class' => 'option option-red']]) !!}
{!! Form::select('animal',['Cats' => ['leopard' => 'Leopard'], 'Dogs' => ['spaniel' => 'Spaniel']], 'spaniel', ['class' => 'select-with-optgroup']) !!}
{!! Form::selectRange('range', 10, 30, 20, ['id' => 'range', 'class' => 'range range-20']) !!}

{!! Form::reset('resetButton', ['name' => 'reset', 'class' => 'reset-now']) !!}
{!! Form::selectMonth('month', 5, ['id' => 'month', 'class' => 'month month-1'], '%h') !!}
{!! Form::selectYear('birthdayDay', 2020, 2000, 2001, ['id' => 'year']) !!}

{!! Form::file('file', ['id' => 'file', 'name' => 'file', 'class' => 'file']) !!}
{!! Form::radio('radio', 'Switch On', null, ['id' => 'radio-1', 'name' => 'radio', 'class' => 'radio tune-in']) !!}
{!! Form::radio('radio', 'Switch Off', true, ['id' => 'radio-2', 'name' => 'radio', 'class' => 'radio tune-in']) !!}

{!! Form::image('images/image.jpg', 'Image Button', ['class' => 'reset-now']) !!}
{!! Form::color('color', '#ff0000', ['id' => 'color', 'class' => 'text-color']) !!}
{!! Form::button('button', ['id' => 'button', 'class' => 'btn btn-success']) !!}
{!! Html::script('app.js', ['type' => 'text/javascript'], true) !!}
{!! Html::style('app.css', ['media' => 'screen'], true) !!}
{!! Html::image('images/image.jpg', 'Image Alt', ['class' => 'image'], true) !!}
{!! Html::favicon('favicon.ico', ['type' => 'image/x-icon'], true) !!}

{!! Html::link('users/1/edit', '<i>Title</i>', ['class' => 'link'], true, false) !!}
{!! Html::linkAsset('img/image.jpg', 'Image', ['class' => 'img'], true) !!}
{!! Html::linkRoute('home', 'Home', ['param' => 'value'], ['class' => 'btn']) !!}
{!! Html::linkAction('HomeController@index', 'Home', ['param' => 'value'], ['class' => 'btn']) !!}

{!! Html::nbsp(5) !!}

{!! Html::mailto('ala@ala.nl', '<i>Mail me</i>', ['class' => 'mail'], false) !!}

{!! Html::ol(['a', 'b', 'c' => ['d', 'e']], ['class' => 'ol-list']) !!}
{!! Html::ul(['a', 'b', 'c' => ['d', 'e']], ['class' => 'ul-list']) !!}
{!! Html::dl(['a' => 'a character', 'b', 'c multiple' => ['d' => 'd is here', 'e' => 'e is there']], ['class' => 'dl-list']) !!}

{!! Html::meta('viewport', 'width=device-width, initial-scale=1') !!}
{!! Html::meta(null, null, ['charset' => 'utf-8']) !!}
{!! Html::meta(null, 'IE=edge', ['http-equiv' => 'X-UA-Compatible']) !!}

{!! Html::tag('input', '', ['type' => 'week', 'value' => '2017-W05']) !!}

Creating pure Laravel counterparts should be straightforward. If you are struggling though, please check an example github repository I've created.

URL generation with global helpers

Laravel Collective Html Laravel
{{ link_to('/') }}
{{ link_to('/', 'Title') }}
{{ link_to('/', 'Title', ['class' => 'btn']) }}
{{ link_to('/', 'Title', ['class' => 'btn'], true) }}
{{ link_to('/', '<strong>Title</strong>', ['class' => 'btn'], true, false) }}
<a href="{{ url('/') }}">{{ url('/') }}</a>
<a href="{{ url('/') }}">Title</a>
<a class="btn" href="{{ url('/') }}">Title</a>
<a class="btn" href="{{ url('/', [], true) }}">Title</a>
<a class="btn" href="{{ url('/', [], true) }}"><strong>Title</strong></a>
{{ link_to_asset('img/image.jpg') }}
{{ link_to_asset('img/image.jpg', 'Image') }}
{{ link_to_asset('img/image.jpg', 'Image', ['class' => 'img']) }}
{{ link_to_asset('img/image.jpg', 'Image', ['class' => 'img'], true) }}
<a href="{{ asset('img/image.jpg') }}">{{ asset('img/image.jpg') }}</a>
<a href="{{ asset('img/image.jpg') }}">Image</a>
<a class="img" href="{{ asset('img/image.jpg') }}">Image</a>
<a class="img" href="{{ asset('img/image.jpg', true) }}">Image</a>
{{ link_to_route('home') }}
{{ link_to_route('home', 'Home') }}
{{ link_to_route('home', 'Home', ['param' => 'value']) }}
{{ link_to_route('home', 'Home', ['param' => 'value'], ['class' => 'btn']) }}
<a href="{{ route('home') }}">{{ route('home') }}</a>
<a href="{{ route('home') }}">Home</a>
<a href="{{ route('home', ['param' => 'value']) }}">Home</a>
<a class="btn" href="{{ route('home', ['param' => 'value']) }}">Home</a>
{{ link_to_action('HomeController@index') }}
{{ link_to_action('HomeController@index', 'Home') }}
{{ link_to_action('HomeController@index', 'Home', ['param' => 'value']) }}
{{ link_to_action('HomeController@index', 'Home', ['param' => 'value'], ['class' => 'btn']) }}
<a href="{{ action('HomeController@index') }}">{{ action('HomeController@index') }}</a>
<a href="{{ action('HomeController@index') }}">Home</a>
<a href="{{ action('HomeController@index', ['param' => 'value']) }}">Home</a>
<a class="btn" href="{{ action('HomeController@index', ['param' => 'value']) }}">Home</a>

It's worth to know that Laravel's route and action methods provide third parameter called absolute, which is not covered by the Laravel Collective counterparts. If you set it to false, you will get the URI without the domain (e.g: /?param=value or /home?param=value).

<a class="btn" href="{{ route('home', ['param' => 'value'], false) }}">Home</a>
<a class="btn" href="{{ action('HomeController@index', ['param' => 'value'], false) }}">Home</a>

Summary

We have gone through long journey together, trying to discover hidden gems of laravelcollective/html package. Along the way we experienced some issues and shortcomings of it, so let's try to enumerate them now

  • not every input type from HTML standards is covered (missing types: range, week, month)
  • model bindings cannot handle html name arrays (e.g users[name], users[description])
  • misleading and not comprehensive documentation (better to explore methods from FormBuilder and HtmlBuilder classes)

    • misleading information about Form::textarea which doesn't extend Form::input
    • Form::textarea uses not documented option like size=10x5
    • zero information about FormAccesible trait
    • no information about HTML facade and its conveniences
  • package tightly coupled to Laravel, but not adapted into Lumen (here is an explanation)
  • generating URLs via link_to* methods or HTML::link* look a bit redundant, comparing to Laravel route, action, url global functions.
  • link_to_action and link_to_route miss 3rd parameter absolute, which is used by Laravel route and action helpers
  • FormBuilder::selectRange, FormBuilder::selectYear and FormBuilder::selectMonth don't use 5th parameter from FormBuilder::select
  • Inconsistent HTML naming conventions

If I missed anything relevant, please write your findings on Twitter.

Sample Code

https://github.com/tekmi/blogging-laravelcollectivehtml