nkjmkzk.net

powered by Kazuki Nakajima

Archive for the ‘force.com’ tag

Lightningコンポーネント開発のケーススタディ

2015/5/12にSalesforce1 Lightning Fast Seminarが開催され、その中でコンポーネント開発のケーススタディについてお話ししました。

このコンテンツを前撮りしたビデオを共有したいと思います。
アジェンダはこんな感じです。

Chart.jsのデモ

  • サンプルページを用いたイントロダクション
  • アプリケーションビルダーでのページ作成デモ
  • Salesforce1へページを表示

Cellのデモ

  • サンプルページを用いたイントロダクション
  • 開発者コンソールでのページ作成デモ
  • Auradocsでのリファレンス確認方法説明

家計簿のデモ

  • イベントを使ったコンポーネント同士の連携
  • Lightningアプリケーション開発にかかった工数

Enjoy.

without comments

Written by 中嶋 一樹

5月 12th, 2015 at 10:20 pm

Force.com REST APIがついにCORSをサポート

長年待ち焦がれていたCORSがついにForce.com REST API全体でサポートされました。 使いかたもめちゃくちゃシンプルです。早速使ってみましょう。

スクリーンショット 0027-03-03 16.54.53

まずは上図にある通り、Salesforce組織のCORS設定で外部サイトを登録します。

管理 > セキュリティのコントロール > CORSを開き、「新規」ボタンをクリックしてオリジンのURLパターンに外部サイトのURLを設定します。

スクリーンショット_0027-03-03_16_58_49

必要な設定はたったこれだけ。

OAuth2.0でアクセストークンを取得し、外部サイトでホストされているJavascriptからSalesforceにAPIアクセスしてみましょう。ツルッと通るはずです。

内部の動きとしては、下記のようになります。

  1. クロスドメインのリクエストであることをブラウザが検知し、APIアクセスに先立ってpreflightリクエストという「このドメインからのクロスドメインアクセスしてもいいですか?」という確認をSalesforce側におこないます。
  2. Salesforceは当該ドメインがCORS設定に登録されているURLパターンと一致すれば、Access-Control-Allow-xxxxヘッダー付きのレスポンスで承認を通知してくれます。
  3. ブラウザはレスポンスの中身を確認し、OriginやMethodに相違がなければクロスドメインのAPIアクセスを実行します。

もちろん、OAuth2.0で認証をおこないますので事前に接続アプリケーションの設定などは忘れずに。

without comments

Written by 中嶋 一樹

3月 3rd, 2015 at 5:17 pm

Posted in Uncategorized

Tagged with , ,

Lightning ComponentをSublime Textで開発する方法

現在Lightning Componentは基本的に開発者コンソールで開発することになっていますが、実はSublime Textでも開発可能で、sublime-lightingというプラグインが用意されています。今回はその環境構築方法を紹介します。なお、環境はMac OS XとSublime Text 3を前提として進めていきます。

sublime-lightningプライグインのインストール

Force.com CLIをインストールする

sublime-lightningは内部でForce.com CLIを利用しているため、まずForce.com CLIをインストールする必要があります。

まずhttps://force-cli.herokuapp.com/より各OS用のモジュールをダウンロードします。Macの場合、「force」というファイルがダウンロードされるのですが、最初一体このファイルをどうしたらいいのか途方に暮れます。このファイルは実はそのまま実行モジュールになっているので、コマンドラインからこのファイルを実行すればForce.com CLIが起動できます。そのためにはまず下記のようにパーミッションを実行可能なように変更する必要があります。

$ chmod 755 $HOME/Downloads/force

次にこのファイルをサーチパスに移してあげます。

$ sudo mv $HOME/Downloads/force /usr/bin/

本当は/usr/local/binにいれたいところですが、sublime-lightningのサーチパスに入っていないようなので/usr/binとしています。多分どこかで設定できると思いますが、またそれは後日。これでForce.com CLIのインストールは完了です。下記のようにコマンドが実行できるはずです。

$ force help

Sublime TextのPackage Controlをインストールする

MavensMateをインストールしている人は多くの場合すでに導入されていると思いますが、もしSublime TextのPackage Controlが導入されていなければ次のように導入します。

Sublime Textを起動し、メニューバーからView > Show Consoleを選択し、コンソールに下記コードをペーストしてEnterキーを押下します。

import urllib.request,os,hashlib; h = ‘eb2297e1a458f27d836c04bb0cbaf282′ + ‘d0e7a3098092775ccb37ca9d6b2e4b7d’; pf = ‘Package Control.sublime-package’; ipp = sublime.installed_packages_path(); urllib.request.install_opener( urllib.request.build_opener( urllib.request.ProxyHandler()) ); by = urllib.request.urlopen( ‘http://packagecontrol.io/’ + pf.replace(‘ ‘, ‘%20′)).read(); dh = hashlib.sha256(by).hexdigest(); print(‘Error validating download (got %s instead of %s), please try manual install’ % (dh, h)) if dh != h else open(os.path.join( ipp, pf), ‘wb’ ).write(by)

これでPackage Controlのインストールが完了です。

sublime-lightningをインストールする

次にPlugin本体を下記の通りインストールします。

  • Sublime TextのメニューバーからTools > Command Paletteを選択し、Package Control: Install Packageを選択。
  • Lightningを検索して選択するとsublime-lightningがインストールされます。

これでプラグインのインストールは完了です。

Lightning Bundleを作成・編集する

まずSublime Textのワークスペースにディレクトリを作成します。このディレクトリはプロジェクトのような位置付けで、この中に複数のLightning Bundleが作成されることになります。*MYPROJECTは適当な名前に置き換えてください。

$ mkdir -p /MYPROJECT/metadata/aura

次にSublime Textを起動し、メニューバーからFile > Openを選択して作成したディレクトリを開きます。

次にサイドバーのディレクトリを右クリックし、Lightning > Salesforce Loginをクリックします。Sublimeウィンドウの最下部にUsernameを入れるフォームがあらわれるのでログインしたいSalesforce組織のユーザー名を入力しEnterキーを押下します。さらにパスワードを入力してEnterキーを押下します。ログインが成功したことを確認します。*Force.com CLIが正しくインストールされていないとエラーになります。

ログインが成功したらサイドバーからauraディレクトリを右クリックし、Lightning > Create App/Component/Eventをクリックすれば新しいLightning Bundleを作成できます。通常どおりファイルを保存するとファイルがSalesforce組織に保存されます。

既存のLightning Bundleを編集するには、サイドバーからauraディレクトリを右クリックし、Lightning > Fetch Lightningを選択して任意のLightning Bundleを選択すれば、auraディレクトリ配下にロードされます。


これで快適にLightning Componentを開発する環境が整いました。 Enjoy.

without comments

Written by 中嶋 一樹

2月 11th, 2015 at 11:11 am

Posted in Uncategorized

Tagged with , , ,

リーン・スタートアップとappexchange

僕はエリック・リースが書いたリーンスタートアップという本が出版された2年半ほど前、この本を読んですぐに「この考え方はForce.com, appexchangeとの相性が抜群だな」と感じました。

その後しばらく適当なチャンスがなかったのですが、今回12/4に開催されたSalesforce Would Tour東京にて、とあるセッションを担当することになり、「はっ、これは、」と思い出し、ようやくリーンスタートアップとForce.comというテーマでプレゼンをすることができました。その内容をこちらにまとめておこうと思います。

リーン・スタートアップとは?

「限られた滑走路の中で、無駄をなくして成長のエンジンを最速でチューニングするための方法論」

runway

1行で言えば、これだと思っています。

リーン・スタートアップは元々は「ジャストインタイム方式」や「一個流し」に代表されるトヨタ生産方式に端を発しています。トヨタはこれらの生産方式で高効率に高品質な車を量産することに成功したわけですが、それ生産方式がリーン生産方式としてMITで焼き直され、さらに製品の製造のみならずスタートアップを成功させるための方法論としてまとめられたのがこのリーン・スタートアップです。

リーンというのは「贅肉をそいだ」とか「無駄をはぎとった」という意味で用いられており、スタートアップがおこなう努力からとにかく徹底的に無駄を省くことがこの方法論の幹になっています。

著者のエリック・リースは、スタートアップとは「とんでもなく不確実な中で新しい製品やサービスを生みださなければいけない人的組織」と定義しています。さらにつけ加えれば、とんでもなく不確実な中でも、金と時間は確実に限りがある環境、だと思います。大企業であれば一つの部門で多少まわり道をしたとしても、事業が揺るぐことはそうそうありませんが、スタートアップは大企業に比べて財政的、時間的余裕が桁外れに少ないことがほとんどで、まわり道はGame Over(倒産)に直結します。

そんな中、無駄な活動をすべて省き、限られた時間内で離陸できるように成長エンジンの加速に集中することがリーン・スタートアップの核心であると僕は理解しました。

挑戦の要

起業する人であれば、誰しもビジョンがあり、そのビジョンを実現させるための仮説を明示的あるいは暗黙的にでも持っているはずです。この仮説のことを挑戦の要と呼んでいます。この仮説が事実となれば事業は成功する、ただしそうでなければスタートアップは戦略の変更を余儀なくされる、そういうものが挑戦の要です。

リーン・スタートアップのファーストステップはこの挑戦の要を、出来る限り分解した形で書き出すことです。

フィードバック・ループ

feedback loop

構築 – MVP

挑戦の要が設定できたら、いよいよ製品・サービスの開発を開始します。多くの人は製品を最初に公開する際に「市場に失望されたくない」と思っているのではないでしょうか。そうなるとビジョンをより完璧に実現するためのかなりリッチな製品を開発する必要がでてきます。

しかし、リーン・スタートアップでは必要最低限の製品(MVP – Minimum Viable Product)をリリースすることを提唱しています。これは、さんざん機能を詰め込んだ製品をリリースした場合、最もベースとなる仮説が間違っていたらそのすべての苦労が水の泡、つまり無駄に終わってしまうからです。

MVPの考え方とは、あくまでも挑戦の要を検証する単位で製品をリリースしていき、仮説をひとつずつ検証しながら進んでいくことを意識したものです。仮説が誤っているかもしれない状況でその仮説に基づいてさらなる仮説を検証するのは「無駄」という考え方です。

計測 – コホート分析・スプリットテスト

MVPをリリースしたらその結果を入手し、判定しなければなりません。仮説はあっていたのか、間違っていたのか。この製品で成長のエンジンはチューニングされたのか、されなかったのか。これを正しく判断するための方法をリーン・スタートアップでは「革新会計」と呼んでいます。

革新会計のミソは、かくあるべき目標数値を定め、現状を真摯に観察することにあります。あたり前の話のようですが、実際の現場では全体の右肩上がりの数字にごまかされ、真に成長を意味する数字が観察されていないことが多々あると著者は説いています。

そして現状の観察には「コホート分析」なる手法が多くの場合適切なパフォーマンスを示すことができるとされています。コホート分析とはある一定セグメントを抽出し、そこだけでの数値をまとめていく分析手法です。もっと簡単に言えば、累積ユーザー数をしめすグラフではなく、単月での増加ユーザー数をしめすようなグラフがコホート分析、ということになります。これをみることで多くのITスタートアップで計測すべき、ユーザーの増加率をつぶさに観察することができ、累積数値の増加のような虚構の数字に惑わされることがなくなる、というわけです。

また、MVPをリリース後に製品を最適化・拡張した場合に、その拡張自体のフィードバックを純粋に計測するにはスプリットテストが用いられます。これは特定顧客にのみ新機能を提供し、機能を提供しなかった顧客グループとの反応差分を確認して、製品拡張のインパクトをテストするというものです。

学習 – 製品最適化・戦略転換(ピボット)

フィードバック・ループの最後のステージでは、計測で得られた結果をもとに製品の最適化が必要かどうか、あるいは仮説が正しかったかを判断し、場合によっては戦略から転換しなければならないかを決断します。


さて、長くなりましたがフィードバック・ループの一連のステージを説明してきました。 ここで重要なのは、このループをできる限り速くまわることです。

例えばあるスタートアップの滑走路が残り12ヶ月だったとします。そのときにこのフィードバック・ループを1周するのにもし10ヶ月かかってしまい、さらにその学習結果ピボットが必要だと判断された場合、そのスタートアップはおおよそ詰んだものと考えられます。もし、2ヶ月で最初の1周をまわっていたら、あと5回は最適化とピボットの余力が残っています。なのでこのループを速くまわることがスタートアップが成功するためには不可欠である、というのがリーン・スタートアップの極めて重要なポイントです。

Force.com + appexchangeでフィードバック・ループをどうまわすか?

MVPの開発

構築のステージをまずみていきましょう。 この記事を呼んでいただいているほとんどの方にとってMVPとはなんらかのアプリ開発(とりわけクラウド型の)を意味すると思います。その場合、最終的にユーザーに価値をもたらすのはアプリの機能であって、インフラではないはずです。

Force.comは「フルマネージド」なアプリケーションPaaSです。優劣の問題ではなく、特性として、Force.comはユーザーが自由にインフラ構成を変えれるようなPaaSではなく、冗長性、拡張性、対災害、セキュリティ(インフラの)といった面において完全にSalesforceが面倒を見てくれます。逆にユーザーがこれをカスタマイズすることはできません。

ただ、前述の通りスタートアップがユーザーに直接価値を提供できるのはアプリの機能であり、限られた金、時間、人を投資するのはアプリであるべきでしょう。そういう観点ではフルマネージドなForce.comはその他面倒なことをスタートアップから取り去ってくれるうってつけのプラットフォームと言えます。

また、Force.comはいわずもがなクイックにアプリを開発することに非常に長けた開発環境です。それは下記3点から導かれると僕は考えています。

  • ノンプログラミング開発
  • モバイルUI自動生成
  • API自動生成

Force.comはそもそもプログラミングをおこなわずしてアプリを作成することができるという点で極めてユニークなPaaSです。ドラッグ&ドロップと設定作業だけでかなりパワフルなアプリを作成することができます。要件がハマれば、いかにHTML, js, php, javaなどにとんがったプログラマーでも、このノンプログラミングの開発スピードにはちょっとかないません。

しかもこのノンプログラミングで開発されたアプリは自動的にモバイル対応します。iPhoneやAndroid端末でアクセスするとあら不思議、モバイルに最適化されたUIがあらわれます。シングルコードであらゆるデバイスに対応するレスポンシブはHTML5界隈では欠かせない手法になっていますが、このノンプログラミング開発では「レスポンシブ」という言葉さえ意識する必要がありません。勝手にモバイル対応UIが生成されるのです。

s1

ただし、こういったUIフレームワークを使うということはある程度規約・制約に縛られることになります。なのでUIに細かな要件が続出してきた場合、ノンプログラミング開発だけで対応するのは難しくなるというもの事実です。

ここで重要なのは、今作っているのはMVPであって、製品の最終形ではないということです。

あくまでもMVPは仮説を検証するものであり、それをベースに顧客からフィードバックを得ることが最大の目的です。そういう意味では、UIフレームワークを使って素早く基本機能を構築し、それをベースにユーザーから正しいUIの姿をヒアリングしてから最終UIの開発に着手しても遅くはないはずです。もしかしたら、「このままでOK」というケースもあり得ます。そうした場合にガッツリUIをコーディングしていたら「無駄」になっていまいます。常に無駄を最小限にすることを意識するのがリーン・スタートアップです。

そして最終的に完全にスクラッチからUIを開発する場合、Force.comはそのまま利用することができます。

現在モダンなHTML5アプリはSPA (Single Page App/Architecture)で設計されていることが多く、こういったアプリはjavascriptからバックエンド(多くの場合クラウドデータベース)にajaxコールをおこなうため、バックエンド側はAPIの整備が必須になります。

spa-api Force.comはデータベースのテーブルを作成した時点で自動的にそのテーブルに対するREST APIが有効になります。つまり基本的なデータベース操作であれば、APIは勝手に作成されるため開発者が作る必要はないのです。そのため、スタートアップ企業はこういったバックエンド側の開発作業から解放され、差別化要素となるUIに開発リソースを集中させることができます。資源が限られたスタートアップ企業であればこういった恩恵は大きいと思います。

CRMによるコホート分析とappexchangeによるスプリットテスト

コホート分析とは、売り上げやユーザー数の累計データではなく、ある一時点での新規売り上げ、新規ユーザー数を定期的に観察する分析方法で、スタートアップの成長エンジンが正しくチューニングされたかどうかを計測するのに有用な分析手法だとされています。

Force.com上でアプリを開発し、appexchangeにアプリを公開する場合、通常1ユーザー15000円/月のSales Cloud(CRM)のライセンスが2ユーザー分無料で提供されるのですが、このCRMを利用するとコホート分析をすぐにおこなうことができます。というのも、appexchangeとはアプリケーションを公開するマーケットプレイスであると同時に、誰がアプリのデモを試したのか、誰がインストールしたのか、誰がアクティベートしたのか、というような情報をフィードバックする仕組みを備えており、このフィードバックが自動的に先ほどのCRMに入ってくるのです。

appexchange feedback

つまりappexchangeにアプリを公開すると黙っていてもフォードバックのデータが蓄積され、それをCRMのレポート機能がすぐにコホート分析することができます。

また、appexchangeはスプリットテストをおこなう仕組みも備えています。スプリットテストとは、個別の機能拡張がユーザーにどのような影響を与えたのかを正確に計るために、1機能ごとにその新機能を組み込んだバージョンと組み込んでいないバージョンを同時提供し、1つのユーザーグループは新機能あり版、もう一つのユーザーグループは新機能なし版を使ってもらい、その反応の差分をみるという計測方法です。

split test

スプリットテストをおこなうには複数のバージョンを同時提供する必要がありますが、通常Webサービスは同じドメインで提供されるため、提供元は一カ所となり、ユーザー毎にバージョンを切り替えるのは簡単ではありません。

しかし、Force.comはWebサービスであるものの、ユーザー毎に専用の環境を提供するマルチテナント方式を採用しています。このため、開発者は能動的にユーザーをグループ分けして異なるバージョンを提供するということが簡単にできてしまいます。そして先ほどのコホート分析でそれぞれのユーザーグループの反応を計測することができるわけです。

ちなみに、Force.com上で開発したアプリを無償で提供する場合、appexchangeへの公開を含むすべてのプロセスで一切費用はかかりません。出展費用も、プラットフォーム費用も。全く出費することなくMVPを開発してマーケティングをおこなうことができるのです。ちょっとびっくりするようなスキームですが、ほんとの話です。

製品最適化と戦略転換(ピボット)

このステージではプラットフォームは利用しません。これまでに計測で得られた結果をもとに、組み込んだ新機能が正しかったのかどうか、製品を修正することが必要なのか、あるいは仮説・戦略そのものがあっていたのかどうかを見極めて、必要に応じて最適化と転換をおこなう決断をおこないます。


いかがでしょうか。

冒頭にも書きましたが、僕はリーン・スタートアップを知ったときにこの考え方はForce.comでの開発哲学に通ずる部分が多く、Force.comとappexchangeを利用することで、手作りする部分を少なくし、フィードバックループを速くまわすことができるはずだと考えています。リーンスタート・アップのコンセプトにSalesforceのアプリケーションPaaSは最適なお供でだと思います。

summary

ご興味をもたれた方はSalesforce丸の内オフィスで毎月開催しているappexchangeスタートアップトレーニングに是非参加してみてください。

without comments

Written by 中嶋 一樹

12月 8th, 2014 at 11:34 am

Force.com Canvasに一般ユーザーでもインストールできる「個人用」が登場

これまでSalesforceにおいてアプリはシステム管理者しかインストールできないものでしたが、Winter’15で「キャンバス個人用アプリケーション」が登場し、システム管理者ではない一般ユーザーが自由にアプリをインストールする、という機能が追加されます。

アプリそのものはこれまでのForce.com Canvasと同じ仕様ですが、以下の点においてユニークなものとなります。

  • 一般ユーザーがインストールできる。
  • インストールしたアプリは自分のChatterタブにのみ表示される。

ISVの方々はこの仕組みを利用すればより広い裾野のユーザーに対して自社のアプリを触ってもらうことが可能になると思います。

このキャンバス個人用アプリを素早くマスターするための「キャンバス個人用アプリ 速習ガイド」を作成してみました。ピンと来た方は是非チェックしてみてください。

キャンバス個人用アプリ 速習ガイド from Kazuki

without comments

Written by 中嶋 一樹

10月 3rd, 2014 at 1:47 pm

Posted in Uncategorized

Tagged with , ,

すごいアンケート2の新機能

すごいアンケート2の新しいバージョンがリリースされました。パッケージインストールURLはこちらです。今回のバージョンアップで搭載された新機能をご紹介します。

各ゲスト専用のアンケート回答URL

各ゲスト毎に専用のアンケートURLが生成されるようになりました。このURLからアンケートページにアクセスすると、名前・Email等ゲスト情報に関する質問は自動的に隠されます。ゲストは自身の情報を入力する必要がなくなりますが、回答は自動的に対象ゲストに紐づけられます。

このURLはゲストタブで各ゲストをクリックして表示されるゲストの詳細画面で確認できます。あわせて、メールで各ゲストにアンケートURLを送信する機能ではこのゲスト専用のURLが送信されます。

url_for_guest

send_survey_link

ゲスト登録ページ

ゲストが各アンケートに事前にサインアップするページを用意しました。これはイベント等で事前にサインアップがおこなわれる場合に利用することができる簡易なゲスト登録機能です。名前やEmailといった個人情報に加えてサインアップ時に収集しておきたい情報を事前アンケートとして同時に収集することができます。

enable_guest_signup

guest_signup_page

インストール時のセットアップガイドをパッケージに同梱

すごいアンケートはインストール後にサイトの設定、権限セットの割り当てというセットアップが必要ですが、これをスムースにおこなえるような初期セットアップ画面を用意しました。インストール後にアプリにアクセスすると自動的にこのセットアップ画面が表示されます。別途マニュアルをダウンロードせずともアプリの中で必要なセットアップを確認して実施することができます。

getting_started

ゲストから取引先および取引先責任者を作成するボタン

ゲスト情報から取引先および取引先責任者を自動的に作成する機能を追加しました。完全名称一致の既存取引先がある場合はその取引先の配下に取引先責任者が作成されます。また、取引先責任者がすでに存在するゲストは右上に取引先責任者へのリンクが表示されます。

create_contact

link_to_contact

新しいアンケート編集ページ

アンケート編集ページがより直感的なUIとなりました。最終的に公開されるアンケートページとほぼ同じUIですので仕上がりを容易にイメージすることができます。

new_survey_edit_page

ブランド設定

アンケートページにロゴを表示することができるようになりました。

branded_survey


加えていくつかの不具合が修正されています。

  • アンケートのプロパティで日付が正しく表示されない。
  • インストール後の初期設定ページが正しく表示されない。
  • ゲストが取引先責任者に紐づかない。

また、今回のバージョンからアンケートのURLが変更になっています。以前までのアンケートURL(旧URL)は、今回のバージョンでは後方互換性のために利用できるようになっていますが、次のバージョンでは利用できなくなる予定です。ご注意ください。

*アンケートURLは今回の変更でかなり長くなっているので、近々URL Shortnerを組み込むようなことを考えています。

デモ環境はこちらのアプリ紹介ページより「テストドライブを利用」をクリックするとアクセスできます。

すごいアンケート on appexchange

Enjoy!

without comments

Written by 中嶋 一樹

9月 4th, 2014 at 10:28 am

AngularJSからForce.com REST APIにアクセスするためのツールキット、forcetk4ng

概要

forcetk4ngはAngularJSのモジュールで、このモジュールを利用することでVisualforce内のJavascriptから簡単にForce.comのREST APIをコールできるようになります。

すでに世の中にはJavascriptからForce.com REST APIにアクセスするためForce.com Javascript REST Toolkitjsforceというすばらしいライブラリが存在するわけですが、forcetk4ngはよりAngularJSの記法に忠実にAPIコールをおこなうことができます。

より具体的にはforcetk4ngはAngularJS標準のpromise/deferred実装である$qを使用しているため、$qのpromiseを利用してAPIコールを含む処理を連続的に記述することができます。

また、forcetk4ngはAngularJS以外に依存するライブラリはありませんのでシンプルな構成となります。

要件

  • AngularJS
  • Force.com Visualforce (現在forcetk4ngはVisualforce上でのみ動作します。)

サンプルコード

どんなふうに記述できるのか、まずはサンプルコードをみておきましょう。

<apex:page standardStylesheets="false" showHeader="false" applyHtmlTag="false" applyBodyTag="false" docType="html-5.0">
<html ng-app="ngbootcamp">
<head>
<script src="{!$Resource.angular_min_js}"></script>
<script src="{!$Resource.forcetk4ng_js}"></script>

<script>
angular.module('ngbootcamp', ['forcetk4ng'])
.controller('guestCtl', function($scope, force){

    force.setAccessToken('{!$Api.Session_ID}');
    $scope.guest = {};

    $scope.getGuests = function(){
        force.query("select Id, Name from guest__c")
        .then(
            function(records){
                $scope.guest.records = records;
            },
            function(event){
                console.log(event);
            }
        );
    }
});
</script>
</head>

<body>
    <div ng-controller="guestCtl">
        <button ng-click="getGuests()">Retrieve Guests</button>
        <div ng-repeat="record in guest.records">
            {{record.Name}}
        </div>
    </div>
</body>
</html>
</apex:page>

使い方

Sample Codeを順に追いかけながら使い方を見ていきます。

ライブラリの入手

Githubのレポジトリからforcetk4ng.jsをダウンロードします。
https://github.com/nkjm/forcetk4ng

静的リソースへアップロード

AngularJS本体とforcetk4ngをForce.comの静的リソースにアップロードします。CDNを利用してもかまいませんが、その場合の後のscriptのパスは適宜読み替えてください。

angular.min.js
- 名前: angular_min_js
- キャッシュコントロール: 公開

forcetk4ng.js
- 名前: forcetk4ng_js
- キャッシュコントロール: 公開

ライブラリの読み込み

新しいVisualforceページを作成するか既存のVisualforceページを開きます。そしてライブラリファイルを読み込みます。

<script src="{!$Resource.angular_min_js"></script>  
<script src="{!$Resource.forcetk4ng_js"></script>

モジュールのInjection

forcetk4ngを利用するモジュールに差し込みます。

angular.module('ngbootcamp', ['forcetk4ng'])

forceサービス利用の宣言

forcetk4ngの関数群は”force”というサービス名で利用できます。 コントローラーでforceサービスの使用を宣言します。

.controller('guestCtl', funciton($scope, force){

アクセストークンのセット

forcetk4ngはForce.com REST APIを利用するため、明示的にアクセストークンをセットする必要があります。Visualforce上では簡単にセッションIDを取得でき、これがアクセストークンとして利用できます。このセッションIDをそのままforce.setAccessToken()関数に渡すことでアクセストークンのセットが完了します。

force.setAccessToken('{!$Api.Session_ID}');

forceサービスの実行

コントローラー内でforceサービスを実行します。forceサービスが提供するAPIをコールする関数群(ajax/query/retrieve/create/update/upsert/delete/describe)はすべて$qサービスが提供するPromiseインスタンスを返します。このPromiseインスタンスではthen()、catch()、finally()といったメソッドが提供されており、APIコールの結果に応じて次の処理を脈々と記述することが可能です。

force.query("select Id, Name from guest__c")
.then(
    // 成功時のコールバック
    function(records){
        $scope.guest.records = records;
    },
    // 失敗時のコールバック
    function(event){
        console.log(event);
    }
);

提供されている関数

現在forcetk4ngでは下記の関数が提供されています。

  • force.setAccessToken(ACCESS_TOKEN)
  • force.setApiVersion(API_VERSION)
  • force.ajax(PATH, METHOD, RESPONSE_TYPE)
  • force.query(SOQL)
  • force.retrieve(OBJECT_TYPE, RECORD_ID, FIELDS)
  • force.create(OBJECT_TYPE, RECORD)
  • force.update(OBJECT_TYPE, RECORD)
  • force.upsert(OBJECT_TYPE, EXTERNAL_ID_FIELD, EXTERNAL_ID, RECORD)
  • force.delete(OBJECT_TYPE, RECORD_ID)
  • force.describe(OBJECT_TYPE)

Enjoy.

without comments

Written by 中嶋 一樹

8月 7th, 2014 at 4:07 pm

Posted in Uncategorized

Tagged with ,

AngularJSではじめるHTML5開発 – Part9 enquire.jsを用いたレスポンシブデザインでモバイル対応

Part8の続きです。

今回はHTML5らしいレスポンシブデザインを適用していきます。

まずは今回開発するレスポンシブデザインをデモでみてみましょう。

デモ

ゲストが選択されていない状態、選択された状態、いろんなシーンでブラウザを横方向に伸縮させてみてください。動的にデザインが切り替わります。(注:IEでは正しく動作しない可能性があります)

それでは実装へ。

 

viewportの設定

実はこのサイトはすでにレスポンシブデザインが適用されています。Bootstrapはデフォルトでレスポンシブ対応となっているからです。

モバイルサイトでおなじみの下記のメタタグを<head>配下に追記してサイトにモバイルデバイス、あるいはブラウザ幅を狭くしてアクセスしてみてください。

<head>
	<meta name="viewport" content="width=device-width, initial-scale=1.0"></meta>
	<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.1.0/css/bootstrap.min.css"></link>

ブラウザ幅が992px未満になるとレイアウトが2列から1列に変更されるはずです。上記viewportの設定を入れればモバイルデバイスでも快適な解像度でサイトが閲覧できますね。

ただし、レイアウトが変更されるだけでは実際には物足りません。

ゲストリストがながーーくなってきた場合、モバイルデバイスで任意のゲストをタップし、その詳細情報をみるには随分と下までスクロールするはめになります。モバイルデバイスでは任意のレコードを選択したらゲスト一覧は非表示にし、詳細情報のみ表示されるようになれば格段にユーザビリティがあがりますよね。

そしてそういうユースケースで便利なのがenquire.jsというライブラリです。enquire.jsはブラウザ幅を常に監視し、ある条件にマッチすると任意の処理を実行することができます。この仕組みを応用すれば真にレスポンシブなサイトが構築できそうです。

 

enquire.jsを読み込む

まずはenquire.jsを読み込みます。このライブラリもCDNで提供されているので下記のように読み込むだけでOKです。

<head>
	<meta name="viewport" content="width=device-width, initial-scale=1.0"></meta>
	<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.1.0/css/bootstrap.min.css"></link>
	<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script>
	<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.1.0/js/bootstrap.min.js"></script>
	<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.10/angular.min.js"></script>
	<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-bootstrap/0.10.0/ui-bootstrap-tpls.min.js"></script>
	<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/enquire.js/2.0.0/enquire.min.js"></script>
	<c:RemoteTK />

 

ブラウザ幅に応じたアクションの設定

早速条件とアクションを設定しましょう。

        enquire.register("screen and (max-width:991px)", {
            match : function(){
            	$scope.device = 'sm';
            	$scope.$apply()
            },
            unmatch : function(){
            	$scope.device = 'md';
            	$scope.$apply();
            }
        });

ブラウザ幅は991px以下の場合にmatchに指定した処理が、992px以上になった場合にunmatchに指定した処理が実行されます。

ここでは$scope.deviceにsm(small、モバイルデバイスと仮定)またはmd(medium)をセットしています。

 

HTMLマークアップをブラウザ幅に応じて表示/非表示を切り替える

まず、モバイルデバイスかつゲストが選択されたときにゲストリストを非表示にする設定です。

			<div class="col-md-4" ng-hide="device == 'sm' && guest != null">
				<div class="panel panel-default">
					<div class="panel-heading">
						ゲスト
						<button type="submit" class="btn btn-xs btn-default pull-right" ng-click="openNewGuestForm()"><span class="glyphicon glyphicon-plus"></span>&nbsp; 新規</button>
					</div>
					<div class="list-group">
						<a class="list-group-item" href="#" ng-click="getGuest(guest.Id)" ng-repeat="guest in guests">{{guest.Name}}</a>
					</div>
				</div>
			</div>

ng-hide (no-showの逆の効果。条件が真になると要素を非表示にします)で、前述の条件を設定します。

 

次に、モバイルデバイスかつゲストが選択されていないときにはゲストの詳細を表示する必要がないのでこれも非表示にしましょう。

			<div class="col-md-8" ng-hide="device == 'sm' && guest == null">
				<h1>{{guest.Name}}</h1>
				<form role="form">
					<div class="form-group">
						<label>ゲスト名</label>
						<input ng-model="guest.Name" type="text" class="form-control" placeholder="ゲスト名" />
					</div>
					<div class="form-group">
						<label>Email</label>
						<input ng-model="guest.email__c" type="email" class="form-control" placeholder="Email" />
					</div>
					<div class="form-group">
						<button class="btn btn-success" ng-click="updateGuest()">更新</button>
					</div>
				</form>
			</div>

 

最後に、モバイルデバイスでゲスト詳細が表示されているとき、ゲスト一覧に戻るためのボタンを設置しておきましょう。

			<div class="col-md-8" ng-hide="device == 'sm' && guest == null">
				<div ng-show="device == 'sm' && guest != null" style="margin-bottom:20px;">
					<button type="button" class="btn btn-default btn-block" ng-click="guest = null">
						<span class="glyphicon glyphicon-chevron-left"></span>&nbsp; ゲスト一覧へ
					</button>
				</div>
				<h1>{{guest.Name}}</h1>
				<form role="form">
					<div class="form-group">
						<label>ゲスト名</label>
						<input ng-model="guest.Name" type="text" class="form-control" placeholder="ゲスト名" />
					</div>
					<div class="form-group">
						<label>Email</label>
						<input ng-model="guest.email__c" type="email" class="form-control" placeholder="Email" />
					</div>
					<div class="form-group">
						<button class="btn btn-success" ng-click="updateGuest()">更新</button>
					</div>
				</form>
			</div>

このボタンをクリックするとng-clickに指定してある通り、guestがクリアされます。これによってゲストが選択されていない状態となり、ゲストリストの表示に切り替わります。

さて、動作を確認してみてください。モバイルデバイスで表示した際には最小限の情報のみが出力されるのはもちろん、PCのブラウザでもブラウザ幅を変更するとリアルタイムにレイアウトと表示される情報が切り替わります。

また、前回までに作成したモーダルダイアログも適切に表示されます。これは元々Bootstrapがレスポンシブに対応している恩恵ですね。

例によってこれまでに作成したindexファイルの全ソースを掲載しておきます。

<apex:page showHeader="false" standardStyleSheets="false" applyBodyTag="false" applyHtmlTag="false" docType="html-5.0" >
  
<html ng-app="ngbootcamp">
<head>
	<meta name="viewport" content="width=device-width, initial-scale=1.0"></meta>
	<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.1.0/css/bootstrap.min.css"></link>
	<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script>
	<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.1.0/js/bootstrap.min.js"></script>
	<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.10/angular.min.js"></script>
	<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-bootstrap/0.10.0/ui-bootstrap-tpls.min.js"></script>
	<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/enquire.js/2.0.0/enquire.min.js"></script>
	<c:RemoteTK />
	<script>
	var ngbootcamp = angular.module('ngbootcamp', ['ui.bootstrap']);

	ngbootcamp.controller('guestCtl', function($scope, $modal, $q, $timeout){
		$scope.openNewGuestForm = function(){
			$scope.newGuest = {};
			$modal.open({
				templateUrl: "T_newGuestForm",
				scope: $scope
			});
		}

		$scope.createGuest = function(){
			$modal.open({
				templateUrl: "T_inProgress",
				backdrop: "static",
				scope: $scope
			});

			$scope.remotingProgress = 33;
			$scope.remotingStatus = "ゲストを作成しています...";

			$scope.deferredCreateGuest()
			.then(
				function(){
					$scope.remotingProgress = 66;
					$scope.remotingStatus = "ゲストリストをリフレッシュしています...";
					return $scope.deferredGetGuests();
				},
				function(result){
					return $q.reject(result);
				}
			)
			.then(
				function(guests){
					$scope.guests = guests;
					$scope.newGuest = {};
					$scope.remotingProgress = 100;
					$scope.remotingStatus = "作成が完了しました。";
				},
				function(result){
					console.log(result);
				}
			);
		}

		$scope.updateGuest = function(){
			$modal.open({
				templateUrl: "T_inProgress",
				backdrop: "static",
				scope: $scope
			});

			$scope.remotingProgress = 33;
			$scope.remotingStatus = "データを更新しています...";

			$scope.deferredUpdateGuest()
			.then(
			function(){
				$scope.remotingProgress = 66;
				$scope.remotingStatus = "ゲストリストをリフレッシュしています...";
				return $scope.deferredGetGuests();
			},
			function(result){
				return $q.reject(result);
			})
			.then(
			function(guests){
				$scope.guests = guests;
				$scope.remotingProgress = 100;
				$scope.remotingStatus = "更新が完了しました。";
			},
			function(result){
				console.log(result);
			});
		}

		$scope.deferredCreateGuest = function(){
			var deferred = $q.defer();

			$scope.force.create(
				"guest__c",
				$scope.newGuest,
				function(result){
					deferred.resolve();
				},
				function(result){
					deferred.reject(result);
				}
			);
			return deferred.promise;
		}

		$scope.deferredUpdateGuest = function(){
			var deferred = $q.defer();
			var guest = angular.copy($scope.guest);
			delete guest.attributes;

			$scope.force.update(
				"guest__c",
				$scope.guest.Id,
				guest,
				function(result){
					deferred.resolve();
				},
				function(result){
					deferred.reject(result);
				}
			);
			return deferred.promise;
		}

		$scope.getGuest = function(recordId){
			$scope.force.retrieve(
				"guest__c",
				recordId,
				"Id,Name,email__c",
				function(result){
					$scope.guest = result;
					$scope.$apply();
				},
					function(result){
					console.log(result);
				}
			);
		}

		$scope.deferredGetGuests = function(){
			var deferred = $q.defer();
			var soql = "select Id, Name, CreatedDate from guest__c";
			$scope.force.query(
				soql,
				function(result){
					deferred.resolve(result.records);
				},
				function(result){
					deferred.reject(result);
				}
			);
			return deferred.promise;	
		}

		$scope.force = new remotetk.Client();
		$scope.deferredGetGuests()
		.then(
			function(guests){
				$scope.guests = guests;
			},
			function(result){
				console.log(result);
			}
		);

        enquire.register("screen and (max-width:991px)", {
            match : function(){
            	$scope.device = 'sm';
            	$scope.$apply()
            },
            unmatch : function(){
            	$scope.device = 'md';
            	$scope.$apply();
            }
        });
	});
	</script>
</head>
<body ng-controller="guestCtl">
	<div class="container" style="margin-top:20px;">
		<div class="row">
			<div class="col-md-4" ng-hide="device == 'sm' && guest != null">
				<div class="panel panel-default">
					<div class="panel-heading">
						ゲスト
						<button type="submit" class="btn btn-xs btn-default pull-right" ng-click="openNewGuestForm()"><span class="glyphicon glyphicon-plus"></span>&nbsp; 新規</button>
					</div>
					<div class="list-group">
						<a class="list-group-item" href="#" ng-click="getGuest(guest.Id)" ng-repeat="guest in guests">{{guest.Name}}</a>
					</div>
				</div>
			</div>
			<div class="col-md-8" ng-hide="device == 'sm' && guest == null">
				<div ng-show="device == 'sm' && guest != null" style="margin-bottom:20px;">
					<button type="button" class="btn btn-default btn-block" ng-click="guest = null">
						<span class="glyphicon glyphicon-chevron-left"></span>&nbsp; ゲスト一覧へ
					</button>
				</div>
				<h1>{{guest.Name}}</h1>
				<form role="form">
					<div class="form-group">
						<label>ゲスト名</label>
						<input ng-model="guest.Name" type="text" class="form-control" placeholder="ゲスト名" />
					</div>
					<div class="form-group">
						<label>Email</label>
						<input ng-model="guest.email__c" type="email" class="form-control" placeholder="Email" />
					</div>
					<div class="form-group">
						<button class="btn btn-success" ng-click="updateGuest()">更新</button>
					</div>
				</form>
			</div>
		</div>
	</div>

	<!-- Modal for newGuestForm -->
	<script type="text/ng-template" id="T_newGuestForm">
		<div class="modal-header">
			<button type="button" class="close" ng-click="$dismiss()">&times;</button>
		    <h3>新規ゲスト</h3>
		</div>
		<div class="modal-body">
			<form role="form">
				<div class="form-group">
					<label>ゲスト名</label>
					<input ng-model="newGuest.Name" type="text" class="form-control" placeholder="ゲスト名" />
				</div>
				<div class="form-group">
					<label>Email</label>
					<input ng-model="newGuest.email__c" type="email" class="form-control" placeholder="Email" />
				</div>
			</form>
		</div>
		<div class="modal-footer">
			<button type="button" class="btn btn-success" ng-click="createGuest()">作成</button>
		</div>
	</script><!-- Modal for newGuestForm-->

	<!-- Modal for inProgress -->
	<script type="text/ng-template" id="T_inProgress">
		<div class="modal-header">
		    <h3>
		    	<span ng-show="remotingProgress < 100">処理中</span>
		    	<span ng-show="remotingProgress == 100">完了</span>
	    	</h3>
		</div>
		<div class="modal-body">
			<div>{{remotingStatus}}</div>
		    <progressbar ng-class="(remotingProgress < 100) ? 'progress-striped active' : 'progress'" value="remotingProgress" type="success"></progressbar>
		</div>
		<div class="modal-footer" ng-show="remotingProgress == 100">
			<button type="button" class="btn btn-success" ng-click="$close()">閉じる</button>
		</div>
	</script><!-- Modal for inProgress-->

</body>
</html>
  
</apex:page>

 

関連情報

without comments

Written by 中嶋 一樹

2月 20th, 2014 at 1:15 pm

AngularJSではじめるHTML5開発 – Part8 モーダルダイアログによる新規レコード作成フォーム

Part7の続きです。

今回は新規ゲスト作成フォームを実装していきます。Part6で登場したモーダルダイアログ、そしてPart7で登場したPromise/Deferredパターンを駆使していきます。

new_guest_form

まずは今回開発する部分をデモでみてみましょう。

デモ

それでは実装へ。

 

新規ゲストフォームのモーダルダイアログを作成する

まずモーダルダイアログとなるHTMLマークアップを作成しておきましょう。

	<!-- Modal for newGuestForm -->
	<script type="text/ng-template" id="T_newGuestForm">
		<div class="modal-header">
			<button type="button" class="close" ng-click="$dismiss()">&times;</button>
		    <h3>新規ゲスト</h3>
		</div>
		<div class="modal-body">
			<form role="form">
				<div class="form-group">
					<label>ゲスト名</label>
					<input ng-model="newGuest.Name" type="text" class="form-control" placeholder="ゲスト名" />
				</div>
				<div class="form-group">
					<label>Email</label>
					<input ng-model="newGuest.email__c" type="email" class="form-control" placeholder="Email" />
				</div>
			</form>
		</div>
		<div class="modal-footer">
			<button type="button" class="btn btn-success" ng-click="createGuest()">作成</button>
		</div>
	</script><!-- Modal for newGuestForm-->

ほとんどのマークアップはPart6で紹介したものなので容易に理解できると思います。

各フォームがバインドするデータモデルは既存のゲストと区別するためにnewGuestとなっています。

 

モーダルダイアログを開くopenNewGuestForm()を作成する

		$scope.openNewGuestForm = function(){
			$scope.newGuest = {};
			$modal.open({
				templateUrl: "T_newGuestForm",
				scope: $scope
			});
		}

モーダルダイアログを開く部分はもうご存知の通りです。重要なのはその手前でデータモデル$scope.newGuestを初期化しているところです。

これはscopeの階層構造上必要となる処理です。

子scopeは親scopeのプロパティを参照できますが、親scopeは子scopeのプロパティにアクセスできません。

今回、openNewGuestForm()は親scopeとなり、モーダルダイアログはその子scopeとして作成されます。したがって、newGuestを親scopeで定義しておくとモーダルダイアログではそれを参照する形になりますが、親scopeでnewGuestを定義していなかった場合は、モーダルダイアログのng-modelの指定によって子scopeでnewGuestが作成されることになります。

後続の処理で親scopeがnewGuestへのアクセスを必要としますので今回は親scope側でデータモデルを作成しています。

 

新規ゲスト作成処理の流れを記述するcreateGuest()を作成する

		$scope.createGuest = function(){
			$modal.open({
				templateUrl: "T_inProgress",
				backdrop: "static",
				scope: $scope
			});

			$scope.remotingProgress = 33;
			$scope.remotingStatus = "ゲストを作成しています...";

			$scope.deferredCreateGuest()
			.then(
				function(){
					$scope.remotingProgress = 66;
					$scope.remotingStatus = "ゲストリストをリフレッシュしています...";
					return $scope.deferredGetGuests();
				},
				function(result){
					return $q.reject(result);
				}
			)
			.then(
				function(guests){
					$scope.guests = guests;
					$scope.newGuest = {};
					$scope.remotingProgress = 100;
					$scope.remotingStatus = "作成が完了しました。";
				},
				function(result){
					console.log(result);
				}
			);
		}

ほとんどはupdateGuest()と同じ流れですね。

一点、下記の$scope.newGuestを再初期化している部分に注目してみてください。

					$scope.newGuest = {};

今回の処理ではゲスト作成処理が完了しても新規ゲストフォームはまだ表示されたままになっています。これはユースケースにもよると思いますが、連続的にデータを作成していきたい場合にはこのような仕様が適していると思います。

その際、そのままだとユーザーが入力した値がそのまま残ってしまいます。したがって一旦$scope.newGuestを初期化してフォームをクリアしているわけです。

 

deferredCreateGuest()を作成する

		$scope.deferredCreateGuest = function(){
			var deferred = $q.defer();

			$scope.force.create(
				"guest__c",
				$scope.newGuest,
				function(result){
					deferred.resolve();
				},
				function(result){
					deferred.reject(result);
				}
			);
			return deferred.promise;
		}

この関数はPart7で学んだPromise/Deferredに対応させています。ここではRemoteTKのcreate()メソッドでデータベースにアクセスし、レコードを作成しています。

create()の第一引数はオブジェクト名、第二引数はデータです。データには$scope.newGuestをそのまま渡してあげればOKです。

 

新規ボタンを設置する

最後にサイドバーの右上に「新規」ボタンを設置しておきましょう。

コード:新規ボタン

				<div class="panel panel-default">
					<div class="panel-heading">
						ゲスト
						<button type="submit" class="btn btn-xs btn-default pull-right" ng-click="openNewGuestForm()"><span class="glyphicon glyphicon-plus"></span>&nbsp; 新規</button>
					</div>
					<div class="list-group">
						<a class="list-group-item" href="#" ng-click="getGuest(guest.Id)" ng-repeat="guest in guests">{{guest.Name}}</a>
					</div>
				</div>

いわずもがな、クリックするとopenNewGuestForm()を実行してモーダルダイアログを呼び出します。

さて、随分と基本機能ができあがってきてアプリらしくなってきました。
次回はこのサイトにレスポンシブデザインを適用し、モバイルデバイスに対応させていきます。

例によって最後に現時点でのindexファイルの全ソースを掲載しておきます。

<apex:page showHeader="false" standardStyleSheets="false" applyBodyTag="false" applyHtmlTag="false" docType="html-5.0" >
  
<html ng-app="ngbootcamp">
<head>
	<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.1.0/css/bootstrap.min.css"></link>
	<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script>
	<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.1.0/js/bootstrap.min.js"></script>
	<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.10/angular.min.js"></script>
	<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-bootstrap/0.10.0/ui-bootstrap-tpls.min.js"></script>
	<c:RemoteTK />
	<script>
	var ngbootcamp = angular.module('ngbootcamp', ['ui.bootstrap']);

	ngbootcamp.controller('guestCtl', function($scope, $modal, $q){
		$scope.openNewGuestForm = function(){
			$scope.newGuest = {};
			$modal.open({
				templateUrl: "T_newGuestForm",
				scope: $scope
			});
		}

		$scope.createGuest = function(){
			$modal.open({
				templateUrl: "T_inProgress",
				backdrop: "static",
				scope: $scope
			});

			$scope.remotingProgress = 33;
			$scope.remotingStatus = "ゲストを作成しています...";

			$scope.deferredCreateGuest()
			.then(
				function(){
					$scope.remotingProgress = 66;
					$scope.remotingStatus = "ゲストリストをリフレッシュしています...";
					return $scope.deferredGetGuests();
				},
				function(result){
					return $q.reject(result);
				}
			)
			.then(
				function(guests){
					$scope.guests = guests;
					$scope.newGuest = {};
					$scope.remotingProgress = 100;
					$scope.remotingStatus = "作成が完了しました。";
				},
				function(result){
					console.log(result);
				}
			);
		}

		$scope.updateGuest = function(){
			$modal.open({
				templateUrl: "T_inProgress",
				backdrop: "static",
				scope: $scope
			});

			$scope.remotingProgress = 33;
			$scope.remotingStatus = "データを更新しています...";

			$scope.deferredUpdateGuest()
			.then(
			function(){
				$scope.remotingProgress = 66;
				$scope.remotingStatus = "ゲストリストをリフレッシュしています...";
				return $scope.deferredGetGuests();
			},
			function(result){
				return $q.reject(result);
			})
			.then(
			function(guests){
				$scope.guests = guests;
				$scope.remotingProgress = 100;
				$scope.remotingStatus = "更新が完了しました。";
			},
			function(result){
				console.log(result);
			});
		}

		$scope.deferredCreateGuest = function(){
			var deferred = $q.defer();

			$scope.force.create(
				"guest__c",
				$scope.newGuest,
				function(result){
					deferred.resolve();
				},
				function(result){
					deferred.reject(result);
				}
			);
			return deferred.promise;
		}

		$scope.deferredUpdateGuest = function(){
			var deferred = $q.defer();
			var guest = angular.copy($scope.guest);
			delete guest.attributes;

			$scope.force.update(
				"guest__c",
				$scope.guest.Id,
				guest,
				function(result){
					deferred.resolve();
				},
				function(result){
					deferred.reject(result);
				}
			);
			return deferred.promise;
		}

		$scope.getGuest = function(recordId){
			$scope.force.retrieve(
				"guest__c",
				recordId,
				"Id,Name,email__c",
				function(result){
					$scope.guest = result;
					$scope.$apply();
				},
					function(result){
					console.log(result);
				}
			);
		}

		$scope.deferredGetGuests = function(){
			var deferred = $q.defer();
			var soql = "select Id, Name, CreatedDate from guest__c";
			$scope.force.query(
				soql,
				function(result){
					deferred.resolve(result.records);
				},
				function(result){
					deferred.reject(result);
				}
			);
			return deferred.promise;	
		}

		$scope.force = new remotetk.Client();
		$scope.deferredGetGuests()
		.then(
			function(guests){
				$scope.guests = guests;
			},
			function(result){
				console.log(result);
			}
		);
	});
	</script>
</head>
<body ng-controller="guestCtl">
	<div class="container" style="margin-top:20px;">
		<div class="row">
			<div class="col-md-4">
				<div class="panel panel-default">
					<div class="panel-heading">
						ゲスト
						<button type="submit" class="btn btn-xs btn-default pull-right" ng-click="openNewGuestForm()"><span class="glyphicon glyphicon-plus"></span>&nbsp; 新規</button>
					</div>
					<div class="list-group">
						<a class="list-group-item" href="#" ng-click="getGuest(guest.Id)" ng-repeat="guest in guests">{{guest.Name}}</a>
					</div>
				</div>
			</div>
			<div class="col-md-8">
				<h1>{{guest.Name}}</h1>
				<form role="form">
					<div class="form-group">
						<label>ゲスト名</label>
						<input ng-model="guest.Name" type="text" class="form-control" placeholder="ゲスト名" />
					</div>
					<div class="form-group">
						<label>Email</label>
						<input ng-model="guest.email__c" type="email" class="form-control" placeholder="Email" />
					</div>
					<div class="form-group">
						<button class="btn btn-success" ng-click="updateGuest()">更新</button>
					</div>
				</form>
			</div>
		</div>
	</div>

	<!-- Modal for newGuestForm -->
	<script type="text/ng-template" id="T_newGuestForm">
		<div class="modal-header">
			<button type="button" class="close" ng-click="$dismiss()">&times;</button>
		    <h3>新規ゲスト</h3>
		</div>
		<div class="modal-body">
			<form role="form">
				<div class="form-group">
					<label>ゲスト名</label>
					<input ng-model="newGuest.Name" type="text" class="form-control" placeholder="ゲスト名" />
				</div>
				<div class="form-group">
					<label>Email</label>
					<input ng-model="newGuest.email__c" type="email" class="form-control" placeholder="Email" />
				</div>
			</form>
		</div>
		<div class="modal-footer">
			<button type="button" class="btn btn-success" ng-click="createGuest()">作成</button>
		</div>
	</script><!-- Modal for newGuestForm-->

	<!-- Modal for inProgress -->
	<script type="text/ng-template" id="T_inProgress">
		<div class="modal-header">
		    <h3>
		    	<span ng-show="remotingProgress < 100">処理中</span>
		    	<span ng-show="remotingProgress == 100">完了</span>
	    	</h3>
		</div>
		<div class="modal-body">
			<div>{{remotingStatus}}</div>
		    <progressbar ng-class="(remotingProgress < 100) ? 'progress-striped active' : 'progress'" value="remotingProgress" type="success"></progressbar>
		</div>
		<div class="modal-footer" ng-show="remotingProgress == 100">
			<button type="button" class="btn btn-success" ng-click="$close()">閉じる</button>
		</div>
	</script><!-- Modal for inProgress-->

</body>
</html>
  
</apex:page>

 

関連情報

without comments

Written by 中嶋 一樹

2月 16th, 2014 at 3:43 pm

AngularJSではじめるHTML5開発 – Part7 Promise/Deferredを用いたデータ更新後の画面リフレッシュ

Part6の続きです。

今回はデータ更新後に、現在画面に表示されている情報を最新状態にアップデートする仕組みを「Promise/Deferred」なるデザインパターンを適用して実装していきます。

まず今回開発する部分をデモでみてみましょう。

デモ

それでは実装へ。

Promise/Deferredって何ですか?

データ更新後にサイドバーの情報をリフレッシュするのは難しいことではありません。

updateGuest()の成功時コールバックでgetGuests()を実行すればいいだけです。

ただし、このコールバックで逐次処理を記載していくと、いくつかの問題が発生してきます。

  • どんどんコールバックが深くなっていく(いわゆるコールバック地獄)。
  • ゲスト情報は更新したいけどその後の処理は必要ない、といったケースがでたときに更新処理だけを実施できない。

今回のようにAjax非同期通信でデータアクセスをおこなう場合、処理を並行ではなく逐次(あれが終わったらコレ。コレが終わったらソレ、という処理)でおこないたいというケースは今後どんどんでてくると思います。

そんなときに便利なのがPromise/Deferredです。

Promise/Deferredはひとつのデザインパターンであり、その実装方式としてAngularJSでは$qという機能が提供されています。Promise/Deferredを用いるとコードをネストせずに必要な非同期処理をケース・バイ・ケースでつなげていくことができます。

このPromise/Deferredを実装するには2つの作業が必要です。

  • 非同期処理をPromise/Deferred対応した関数に仕立て上げる
  • 非同期処理を実行する流れを記述する

なるほど。実際にコードをPromise/Deferredに対応させて理解していきましょう。

 

getGuests()をPromise/Deferred対応に書き換える

getGuests()関数をdeferredGetGuests()として下記のように改変します。

		$scope.deferredGetGuests = function(){
			var deferred = $q.defer();
			var soql = "select Id, Name, CreatedDate from guest__c";
			$scope.force.query(
				soql,
				function(result){
					deferred.resolve(result.records);
				},
				function(result){
					deferred.reject(result);
				}
			);
			return deferred.promise;	
		}

 

ひとつずつみていきましょう。

 

			var deferred = $q.defer();

Promise/Deferred対応させる関数には決まりごととして最初に$q.defer()を実行してdeferredインスタンスを作成します。

 

					deferred.resolve(result.records);

resolve()は成功時に返す変数を指定します。今回は取得したゲストリストを返しています。

 

					deferred.reject(result);

reject()は失敗時に返す変数を指定します。

 

			return deferred.promise;	

これも決まりごとです。Promise/Deferred対応させる関数は処理の最後にdeferred.promiseを返す必要があります。

次に実行される処理はこのpromiseを受け取って処理を開始します。promiseは文字通りPromise/Deferred対応関数の処理が完了したことを保証し、その結果を渡してくれます。

これでgetGuests()はdeferredGetGuest()として生まれ変わりました。従来はこのgetGuests()はゲストリストを取得して$scope.guestsにセットするまでを担っていましたが、現在は純粋にゲストリストを呼び出し元に返すのみとなりました。したがってこのgetGuests()を呼んでいたコードをそれに対応させて修正する必要があります。

 

		$scope.deferredGetGuests()
		.then(
			function(guests){
				$scope.guests = guests;
			},
			function(result){
				console.log(result);
			}
		);

前述の通り、Promise/Deferred対応した関数はdeferred.promiseを返します。このdeferred.promiseはthen()というインスタンスメソッドを持っています。then()は第一引数に成功時のコールバック、第二引数にエラー時のコールバックをとります。(第三引数もありますが今回は割愛します)

ここでは成功時のコールバックで$scope.guestsにdeferredGetGuests()が取得してきたゲストリストをセットしています。

これで結果的に変更前と同じ挙動になりました。

これだけだと何のためにこの複雑な変更をおこなったのかよくわかりませんね。おそらく次のupdateGuest()をPromise/Deferred対応させていくと少しずつわかってくるのではないかと思います。

 

updateGuest()をPromise/Deferred対応に書き換える

同じようにupdateGuest()をPromise/Deferred対応させたdeferredUpdateGuest()として改変します。

		$scope.deferredUpdateGuest = function(){
			var deferred = $q.defer();
			var guest = angular.copy($scope.guest);
			delete guest.attributes;

			$scope.force.update(
				"guest__c",
				$scope.guest.Id,
				guest,
				function(result){
					deferred.resolve();
				},
				function(result){
					deferred.reject(result);
				}
			);
			return deferred.promise;
		}

仕組みは先ほどのdeferredGetGuests()と同じです。deferredUpdateGuest()は何も返さないのでresolve()は何も指定していませんが、処理完了後に次の処理がおこなわれる挙動は同じです。

次にそもそもng-click()で更新ボタンクリック時によばれていたupdateGuest()を違う形で復活させます。

前回作成したモーダルウィンドウの表示に加えて、更新ボタンがクリックされた後に必要な処理の流れをすべてこのupdateGuest()に記述していきます。

 

		$scope.updateGuest = function(){
			$modal.open({
				templateUrl: "T_inProgress",
				backdrop: "static",
				scope: $scope
			});

			$scope.remotingProgress = 33;
			$scope.remotingStatus = "データを更新しています...";

			$scope.deferredUpdateGuest()
			.then(
			function(){
				$scope.remotingProgress = 66;
				$scope.remotingStatus = "ゲストリストをリフレッシュしています...";
				return $scope.deferredGetGuests();
			},
			function(result){
				return $q.reject(result);
			})
			.then(
			function(guests){
				$scope.guests = guests;
				$scope.remotingProgress = 100;
				$scope.remotingStatus = "更新が完了しました。";
			},
			function(result){
				console.log(result);
			});
		}

今回行う処理は下記の2つです。

  • レコード更新
  • ゲストリストのリフレッシュ(ゲストリストを再度取得することで実現します)

両方非同期処理ですが、すでに両方の関数がPromise/Deferred対応しているので同期的に記述することができます。

 

			$scope.remotingProgress = 33;
			$scope.remotingStatus = "データを更新しています...";

まず一つ目のレコード更新処理の前にremotingProgressを33にセットしています。

 

			.then(
			function(){
				$scope.remotingProgress = 66;
				$scope.remotingStatus = "ゲストリストをリフレッシュしています...";
				return $scope.deferredGetGuests();
			},
			function(result){
				return $q.reject(result);
			})

更新処理完了後の処理をthen()でつなげています。この中でremotingProgressを66に進めつつ、次の処理であるゲストリスト取得を実施しています。

そしてその結果はreturnで返してあげます。そうするとこのthen()のあとにさらにthen()をつなげて2番目の処理を記述できます。deferredGetGuest()で取得したデータは次のthen()に渡されます。

もし更新処理が失敗した場合は$q.reject(result)が実行され、その結果が次のthen()に渡されます。$q.reject()を実行すると次のthen()においても処理が失敗したものとしてみなされ、第二引数のエラー時コールバックが実行されることになります。

 

			function(guests){
				$scope.guests = guests;
				$scope.remotingProgress = 100;
				$scope.remotingStatus = "更新が完了しました。";
			},
			function(result){
				console.log(result);
			});

remotingProgressを100にし、受け取ったゲストリストを$scope.guestsにセットしています。

これで更新ボタンをクリックされた後に必要な一連の処理が記述できました。

$scope.guestsとサイドバーに表示されているゲストの一覧はバインドされているので、$scope.guestsを更新するだけで表示も更新されます。

この$qによるPromise/Deferredをマスターすれば、Ajax非同期通信を多様するHTML5アプリにおいても、より可読性と拡張性の高いコードが記述できるようになるはずです。

次回はモーダルダイアログを利用したレコード新規作成機能を実装していきます。

例によって現時点でのindexページの全ソースを掲載しておきます。

<apex:page showHeader="false" standardStyleSheets="false" applyBodyTag="false" applyHtmlTag="false" docType="html-5.0" >
  
<html ng-app="ngbootcamp">
<head>
	<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.1.0/css/bootstrap.min.css"></link>
	<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script>
	<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.1.0/js/bootstrap.min.js"></script>
	<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.10/angular.min.js"></script>
	<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-bootstrap/0.10.0/ui-bootstrap-tpls.min.js"></script>
	<c:RemoteTK />
	<script>
	var ngbootcamp = angular.module('ngbootcamp', ['ui.bootstrap']);

	ngbootcamp.controller('guestCtl', function($scope, $modal, $q){
		$scope.updateGuest = function(){
			$modal.open({
				templateUrl: "T_inProgress",
				backdrop: "static",
				scope: $scope
			});

			$scope.remotingProgress = 33;
			$scope.remotingStatus = "データを更新しています...";

			$scope.deferredUpdateGuest()
			.then(
			function(){
				$scope.remotingProgress = 66;
				$scope.remotingStatus = "ゲストリストをリフレッシュしています...";
				return $scope.deferredGetGuests();
			},
			function(result){
				return $q.reject(result);
			})
			.then(
			function(guests){
				$scope.guests = guests;
				$scope.remotingProgress = 100;
				$scope.remotingStatus = "更新が完了しました。";
			},
			function(result){
				console.log(result);
			});
		}

		$scope.deferredUpdateGuest = function(){
			var deferred = $q.defer();
			var guest = angular.copy($scope.guest);
			delete guest.attributes;

			$scope.force.update(
				"guest__c",
				$scope.guest.Id,
				guest,
				function(result){
					deferred.resolve();
				},
				function(result){
					deferred.reject(result);
				}
			);
			return deferred.promise;
		}

		$scope.getGuest = function(recordId){
			$scope.force.retrieve(
				"guest__c",
				recordId,
				"Id,Name,email__c",
				function(result){
					$scope.guest = result;
					$scope.$apply();
				},
					function(result){
					console.log(result);
				}
			);
		}

		$scope.deferredGetGuests = function(){
			var deferred = $q.defer();
			var soql = "select Id, Name, CreatedDate from guest__c";
			$scope.force.query(
				soql,
				function(result){
					deferred.resolve(result.records);
				},
				function(result){
					deferred.reject(result);
				}
			);
			return deferred.promise;	
		}

		$scope.force = new remotetk.Client();
		$scope.deferredGetGuests()
		.then(
			function(guests){
				$scope.guests = guests;
			},
			function(result){
				console.log(result);
			}
		);
	});
	</script>
</head>
<body ng-controller="guestCtl">
	<div class="container" style="margin-top:20px;">
		<div class="row">
			<div class="col-md-4">
				<div class="panel panel-default">
					<div class="panel-heading">
						ゲスト
					</div>
					<div class="list-group">
						<a class="list-group-item" href="#" ng-click="getGuest(guest.Id)" ng-repeat="guest in guests">{{guest.Name}}</a>
					</div>
				</div>
			</div>
			<div class="col-md-8">
				<h1>{{guest.Name}}</h1>
				<form role="form">
					<div class="form-group">
						<label>ゲスト名</label>
						<input ng-model="guest.Name" type="text" class="form-control" placeholder="ゲスト名" />
					</div>
					<div class="form-group">
						<label>Email</label>
						<input ng-model="guest.email__c" type="email" class="form-control" placeholder="Email" />
					</div>
					<div class="form-group">
						<button class="btn btn-success" ng-click="updateGuest()">更新</button>
					</div>
				</form>
			</div>
		</div>
	</div>

	<!-- Modal for inProgress -->
	<script type="text/ng-template" id="T_inProgress">
		<div class="modal-header">
		    <h3>
		    	<span ng-show="remotingProgress < 100">処理中</span>
		    	<span ng-show="remotingProgress == 100">完了</span>
	    	</h3>
		</div>
		<div class="modal-body">
			<div>{{remotingStatus}}</div>
		    <progressbar ng-class="(remotingProgress < 100) ? 'progress-striped active' : 'progress'" value="remotingProgress" type="success"></progressbar>
		</div>
		<div class="modal-footer" ng-show="remotingProgress == 100">
			<button type="button" class="btn btn-success" ng-click="$close()">閉じる</button>
		</div>
	</script><!-- Modal for inProgress-->

</body>
</html>
  
</apex:page>

 

関連情報

without comments

Written by 中嶋 一樹

2月 16th, 2014 at 11:49 am