Quick Introduction to Anonymous Blade Components In Laravel 8

Created by: SD Rosenthal
Laravel
quick-introduction-to-anonymous-blade-components-in-laravel-7

Laravel 7 introduced not only a new syntax for Laravel Blade components, but some awesome new features that can greatly enhance your development by increasing your productivity. This tutorial will provide a brief overview of the new Anonymous components in Laravel 7, by teaching you how to create dynamic, reusable form fields.

Prerequisites

Before you begin this tutorial, make sure you have the following set up/installed:
A Laravel 7 application already installed
General knowledge on Laravel and Blade components

What We’ll Build

We’ll put together a simple, profile-like form, and then extract data from the Blade components. I will be using Tailwind CSS for the styling which for your reference, can be brought in through their CDN. Create a new folder and layout located at resources/views/layouts/main.blade.php. Add the following code to the newly created file:


<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Laravel 7 Blade Components</title>
    <link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet">
</head>
<body class="h-screen w-screen flex items-center justify-center">
@yield('content')
</body>
</html>

Now create a route in routes/web.php to point to a view that houses the profile form. Later we will add some data inline to simulate pulling data from a database and sending to our form.

Route::get('profile', function(){
   return view('profile'); 
});

Create the profile.blade.php in the resources/views, and add the following code:

@extends('layouts.main')

@section('content')
    <form>
        <div>
            <div>
                <div>
                    <h3 class="text-lg leading-6 font-medium text-gray-900">
                        Personal Information
                    </h3>
                </div>
                <div class="mt-6 sm:mt-5">
                    <div class="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
                        <label for="first_name"
                               class="block text-sm font-medium leading-5 text-gray-700 sm:mt-px sm:pt-2">
                            First name
                        </label>
                        <div class="mt-1 sm:mt-0 sm:col-span-2">
                            <div class="rounded-md shadow-sm">
                                <input id="first_name"
                                       class="bg-white border border-gray-200 py-2 px-3 rounded-lg w-full block w-full transition duration-150 ease-in-out sm:text-sm sm:leading-5"/>
                            </div>
                        </div>
                    </div>
                    <div
                        class="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
                        <label for="last_name"
                               class="block text-sm font-medium leading-5 text-gray-700 sm:mt-px sm:pt-2">
                            Last name
                        </label>
                        <div class="mt-1 sm:mt-0 sm:col-span-2">
                            <div class="rounded-md shadow-sm">
                                <input id="las_name"
                                       class="bg-white border border-gray-200 py-2 px-3 rounded-lg w-full block w-full transition duration-150 ease-in-out sm:text-sm sm:leading-5"/>
                            </div>
                        </div>
                    </div>
                    <div
                        class="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
                        <label for="email" class="block text-sm font-medium leading-5 text-gray-700 sm:mt-px sm:pt-2">
                            Email address
                        </label>
                        <div class="mt-1 sm:mt-0 sm:col-span-2">
                            <div class="mt-1 sm:mt-0 sm:col-span-2">
                                <div class="rounded-md shadow-sm">
                                    <input id="email" type="email"
                                           class="bg-white border border-gray-200 py-2 px-3 rounded-lg w-full block w-full transition duration-150 ease-in-out sm:text-sm sm:leading-5"/>
                                </div>
                            </div>
                        </div>
                    </div>
                    <div
                        class="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
                        <label for="street_address"
                               class="block text-sm font-medium leading-5 text-gray-700 sm:mt-px sm:pt-2">
                            Street address
                        </label>
                        <div class="mt-1 sm:mt-0 sm:col-span-2">
                            <div class="rounded-md shadow-sm">
                                <input id="street_address"
                                       class="bg-white border border-gray-200 py-2 px-3 rounded-lg w-full block w-full transition duration-150 ease-in-out sm:text-sm sm:leading-5"/>
                            </div>
                        </div>
                    </div>
                    <div
                        class="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
                        <label for="city" class="block text-sm font-medium leading-5 text-gray-700 sm:mt-px sm:pt-2">
                            City
                        </label>
                        <div class="mt-1 sm:mt-0 sm:col-span-2">
                            <div class="rounded-md shadow-sm">
                                <input id="city"
                                       class="bg-white border border-gray-200 py-2 px-3 rounded-lg w-full block w-full transition duration-150 ease-in-out sm:text-sm sm:leading-5"/>
                            </div>
                        </div>
                    </div>
                    <div
                        class="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
                        <label for="state" class="block text-sm font-medium leading-5 text-gray-700 sm:mt-px sm:pt-2">
                            State / Province
                        </label>
                        <div class="mt-1 sm:mt-0 sm:col-span-2">
                            <div class="rounded-md shadow-sm">
                                <input id="state"
                                       class="bg-white border border-gray-200 py-2 px-3 rounded-lg w-full block w-full transition duration-150 ease-in-out sm:text-sm sm:leading-5"/>
                            </div>
                        </div>
                    </div>
                    <div
                        class="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
                        <label for="zip" class="block text-sm font-medium leading-5 text-gray-700 sm:mt-px sm:pt-2">
                            ZIP / Postal
                        </label>
                        <div class="mt-1 sm:mt-0 sm:col-span-2">
                            <div class="rounded-md shadow-sm">
                                <input id="zip"
                                       class="bg-white border border-gray-200 py-2 px-3 rounded-lg w-full block w-full transition duration-150 ease-in-out sm:text-sm sm:leading-5"/>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
        <div class="mt-8 border-t border-gray-200 pt-5">
            <div class="flex justify-end">
                <span class="ml-3 inline-flex rounded-md shadow-sm">
                    <button type="submit"
                            class="inline-flex justify-center py-2 px-4 border border-transparent text-sm leading-5 font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-500 focus:outline-none focus:border-indigo-700 focus:shadow-outline-indigo active:bg-indigo-700 transition duration-150 ease-in-out">
                      Save
                    </button>
                 </span>
            </div>
        </div>
    </form>
@endsection

Take a moment and start your Laravel application by running php artisan serve. Navigate to http://127.0.0.1:8000/profile and you should see the following yield:

https://twilio-cms-prod.s3.amazonaws.com/images/_6EoSs7-ZNBxGvhVfUPgmE7sR0yoJ7eoTulmJAx-ikVOT.width-1600.png

Anonymous Blade Components

Anonymous components use Laravel’s new <x-*> tags to render dynamic content within a blade. These components differ from inline components in that they provide a mechanism for managing a component via a single file by utilizing a single view file, and have no associated class.” To add, they are super easy to create and use.

We’ll use anonymous blade components to refactor the profile form fields and create elements we can reuse anywhere in the site.

Begin by creating a components directory in the resources/views folder. Then create a subdirectory called inputs. Our first component will be for our text inputs, so create a text.blade.php file here and paste in the following code:

<div class="rounded-md shadow-sm">
    <input id="first_name"
           class="bg-white border border-gray-200 py-2 px-3 rounded-lg w-full block w-full transition duration-150 ease-in-out sm:text-sm sm:leading-5"/>
</div>

Now in our form markup we can replace all of our text inputs with <x-inputs.text />! However, you’ll notice that our id is hard-coded and this won’t properly support every field defined in the form above.

Props and Attributes

One of the biggest benefits of Laravel’s new anonymous components is their flexibility. Let’s start by adding a property that all of our text inputs should implement. Back in our text component, declare a @props function that takes an array of items that you want to pass into the component. The component now looks like this:

@props([
    'for'
])
<div class="rounded-md shadow-sm">
    <input id="{{ $for }}"
           class="bg-white border border-gray-200 py-2 px-3 rounded-lg w-full block w-full transition duration-150 ease-in-out sm:text-sm sm:leading-5"/>
</div>

With this addition, dynamic properties such as the id we mentioned early can be passed into the generated form. To pass the property into the form, simply add it as an attribute when declaring the component:

<x-inputs.text for="first_name"/>

Try replacing the first name field <input id="first_name" class="bg-white border border-gray-200 py-2 px-3 rounded-lg w-full block w-full transition duration-150 ease-in-out sm:text-sm sm:leading-5"/> with <x-inputs.text for="first_name"/>

NOTE: There may be times when a property may not be required. all we need to do is add a default value for the property like below:

@props([
    'for',
    ‘type’ => 'text'
])

Maybe we also want to add a name, placeholder, or additional classes to override any defaults. This is where $attributes come in handy. All we have to do is add them when declaring the component as follows:

<x-inputs.text for="first_name" placeholder="First name" name="name"/>

And in our input:

@props([
    'for',
    'type' => 'text'
])
<div class="rounded-md shadow-sm">
    <input
        type="{{ $type }}"
        {{ $attributes->merge(['class' => 'bg-white border border-gray-200 py-2 px-3 rounded-lg w-full block w-full transition duration-150 ease-in-out sm:text-sm sm:leading-5']) }}
        id="{{ $for }}"
        />
</div>

Replace the content in each respective file as previously shown.

Notice, we did something a little tricky here. We are adding the $attributes directly onto the input and simultaneously allowing ourselves the flexibility to add classes that will be merged with our default classes. We can also use it like so:

<x-inputs.text for="first_name" placeholder="First name" name="name" class="bg-red-600 text-white placeholder-white"/>

Which renders:
https://twilio-cms-prod.s3.amazonaws.com/images/g12NG78YdAOnkl9JchABIs8Y19ZU0AFGWzxWtbxwdlOK3.width-1600.png

Slots

If you notice, our form still contains quite a bit of “bloat”. We are duplicating the label and encapsulating divs several times throughout the form. We can clean that up by making another component to house this markup and use $slot to render our inputs inside.

Start by making a new component in resources/views/components/inputs called group.blade.php and add the following content:

<div class="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
    <label for="first_name"
           class="block text-sm font-medium leading-5 text-gray-700 sm:mt-px sm:pt-2">
        First name
    </label>
    <div class="mt-1 sm:mt-0 sm:col-span-2">
        {{ $slot }}
    </div>
</div>

Notice the $slot allows us to add whatever content we want in our component, in the specific place that we are yielding it. Back in our form we can replace the entire divs surrounding our input components like so:

<x-inputs.group>
    <x-inputs.text for="first_name" placeholder="First name" name="name"/>
</x-inputs.group>

Adjusting for passing in props to replace hard-coded items we end up with:

@props(['label', 'for'])
<div class="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
    <label for="{{ $for }}"
           class="block text-sm font-medium leading-5 text-gray-700 sm:mt-px sm:pt-2">
        {{ $label }}
    </label>
    <div class="mt-1 sm:mt-0 sm:col-span-2">
        {{ $slot }}
    </div>
</div>

And our final profile form is now:

@extends('layouts.main')

@section('content')
    <form>
        <div>
            <div>
                <div>
                    <h3 class="text-lg leading-6 font-medium text-gray-900">
                        Personal Information
                    </h3>
                </div>
                <div class="space-y-6 sm:mt-5">
                    <x-inputs.group label="First Name" for="first_name">
                        <x-inputs.text for="first_name"/>
                    </x-inputs.group>
                    <x-inputs.group label="Last Name" for="last_name">
                        <x-inputs.text for="last_name"/>
                    </x-inputs.group>
                    <x-inputs.group label="Email address" for="email">
                        <x-inputs.text for="email" type="email"/>
                    </x-inputs.group>
                    <x-inputs.group label="Street address" for="street_address">
                        <x-inputs.text for="street_address"/>
                    </x-inputs.group>
                    <x-inputs.group label="City" for="city">
                        <x-inputs.text for="city"/>
                    </x-inputs.group>
                    <x-inputs.group label="State" for="state">
                        <x-inputs.text for="state"/>
                    </x-inputs.group>
                    <x-inputs.group label="Zip" for="zip">
                        <x-inputs.text for="zip"/>
                    </x-inputs.group>
                </div>
            </div>
        </div>
        <div class="mt-8 border-t border-gray-200 pt-5">
            <div class="flex justify-end">
                <span class="ml-3 inline-flex rounded-md shadow-sm">
                    <button type="submit"
                            class="inline-flex justify-center py-2 px-4 border border-transparent text-sm leading-5 font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-500 focus:outline-none focus:border-indigo-700 focus:shadow-outline-indigo active:bg-indigo-700 transition duration-150 ease-in-out">
                      Save
                    </button>
                 </span>
            </div>
        </div>
    </form>
@endsection

Much cleaner!

NOTE: On the div surrounding all of the components I changed the class ‘mt-6- to ‘space-y-6’ which evenly spaces out our elements vertically.

Passing In Default Data

To wrap up, jump back over to web.php and change your function that renders the view to the following. This will simulate retrieving some of the data from your database and passing it to the view:

Route::get('profile', function(){
    $data = [
        'first_name' => 'Shane',
        'last_name' => 'Rosenthal',
        'email' => '[email protected]',
    ];
   return view('profile', compact('data'));
});

Obviously, the data is not bound to our input fields. We can handle that by passing a new prop into our inputs with a default value in text.blade.php:

@props([
    'for',
    'type' => 'text',
    'value' => ''
])
<div class="rounded-md shadow-sm">
    <input
        type="{{ $type }}"
        value="{{ $value }}"
        {{ $attributes->merge(['class' => 'bg-white border border-gray-200 py-2 px-3 rounded-lg w-full block w-full transition duration-150 ease-in-out sm:text-sm sm:leading-5']) }}
        id="{{ $for }}"
        />
</div>

However, when passing this data into our input field we end up with a less than desirable effect:

<x-inputs.text for="first_name" value="$data['first_name']"/>

https://twilio-cms-prod.s3.amazonaws.com/images/1bz9J_bzMrWl-bgey2Js9Y6aq2ch0rAXXoNtM1ojDF8Qv.width-1600.png

Thankfully, Laravel provides us with an easy and likely familiar syntax to correct this. If you have used a JavaScript framework like Vue.js in the past, then this next step will feel familiar. Simply add a colon before any prop that should render PHP code:

<x-inputs.text for="first_name" :value="$data['first_name']"/>

Add the remaining values as defined in the route to complete testing.
https://twilio-cms-prod.s3.amazonaws.com/images/w87LcUyGquKa-hK4f5YcjOqpOKt-a7l4ugz794rZWisL1.width-1600.png

Next Steps

Hopefully, you can see just how easy and flexible the anonymous blade components are with Laravel 7. This was a simple example of how to extract a reusable form input, but you can use these principles for any html component your site needs. In fact, recently Taylor Otwell, the creator of Laravel, tweeted an example of a form component that will conditionally allow you to accept file uploads. What components will you add to your projects? Leave some comments below and continue the conversation!

Shane D. Rosenthal is an AWS Certified and experienced full-stack developer that has worked with Laravel since 2013. He is open for consultation regarding Laravel, Vue.js applications, and AWS architecture