はじめに
今回はCybozu CDNにストックされたspin.jsを使ったスピナーの設定を行いたいと思います。
デモ環境
こちらのデモ環境から実際に動作を確認できます。
https://dev-demo.cybozu.com/k/41/
デモ環境アカウントとパスワードは、サインイン後にこちらのページでご確認ください。
spin.js
spin.jsは、スピナー(スピンアイコン、ローディングアイコン、処理中表示)を設置するためのライブラリです。
ストックされたCDNを見てもわかる通り、CSSやjQueryを伴わずJavaScriptのみで取扱いできることが特長とされています。
今回の適用方法・シナリオ
spin.jsの基本的な使い方は設置する要素を設定してスタート/ストップをかけるという流れになります。
しかし、今回はkintoneでの利用ということですので、REST APIによる処理量が大きくなった時に画面中央に処理中表示して、処理が終わったら非表示にするということをやってみたいと思います。
今回は、落合 雄一さんが執筆されている「一覧画面で条件に応じたレコード件数を取得するテクニック」に適用してみます(まったく同様のアプリではないためqueryは変更しています)。
こちらでは、レコード総数が100件を超えるアプリに対して処理中レコードをカウントできますが、
レコード総数が増えてくるとAPIコール数も増え、「処理中のレコード件数」を表示するまでに多少の時間を要するようになってきます。
そこで、ユーザーが処理中ということが分かるようにスピナーを表示させたいと思います。
準備(スピナー表示/非表示関数)
スピナーを表示、非表示させる関数を準備します。
これらの関数を呼び出せるように宣言しておけば、あとは showSpinner() で表示、hideSpinner() で非表示することができます。
spin.jsの元の特長を極力損なわず、CSSの指定をし直さなくて良いようこれらの関数中でスタイルを指定しておきます。
なお、スピナー設置用のDOMの生成にjQueryを利用していますので、読み込んでおきましょう。
JavaScriptソースコード(countRecords.js)
※ レコード一括取得時にその結果が1万を超える可能性がある場合には、運用・適用中のプログラムのご確認ならびに修正対応の検討をお願いいたします。
詳細はこちらの記事を確認ください。
JavaScript/CSSの設定
今回は、spin.jsとjQueryのJavaScriptをCybozu CDNからURL指定して、上のサンプル(countRecords.js)をファイルアップロードします。
動作確認
APIをコールして処理がストップするまでスピナーが表示され、ユーザーに処理中であることを知らせます。
処理が終了すると、スピナーが消えて「処理中のレコード件数」が表示されます。
ポイントとなるスピナー設定箇所は次の部分です。
APIのコール前にshowSpinner()、APIの処理終了時(コールバック内)にhideSpinner()を記述しています。
注意事項
今回のkintoneアプリ周辺の注意点は次の通りです。
- 筆者はMacintoshのGoogle Chromeでkintoneアプリの動作確認を行いました
最後に
今回は、spin.jsを使った全画面イメージのスピナーの設定についてお届けしました。
今回のシナリオ(単純なレコード取得)ではあまり支障にはならないと思いますが、
請求や勤怠データの翌月コピー等レコードの登録・更新時は途中でユーザー自身が別の操作に移ってしまったりするとトラブルの元になりますので、
処理中・処理完了がこのような形で表現されると間違い防止・ユーザビリティの向上に繋がっていくと思います。
是非お試し頂ければと思います!
上のspinだけのコードの箇所で ,(カンマ)が抜けています!
'left': '0px' の箇所です!
Hosoya 様
いつもお世話になっております。
ご指摘の点、修正いたしました。ご連絡をありがとうございました。
今後ともよろしくお願いいたします。
Takashi Hosoya様
お世話になります、
以下の通り一覧画面に設置したボタンクリックした際にSpinnerを表示するよう定義しましたが、表示されません。
【URL指定】
https://js.cybozu.com/spinjs/2.0.1/spin.min.js
https://js.cybozu.com/jquery/1.11.1/jquery.min.js
【JavaScript】
kintone.events.on('app.record.index.show', function (event) {
/*#################################### ボタン表示 ###########################################*/
if (document.getElementById ('my_index_button') != null) {
return;
}
var myIndexButton = document.createElement('button');
myIndexButton.id = 'my_index_button';
myIndexButton.innerHTML = 'ボタン';
//ボタンクリック時
myIndexButton.onclick = function() {
showSpinner(); // スピナー表示
// ~レコード読みこみ処理~
//処理終了時
hideSpinner(); // スピナー非表示
新デザインになった際に、別途修正が必要な記述がありますか。
ちなみにブラウザはGoogleChromeを使用しております。
-------------------------------------------------------------------------------------------------
<追加>
デバック時に、JS中にエラーがあるときのみスピナーが表示されました。
エラーを修正するとスピナーが表示されなくなります。
使い方が悪いのでしょうか。
⇒再度確認したところデータ取得(GET)と更新(PUT)を同期で実行していたことが原因でした。
非同期の場合表示されるようです。
しかし、100件以上のデータなので同期処理でなくてはなりません。
同期処理の場合に実装する方法はありますでしょうか。
takokichiさん
デザイン変更に対する影響は基本的に受けないはずですが、このサンプル中で追加する要素の position が absolute になっている記載箇所がありますが、こちらを fixed に変えてお試しください。準備のパートのサンプルコードの spin_bg_div の部分です。
また、(Promiseを用いた記述を含めた)同期処理、もしくは非同期処理に関わらず適切箇所が適切であれば、意図通りに動かすことができると思います。良ければ、
//ボタンクリック時
myIndexButton.onclick = function() {
showSpinner(); // スピナー表示
// ~レコード読みこみ処理~
//処理終了時
hideSpinner(); // スピナー非表示
の部分のコード詳細を記述頂けると、詳細にお応えできるかと思います。よろしくお願いします。
Ryu Yamashitaさん
お返事ありがとうございます。
レコードはGET①(非同期)⇒GET②⇒(非同期)⇒PUT(同期)の処理です。
希望としてはGET①(非同期)からPUTまでの間表示したいのですが、
PUTに変更し実行すると表示されますが、クリックして20秒くらいしてスピナーが表示されるのでPUT(同期)のときのみ表示されているのではないかと。
なおPUTをコメントアウトするとスピナーが表示されなくなります。
(function () {
"use strict";
kintone.events.on('app.record.index.show', function (event) {
/*#################################### ボタン表示 ###########################################*/
if (document.getElementById ('my_index_button') != null) {
return;
}
var myIndexButton = document.createElement('button');
myIndexButton.id = 'my_index_button';
myIndexButton.innerHTML = '入金差額計算';
// 関連テーブルのAppIdを取得
var siirerelated = kintone.app.getRelatedRecordsTargetAppId('仕入れ情報');
/*##################################### 関数定義 ###########################################*/
/* ==========レコード取得関数(100行制限)============*/
function siireRecords(bukkenRec,opt_offset, opt_limit, opt_records) {
//関連テーブルから利益率、仕入れ計算出(100件制限有)
var offset = opt_offset || 0;
var limit = opt_limit || 100;
var allRecords = opt_records || [];
// 詳細画面のレコードの情報取得
var bukkenNo = bukkenRec['物件No']['value'];
var s_query = '物件No = '+bukkenNo;
s_query = encodeURIComponent(s_query);
var s_appUrl = kintone.api.url('/k/v1/records') + '?app='+ siirerelated + '&query=' + s_query;
// 同期リクエストを行う
var s_xmlHttp = new XMLHttpRequest();
s_xmlHttp.open("GET", s_appUrl, false);
s_xmlHttp.setRequestHeader('X-Requested-With','XMLHttpRequest');
s_xmlHttp.send(null);
//取得したレコードをArrayに格納
var s_respdata = JSON.parse(s_xmlHttp.responseText);
if(s_respdata.records.length > 0){
for(var i = 0; s_respdata.records.length > i; i++){
allRecords.push(s_respdata.records[i]);
}
}
return allRecords;
}
/* ==========スピナー(処理中歯車)を表示=============*/
// スピナーを動作させる関数
function showSpinner() {
// 要素作成等初期化処理
if ($('.kintone-spinner').length == 0) {
// スピナー設置用要素と背景要素の作成
var spin_div = $('<div id ="kintone-spin" class="kintone-spinner"></div>');
var spin_bg_div = $('<div id ="kintone-spin-bg" class="kintone-spinner"></div>');
// スピナー用要素をbodyにappend
$(document.body).append(spin_div, spin_bg_div);
// スピナー動作に伴うスタイル設定
$(spin_div).css({
'position': 'fixed',
'top': '50%',
'left': '50%',
'z-index': '510',
'background-color': '#fff',
'padding': '26px',
'-moz-border-radius': '4px',
'-webkit-border-radius': '4px',
'border-radius': '4px'
});
$(spin_bg_div).css({
'position': 'fixed',
'top': '0px',
'left': '0px',
'z-index': '500',
'width': '100%',
'height': '200%',
'background-color': '#000',
'opacity': '0.5',
'filter': 'alpha(opacity=50)',
'-ms-filter': "alpha(opacity=50)"
});
// スピナーに対するオプション設定
var opts = {
'color': '#000'
};
// スピナーを作動
new Spinner(opts).spin(document.getElementById('kintone-spin'));
}
// スピナー始動(表示)
$('.kintone-spinner').show();
}
// スピナーを停止させる関数
function hideSpinner() {
// スピナー停止(非表示)
$('.kintone-spinner').hide();
}
/*##################################### メイン処理 ###########################################*/
// ボタンクリック時の処理 100件以上の処理のため同期リクエスト
myIndexButton.onclick = function() {
//空更新ループ
var offset = 0;
var loopendflg = false;
var appId = kintone.app.getId();
//////// 空更新オブジェクトの生成
var param = {
"app": appId,
"records": []
};
// 確認ダイアログ
var kakunin = confirm("以下が更新されます。実行しますか?\n" + "・仕入れ計\n" + "・利益率\n" + "・請求金額\n");
if (kakunin === false) {
event.error = "キャンセルしました。";
window.location.href = window.location.origin + window.location.pathname;
return event;
}
showSpinner(); // スピナー表示
while(!loopendflg){
// Kintoneのレコード取得上限値100単位にデータを取得
var query = encodeURIComponent('order by 物件No asc limit 100 offset ' + offset);
var appUrl = kintone.api.url('/k/v1/records') + '?app='+ appId + '&query=' + query;
// 同期リクエストを行う
var xmlHttp = new XMLHttpRequest();
xmlHttp.open("GET", appUrl, false);
xmlHttp.setRequestHeader('X-Requested-With','XMLHttpRequest');
xmlHttp.send(null);
//取得したレコードをArrayに格納
var respdata = JSON.parse(xmlHttp.responseText);
if(respdata.records.length > 0){
//1件単位の処理
for(var i = 0; respdata.records.length > i; i++){
var siireRec = siireRecords(respdata['records'][i]);
var amount = 0;
var data_count = 0;
// 仕入情報を合計する
for (var j = 0; j < siireRec.length; j++) {
amount = amount + parseFloat(siireRec[j].税抜き金額.value);
data_count++;
}
var sales = respdata['records'][i]['売上高']['value'];
var interestRate = 0
var seikyu = 0
//利益率、請求額計算
//利益率=(売上高-仕入れ情報「税抜き金額」小計)/売上高 × 100
//請求額=売上×1.08(四捨五入)
if(sales != 0){
interestRate = (sales - amount)/sales
interestRate = multiply(interestRate, 100)
seikyu = multiply(sales, 1.08)
}
// 端数処理
interestRate = floatFormat(interestRate,2);
seikyu = floatFormat_1(seikyu,0);
//オブジェクトに値をセット
param['records'][i] = {
"id": respdata['records'][i]['物件No']['value'],
"record":{
"仕入れ計":{
"value": amount
},
"利益率":{
"value": interestRate
},
"請求金額":{
"value": seikyu
},
},
}
//}
//);
}
offset += respdata.records.length;
// 取得した100件ごとに値を格納レコード
kintone.api(kintone.api.url('/k/v1/records', true), 'PUT', param, function(respdata) {
// success
console.log(respdata);
hideSpinner(); // スピナー非表示
location.reload(true);
alert("更新が完了しました。");
}, function(respdata) {
// error
console.log(param);
console.log(error);
});
}else{
loopendflg = true;
}
}
}
kintone.app.getHeaderMenuSpaceElement().appendChild(myIndexButton);
});
})();