В данной статье я приведу пример динамической подгрузки результатов поиска по сайту. При этом контент (результат) будет получен в виде объекта пагинации, только вместо вывода стандартных кнопок с номерами страниц или стрелками "вперед" - "назад" и перехода на следующие страницы - строки будут добавляться в конец блока при его скролле вниз. Таким же образом можно реализовать не страницу результатов поиска, а, например, список постов и тд.

В шапку страницы или другое нужное место добавляем форму поиска:
<div class="search-block">
    <h1>@lang('search.enter-search')</h1>
    <form action="{{route('searchSimple')}}" method="GET" class="search-simple">
        <div class="row">
            <div class="col-xs-10">
                <div class="form-group">
                    <input type="text" class="form-control" name="q" value="{{ old('q') }}" required>
                </div>
            </div>
            <div class="col-xs-2">
                <div class="form-group">
                    <input class="btn btn-info" type="submit" value="Искать">
                </div>
            </div>
        </div>
    </form>
</div>

Как видно, запрос будет отправлен методом GET на маршрут searchSimple. Поэтому в файле routes/web.php добавляем данный маршрут:
Route::match(['get','post'],'searchSimple',['uses'=>'SearchController@index','as'=>'searchSimple']);

Указываем в нем так же и метод POST, т.к. AJAX запрос будет обрабатывать тот же самый метод контроллера (index).

В своих примерах я буду использовать модель(таблицу) Student, содержащую данные по студентам (ФИО, email и тд.).

Создаем SearchController:
<?php
namespace App\Http\Controllers;
use App\Student;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Input;
class SearchController extends Controller
{
    public $qQeury;
    public function index(Request $request){
        $q = $request->input('q');
        $max_page = 30;
        //Полнотекстовый поиск с пагинацией
        $results = $this->searchSimple($q, $max_page);
        //AJAX-запрос
        if($request->ajax() && $request->input('page')){
            return view('search.table-ajax', [
                    'qQeury' => $this->qQeury,
                    'students' => $results,
                ]);
        }
        return view('search.index', [
            'qQeury' => $this->qQeury,
            'include' => 'search.table',
            'students' => $results,
        ])->render();;
    }
    /**
     * Полнотекстовый поиск (простая форма)
     *
     * @param string $q
     * @param integer $count
     * @return mixed
     */
    public function searchSimple($q, $count){
        $page = Input::get('page');
        if(!$page) $page =1;
        $query = mb_strtolower($q, 'UTF-8');
        $tmp = explode(" ", $query);
        $query = [];
        foreach ($tmp as $word)
        {
            $len = mb_strlen($word, 'UTF-8');
            switch (true)
            {
                case ($len <= 3):
                {
                    $query[] = $word . "*";
                    break;
                }
                case ($len > 3 && $len <= 6):
                {
                    $query[] = mb_substr($word, 0, -1, 'UTF-8') . "*";
                    break;
                }
                case ($len > 6 && $len <= 9):
                {
                    $query[] = mb_substr($word, 0, -2, 'UTF-8') . "*";
                    break;
                }
                case ($len > 9):
                {
                    $query[] = mb_substr($word, 0, -3, 'UTF-8') . "*";
                    break;
                }
                default:
                {
                    break;
                }
            }
        }
        $query = array_unique($query, SORT_STRING);
        $qQeury = implode(" ", $query);
        $this->qQeury = $qQeury;
        // Таблица для поиска
        $results = Student::whereRaw(
            "MATCH(name,email) AGAINST(? IN BOOLEAN MODE)", // name,email - поля, по которым нужно искать
            array($qQeury))->paginate($count) ;
        return $results;
    }
}


В методе index указываем кол-во записей которые нужно получить и вывести за 1 раз, тут «30». Метод содержит проверку: если это AJAX - подключаем шаблон «search.table-ajax», который выводит только сами строки без другого контента и заголовков таблицы для добавления их в конец таблицы.
У меня данный шаблон выглядит так (файл resources/views/search/table-ajax.blade.php):
@foreach($students as $student)
    <tr>
        <td class="align-left">{{$student->id}}</td>
        <td>{{$student->name}}</td>
        <td>{{$student->age}}</td>
        <td>{{$student->email }}</td>
        <td>{{$student->country->name_ru}}</td>
        <td>{{$student->city->name_ru}}</td>
        <td>{{$student->user->name}}</td>
    </tr>
@endforeach

Ну а если был GET запрос, то выполнение кода пойдет дальше и будет подключен шаблон 'search.index', файл resources/views/search/index.blade.php:
@extends('layouts.layout')
@section('content')
    @include($include)
@endsection
{{--JS только для страниц поиска--}}
@section('individualJS')
    <script type="text/javascript" src="{{ asset('js/pagination.js') }}"></script>
@endsection

Тут, у меня, подключается макет layout.blade.php из папки layouts, соответственно не забудьте прописать в своем макете 2 директивы для подключения указанных секций:
  • @yield('content') – шаблон таблицы которая будет выводить результаты поиска;
  • в конце блока body подключение js скрипта - @yield('individualJS').

Я специально создал отдельную секцию для подключения js скрипта, теперь он будет подключен только на странице вывода результатов поиска, а не на всех.

Шаблон таблицы выводящий результаты поиска (файл resources/views/search/table.blade.php):
<div class="students-table">
    {{--token--}}
    <div id="id-token" data-token="{{csrf_token()}}" data-last-page="{{$students->lastPage()}}" style="display: none"></div>
    <table>
        <thead>
        <tr>
            <th class="align-left">id</th>
            <th>@lang('students.name')</th>
            <th>@lang('students.age')</th>
            <th>email</th>
            <th>@lang('students.country')</th>
            <th>@lang('students.city')</th>
            <th>@lang('students.curator')</th>
        </tr>
        </thead>
        <tbody>
        @foreach($students as $student)
            <tr>
                <td class="align-left">{{$student->id}}</td>
                <td class="align-left">{!! isset($qQeury) ? \App\Http\Controllers\SearchController::search_backlight($student->name, $qQeury) : $student->name !!}</td>
                <td>{{$student->age}}</td>
                <td>{!! isset($qQeury) ? \App\Http\Controllers\SearchController::search_backlight($student->email, $qQeury) : $student->email !!}</td>
                <td>{{$student->country->name_ru}}</td>
                <td>{{$student->city->name_ru}}</td>
                <td>{{$student->user->name}}</td>
            </tr>
        @endforeach
        </tbody>
    </table>
</div>

В блоке
<div id="id-token" data-token="{{csrf_token()}}" data-last-page="{{$students->lastPage()}}" style="display: none"></div>
в атрибуте data-token я указываю токен, который понадобится для осуществления POST запроса из JS скрипта. Так же, в атрибуте data-last-page я прописываю общее количество частей на которые был разделен результат запроса (пагинация). Данные атрибуты будут считаны js скриптом.


Второй метод контроллера «searchSimple» непосредственно осуществляет поиск в базе данных. У меня это полнотекстовый поиск, который использует FULLTEXT индексы для увеличения скорости поиска и снижения нагрузки на сервер. В своей заметке я писал как создать такие индексы для нужных полей таблицы. Вы же можете и не использовать полнотекстовый поиск, главное, чтобы метод searchSimple возвращал объект LengthAwarePaginator, который создается при вызове метода paginate().

Я использую пагинацию для загрузки результатов поиска частями. Только вместо стандартных кнопок пагинации у меня контент будет добавляться в конец блока при скролле (прокрутке) вниз.

Осталось только привести пример javascript (jquery) который мы подключили в шаблоне search/index.blade.php.
Файл public/js/pagination.js:
$(document).ready(function() {
    var inProgress = false; //флаг для отслеживания того, происходит ли в данный момент ajax-запрос
    var count = 2; //начинать пагинацию со 2 страницы, т.к. первая выводится сразу
    var countPage = $('#id-token').attr("data-last-page"); //токен для проверки запроса
    var table = $(".students-table").get(0); //таблица, в которую будут добавляться строки
    /* Обработчик скролла страницы */
    $('.students-table').on('scroll', function() {
        /* Если скролл в блоке(таблице) прокручен вниз до конца и ajax-запрос в настоящий момент не выполняется
        и есть страницы для пагинации */
        if((table.scrollHeight - table.scrollTop === table.clientHeight) && !inProgress && count<=countPage ){
            var data = {};
            data["token"] = $('#id-token').attr("data-token");
            data["page"] = count;
            $.ajax({
                url: '',
                data: data,
                type: 'POST',
                headers: {
                    'X-CSRF-TOKEN': data["token"]
                },
                /* выполнить до отправки запрса */
                beforeSend: function() {
                    inProgress = true;
                },
                // Ответ от сервера
                success: function(html){
                    $('.students-table tbody').append(html);
                },
                // Ошибка AJAX
                error: function(result){
                    console.log(result);
                }
                /* сразу после выполнения запроса */
            }).done(function(data){
                    inProgress = false;
                    count++;
            });
        }
    });
});

Данный код так же хорошо прокомментарирован. При скролле блока с результатами поиска вниз вызывается событие, которое запускает выполнение анонимной функции:
$('.students-table').on('scroll', function(){...}

В ней прописана проверка, которая выполняет AJAX запрос только если посетитель прокрутил блок вниз до конца. Так же проверяется не осуществляется ли в данное время AJAX запрос и есть ли еще данные для вывода.
При получении успешного ответа от сервера, а это у нас строки из шаблона search/table-ajax.blade.php, он вставляется в конец таблицы. Затем пользователь прокручивает вниз чтобы просмотреть появившиеся данные и подгружается следующая порция строк…


Еще важный момент – т.к. контент подгружается при скроллинге, необходимо ограничить высоту блока в который будет подгружаться контент. Например указать такие CSS стили для блока:
.students-table{
    max-height: 500px;
    overflow: auto;
}

При этом, порция строк, которую вы указываете для получения (у меня 30), не должна полностью помещаться по высоте, т.е. у вас должна появиться полоска скролла в правой части блока. Таким образом пользователь будет видеть, что не все поместилось в видимой области блока, прокручивать его и таким образом вызывать js событие с подгрузкой нового контента.