In this tutorial, we will create a dashboard that will automatically refresh every 5 seconds to show the next piece of data, with an example of sports leagues tables. We will use Livewire feature polling for this.
The main point is to show the first page of the data, then jump to the second page after 5 seconds, then to the third, and so on, and then back to the first one. Similar to how they show sports tables on TV in the news.
To make it more realistic, we will show results from four English Football Leagues with random points for the teams.
First, we show teams no.1-5 for every league. Then it refreshes to 6-10th teams, then to 11-15th, then to 16-20th, and then back to 1-5.
For this tutorial, we will need two Models: League
and Team
:
php artisan make:model League -mphp artisan make:model Team -m
database/migrations/xxxx_create_leagues_table.php:
public function up(): void{ Schema::create('leagues', function (Blueprint $table) { $table->id(); $table->string('name'); $table->timestamps(); });}
database/migrations/xxxx_create_teams_table.php:
public function up(): void{ Schema::create('teams', function (Blueprint $table) { $table->id(); $table->string('name'); $table->integer('points'); $table->foreignId('league_id')->constrained(); $table->timestamps(); });}
app/Models/League.php:
use Illuminate\Database\Eloquent\Relations\HasMany; class League extends Model{ protected $fillable = [ 'name', ]; public function teams(): HasMany { return $this->hasMany(Team::class); }}
app/Models/Team.php:
class Team extends Model{ protected $fillable = [ 'name', 'points', 'league_id', ];}
For this example, we will use one of the themes from our package Larastarters:
composer require laraveldaily/larastarters --devphp artisan larastarters:install
I chose the Laravel Breeze (Tailwind)
option and the Windmill theme, but you can use whichever you like.
We need to add Livewire and create a component.
composer require livewire/livewire
resources/views/layouts/app.blade.php:
<!DOCTYPE html><html x-data="data" lang="en"><head> // ... @vite(['resources/css/app.css', 'resources/js/app.js']) @livewireStyles <!-- Scripts --> <script src="{{ asset('js/init-alpine.js') }}"></script></head><body> // ... @livewireScripts </body></html>
For the Livewire Component, let's simply name it Dashboard
.
php artisan make:livewire Dashboard
resources/views/dashboard.blade.php:
<x-app-layout> <x-slot name="header"> {{ __('Dashboard') }} </x-slot> <div class="p-4 bg-white rounded-lg shadow-xs"> {{ __('You are logged in!') }} @livewire('dashboard') </div></x-app-layout>
We will build the dynamic functionality step by step.
The first milestone objective is to show all the teams in the tables.
When the Component gets mounted, first, we need to assign the default values in the mount()
method. We need to get all the leagues, and we will pluck
it to have a result as ID => name
. For the teams, first, we will make an empty Collection.
app/Http/Livewire/Dashboard.php:
use App\Models\League;use App\Models\Team;use Illuminate\Support\Collection;use Illuminate\Contracts\View\View; class Dashboard extends Component{ public Collection $teams; public Collection $leagues; public function mount(): void { $this->leagues = League::pluck('name', 'id'); $this->teams = collect(); } public function render(): View { return view('livewire.dashboard'); }}
Next, to get the data for the $teams
, we will create a separate method called getLeagueResults()
because we will call it in a few places. For starters, we call it at the end of the mount()
method.
app/Http/Livewire/Dashboard.php:
class Dashboard extends Component{ public Collection $teams; public Collection $leagues; public function mount(): void { $this->leagues = League::pluck('name', 'id'); $this->teams = collect(); $this->getLeagueResults(); } // ... private function getLeagueResults(): void { foreach ($this->leagues as $leagueId => $league) { $this->teams[$leagueId] = Team::where('league_id', $leagueId) ->orderByDesc('points') ->get(); } } }
Now let's show them in the table. In the Livewire component, the public property $this->teams
can be accessible in the Blade file automatically.
resources/views/livewire/dashboard.blade.php:
<div> <div class="grid gap-6 mb-8 md:grid-cols-4"> @foreach ($teams as $leagueId => $leagueTeams) <div class="p-1 bg-white rounded-lg shadow-xs"> <h2 class="text-2xl mb-4">{{ $leagues[$leagueId] }}</h2> <div class="overflow-hidden mb-8 w-full rounded-lg border shadow-xs"> <div class="overflow-x-auto w-full"> <table class="w-full whitespace-no-wrap"> <thead> <tr class="text-xs font-semibold tracking-wide text-left text-gray-500 uppercase bg-gray-50 border-b"> <th class="px-4 py-3 w-3/4">Team</th> <th class="px-4 py-3 text-center">Points</th> </tr> </thead> <tbody class="bg-white divide-y"> @foreach($leagueTeams as $team) <tr class="text-gray-700"> <td class="px-4 py-3 text-sm"> {{ $loop->iteration }}. {{ $team->name }} </td> <td class="px-4 py-3 text-sm text-center"> {{ $team->points }} </td> </tr> @endforeach </tbody> </table> </div> </div> </div> @endforeach </div></div>
As you can see, we also use $leagues[$leagueId]
from the public property $this->leagues
to get and show the league's name.
For now, we should see a complete list of teams for every league.
Now that we can view all the teams let's add pagination to them and update the table after 5s. In this example, we won't directly use Laravel or Livewire built-in pagination but instead build the queries manually, as we don't need pagination links in this example.
So, first, the public properties. We will need four of them. I guess their names are self-explanatory:
$teamsPerPage
$totalPages
$currentPage
$currentOffset
app/Http/Livewire/Dashboard.php:
class Dashboard extends Component{ public Collection $teams; public Collection $leagues; public int $teamsPerPage = 5; public int $totalPages = 0; public int $currentPage = 1; public int $currentOffset = 0; // ...}
Next, in the mount()
method, we need to calculate how many total pages we will have.
app/Http/Livewire/Dashboard.php:
class Dashboard extends Component{ // ... public function mount(): void { $this->leagues = League::pluck('name', 'id'); $this->teams = collect(); $maxTeamsPerLeague = League::withCount('teams') ->orderBy('teams_count', 'desc') ->value('teams_count'); $this->totalPages = ceil($maxTeamsPerLeague / $this->teamsPerPage); $this->getLeagueResults(); } // ...}
And when setting the $teams
property, we need to take records for how many are set in the $teamsPerPage
property and offset with the $currentOffset
property.
app/Http/Livewire/Dashboard.php:
class Dashboard extends Component{ // ... private function getLeagueResults(): void { foreach ($this->leagues as $leagueId => $league) { $this->teams[$leagueId] = Team::where('league_id', $leagueId) ->orderByDesc('points') ->take($this->teamsPerPage) ->offset($this->currentOffset) ->get(); } }}
Now we should see a table with only five teams per league.
And lastly, let's add polling for auto-refresh. First, we will add the wire:poll
directive, set the frequency to 5s
, and call a specific method called update
.
resources/views/livewire/dashboard.blade.php:
<div> <div wire:poll.5s="update"> // ...</div>
Now we can add the logic for the polling, creating that method.
app/Http/Livewire/Dashboard.php:
class Dashboard extends Component{ // ... public function update(): void { $this->currentPage++; if ($this->currentPage > $this->totalPages) { $this->currentPage = 1; } $this->currentOffset = ($this->currentPage - 1) * $this->teamsPerPage; $this->getLeagueResults(); } // ...}
What are we doing here? First, increasing the currentPage
. Then we need to check if the currentPage
isn't bigger than there are total pages. Otherwise, it will break the dashboard. If we show the last page, we go to the first page again.
Next, we must calculate the currentOffset
and re-fetch the team results.
Finally, we need to add the offset to the number of the team to show the correct place.
resources/views/livewire/dashboard.blade.php:
// ...@foreach($leagueTeams as $team) <tr class="text-gray-700"> <td class="px-4 py-3 text-sm"> {{ $loop->iteration }}. {{ $currentOffset + $loop->iteration }}. {{ $team->name }} </td> <td class="px-4 py-3 text-sm text-center"> {{ $team->points }} </td> </tr>@endforeach // ...
Perfect! Now we have the result as expected.