Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Laravel Livewire Pagination

Laravel - 8.6.0 (Jetstream), Livewire - 2.2.7

Among other stuff, I have two simple tables with search input rendered with Livewire. Both Livewire components have almost the same logic, the main difference is DB query format. Everything is working perfectly in one table (search, pagination), but in the second table, there's a problem with pagination.

What's wrong? When I click on the pagination link (eg. from first-page go to page(2)), the request is sent, the URL in the browser changes to a proper query string, a response is received, but there's no HTML part in the response, there's no change in the DOM and pagination shows the first page is active. If I manually refresh the page, the table shows data for page 2, and after that, I can even get new data for the next or previous page, but only once after the initial page refresh. If I click on pagination links again, no HTML data in response, no DOM change ...

What have I tried?

  1. I've gone through the Livewire documentation, tried all suggested solutions from the "troubleshooting" section.
  2. I've checked my code multiple times, compared both Livewire components and Blade views, haven't been able to find some obvious mistake.
  3. For the past 10 days, I've been searching for the answer on SO, Livewire forum, Laracast forum, ... found some good suggestions, tried most of them, but none of them solves my issue.
  4. Eliminated the possibility that logic for DB query causes some issues, problem exits even if I don't use complex record filtering (eg. if I just paginate all records - Model::all()->paginate(10)).
  5. Problematic table has some additional record filtering via radio buttons, I've temporarily removed that, but the problem remains.

Code for the working Livewire component and Blade view:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;

class Extension extends Model
{
    use HasFactory;
    protected $guarded = [];

    public function user()
    {
        return $this->belongsTo('App\Models\User', 'directory', 'ext');
    }

    public static function search($query)
    {
        return empty($query) ? static::query()
            : static::where('directory', 'like', '%' . $query . '%')
            ->orWhere('mobile', 'like', '%' . $query . '%')
            ->orWhere('department', 'ilike', '%' . $query . '%')
            ->orWhere('name1', 'ilike', '%' . $query . '%')
            ->orWhere('name2', 'ilike', '%' . $query . '%');
    }
}
<?php

namespace App\Http\Livewire;

use Livewire\Component;
use Livewire\WithPagination;

class PhonebookTable extends Component
{
    use WithPagination;

    public $perPage = 10;
    public $search = '';

    public function render()
    {
       return view('phonebook.phonebook-table', [
          'phonebook' => \App\Models\Extension::search($this->search)
              ->with('user')
              ->orderBy('directory', 'asc')
              ->paginate(10)
            ]);            
        }
    }

    /**
     *  Livewire Lifecycle Hook
     */
    public function updatingSearch(): void
    {
        $this->resetPage();
    }
}
<div>
    <div class="flex items-center justify-between">
        {{-- Search Input --}}
        <div class="relative text-gray-700 focus-within:text-gray-600 m-2">
            <span class="absolute inset-y-0 left-0 flex items-center pl-2">
                <button type="submit" class="p-1 focus:outline-none focus:shadow-outline">
                    <svg fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
                        stroke-width="2" viewBox="0 0 24 24" class="w-6 h-6">
                        <path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
                    </svg>
                </button>
            </span>
            <input wire:model="search" type="search" name="q"
                class="py-2 text-sm text-gray 
                bg-gray-100 rounded-md pl-10 focus:outline-none 
                focus:bg-gray-200 focus:text-gray-700"
                placeholder="{{ __('Search...') }}" 
                autocomplete="off">
        </div>
        {{-- End Of Search Input --}}

        {{-- PerPage Select Dropdown --}}
        <div>
            <span>{{ __('Per page:') }}</span>
            <div class="inline-block relative mr-2">
                <select wire:model="perPage" class="block appearance-none w-full
                    bg-white border border-gray-400 hover:border-gray-500 px-4 py-2
                    pr-8 rounded shadow leading-tight focus:outline-none 
                    focus:shadow-outline">
                    <option>10</option>
                    <option>25</option>
                    <option>50</option>
                </select>
                <div class="pointer-events-none absolute 
                    inset-y-0 right-0 flex items-center px-2 text-gray-700">
                    <svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" 
                        viewBox="0 0 20 20">
                        <path d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757
                            6.586 4.343 8z" />
                    </svg>
                </div>
            </div>
        </div>
        {{-- End Of PerPage Select Dropdown --}}
    </div>

    {{-- Phonebook Table --}}
    <div class="container p-2">
        <table class="text-center w-full">
            <thead class="bg-gray-400 flex text-white w-full item-center rounded-md">
                <tr class="flex w-full py-2">
                    <th class="px-4 w-1/4">{{ __('Name1') }}</th>
                    <th class="px-4 w-1/4">{{ __('Name2') }}</th>
                    <th class="px-4 w-1/4">{{ __('Department') }}</th>
                    <th class="px-4 w-1/4">{{ __('Extension') }}</th>
                    <th class="px-4 w-1/4">{{ __('Mobile') }}</th>
                </tr>
            </thead>
            <tbody class="">
                @foreach ($phonebook as $directory)
                <tr class="flex w-full py-2 hover:bg-gray-100">
                    <td class="px-4 w-1/4">{{ $directory->name1??$directory->name }}
                    </td>
                    <td class="px-4 w-1/4">{{ $directory->name2 }}</td>
                    <td class="px-4 w-1/4">{{ $directory->user->department??'' }}</td>
                    <td class="px-4 w-1/4">{{ $directory->directory }}</td>
                    <td class="px-4 w-1/4">{{ $directory->user->mobile??'' }}</td>
                </tr>
                @endforeach
            </tbody>
        </table>
    </div>
    {{-- End Of Phonebook Table --}}

    {{-- Pagination and Records Count info --}}
    <div class="p-2 bg-gray-200">
        {{ $phonebook->links('vendor.pagination.livewire-tailwind') }}
    </div>
    {{-- End Of Pagination and Records Count info --}}
</div>

Code for the NON-working Livewire component and Blade view:

<?php

namespace App\Models;

use Illuminate\Support\Facades\Auth;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Livewire\WithPagination;

class Phonecall extends Model
{
    use HasFactory;
    use WithPagination;

    protected $table = "md_phonecall";

    // custom method for "live" searching from livewire component
    public static function search($query)
    {
        return empty($query) ? static::query() : static::where('dialednumber', 'like', '%' . $query . '%')
            ->orWhere('startdate', 'like', $query)
            ->orWhere('starttime', 'like', $query)
            ->orWhere('chargednumber', 'like', '%' . $query . '%')
            ->orWhere('conditioncode', 'ilike', '%' . $query . '%');
    }

    // 'conditioncode' accessor for casting integer value from DB to String shown in table View
    public function getConditioncodeAttribute($value)
    {
        switch ($value) {
            case 7;
                return 'Incoming call (' . $value . ')';
            case 8;
                return 'Internal call (' . $value . ')';
            case 10;
                return 'Outgoing call (' . $value . ')';
            case 15;
                return 'Transfer call (' . $value . ')';
            case 19;
                return 'Outgoing call (' . $value . ')';
            case 23;
                return 'Missed call (' . $value . ')';
            case 25;
                return 'Internal call (' . $value . ')';
            case 28;
                return 'Outgoing call (' . $value . ')';
            case 29;
                return 'Outgoing call (' . $value . ')';
            default;
                return 'Unknown type (' . $value . ')';
        }
    }

    // Local Scope for filtering only calls belonging to logged User
    public function scopeMycalls($query)
    {
        return $query->where('chargednumber', '=', Auth::user()->ext)->orWhere('dialednumber', '=', Auth::user()->ext);
    }

    /**
     * Scope a query to only include phone calls of a given type.
     *
     * @param  \Illuminate\Database\Eloquent\Builder  $query
     * @param  mixed  $type
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function scopeOfType($query, $type)
    {
        if ($type === 'all') return $query->where('conditioncode', 'like', '%');
        if ($type === 'incoming') return $query->where('conditioncode', '=', 7);
        if ($type === 'outgoing') return $query->where('conditioncode', '=', 10)->orWhere('conditioncode', '=', 19)->orWhere('conditioncode', '=', 28);
        if ($type === 'missed') return $query->where('conditioncode', '=', 23);
        if ($type === 'internal') return $query->where('conditioncode', '=', 8)->orWhere('conditioncode', '=', 15)->orWhere('conditioncode', '=', 25);
    }

    public function scopePeriod($query, $from, $to)
    {
        return $query->whereBetween('startdate', [$from, $to]);
    }

    public function scopeOfExtension($query, $extension)
    {
        return $query->where('dialednumber', 'like', '%' . $extension . '%')
            ->orWhere('chargednumber', 'like', '%' . $extension . "%");
    }
}
<?php

namespace App\Http\Livewire;

use Livewire\Component;
use Livewire\WithPagination;

class MyPhonecallsTable extends Component
{
    use WithPagination;

    public $perPage = 10;
    public $search = '';

    public $calltype = 'all';

    public function render()
    {
        return view('mycalls.my-phonecalls-table', [
            'mycalls' => \App\Models\Phonecall::search(
                $this->search
            )->mycalls()
                ->ofType($this->calltype)
                ->orderBy('startdate', 'desc')->orderBy('starttime', 'desc')->paginate($this->perPage),
        ]);
    }

    /**
     *  Livewire Lifecycle Hook
     */
    public function updatingSearchInput(): void
    {
        $this->resetPage();
    }
}

<div>
    <div class="flex items-center justify-between">
        {{-- Search Input --}}
        <div class="relative text-gray-700 focus-within:text-gray-600 m-2">
            <span class="absolute inset-y-0 left-0 flex items-center pl-2">
                <button type="submit" class="p-1 focus:outline-none focus:shadow-outline">
                    <svg fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
                        stroke-width="2" viewBox="0 0 24 24" class="w-6 h-6">
                        <path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
                    </svg>
                </button>
            </span>
            <input wire:model="search" type="search" name="q"
                class="py-2 text-sm text-gray bg-gray-100 rounded-md pl-10 focus:outline-none focus:bg-gray-200 focus:text-gray-700"
                placeholder="{{ __('Search...') }}" autocomplete="off">
        </div>
        {{-- End Of Search Input --}}

        {{-- Call type selector --}}
        @include('partials.calltypes')
        {{-- End of Call type selector --}}

        {{-- Paginator Select Dropdown --}}
        <div>
            <span>{{ __('Per page:') }}</span>
            <div class="inline-block relative mr-2">
                <select wire:model="perPage"
                    class="block appearance-none w-full bg-white border border-gray-400 hover:border-gray-500 px-4 py-2 pr-8 rounded shadow leading-tight focus:outline-none focus:shadow-outline">
                    <option>10</option>
                    <option>25</option>
                    <option>50</option>
                </select>
                <div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700">
                    <svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
                        <path d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z" /></svg>
                </div>
            </div>
        </div>
        {{-- End Of Paginator Select Dropdown --}}
    </div>

    {{-- Phonecalls Table --}}
    <div class="container p-2" id="my-calls">
        <table class="text-center w-full" id="my-calls-table">
            <thead class="bg-gray-400 flex text-white w-full item-center rounded-md">
                <tr class="flex w-full py-2">
                    <th class="px-4 w-1/5">{{ __('Date') }}</th>
                    <th class="px-4 w-1/5">{{ __('Time') }}</th>
                    <th class="px-4 w-1/5">{{ __('Duration') }}</th>
                    <th class="px-4 w-1/5">{{ __('Calling No') }}</th>
                    <th class="px-4 w-1/5">{{ __('Called No') }}</th>
                    {{-- <th class="px-4 w-1/6">{{ __('Call Type') }}</th> --}}
                </tr>
            </thead>
            <!-- Remove the nasty inline CSS fixed height on production and replace it with a CSS class — this is just for demonstration purposes! -->
            <tbody class="">
                @foreach ($mycalls as $mycall)
                <tr class="flex w-full py-2 hover:bg-gray-100">
                    <td class="px-4 w-1/5">
                        {{ \Carbon\Carbon::createFromFormat('Y-m-d',$mycall->startdate)->format('d.m.Y.')}}
                    </td>
                    <td class="px-4 w-1/5">
                        {{ \Carbon\Carbon::createFromFormat('H:i:s', $mycall->starttime, 'UTC')->tz('Europe/Belgrade')->toTImeString() }}
                    </td>
                    <td class="px-4 w-1/5">
                        {{ gmdate('H:i:s', $mycall->duration) }}</td>
                    <td class="px-4 w-1/5">{{ $mycall->chargednumber}}</td>
                    <td class="px-4 w-1/5">{{ $mycall->dialednumber}}</td>
                    {{-- <td class="px-4 w-1/6">{{ $phonecall->conditioncode}}</td> --}}
                </tr>
                @endforeach
            </tbody>
        </table>
    </div>
    {{-- End Of Phonecalls Table --}}

    {{-- Pagination and Records Count info --}}
    <div class="p-2 bg-gray-200">
        {{ $mycalls->links('vendor.pagination.livewire-tailwind') }}
    </div>
    {{-- End Of Pagination and Records Count info --}}
</div>

While I'm waiting for any suggestion, I'll try one more clean Laravel/Jetstream/Livewire install. Thanks in advance, any help is much appreciated!

like image 442
Igor Avatar asked Oct 06 '20 08:10

Igor


People also ask

How do I Paginate on livewire?

You can paginate the results by using the WithPagination trait provided by Livewire. Now there will be rendered HTML links for the different pages at the bottom of your posts, and the results will be paginated.

What is livewire in laravel?

Laravel Livewire is a library that makes it simple to build modern, reactive, dynamic interfaces using Laravel Blade as your templating language. This is a great stack to choose if you want to build an application that is dynamic and reactive but don't feel comfortable jumping into a full JavaScript framework like Vue.


Video Answer


1 Answers

By looking at my code one more time, I've noticed some "leftovers" in model classes. In model class there shouldn't be "use WithPagination;" Livewire trait! When I removed "use" statemnets and cleared view cache, everything is working OK!

like image 130
Igor Avatar answered Oct 16 '22 16:10

Igor