last update: Tue, 26 May 2020 23:45:21 GMT ( 4 years ago )
BOOK.broadcast.save
RSD_ID タイトル 説明 rRxLU0zK DEMO1 - Chef ダイナミックロードをアシストする RSD です。
プロジェクトの 起動RSD として設定してください。QDWoV0Er DEMO1 - Main - BOOK.broadcast.save 画像入力を表示する HTML, CSS と JSです。
[JavaScript]
// Prevent executing on Agile Developers
if(!/mode=edit/.test(window.top.location.search))
main();
function main() {
// モジュールがロードされると、まず exports 関数が実行されます。
exports = function() {
// $S.drawIn() 関数で body 全体に book の標準デザインを適用します。
$S.drawIn();
// ここでの "this" は モジュールそのもの(インスタンス)です。
// モジュールの setup イベントに対して、 IFSetup 関数をバインドします。
this.setup(IFSetup);
};
function IFSetup() {
// データベースの "test" コレクションと 紐付いた BOOK を生成します。
var book = window.book = new $S.BOOK('test');
var n = book.name();
// セレクタ #FORM で取得される DOMツリー を、生成した BOOK にアサインします。
// このとき、該当のDOMノードは data-book-elem で紐付けられたクラス定義に
// 従って加工されます。
book.setup('#FORM', {
create: {
// "create" は各要素生成時のオプションです。
photo: {
renderCss: {
width: '100%',
height: 'auto'
},
render_limit: {
size: 500 * 1000
}
},
},
setup: {
// "setup" は各要素に機能をつける際のオプションです。
save: save
// 'save' is a button with function "save"
}
});
// セレクタ (#FORM) で取得された 要素群 には名前をつけておく必要があり、
// これを "ラッパー名" と呼びます。
// "ラッパー名" を指定しない場合、自動的にブック名である "test"
// (2つ目以降は連番付き) となります。上記で定義している "n" は
// "ラッパー名"を指定するために利用します。
// BOOK にアサインされた要素 "view" に対して、drop イベントを
// バインドしています。
// 取得するときのラッパー名に "n" を利用します。
var view = book.elem(n, 'view').elem;
book.elem(n, 'photo').elem.on('drop', function() {
var html = '<div>読み込み中... <small>(1Mb/1秒程度)</small></div>';
view.$elem.addClass('fade-in-out');
view.data(html);
}).on('render', function(d) {
var html = '<div>' + d.name + '</div>';
html += '<div>元サイズ: ' + d.size + ' byte</div>';
html += '<div>(調整: ' + (d.scale * 100) + ' %)</div>';
view.$elem.removeClass('fade-in-out');
view.data(html);
});
// こちらは save ボタンに対するイベントです。
function save() {
book.talk('findOne', [{
email: book.value(n, 'email')
}, {
_id: 1
}], function(e, r) {
e ? alert('error.'): saveNow(r);
});
}
function saveNow(r) {
// "r" (入力した email に紐づくデータ) があれば
// "追加" ではなく "更新" にするために、_id (一意にするためのキー) を
// セットしています。
(r || '')._id && book.value(n, '_id', r._id);
book.save({
name: n,
// view, save 要素の .data() 値は保存しません
except: ['view', 'save'],
// 取得した値を onGetValue 関数で補正します
onGetValue: onGetValue,
// photo データ を削除した値を broadcast します。
// (デフォルトでは _id のみがブロードキャストされます。)
broadcast: function(v) {
return delete v.photo.src, v;
}
}).progress(function(type, cnt, len) {
switch(type) {
case 'start':
view.data('アップロード開始しました。');
break;
case 'progress':
view.data(parseInt(cnt / len * 1000) / 10 + '% ... ');
break;
case 'complete':
view.data('アップロード完了しました。');
break;
case 'error':
view.data('中止しました。');
}
}).then(done, fail);
function done(r) {
view.data(r._id + ' が保存されました.<br/>'
+ '<small style="white-space:nowrap;">' + new Date().toGMTString()
+ '</small>');
$.each(book.elem(), function(e_id, elem) {
(elem.disabled || $.noop)();
});
console.log(r);
}
function fail(e) {
if(typeof e == 'string')
e = new Error(e);
alert(e.message);
console.error('saveError', e);
// error handling, draft save
}
}
function onGetValue(v) {
if(~["sOvYZgbh", "0Eh0fzdb", "H7HZw9Om"].indexOf(v._id))
return '更新できないメールアドレスです。(' + v.email + ')';
if(!v.photo)
return '写真の登録は必須です。';
if(typeof v.email != 'string' || v.email.length == 0
|| !/@/.test(v.email))
return 'メールアドレスが不正です。';
if(v.password != v.confirm)
return 'パスワードが一致していません。';
}
}
}[HTML]
<div class="wrapper" id="FORM">
<div class="row">
<div class="drag-drop-wrap">
<div data-book-elem="drag-drop" id="photo"></div>
</div>
</div>
<div class="row">
<div data-book-elem="html" id="view"></div>
</div>
<div class="row">
<div data-book-elem="html" id="_id" style="display:none;"></div>
</div>
<div class="row">
<input type="email" id="email" placeholder="メールアドレス">
</div>
<div class="row">
<input data-book-elem="input" type="password" id="password" placeholder="パスワード">
</div>
<div class="row">
<input data-book-elem="input" type="password" id="confirm" placeholder="パスワード(確認)">
</div>
<div class="row">
<button data-book-elem="button" id="save"> 保存 </button>
</div>
</div>[CSS]
#FORM {
width: 240px;
position: relative;
margin: 24px auto;
text-align: center;
}
#FORM .drag-drop-wrap {
position: relative;
width: 196px;
height: 244px;
margin: 8px auto;
border: 1px solid #ccc;
overflow: hidden;
}
@keyframes fade {
0%{opacity:0.9} 50%{opacity:0} 100%{opacity:0.9}
}
@-moz-keyframes fade {
0%{opacity:0.9} 50%{opacity:0} 100%{opacity:0.9}
}
@-webkit-keyframes fade {
0%{opacity:0.9} 50%{opacity:0} 100%{opacity:0.9}
}
@-o-keyframes fade {
0%{opacity:0.9} 50%{opacity:0} 100%{opacity:0.9}
}
@-ms-keyframes fade {
0%{opacity:0.9} 50%{opacity:0} 100%{opacity:0.9}
}
.fade-in-out {
animation-name: fade;
animation-duration: 800ms;
animation-timing-function: ease;
animation-iteration-count: infinite;
-moz-animation-name: fade;
-moz-animation-duration: 800ms;
-moz-animation-timing-function: ease;
-moz-animation-iteration-count: infinite;
-webkit-animation-name: fade;
-webkit-animation-duration: 800ms;
-webkit-animation-timing-function: ease;
-webkit-animation-iteration-count: infinite;
-o-animation-name: fade;
-o-animation-duration: 800ms;
-o-animation-timing-function: ease;
-o-animation-iteration-count: infinite;
-ms-animation-name: fade;
-ms-animation-duration: 800ms;
-ms-animation-timing-function: ease;
-ms-animation-iteration-count: infinite;
}bBx9DhvI DEMO1 - Main - BOOK.broadcast.receive 一覧を表示する HTML, CSS と JSです。
/***/
// Prevent executing on Agile Developers
if(!/mode=edit/.test(window.top.location.search))
main();
function main() {
// モジュールがロードされると、まず exports 関数が実行されます。
exports = function() {
// $S.drawIn() 関数で body 全体に book の標準デザインを適用します。
$S.drawIn();
// ここでの "this" は モジュールそのもの(インスタンス)です。
// モジュールの setup イベントに対して、 IFSetup 関数をバインドします。
this.setup(IFSetup);
};
function IFSetup() {
// データベースの "test" コレクションと 紐付いた BOOK を生成します。
var book = window.book = new $S.BOOK('test');
var bnam = book.name();
// >> assign a form (have item "list" type=[dynamic-list]) >>
// wrapper_name is set as "test"
book.setup('#FORM', {
setup: {
list: $('#TEMPLATE')
}
});
var dynamicList = book.elem('test', 'list').elem;
// >> assign a form (have item "list" type=[html]) >>
// wrapper_name is set as "test1"
book.setup('#LABEL');
var label = book.elem('test1', 'list').elem;
// >> get current data list >>
// book の中に image を入れてやっているので
// 項目を指定して取得してやることで、レスポンスに配慮します。
var img_dfds, ls_clear, ls_save;
book.talk('find', [{}, {
"photo.src": 0
}], function(err, res) {
if(err)
return alert('データ取得に失敗しました。');
// put datas to list
book.value(bnam, 'list', convertForList(res));
// image data gets on stream.
// TODO support by book.stream()
img_dfds = [], ls_clear = false, ls_save = {};
res.forEach(putImage), $.when.apply($, img_dfds).done(function() {
ls_clear && LS.clear(), $.each(ls_save, function(k, v) {
LS.setItem(k, v);
});
});
// add event photo open
// * Modal Window 表示モジュールです。
var SHOWING = false;
book.elem(bnam, 'list').elem.$wrap.on('click', '.s-row', function() {
if(SHOWING)
return;
$('body').css('overflow', 'hidden');
SHOWING = true;
var $this = $(this), $photo = $this.find('.photo'), img = new Image();
img.onload = function() {
var img_w = Math.min($(window).width() - 60, img.width);
var img_h = Math.min($(window).height() - 60, img.height);
var rw = img_w / img.width, rh = img_h / img.height;
var r = Math.min(rw, rh);
img_w = img.width * r, img_h = img.height * r;
var $img = $(img).css({
position: 'absolute',
left: ($(window).width() - img_w) / 2,
top: window.scrollY + ($(window).height() - img_h) / 2,
width: img_w,
height: img_h,
background: '#fff',
borderRadius: 16,
zIndex: 10,
opacity: 0
}).appendTo('body').animate({
opacity: 1
});
var $overlay = $('').css({
position: 'absolute',
left: 0,
top: 0,
background: '#000',
opacity: 0,
zIndex: 9,
width: $(window).width(),
height: $(document).height(),
cursor: 'pointer'
}).appendTo('body').click(function() {
$img.remove()
$(this).fadeOut(function() {
$overlay.remove(), SHOWING = false;
$('body').css('overflow', '');
});
}).animate({
opacity: 0.8
});
};
img.src = $photo.attr('src');
}).on('contextmenu', '.s-row', function($e) {
$e.preventDefault();
var $this = $(this), $_id = $this.find('._id');
if(~["sOvYZgbh", "0Eh0fzdb", "H7HZw9Om"].indexOf($_id.text()))
return alert('このデータは削除できません。');
confirm($_id.text() + 'を削除しますか?') && (function() {
book.del($_id.text(), function(e, r) {
dynamicList.dec($this);
$('body').animate({
scrollTop: 0
});
label.data($_id.text() + 'を削除しました。');
});
})();
});
});
function putImage(d, i) {
var src = [], skip = 0, _id = d._id;
var dfd = null;
dfd = Array.isArray(img_dfds) ? img_dfds[img_dfds.push($.Deferred()) - 1]
: $.Deferred();
var ls_key = _id + '' + d.stamp, ls_val = LS.getItem(ls_key);
ls_val == null && (ls_clear = true);
var $pbar = $('<progress min="0" value="0"/>');
var $ptxt = $('<div><div class="size">'
+ '<span class="progress"></span><span>/</span><span class="max"></span>'
+ '</div>');
var $wbar = $('');
var $img = dynamicList.$wrap.find('.s-row').eq(i).find('img');
$img.parent().css('text-align', 'center');
countPhotoGet();
function countPhotoGet() {
book.talk('aggregate', [[{
$match: {
_id: _id
}
}, {
$project: {
photo: "$photo.src"
}
}, {
$unwind: "$photo"
}, {
$group: {
_id: null,
count: {
$sum: 1
}
}
}]], function(err, res) {
(function(r) {
$pbar.attr('max', r ? r.count: 1);
$img.after($wbar.append($pbar, $ptxt));
r && !ls_val ? (function() {
$ptxt.find('.size').text('約 ' + (r.count * 8192) + ' byte');
$ptxt.find('.max').text(r.count);
streamPhotoGet(r.count);
})(): simplePhotoGet(ls_val, $pbar.attr('max'));
})(res[0]);
});
}
function simplePhotoGet(raw, max) {
raw == null ? book.talk('findOne', [{
_id: _id
}, {
"photo.src": 1
}], function(err, res) {
err ? simplePhotoGet(): simpleRender(res.photo.src);
}): simpleRender(raw);
function simpleRender(img_src) {
$pbar.attr('value', max || 1), render(img_src);
ls_save && (ls_save[ls_key] = img_src), dfd.resolve();
}
}
function streamPhotoGet(max) {
book.talk('aggregate', [[{
$match: {
_id: _id
}
}, {
$project: {
photo: "$photo.src"
}
}, {
$unwind: "$photo"
}, {
$skip: skip
}, {
$limit: 8
}] // ~= 64kb
], function(err, res) {
if(err || res == null) // error and retry.
return setTimeout(function() {
streamPhotoGet(max);
}, 5000 * Math.random())
res.length ? foonyah.nextTick(function() {
skip += 8, skip = Math.min(skip, max);
$pbar.attr('value', skip), $ptxt.find('.progress').text(skip);
src.push(res.map(function(d) {
return d.photo;
}).join('')), streamPhotoGet(max);
}): (function() {
var img_src = src.join('');
render(img_src);
ls_save && (ls_save[ls_key] = img_src), dfd.resolve();
})();
});
}
function render(src) {
$img.attr('src', src), $pbar.fadeOut(function() {
$wbar.remove();
});
}
}
// >> on broadcast event >>
// !!CHECK!! "password", "confirm" is not broadcasted
book.on('broadcast', function(r, h) {
debugger;
console.log('book.broadcast.recieve', r, h);
if($S.DB().isMyToken(h.token))
return;
if(Array.isArray(r)) { // remove will broadcasted as array
var rms = r.map(function(v) {
return v._id;
});
var idx = [];
dynamicList.$elem.find('.s-data._id').each(function(i) {
rms.indexOf($(this).text()) != -1 && idx.push(i);
});
dynamicList.dec(idx);
label.data(rms.join(',') + 'が削除されました。');
return;
}
var _id = r._id, email = r.email, n = book.name(), v = book.value(n,
'list');
r.lastupdate = stampToLastupdate(r.stamp);
// dynamic-list will be increased or updated, and html will be updated.
var r_idx = v.email.indexOf(email), r_len = v.email.length;
(r_idx != -1 ? function() {
// once remove image data.
var rem_d = {};
rem_d['photo[' + r_idx + ']'] = '';
book.elem(n, 'list').elem.data(rem_d);
$.each(book.elem(null, 'list'), function(k, item) {
item.name == n ? item.elem.update(r_idx, r) && putImage(r, r_idx)
: item.elem.data('更新:' + r.email + ' (最終更新:' + r.stamp + ')');
});
console.log('(recieve-side)' + n + ' の _id ' + _id + ' が更新されました。');
}: function() {
$.each(book.elem(null, 'list'), function(k, item) {
item.name == n ? item.elem.inc(r)
&& putImage(r, item.elem.count() - 1): item.elem.data('追加:'
+ r.email + ' (最終更新:' + r.stamp + ')');
});
console.log('(recieve-side)' + n + ' の _id ' + _id + ' が追加されました。');
$('body').animate({
scrollTop: $(document).height()
});
})();
});
// >> utility >>
function convertForList(arr) {
var obj = {};
arr.forEach(function(v, i) {
$.each($.extend(true, {
lastupdate: stampToLastupdate(v.stamp)
}, v), function(k, v) {
!Array.isArray(obj[k]) && (obj[k] = []);
obj[k][i] = v;
});
});
return obj;
}
function stampToLastupdate(stamp) {
return new Date(stamp).toGMTString();
}
}
}
"Hello, Synquery!"
早速実践的なモジュール作成を行う場合の手順です。
用意されたテンプレートアプリケーションに自分のプロジェクトにインストールしてみましょう。
(1)synquery アカウントを https://www.synquery.com/ から取得してください。
(2)Account Manager で RSD を検索、複製して自分のプロジェクトに取り込みます。
(「RSDの呼出・複製編集」→「必要箇所の修正」→「新規保存とProjectへの取り込み」を順に3回繰り返す)
※ 手動動作は RSDと Project の関係を理解する上で行っていただく動作です。実際には Lean Property タブの 「テンプレート Project 取込」で本動作は自動的に実施されます。
[事前準備]
プロジェクトを呼び出して、編集モードに切り替えます。
[複製するRSD]ID タイトル 説明/修正箇所 (1)
pDq2bTW1Hello, Synquery! 実際の Hello, Synquery の表示モジュールです。
(修正の必要はありません。)(2)
ZNNxSgQI[Lean Synquery Tmpl] Initialize レシピ形式でシステムを読み込むモジュールです。ソース内の "pDq2bTW1" を 1. の複製時に生成されたRSD IDに置き換えて下さい。 (3)
Q8zJstt0[Lean Synquery Parts] Launch from Agile Developers Agile Developer から起動するためのモジュールです。
(修正の必要はありません。)
(3)プロジェクトの lean startup を設定し、一般公開します。
2. の複製時に生成されたRSD IDに置き換えて下さい。
※ プロジェクトの「プロジェクト基本情報」タブ内に「lean startup」のエリアがない場合、お手数ですが 株式会社 東雲 サカモト までお問い合わせ下さい。
(4)プロジェクトの実行を確認します。
プロジェクト一覧の「開発環境」から Agile Developer を起動し、3. の複製(タイトル:起動)を選択した状態で実行します。アニメーションが始まり、関西弁のメッセージが表示されたら、成功です。
introduction sample
remarks
※ アプリケーションの記述部分のみです。ライブラリの仕様に関しては クライアントサイド を参照下さい。
※ データ問い合わせで用いる mongodb コマンドの挙動は、 Mongodb Command Exercise で確認できます。併せてご利用下さい。
Public pracitice source
※ アプリケーションの記述部分のみです。ライブラリの仕様に関しては クライアントサイド を参照下さい。
※ データ問い合わせで用いる mongodb コマンドの挙動は、 Mongodb Command Exercise で確認できます。
※ データ問い合わせで用いる mongodb コマンドの挙動は、 Mongodb Command Exercise で確認できます。