Laravel Livewire Pagination

Oct 06 2020

Laravel - 8.6.0 (Jetstream), Livewire - 2.2.7

Między innymi mam dwie proste tabele z danymi wejściowymi wyszukiwania renderowanymi za pomocą Livewire. Oba komponenty Livewire mają prawie taką samą logikę, główną różnicą jest format zapytania DB. W jednej tabeli wszystko działa idealnie (wyszukiwanie, paginacja), ale w drugiej jest problem z paginacją.

Co jest nie tak? Kiedy klikam na link do paginacji (np. Z pierwszej strony gotoPage (2)), żądanie jest wysyłane, adres URL w przeglądarce zmienia się na właściwy ciąg zapytania, odpowiedź jest odbierana, ale nie ma części HTML w odpowiedzi, nie ma zmiany w DOM a paginacja pokazuje, że pierwsza strona jest aktywna. Jeśli ręcznie odświeżę stronę, tabela pokazuje dane dla strony 2, a potem mogę nawet pobrać nowe dane dla następnej lub poprzedniej strony, ale tylko raz po początkowym odświeżeniu strony. Jeśli ponownie kliknę linki paginacji, brak danych HTML w odpowiedzi, brak zmiany DOM ...

Czego próbowałem?

  1. Przejrzałem dokumentację Livewire, wypróbowałem wszystkie sugerowane rozwiązania z sekcji „Rozwiązywanie problemów”.
  2. Sprawdzałem swój kod wiele razy, porównywałem zarówno komponenty Livewire, jak i widoki Blade, nie byłem w stanie znaleźć jakiegoś oczywistego błędu.
  3. Przez ostatnie 10 dni szukałem odpowiedzi na SO, forum Livewire, forum Laracast, ... znalazłem kilka dobrych sugestii, większość z nich wypróbowałem, ale żadna z nich nie rozwiązuje mojego problemu.
  4. Wyeliminowano możliwość, że logika dla zapytania DB powoduje pewne problemy, problem kończy się, nawet jeśli nie używam złożonego filtrowania rekordów (np. Jeśli po prostu podzielę wszystkie rekordy na strony - Model :: all () -> paginate (10)).
  5. Problematyczna tabela ma dodatkowe filtrowanie rekordów za pomocą przycisków radiowych, tymczasowo to usunąłem, ale problem pozostaje.

Kod działającego komponentu Livewire i widoku Blade:

<?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>

Kod NIEdziałającego komponentu Livewire i widoku Blade:

<?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 phonecalls 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>

Czekając na sugestie, spróbuję jeszcze jednej czystej instalacji Laravel / Jetstream / Livewire. Z góry dziękuję, każda pomoc jest bardzo mile widziana!

Odpowiedzi

2 Igor Oct 06 2020 at 17:23

Patrząc jeszcze raz na mój kod, zauważyłem pewne „resztki” w klasach modeli. W klasie modelu nie powinno być "użyj WithPagination;" Cecha Livewire! Kiedy usunąłem statystyki „użyj” i wyczyściłem pamięć podręczną widoku, wszystko działa OK!