18. 18日目: AJAX

  • この記事は Symfony 1.4 向けのオリジナルの Jobeet Tutorial の一部分です。
昨日は、 Zend Lucene ライブラリのおかげで、 Jobeet に非常に強力な検索エンジンを実装しました。
今日は、検索エンジンの応答性を向上させるために、 AJAX を利用して検索エンジンを動的なものに変換し ます。
JavaScript が有効でも無効でもフォームが動作するように、ライブ検索機能は控えめな( unobtrusive ) JavaScript を使用して実装されます。
控えめな JavaScript を使うことは、クライアントコードの HTML、CSS と JavaScript の振舞いの良好な分離が可能になります。

18.1. jQueryのインストール

jQuery の Web サイトにアクセスし、最新バージョンをダウンロードして、 src/Ibw/JobeetBundle/Resources/public/js/ 以下に .js ファイルを置きます。
js ディレクトリに .js ファイルを入れた後に、次のコマンドを実行して Symfony に公開状態にするよう指示します。
$ php app/console assets:install web --symlink

18.2. jQueryを含める

すべてのページで jQuery を必要とするため、それが含まれるようにレイアウトを更新します。

src/Ibw/JobeetBundle/Resources/views/layout.html.twig

<!-- ... -->
    {% block javascripts %}
        <script type="text/javascript" src="{{ asset('bundles/ibwjobeet/js/jquery-2.0.3.min.js') }}"></script>
    {% endblock %}
<!-- ... -->

18.3. ビヘイビアの追加

ライブ検索を実装すると、検索ボックスにユーザーが文字を入力するたびに、サーバーへのコールが行われます。
サーバは、ページ全体を更新せずに、ページの一部の領域を更新するための必要な情報を返します。
jQuery の背景にある主要な原則は、 HTML 属性に on*() というビヘイビアを追加するのではなく、ページが完全にロードされた後に DOM にビヘイビアを追加することです。
この方法では、お使いのブラウザが JavaScript のサポートを無効にした場合、ビヘイビアは全く登録されませんが、フォームは以前と同じように動作します。
最初のステップは、ユーザが検索ボックスにキーを入力することを常時傍受することです。

Note

実装前のコードの説明

$('#search_keywords').keyup(function(key)
{
    if (this.value.length >= 3 || this.value == '')
    {
        // do something
    }
});

Note

後で大きく修正するので、今はコードを追加しないでください。 最終的な JavaScript コードは、次のセクションでレイアウトに追加されます。

jQuery はユーザーのキー入力のたびに、上記のコードで定義された無名関数を実行します。
ただし、ユーザーが 3 文字以上入力した場合、または、 input タグからすべてを削除した場合に限ります。
サーバへの AJAX のコールを作ることは DOM 要素上の load() メソッドを使用するのと同じくらい簡単です。

Note

実装前のコードの説明

$('#search_keywords').keyup(function(key)
{
    if (this.value.length >= 3 || this.value == '')
    {
        $('#jobs').load(
            $(this).parents('form').attr('action'), { query: this.value + '*' }
        );
    }
});
AJAX のコールは、通常と同じ呼び出しで行います。
アクションへの必要な変更は、次のセクションで行います。
JavaScript が有効になっている場合、少なくとも最後には、検索ボタンを削除したいと思うでしょう。

Note

実装前のコードの説明

$('.search input[type="submit"]').hide();

18.4. ユーザーのフィードバック

AJAX 呼び出しを行う際、ページはすぐには更新されません。
ブラウザがページを更新する前に、サーバーの応答を待ちます。
それまでの間、視覚的なフィードバックを提供し、ユーザーに何かが起きていることを知らせます。
慣例では、 AJAX 呼び出しの間に読み込みアイコンを表示します。
レイアウトに読み込みイメージを追加し、デフォルトではそれを非表示にします。

src/Ibw/JobeetBundle/Resources/views/layout.html.twig

<!-- ... -->
    <div class="search">
        <h2>Ask for a job</h2>
        <form action="{{ path('ibw_job_search') }}" method="get">
            <input type="text" name="query" value="{{ app.request.get('query') }}" id="search_keywords" />
            <input type="submit" value="search" />
            <img id="loader" src="{{ asset('bundles/ibwjobeet/images/loader.gif') }}" style="vertical-align: middle; display: none" />
            <div class="help">
                Enter some keywords (city, country, position, ...)
            </div>
        </form>
    </div>
<!-- ... -->
これで HTML を動作させるために必要なすべての部分を持ちましたので、 search.js ファイルを作成し、これまでに説明した JavaScript を含めます。

src/Ibw/JobeetBundle/Resources/public/js/search.js

$(document).ready(function()
{
    $('.search input[type="submit"]').hide();

    $('#search_keywords').keyup(function(key)
    {
        if(this.value.length >= 3 || this.value == '') {
            $('#loader').show();
            $('#jobs').load(
                $(this).parent('form').attr('action'),
                { query: this.value ? this.value + '*' : this.value },
                function() {
                    $('#loader').hide();
                }
            );
        }
    });
});

Symfony に公開状態にするよう指示するコマンドを実行します。

$ php app/console assets:install web --symlink

また、この新しいファイルが含まれるように、レイアウトを更新する必要があります。

src/Ibw/JobeetBundle/Resources/views/layout.html.twig

<!-- ... -->
    {% block javascripts %}
        <script type="text/javascript" src="{{ asset('bundles/ibwjobeet/js/jquery-2.0.3.min.js') }}"></script>
        <script type="text/javascript" src="{{ asset('bundles/ibwjobeet/js/search.js') }}"></script>
    {% endblock %}
<!-- ... -->

18.5. アクションの中での AJAX

JavaScript が有効になっている場合、 jQuery は検索ボックスでのすべてのキー入力を傍受し、 search アクションを呼び出します。
有効になっていない場合も、ユーザーがキーを押してフォーム送信し、同じ search アクションを呼び出します。
そのため、 search アクションが、AJAX 経由での呼び出しか、そうでないかを判断する必要があります。
リクエストが AJAX でコールされているときは常に、リクエストオブジェクトの isXmlHttpRequest() メソッドが true を返します。

src/Ibw/JobeetBundle/Controller/JobController.php

use Symfony\Component\HttpFoundation\Response;

class JobController extends Controller
{
    // ...

    public function searchAction(Request $request)
    {
        $em = $this->getDoctrine()->getManager();
        $query = $this->getRequest()->get('query');

        if(!$query) {
            if(!$request->isXmlHttpRequest()) {
                return $this->redirect($this->generateUrl('ibw_job'));
            } else {
                return new Response('No results.');
            }
        }

        $jobs = $em->getRepository('IbwJobeetBundle:Job')->getForLuceneQuery($query);

        if($request->isXmlHttpRequest()) {

            return $this->render('IbwJobeetBundle:Job:list.html.twig', array('jobs' => $jobs));
        }

        return $this->render('IbwJobeetBundle:Job:search.html.twig', array('jobs' => $jobs));
    }
}
検索が結果を返さない場合は、空白のページの代わりにメッセージを表示する必要があります。
ここでは、単純な文字列を返します。

src/Ibw/JobeetBundle/Controller/JobController.php

public function searchAction(Request $request)
{
  $em = $this->getDoctrine()->getManager();
  $query = $this->getRequest()->get('query');

  if(!$query) {
      if(!$request->isXmlHttpRequest()) {
          return $this->redirect($this->generateUrl('ibw_job'));
      } else {
          return new Response('No results.');
      }
  }

  $jobs = $em->getRepository('IbwJobeetBundle:Job')->getForLuceneQuery($query);

  if($request->isXmlHttpRequest()) {
      if('*' == $query || !$jobs || $query == '') {
          return new Response('No results.');
      }

      return $this->render('IbwJobeetBundle:Job:list.html.twig', array('jobs' => $jobs));
  }

  return $this->render('IbwJobeetBundle:Job:search.html.twig', array('jobs' => $jobs));
}

18.6. AJAX のテスト

Symfony のブラウザは JavaScript をシミュレートすることができなため、AJAX の呼び出しをテストする際に、手助けしてあげる必要があります。
これは主に、 jQuery と他のすべての主要な JavaScript ライブラリが送信するリクエストに、手動でヘッダを追加する必要があることを意味します。

src/Ibw/JobeetBundle/Tests/Controller/JobControllerTest.php

class JobControllerTest extends WebTestCase
{
    // ...

    public function testSearch()
    {
        $client = static::createClient();

        $crawler = $client->request('GET', '/job/search');
        $this->assertEquals('Ibw\JobeetBundle\Controller\JobController::searchAction', $client->getRequest()->attributes->get('_controller'));

        $crawler = $client->request('GET', '/job/search?query=sens*', array(), array(), array(
            'X-Requested-With' => 'XMLHttpRequest',
        ));
        $this->assertTrue($crawler->filter('tr')->count()== 2);
    }
}
17 日目では、検索エンジンを実装するために Zend Luceneライブラリを使用しました。
今日、それをより反応よくするために jQuery を使用しました。
Symfony フレームワークは、簡単に MVC アプリケーションを構築するためにすべての基本的なツールを提供し、また他のコンポーネントと上手に共存します。
いつものように、作業のためには最適なツールを使用するようにしてください。
明日は、 Jobeet の Web サイトを国際化する方法を説明します。

See also

Symfony2日本語ドキュメント

豊富な日本語ドキュメントがありますので合わせて読み進めてみましょう。

Note

Creative Commons License

このチュートリアルは、クリエイティブ・コモンズ・ライセンス 表示 - 継承 3.0 非移植 (CC BY-SA 3.0) のもとでライセンスされています。 翻訳の元にしたオリジナルはこちらです。 Symfony2 Jobeet http://www.intelligentbee.com/blog/tag/symfony2-jobeet/.