DEV Community

Daniel Newns for Jump24

Posted on

Laravel Blade Components

Laravel Blade is a powerful templating engine that allows developers to create dynamic and reusable views in a Laravel application. One of the key features of Blade is the ability to create reusable and composable components, which can help speed up front-end development. By enabling the creation of reusable components that provide consistent styles and behaviour, developers can avoid the need to construct elements from scratch. Instead, they can simply make use of the components that already exist.

In this article we will create a basic form that shows you some benefits and techniques of blade components.

Creating your first component

There are two types of components: class-based and anonymous. Class-based components have a class and a view template, while anonymous components only have a view template. In most cases, anonymous components are sufficient, and I tend to use class-based components only when I need to use dependency injection.

php artisan make:component layouts.app --view
Enter fullscreen mode Exit fullscreen mode

Running the above command will create our first component, a layout file named resources/views/components/layouts/app.blade.php. This file will be the layout of the app.

<!-- /resources/views/components/layouts/app.blade.php -->

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <title>Laravel Blade Components</title>

    <script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
    {{ $slot }}
</body>
</html>

<!-- /resources/views/example.blade.php -->

<x-layouts.app>
    Hello World!
</x-layouts.app>
Enter fullscreen mode Exit fullscreen mode

{{ $slot }} will render whatever we pass to it. For example, passing Hello World! will render it on the page. Slots are what make blade components composable. We can pass components to other components and even use named slots!

Create a form

Run the following to create our anonymous index component

php artisan make:component form.index --view
Enter fullscreen mode Exit fullscreen mode

Normally to group components together you would have to have a form component then have the rest of the forms components inside a folder.

/resources/views/components/form.blade.php
/resources/views/components/form/group.blade.php
Enter fullscreen mode Exit fullscreen mode

Anonymous index components allow you to group your components together.

/resources/views/components/form/index.blade.php
/resources/views/components/form/group.blade.php
Enter fullscreen mode Exit fullscreen mode

Usage would look like

<x-layouts.app>
    <x-form>
        <x-form.group>
            Hello!
        </xform.group>
    </x-form>
</x-layouts.app>
Enter fullscreen mode Exit fullscreen mode
<!-- /resources/views/components/form/index.blade.php -->

@props([
    'method' => 'POST',
    'action',
    'hasFiles' => false,
])

<form
    method="{{ $method !== 'GET' ? 'POST' : 'GET' }}"
    action="{{ $action }}"
    {!! $hasFiles ? 'enctype="multipart/form-data"' : '' !!}
    {{ $attributes->except(['method', 'action']) }}
>
    @csrf
    @method($method)

    {{ $slot }}
</form>
Enter fullscreen mode Exit fullscreen mode

We define the props we want the component to accept. We are able to set some sensible defaults. Most forms have the method as POST so we set that as default. When forms are sending files we need to have the enctype attribute. Instead of typing it out every time we make the component add this attribute on when we need it.

We want to make sure the developer passes an action when using this component so we set action as a prop but set no default value. When no action is passed Laravel will throw an error if we do not do an isset() check.

Echoing out $attributes allows us to pass any additional attributes we need without having to define them in the props.

<x-form action="/users"></x-form>
<x-form :action="route('users.store')"></x-form>

<x-form method="GET"></x-form>

<x-form has-files></x-form>

<x-form class="border border-red-200"></x-form>

<x-form 
    :action="route('photos.store')"
    has-files
    class="p-5"
>
    //
</x-form>
Enter fullscreen mode Exit fullscreen mode

In the above example we use the :action attribute. We use the : prefix to denote that it is a PHP expression or variable. We could also do :action="$someRoute". If it is a hardcoded or primitive value we do action="/" like normal.

Create text input

When working with inputs I like to create a group component that can take the input component. This group will display the label, help text and any errors.

php artisan make:component input.group --view
Enter fullscreen mode Exit fullscreen mode

Here we are setting a default class using $attributes->class(['block']). If we were to pass the class attribute any classes would be merged with block.

We use @class() to conditionally set classes. text-gray-700 inline-block mb-1 is always displayed, text-red-500 is only merged into the default when $error is present.

If we don’t pass a prop in then it is not defined, that is why we use @isset($help) and @isset($error) to check if they are set. We could also set the props to have null as their default value and check for that.

<!-- /resources/views/components/input/group.blade.php -->

@props([
    'label',
    'for',
    'help',
    'error',
])

<label
    {{ $attributes }}
    {{ $attributes->class(['block']) }}
    for="{{ $for }}"
>
    <span
        @class([
            'text-gray-700 inline-block mb-1',
            'text-red-500' => isset($error)
        ])
    >{{ $label ?? '' }}</span>
    <div class="mt-1">
        {{ $slot }}
    </div>
    @isset($help)
        <p class="mt-2 text-sm text-gray-500" id="{{ $for }}">{{ $help }}</p>
    @endif
    @isset($error)
        <div class="mt-1 text-red-500 text-sm">{{ $error }}</div>
    @endif
</label>
Enter fullscreen mode Exit fullscreen mode

Next we will create a text input

php artisan make:component input.text --view
Enter fullscreen mode Exit fullscreen mode

This component uses @aware(). This allows the child component to access data in the parent component.

<!-- /resources/views/components/input/text.blade.php -->

@aware([
    'error',
])

@props([
    'value',
    'name',
    'for',
])

<input
    {{ $attributes->class([
        'shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md',
        'border-red-500' => $error
    ]) }}
    @isset($name) name="{{ $name }}" @endif
    type="text"
    @isset($value) value="{{ $value }}" @endif
    {{ $attributes }}
/>
Enter fullscreen mode Exit fullscreen mode

Create button

Finally we can create a button to submit our form. We will use an anonymous index component again. This is because I don’t want to access the button directly, instead I want to use this component inside primary, secondary buttons.

php artisan make:component button.index --view

<!-- /resources/views/components/button/index.blade.php -->

@aware([
    'type',
])

<button
    type="{{ $type }}"
    {{ $attributes->class([
        'inline-flex items-center border font-medium rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2',
    ]) }}
>
    {{ $slot }}
</button>
Enter fullscreen mode Exit fullscreen mode

Next we create our primary button. You can see that we are using the x-button inside our primary component.

php artisan make:component button.primary --view
Enter fullscreen mode Exit fullscreen mode
<!-- /resources/views/components/button/primary.blade.php -->

@props([
    'type' => 'button',
])

<x-button
    {{ $attributes->merge(['class' => 'border-transparent shadow-sm text-white bg-indigo-600 hover:bg-indigo-700']) }}>
    {{ $slot }}
</x-button>
Enter fullscreen mode Exit fullscreen mode

Separating the x-button component means that we can re-use it in multiple places, even using it on its own.

Putting it all together

Now we have created our components we can start to use them together. We first use our form component to wrap all our inputs, in this instance we only need to pass the route as the action. Next we have a couple of input groups, these take a label, for and an error. The error comes from the validation error bag that is available to all views. Passed to the input groups slot is the input. In this example we are using automatic attributes to pass along the old value to the value attribute. Lastly we use our button to submit the form.

<x-form :action="route('users.store')">
    <x-input.group label="Email" for="email" :error="$errors->first('email')">
        <x-input.email name="email" :value="old('email')" />
    </x-input.group>
    <x-input.group label="Password" for="password" :error="$errors->first('password')">
        <x-input.password name="password" :value="old('password')" />
    </x-input.group>
    <x-button.primary>
      Submit
    </x-button.primary>
</x-form>
Enter fullscreen mode Exit fullscreen mode

We can mix blade components with plain HTML as well.

<x-form action="/photo" has-files>
    <div class="p-3 border border-slate-200">
      <x-input.group label="Password" for="photo" :error="$errors->first('photo')">
          <input type="file" name="photo" />
      </x-input.group>
      <button>Submit</button>
    </div>
</x-form>
Enter fullscreen mode Exit fullscreen mode

Bonus Tips

Short Attribute Syntax

{{-- Short attribute syntax... --}}
<x-input.text $name />

{{-- Is equivalent to... --}}
<x-input.text name="{{ $name }}" />
Enter fullscreen mode Exit fullscreen mode

Named Slots - You can have multiple slots in a component by giving them names.

<x-modal>
    <x-slot:header>
      Hello World!
    </x-slot>

    Some lovely content

    <x-slot:footer>
      Goodbye World!
    </x-slot>
</x-modal>
Enter fullscreen mode Exit fullscreen mode

You’d achieve this by doing the following

<!-- /resources/views/components/modal/index.blade.php -->

<div>
  <header>
    {{ $header }}
  </header>
  <main>
    {{ $slot }}
  </main>
  <footer>
    {{ $footer }}
  </footer>
</div>
Enter fullscreen mode Exit fullscreen mode

Top comments (2)

Collapse
 
tuandoan193 profile image
TuanDoan193

It is very clear. Thank you very much!

Collapse
 
ksvanquy profile image
QuyDev

thank you.