CakePHP3のページネーションをAjaxで行う

CakePHP3で通常のページネーションのように画面を再表示せず、Ajaxで一覧部分だけを切り替える方法を紹介したいと思います。

## バージョン
CakePHP 3.6

## 画面
高校野球の歴代優勝校の一覧を作ったので、これを例にします。
![base.png](https://qiita-image-store.s3.amazonaws.com/0/323635/c51bb67b-5da4-2caf-8404-32bfd5838766.png)

# データ
high_schoolsテーブルに2000年以降のデータが入っています。

| id  | year  | name  |area |prefecture |
|:----:|:------:|:-------:|:------:|:-------:|
| 1   |  2000 |   智弁和歌山 | 近畿 | 和歌山 |
| 2   |  2001 |   日大三  | 関東 | 東京 |
| 3   |  2002 |   明徳義塾	 | 四国 |  高知 |
| …   |   |   	 |  |   |
| 19   |  2018 |   大阪桐蔭	 | 近畿 |  大阪 |


## Controller
indexが元になる画面で、listはAjaxで一覧部分を取得するためのものです。
検索はfriendsofcake/searchで行います。

```/src/Controller/HighSchoolsController.php
public function index()
{
	$area = ['北海道' => '北海道', '東北' => '東北', '関東' => '関東', '中部' => '中部', '近畿' => '近畿', '中国' => '中国', '四国' => '四国', '九州' => '九州'];
	for ($i = 2000;$i <= 2018;$i++) {
		$years[$i] = $i;
	}
	$this->set(compact('area', 'years'));
}

public function list()
{
	if ($this->request->is('ajax')) {
		$this->viewBuilder()->setLayout('');
		$high_schools = $this->HighSchools
			->find('search', ['search' => $this->request->getQueryParams()])
			->order(['year' => 'DESC']);
		$high_schools = $this->paginate($high_schools);
		$this->set(compact('high_schools'));
	}
}
```

## Template
index.ctpで検索部分を作ります。
jsにURLを渡しておきます。
検索ボタンはsubmitではなくただのbuttonにします。

```/src/Template/HighSchools/index.ctp
<?php
	$this->Html->script('high_schools', ['block' => true]);
	$this->Html->scriptStart(['block' => true]);
	echo 'var baseUrl = "'.$this->Url->build('/high-schools', false).'";';
	$this->Html->scriptEnd();
?>
<h1>高校野球 歴代優勝校</h1>
<?= $this->Form->create(null, ['id'=>'search']) ?>
<label>年</label>
<?= $this->Form->select('year_from', $years, ['empty'=>true]) ?> ~ <?= $this->Form->select('year_to', $years, ['empty'=>true]) ?>
<label>地域</label>
<?= $this->Form->select('area', $area, ['empty'=>'地域を選択してください']) ?>
<?= $this->Form->button('検索', ['type'=>'button', 'id' => 'button']) ?>
<?= $this->Form->end() ?>
<div id="ajax_template"></div>
```

list.ctpはページネーションと一覧のみです。

```/src/Template/HighSchools/list.ctp
<ul class="pagination">
	<?php
        if ($this->Paginator->hasPrev()) {
            echo $this->Paginator->prev('<');
        }
        echo $this->Paginator->numbers();
        if ($this->Paginator->hasNext()) {
        	echo $this->Paginator->next('>');
        }
    ?>
</ul>
<table>
	<thead>
		<tr>
	        <th>年</th><th>高校</th><th>地域</th><th>都道府県</th>
		</tr>
	</thead>
	<tbody>
	<?php foreach($high_schools as $high_school){ ?>
        <tr>
            <td><?= $high_school->year ?></td>
            <td><?= $high_school->name ?></td>
            <td><?= $high_school->area ?></td>
            <td><?= $high_school->prefecture ?></td>
    	</tr>
	<?php } ?>
	</tbody>
</table>
```

## Model
friendsofcake/searchで検索できるようにします。

```/src/Model/Table/HighSchoolsTable.php
public function searchManager()
{
	$searchManager = $this->behaviors()->Search->searchManager();
	$searchManager
		->add('year_from', 'Search.Compare', ['operator' => '>=', 'field' => 'year'])
		->add('year_to', 'Search.Compare', ['operator' => '<=', 'field' => 'year'])
		->value('area');

	return $searchManager;
}
```

## JavaScript
そして肝心のjsです。
Ajaxでhigh-schools/listにアクセスして、戻り値としてlist.ctpの中身を受け取り、jqueryでHTMLを書き換えています。
Ajaxのurlは検索ボタンが押下されたときは入力項目のnameとvalueから作成します。
ページネーションのリンクが押下されたときはdata属性のurlから取得しています。
あらかじめページネーションのリンクは無効にして、元のhrefの値はdata属性のurlに保持しておくのがポイントです。

```/webroot/js/high_schools.js
$(function(){
    // 検索ボタン押下時
    $(document).on('click', '#button', function() {
        // 検索の入力項目からurlを生成
        var url = baseUrl + '/list?';
        $('#search select').each(function(index, element) {
            if($(this).val()){
                url += $(this).attr('name') + '=' + $(this).val() + '&';
            }
        });

        searchList(url);
    });

    // ページネーションのリンク押下時
    $(document).on('click', '.pagination a', function() {
        var url = $(this).data('url');
        if(url){
            searchList(url);
        }
    });
    
    // 初期表示の一覧を取得するため検索ボタン押下
    $('#button').click();
});

// 一覧を取得
function searchList(url){
    $.ajax({
        url:url,
        type:'POST',
    })
    .done(function(data){
        $('#ajax_template').html(data);
        linkDisable();
    });
}

// ページネーションのリンクを無効化し、data属性のurlに遷移先を保持
function linkDisable(){
    $('.pagination a').each(function(index, element) {
        if(!$(this).data('url')){
            $(this).data('url', $(this).attr('href'));
            $(this).attr('href', '#');
        }
    });
}
```