Yearly archives: 2020


Reusing form in Laravel for both create and update   Recently updated !

This is the pattern I use for simple CRUD operations. It makes use of Route Model Binding to inject the model being created, and model properties to determine if the model is in the process of being created or updated.

The forms also make use of the old() helper to pull in previous form value from the model, or from previously submitted form.

Note that the form elements here are styled with tailwind utility classes. If you are not into Tailwind, look past that as its not relevant to this article.

The example is CRUD for something called a Template. In this particular application, its just a form with a bunch of input fields.

The Controller

It starts with simplifying the controller;

<?php

namespace App\Http\Controllers;

use App\Http\Requests\TemplateForm;
use App\Template;
use Illuminate\Http\Request;

class TemplateController extends Controller
{

    public function create()
    {
        return $this->edit(new Template());
    }

    public function store(TemplateForm $request)
    {
        return $this->update($request, new Template());
    }

    public function edit(Template $template)
    {
        return view('template.edit')->withTemplate($template);
    }

    public function update(TemplateForm $request, Template $template)
    {
        $request->persist($template);

        return redirect(route('templates.index'));
    }

}

When creating a record, we create a new model and pass it into the edit function. The edit is responsible for returning the view. Because we pass a new model into edit, we don’t need to worry about how we use the model in the view (more later).

When the data is returned from the form, if it is a new model then again, we create a new instance of the Template model and pass it to the Update method. The Update does not care if the model is new or an existing one looked up by Route Model binding. All it needs to do is to pass the model back to the Form Request and ask it to persist the model with the form data.

Sharing the form

The same form is shared for both edit and update functions. This is possible because either way, an instance of our model is passed to the form.

I have abbreviated the form because its not relevant to the discussion, but you will see validation and persist for items that are not visible below.

<div class="w-full p-6 flex">
    @if($template->exists)
        <form class="flex flex-col w-full" method="POST" action="{{ route('templates.update',$template) }}">
            @method('put')
    @else
        <form class="flex flex-col w-full" method="POST" action="{{ route('templates.store') }}">
    @endif
            @csrf
            <div class="flex w-full">
                {{-- form input element --}}
                <div class="flex flex-wrap mb-6 w-1/3">
                    <label for="name" class="block text-gray-700 text-sm font-bold mb-2">Template Name:</label>

                    <input id="name" type="text" required name="name"
                        value="{{ old('name', $template->name) }}"
                        class="text-base font-mono shadow appearance-none border rounded 
                            w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline 
                            @error('name') border-red-500 @enderror">
                    @error('name')
                    <p class="text-red-500 text-xs italic mt-4">{{ $message }}</p>
                    @enderror
                </div>

                {{-- form input element --}}
                <div class="flex flex-wrap mb-6 w-2/3 ml-4">
                    <label for="description" class="block text-gray-700 text-sm font-bold mb-2">Description:</label>

                    <input id="description" type="text" required name="description" value="{{ old('description', $template->description) }}"
                        class="text-base font-mono shadow appearance-none border rounded w-full 
                        py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline 
                        @error('description') border-red-500 @enderror">
                        
                    @error('description')
                    <p class="text-red-500 text-xs italic mt-4">{{ $message }}</p>
                    @enderror
                </div>
            </div>

            // irrelevant form elements removed.....

            <button class="positive-button" type="submit">Save </button>
        <form>
</div>

It is not possible to use the same form tag for both update and create because we need to pass the model ID for an update and make it a PUT request rather than a POST request. In line 2 we are checking if our model actually exists in the database so that we know which case it is. After this @if @else section, the rest of the form does not care if the model is new or not.

The form inputs themselves use the old()helper to insert the previous value, the value from validation or an empty value. for instance value="{{ old('name', $template->name) }}" . It helps a lot if you name the form field the same as the model attribute.

old() takes two parameters, the first is the field name that was submitted previously (in the case of validation failures, the second parameter is the default value. In our case, for a model that is being edited, the previous value is inserted. If it is a new model then NULL is returned and no errors are produced. First time around the value of the form field will be empty.

If you want a default value for the field then the null coalesce operator ?? can be used. For instance; value="{{ old('type', $template->type ?? 'banana') }}". If the model is new then the default value of ‘banana’ will be inserted into the form.

Form Request

I appreciate that this will be controversial, but its what I do, and is optional. You can still go ahead and store the updated model in the controller, or use repository pattern or whatever. I prefer to use the form request class to save the form data also.

<?php

namespace App\Http\Requests;

use App\Template;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Storage;

class TemplateForm extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'name' => 'required|max:100',
            'description' => 'required|max:200',
            'subject' => 'max:200',
        ];
    }

    public function persist(Template $template)
    {
        $template->name = $this->name;
        $template->description = $this->description;
        $template->type = $this->type;
        $template->subject = $this->subject;
        $template->email_template = $this->email_template;
        $template->sms_template = $this->sms_template;

        $template->save();
    }
}

So, yes rules() is standard, but I have added a persist() method. This expects to be passed a model instance, to which it saves the form data.

The model instance was passed from the update function of the controller with $request->persist($template); If we are creating a model then an empty model was passed from the store method, and if we are updating an existing model then this was injected by Route Model Binding. In the form request class we just pop the values into the model and save it.

Conclusion

Unfortunately, too many people think that if you want to bind model data to a form then you must use the Laravel Collective Form components. This is not the case. Understanding the old() helper is fundamental to building simple crud operations, and passing an instance of a new model to your form means that you can share the same form with create and update operations.


Styling Livewire Paginator with Tailwind css

Spent some time today, styling the Livewire paginator in Tailwind. May be useful to someone.

Wrap the pagination links in a container;

<div class="livewire-pagination">{{ $flowStatus->onEachSide(2)->links() }}</div>

Add styles to your app.css

/* purgecss start ignore */
.livewire-pagination {
     @apply inline-block w-auto ml-auto float-right;
}
ul.pagination {
    @apply flex border border-gray-200 rounded font-mono;
}
.page-link {
    @apply block bg-white text-blue-800 border-r border-gray-200 outline-none py-2 w-12 text-sm text-center;
}
.page-link:last-child {
    @apply border-0;
}
.page-link:hover {
    @apply bg-blue-700 text-white border-blue-700;
}
.page-item.active .page-link {
    @apply bg-blue-700 text-white border-blue-700;
}
.page-item.disabled .page-link {
    @apply bg-white text-gray-300 border-gray-200;
}

/* purgecss end ignore */

Be sure to wrap your css in purgecss ignore comments since the classes used by the paginator are not in any files that are scanned by purgecss and will be stripped if you don’t tell it to ignore.


Dynamic Cascading Dropdown with Livewire

A problem I see frequently on the Laracasts Forum is people struggling with the Javascript required to create dynamic cascading dropdowns. A cascading dropdown uses one form input select box to determine the list presented by a second select. If the dataset is small, all the options can be held locally and the problem is relatively simple Javascript one.

On the otherhand, if the dataset is large, the options for the second select might need to be queried from the backend. This then adds the challenge of creating an AJAX request in the browser, creating an API on the server side, and merging the returned data into the current document.

This is a lot of work for someone not comfortable with Javascript, and a lot of opportunity for issues.

I decided to see just how simple this could be using Livewire by Caleb Porzio. Livewire provides client side components that are ‘hotwired’ to Laravel components, providing two-way data-binding and automatic DOM updates.

Setup

If you already have a project with suitable dataset, then you can skip this section.

I started with a new Laravel project (in the examples below, using Tailwind, but not relevant) and then added a dataset that could be used by the dropdowns.

After (not much!) searching, I came across a Laravel package of countries and cities which seemed it would be suitable. Other datasets are available, but this one migrated then seeded the database tables.

The package is https://github.com/khsing/laravel-world . Follow the instructions on the Github page to add the package, service provider and initialise the database.

Unfortunately, the package has not been recently maintained and does not understand that the string helpers have been removed. For our purposes this is not a great issue, we can create new Eloquent models and just tell them to use the world_ tables;

~/Sites/livewire (master) $ php artisan make:model Country

~/Sites/livewire (master) $ php artisan make:model City

// Country model
protected $table = 'world_countries';

//City model
protected $table = 'world_cities';

Install Livewire

composer require livewire/livewire

All the magic of Livewire happens client side through a Javascript library that can be included in the <head>of any page that uses Livewire using a simple blade directive;

    <link href="{{ mix('css/app.css') }}" rel="stylesheet">

    @livewireAssets
    
</head>

OK, so now our page is ready for our component. I’m going to call this one simply dropdowns. An Artisan command helpfully scaffolds the Laravel module and the blade view.

php artisan make:livewire dropdowns

The command creates two files app\Http\Livewire\Dropdowns.php and resources\views\livewire\dropdowns.blade.php

The view

The view element of the component is not so different from a regular blade include file. I just create the view pretty much as I would when creating a form containing select dropdowns.

<div>
    <div class="mb-8">
        <label class="inline-block w-32 font-bold">Country:</label>
        <select name="country" wire:model="country" class="border shadow p-2 bg-white">
            <option value=''>Choose a country</option>
            @foreach($countries as $country)
                <option value={{ $country->id }}>{{ $country->name }}</option>
            @endforeach
        </select>
    </div>

    <div class="mb-8">
        <label class="inline-block w-32 font-bold">City:</label>
        <select name="city" wire:model="city" class="border shadow p-2 bg-white
            {{ count($this->cities)==0 ? 'hidden' : '' }}">
            <option value=''>Choose a city</option>
            @foreach($this->cities as $city)
                <option value={{ $city->id }}>{{ $city->name }}</option>
            @endforeach
        </select>
    </div>

</div>

The only things you might not recognise here are the wire:model directives. These provide two-way data binding with public attributes of the back-end component.

The component is included in the page blade file with a Livewire directive;

    <div class="flex flex-col justify-around h-full">

        @livewire('dropdowns')

    </div>

Dropdowns Component

<?php

namespace App\Http\Livewire;

use App\City;
use App\Country;
use Livewire\Component;

class Dropdowns extends Component
{
    public $country;
    public $cities=[];
    public $city;

    public function mount($country, $city)
    {
        $this->country=$country;
        $this->city=$city;
    }

    public function render()
    {
        if(!empty($this->country)) {
            $this->cities = City::where('country_id', $this->country)->get();
        }

        return view('livewire.dropdowns')
            ->withCountries(Country::orderBy('name')->get());
    }
}

Ok, some new stuff to get to grips with here. The public attributes are shared with the view ‘live’ whatever the public property contains, the view has access to. Initially, the cities is an empty array, as until we select a country we don’t know which cities to show.

Skip over the mount() method for a moment, we will cover that later.

The render() method is called whenever one of the elements in the view component changes, such as when the user changes the Country dropdown. Before invoking render, Livewire re-hydrates the public properties of the component. Thanks to the wire:model attribute on the select element, the select’s value is bound to the country property. We can then use this to set the cities array using an Eloquent query. When the render method ends by returning the view component, the view is updated with the cities populated in the second dropdown.

We now have a working cascading dropdown. Changing the Country field presents a list of cities in the second dropdown. Not a single line of Javascript was written.. not even a script tag.

Extra: The Mount() method

Suppose these dropdowns are on an edit page, where the user’s previous selection must be presented. The previous values can be passed into the @livewire directive;

    <div class="flex flex-col justify-around h-full">

        @livewire('dropdowns', $event->country_id, $event->city_id)

    </div>

The additional two properties are passed into the mount() method where they can be used to initialise the country and city public properties of the Dropdown component. Since the data is bound two-way to the select element, when the page is rendered, the previous entries will be selected.

Conclusion

Livewire makes it super easy to provide areas of your web application front-end that can interact directly with your backend without writing any API or Javascript. It requires a bit of a mind shift in the way you think about how applications should be built. I’m a fan!