Building a Laravel Blade Table Component With Alpine.js

Building a Laravel Blade Table Component With Alpine.js

Written by Mithicher Baro on Oct 5th, 2021 Views Report Post

If you’re building an admin panel, chances are that you will need to display the data in some tabular format. Writing every time table, tr, td is a little bit cumbersome, when we need our work to be done quickly.

Table UIs are very common in web development to organize complex data.

In this blog post, we’ll see how we can build a simple reusable table component with Laravel Bade Components, AlpineJs and TailwindCSS.

I'm assuming you're familiar with AlpineJS, Laravel Blade Components, and TailwindCSS. If you haven't tried AlpineJS and TailwindCSS you should definitely try it, it's awesome.

Here are a few resources to get you started:

Let's take a look at how our blade component will look in its most basic form.

<x-table-simple

  :columns='[
    [
      "name" => "Name",
      "field" => "name",
      "columnClasses" => "", // classes to style table th      
      "rowClasses" => "" // classes to style table td
    ],
    [
      "name" => "Email",
      "field" => "email",
      "columnClasses" => "",      
      "rowClasses" => ""
    ]
  ]'

  :rows='[
    [
      "name" => "Thor",
      "email" => "[email protected]"
    ],
    [
      "name" => "Loki",
      "email" => "[email protected]"
    ]
  ]'

/>

Columns

The :columns array will define the headings of the table as well as the key of the rows data to display.

Rows

The :rows is an associative array of data to display as rows in a table data.

Assuming you have already configured alpine.js and tailwind css in your app.blade.php let us a create a blade component named table-simple.blade.php inside your resources/views/components folder.

You can find the full component gists here:

https://gist.github.com/mithicher/3a557361b8576c09c14e0995f1ce4022

The final component is as follows:

@props([
	'rows' => [],
	'columns' => [],
	'striped' => false,
	'actionText' => 'Action',
	'tableTextLinkLabel' => 'Link',
])

<div 
	x-data="{
		columns: {{ collect($columns) }},
		rows: {{ collect($rows) }},
		isStriped: Boolean({{ $striped }})
	}"
	x-cloak
>
	<div class="mb-5 overflow-x-auto bg-white rounded-lg shadow overflow-y-auto relative">          
		<table class="border-collapse table-auto w-full whitespace-no-wrap bg-white table-striped relative">
			<thead>
				<tr class="text-left">
					@isset($tableColumns)
						{{ $tableColumns }}
					@else	 
						@isset($tableTextLink)
							<th class="bg-gray-50 sticky top-0 border-b border-gray-100 px-6 py-3 text-gray-500 font-bold tracking-wider uppercase text-xs truncate">
								{{ $tableTextLinkLabel }}
							</th>
						@endisset

						<template x-for="column in columns">
							<th 
								:class="`${column.columnClasses}`"
								class="bg-gray-50 sticky top-0 border-b border-gray-100 px-6 py-3 text-gray-500 font-bold tracking-wider uppercase text-xs truncate" 
								x-text="column.name"></th>
						</template>

						@isset($tableActions)
							<th class="bg-gray-50 sticky top-0 border-b border-gray-100 px-6 py-3 text-gray-500 font-bold tracking-wider uppercase text-xs truncate">{{ $actionText }}</th>
						@endisset
					@endisset
				</tr>
			</thead>
			<tbody>

				<template x-if="rows.length === 0">
					@isset($empty)
						{{ $empty }}
					@else
						<tr>
							<td colspan="100%" class="text-center py-10 px-4 py-1 text-sm">
								No records found
							</td>
						</tr>
					@endisset
				</template>

				<template x-for="(row, rowIndex) in rows" :key="'row-' +rowIndex">
					<tr :class="{'bg-gray-50': isStriped === true && ((rowIndex+1) % 2 === 0) }">
						@isset($tableRows)
							{{ $tableRows }}
						@else
							@isset($tableTextLink)
								<td
									class="text-gray-600 px-6 py-3 border-t border-gray-100 whitespace-nowrap">
									{{ $tableTextLink }}
								</td>
							@endisset

							<template x-for="(column, columnIndex) in columns" :key="'column-' + columnIndex">
								<td 
									:class="`${column.rowClasses}`"
									class="text-gray-600 px-6 py-3 border-t border-gray-100 whitespace-nowrap">
									<div x-text="`${row[column.field]}`" class="truncate"></div>
								</td>
							</template>

							@isset($tableActions)
								<td
									class="text-gray-600 px-6 py-3 border-t border-gray-100 whitespace-nowrap">
									{{ $tableActions }}
								</td>
							@endisset
						@endisset
					</tr>
				</template>

			</tbody>
		</table>
	</div>
</div>

Let's have a look at how it works.

@props([
	'rows' => [],
	'columns' => [],
	'striped' => false,
	'actionText' => 'Action',
	'tableTextLinkLabel' => 'Link',
])

<div 
	x-data="{
		columns: {{ collect($columns) }},
		rows: {{ collect($rows) }}
	}"
	x-cloak
>

  ....

</div>

First we are getting the props value passed in our blade commpoents via :rows and :columns then define an x-data along with some initial values.

Creating Table Columns

We loop through columns data to generate our table headers

  // rest of the code

<thead>
    <tr class="text-left">
      {{-- Custom slots for customization --}}
      @isset($tableColumns)
        {{ $tableColumns }}
      @else	 
        <template x-for="column in columns">
          <th 
            :class="`${column.columnClasses}`"
            class="bg-gray-50 sticky top-0 border-b border-gray-100 px-6 py-3 text-gray-500 font-bold tracking-wider uppercase text-xs truncate" 
            x-text="column.name"></th>
        </template>
      @endisset
    </tr>
 </thead>

  // rest of the code

As stated in the alpinejs.dev website:

Alpine's x-for directive allows you to create DOM elements by iterating through a list.

Creating table rows

  // rest of the code

	<thead>
    <tr class="text-left">
      {{-- Custom slots for customization --}}
      @isset($tableColumns)
        {{ $tableColumns }}
      @else	 
        <template x-for="column in columns">
          <th 
            :class="`${column.columnClasses}`"
            class="bg-gray-50 sticky top-0 border-b border-gray-100 px-6 py-3 text-gray-500 font-bold tracking-wider uppercase text-xs truncate" 
            x-text="column.label"></th>
        </template>
      @endisset
    </tr>
  </thead>

  // rest of the code

As stated in the alpinejs.dev website:

Alpine's x-for directive allows you to create DOM elements by iterating through a list.

Creating table rows

  // rest of the code

  <tbody>

    <template x-if="rows.length === 0">

      @isset($empty)
        {{ $empty }}
      @else
        <tr>
          <td colspan="100%" class="text-center py-10 px-4 py-1 text-sm">
            No records found
          </td>
        </tr>
      @endisset
    </template>

    <template x-for="(row, rowIndex) in rows" :key="'row-' +rowIndex">
      <tr :class="{'bg-gray-50': isStriped === true && ((rowIndex+1) % 2 === 0) }">
        {{-- Custom slots for all rows customization --}}
        @isset($tableRows)
          {{ $tableRows }}
        @else

          {{-- Custom slots to display a link text --}}
          @isset($tableTextLink)
            <td
              class="text-gray-600 px-6 py-3 border-t border-gray-100 whitespace-nowrap">
              {{ $tableTextLink }}
            </td>
          @endisset

          <template x-for="(column, columnIndex) in columns" :key="'column-' + columnIndex">
            <td 
              :class="`${column.rowClasses}`"
              class="text-gray-600 px-6 py-3 border-t border-gray-100 whitespace-nowrap">
              <div x-text="`${row[column.field]}`" class="truncate"></div>
            </td>
          </template>

          {{-- Custom slots for action links --}}
          @isset($tableActions)
            <td
              class="text-gray-600 px-6 py-3 border-t border-gray-100 whitespace-nowrap">
              {{ $tableActions }}
            </td>
          @endisset
        @endisset
      </tr>
    </template>

  </tbody>

// rest of the code

It is to be noted that all our custom name slots will receive data in JavaScript with object key row.

Custom Actions

Till now our blade components display data based on the array given via props. But if we want to display some edit and delete action links we can leverage the power of laravel blade name slots.

<x-table-simple

  :columns='[
    [
      "name" => "Name",
      "field" => "name",
      "columnClasses" => "", // classes to style table th      
      "rowClasses" => "" // classes to style table td
    ],
    [
      "name" => "Email",
      "field" => "email",
      "columnClasses" => "",      
      "rowClasses" => ""
    ]
  ]'

  :rows='[
    [
      "name" => "Thor",
      "email" => "[email protected]"
    ],
    [
      "name" => "Loki",
      "email" => "[email protected]"
    ]
  ]'
>

  {{-- "tableActions" is defined in our component --}}  
  {{-- custom name slots will receive data in JavaScript with object key "row" --}}
  <x-slot name="tableActions">
    <div class="flex flex-wrap space-x-4">
      <a :href="`users/${row.id}/show`" class="text-blue-500 underline">Edit</a>
      <a :href="`users/${row.id}/delete`" class="text-red-500 underline">Delete</a>
    </div>
  </x-slot>


</x-table-simple>

Here are some resources that you should definitely check it out:

Check out my previous blog post on:

https://devdojo.com/mithicher/create-a-meilisearch-laravel-blade-component

Conclusion

There we have it, a simple reusable laravel blade table component. Feel free to follow me on twitter @mithicher.

That's all for now. See you later!

Buy me a coffee

Comments (1)