yonet77的な雑記帳

日々思いついたネタなどを書き留めておきます

Salesforce World Tour Tokyo2014: MiniHack を(ちょこっと)やってみたよ

先日、Salesforce World Tour Tokyoが盛大に開催されましたね。Dev向けの会場は虎ノ門ヒルズでしたので、当日は虎ノ門ヒルズにずっと居座ってました。(別会場の方も行ってみたかったのですが...色々な事情で諦めました。。)

さて、今回も恒例の"MiniHack"が行われました。6つのお題("H", "A", "C", "K", "E", "R")に答えて景品をGETする!という類いのものです。

「C」のお題:Visualforce Remote Object

お題の内容はこんなところです。

【チャレンジ内容】

Visualforce Remote Objectを駆使して、Apexを利用せずに、取引先検索のVisualforceページを実現してみましょう。

【要件】

  1. 任意のDeveloper Edition組織に以下のパッケージをインストールします。
  2. "ContactSearch" Visualforceページを更新し、Apexコントローラを利用しないで、Visualforce Remote Object を使って同様の機能を実現してください。もし必要ならば外部のJavaScriptライブラリは自由に使用できます。
  3. このようなrerender属性を使用しない場合、Backbone.jsやAngular.jsなど、動的に画面を更新する機能を持つライブラリの利用も有効です。


元々のApexコントローラを見てみましょう。

public class ContactSearchController {
      
    @RemoteAction
	public static List<Contact> retrieveContactRemote(String searchQuery){
		String likeQuery = '%' + searchQuery + '%';
		return [SELECT ID,Name,FirstName,LastName,Email,Phone 
                                         FROM Contact
                                         WHERE
                                         	Firstname Like :likeQuery
                                         OR LastName like :likeQuery
                                         OR Email like :likeQuery Limit 100];
    }
}

なるほど、検索キーを受け取って、性、名、Emailで取引先責任者を検索してます。さらに、JavaScript RemoteActionを使ってJavaScriptから利用できるようにしてますね。

ではVisualforceページも見てみましょう。

<apex:page controller="ContactSearchController" showHeader="false" applyHtmlTag="false" applyBodyTag="false" standardStylesheets="false">
<html>
    <head>
        <script type="text/javascript" src="//code.jquery.com/jquery-2.1.1.min.js"></script>
        <script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.7.0/underscore-min.js"></script>
        <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css"></link>
        
        <script id="resultTemplateHead" type="text/template">
            <thead>
                <tr>
                    <th>氏名</th> 
                    <th>電話</th> 
                    <th>Email</th> 
                </tr>
            </thead>
        </script>
        <script id="resultTemplate" type="text/template">
            <tr>
                <td><a href="/<%= contact.Id %>"><%= contact.LastName %> <%= contact.FirstName %></a></td>
                <td><%= contact.Phone %></td>
                <td><%= contact.Email %></td>
            </tr>
        </script>
        <script type="text/javascript">
            var resultHeadCompiled = _.template($('#resultTemplateHead').html());
            var resultCompiled = _.template($('#resultTemplate').html());
            function fireSearch(){
                var searchQuery = $("#searchQuery").val();
                Visualforce.remoting.Manager.invokeAction(
                    '{!$RemoteAction.ContactSearchController.retrieveContactRemote}',
                    searchQuery,
                    function(result, event){
                        $("#searchResult").empty();
                        $("#searchResult").append(resultHeadCompiled({}));
                        $("#searchResult").append("<tbody></tbody>");
                        for(var i = 0; i < result.length; i++){
                            $("#searchResult > tbody").append(resultCompiled({contact : result[i]}));
                        }
                    },
                    {escape: false}
                );
            }
            $(function(){
                $("#searchButton").click(function(){fireSearch();});
            });
        </script>
    </head>
    <body>
        <div id="container">
            <h2>取引先責任者</h2>
            <form class="navbar-form navbar-left" role="search">
                <div class="form-group">
                    <input class="form-control" id="searchQuery" type="text" value=""/>
                </div>
                <button type="button" class="btn btn-primary" id="searchButton">検索</button>
            </form>
            <div>
                <table id="searchResult" class="table table-hover"></table>                
            </div>
        </div>
    </body>
</html>
</apex:page>

JavaScript RemoteActionで受け取ったデータを、テーブル形式に表示すれば良いようです。

いざ実装

まず、Remote Objectを利用するには、apex:remoteObjectsタグを使って公開するオブジェクトとフィールドを宣言します。

<apex:page controller="ContactSearchController" showHeader="false" applyHtmlTag="false" applyBodyTag="false" standardStylesheets="false">
<apex:remoteObjects >
    <apex:remoteObjectModel name="Contact" jsShorthand="con" fields="Id,Name,LastName,FirstName,Phone,Email">
    </apex:remoteObjectModel>
</apex:remoteObjects>
<html>
...

次に、RemoteActionで検索している箇所を、Remote Objectを使った検索に切り替えます。具体的には、"fireSearch()"の中身を一部修正しています。

...
        <script type="text/javascript">
            var resultHeadCompiled = _.template($('#resultTemplateHead').html());
            var resultCompiled = _.template($('#resultTemplate').html());
            function fireSearch(){
                var searchQuery = $("#searchQuery").val(),
                    queryParam = '%' + searchQuery + '%';
                var contactSearch = new SObjectModel.con();
                contactSearch.retrieve({
                    where: {
                        or: {
                            FirstName: {like:  queryParam},
                            LastName: {like:  queryParam},
                            Email: {like: queryParam}
                        }
                    },
                    limit: 100
                }, function(err, result, event){
                        $("#searchResult").empty();
                        $("#searchResult").append(resultHeadCompiled({}));
                        $("#searchResult").append("<tbody></tbody>");
                        for(var i = 0; i < result.length; i++){
                            $("#searchResult > tbody").append(resultCompiled({contact : result[i]}));
                        }
                    });
            }
            $(function(){
                $("#searchButton").click(function(){fireSearch();});
            });
        </script>
...

最後に、表示する箇所をRemote Object用に修正します。具体的には contact.Id と書かれた箇所を contact.get('Id') と変更しております。(その他、Phone, Emailも)

...
        <script id="resultTemplate" type="text/template">
            <tr>
                <td><a href="/<%= contact.get('Id') %>"><%= contact.get('LastName') %> <%= contact.get('FirstName') %></a></td>
                <td><%= contact.get('Phone') %></td>
                <td><%= contact.get('Email') %></td>
            </tr>
        </script>
...

だけれども

いざ実際に画面で確認すると、以下のエラーが返ってきて、うまく検索できません。。。うーむ....

無効な取得条件が指定されています。ValidationError [code=11, message=Data does not match any schemas from "oneOf" Where schemaKey = null

どうも、類似した現象が他にも報告されているようで、ORに指定する条件が3つ以上だとエラーになるようです。(え?

...というわけで、同等の機能を実装するために、姓、名の検索をNameの検索に切り替えました。。

...
        <script type="text/javascript">
            var resultHeadCompiled = _.template($('#resultTemplateHead').html());
            var resultCompiled = _.template($('#resultTemplate').html());
            function fireSearch(){
                var searchQuery = $("#searchQuery").val(),
                    queryParam = '%' + searchQuery + '%';
                var contactSearch = new SObjectModel.con();
                contactSearch.retrieve({
                    where: {
                        or: {
                            Name: {like:  queryParam},
                            Email: {like: queryParam}
                        }
                    },
                    limit: 100
...

結果

修正したVisualforceページを表示して、検索ボタンをクリックすると、取引先責任者が正しく表示されるようになりました!

f:id:yonet77:20141207003557p:plain

おわりに

それにしても、Remote Objectにて、ORの検索条件を3つ以上並べるのはNG...という事実にはビックリしました。。
※1 うっかり、こういったバグ?も踏むことができる MiniHack はなかなか興味深いと思いませんか?(笑)
※2 で、実際のところはどうなのか(仕様?バグ?)中の人に教えて頂きたいです!

ちなみに...今回、取引先責任者を検索するようにしてますが、お題の方には

Visualforce Remote Objectを駆使して、Apexを利用せずに、取引先検索のVisualforceページを実現してみましょう。

そう、取引先責任者を使うのではなくて、取引先の検索なのです。。
【要件】に指定されている

同様の機能を実現してください

ということで、取引先責任者の検索と勘違いして、完全に見落としてました。(でもOK頂きました...ありがとうございます)

ちなみに、今回の景品はこんな感じでした。

  • 2枚クリアで、Amazon Gift Card ¥5,000 分(先着20名)
  • 4枚クリアで、PS4(先着3名?)
  • 6枚クリアで、Macbook Air 13inch(抽選1名)

4枚クリアの景品も終了間際まで残ってたりして、結構チャンスはあるようでしたね。
今年参加できなかった方は、来年こそは賞品ゲット...にトライしてみてください!


なお、この記事はSalesforce1 Advent Calendar 2014 の12/8分となります。
いやー今年も無事に書けて良かった....