1 // $$ inspired by @wycats: http://yehudakatz.com/2009/04/20/evented-programming-with-jquery/
3 var data = $(node).data("$$");
8 $(node).data("$$", data);
14 // utility functions used in the implementation
16 function forIn(obj, fun) {
19 if (obj.hasOwnProperty(name)) {
25 function funViaString(fun, hint) {
26 if (fun && fun.match && fun.match(/^function/)) {
28 if (typeof f == "function") {
31 return f.apply(this, arguments);
33 // IF YOU SEE AN ERROR HERE IT HAPPENED WHEN WE TRIED TO RUN YOUR FUNCTION
34 $.log({"message": "Error in evently function.", "error": e,
35 "src" : fun, "hint":hint});
44 function runIfFun(me, fun, args) {
45 // if the field is a function, call it, bound to the widget
46 var f = funViaString(fun, me);
47 if (typeof f == "function") {
48 return f.apply(me, args);
55 connect : function(source, target, events) {
56 events.forEach(function(ev) {
57 $(source).bind(ev, function() {
58 var args = $.makeArray(arguments);
59 // remove the original event to keep from stacking args extra deep
60 // it would be nice if jquery had a way to pass the original
61 // event to the trigger method.
63 $(target).trigger(ev, args);
73 function extractFrom(name, evs) {
77 function extractEvents(name, ddoc) {
78 // extract events from ddoc.evently and ddoc.vendor.*.evently
79 var events = [true, {}]
80 , vendor = ddoc.vendor || {}
81 , evently = ddoc.evently || {}
83 $.forIn(vendor, function(k, v) {
84 if (v.evently && v.evently[name]) {
85 events.push(v.evently[name]);
88 if (evently[name]) {events.push(evently[name]);}
89 return $.extend.apply(null, events);
92 function extractPartials(ddoc) {
93 var partials = [true, {}]
94 , vendor = ddoc.vendor || {}
95 , evently = ddoc.evently || {}
97 $.forIn(vendor, function(k, v) {
98 if (v.evently && v.evently._partials) {
99 partials.push(v.evently._partials);
102 if (evently._partials) {partials.push(evently._partials);}
103 return $.extend.apply(null, partials);
106 function applyCommon(events) {
107 if (events._common) {
108 $.forIn(events, function(k, v) {
109 events[k] = $.extend(true, {}, events._common, v);
111 delete events._common;
118 $.fn.evently = function(events, app, args) {
120 // store the app on the element for later use
125 if (typeof events == "string") {
126 events = extractEvents(events, app.ddoc);
128 events = applyCommon(events);
129 $$(elem).evently = events;
130 if (app && app.ddoc) {
131 $$(elem).partials = extractPartials(app.ddoc);
133 // setup the handlers onto elem
134 forIn(events, function(name, h) {
135 eventlyHandler(elem, name, h, args);
139 elem.trigger("_init", args);
142 if (app && events._changes) {
143 $("body").bind("evently-changes-"+app.db.name, function() {
144 elem.trigger("_changes");
147 elem.trigger("_changes");
151 // eventlyHandler applies the user's handler (h) to the
152 // elem, bound to trigger based on name.
153 function eventlyHandler(elem, name, h, args) {
155 elem.bind(name, function() {
160 elem.pathbinder(name, h.path);
162 var f = funViaString(h, name);
163 if (typeof f == "function") {
164 elem.bind(name, {args:args}, f);
165 } else if (typeof f == "string") {
166 elem.bind(name, {args:args}, function() {
167 $(this).trigger(f, arguments);
170 } else if ($.isArray(h)) {
171 // handle arrays recursively
172 for (var i=0; i < h.length; i++) {
173 eventlyHandler(elem, name, h[i], args);
176 // an object is using the evently / mustache template system
178 throw("e.fun has been removed, please rename to e.before")
180 // templates, selectors, etc are intepreted
181 // when our named event is triggered.
182 elem.bind(name, {args:args}, function() {
183 renderElement($(this), h, arguments);
189 $.fn.replace = function(elem) {
190 // $.log("Replace", this)
191 $(this).empty().append(elem);
194 // todo: ability to call this
195 // to render and "prepend/append/etc" a new element to the host element (me)
196 // as well as call this in a way that replaces the host elements content
197 // this would be easy if there is a simple way to get at the element we just appended
198 // (as html) so that we can attache the selectors
199 function renderElement(me, h, args, qrun, arun) {
200 // if there's a query object we run the query,
201 // and then call the data function with the response.
202 if (h.before && (!qrun || !arun)) {
203 funViaString(h.before, me).apply(me, args);
205 if (h.async && !arun) {
206 runAsync(me, h, args)
207 } else if (h.query && !qrun) {
208 // $.log("query before renderElement", arguments)
209 runQuery(me, h, args)
211 // $.log("renderElement")
212 // $.log(me, h, args, qrun)
213 // otherwise we just render the template with the current args
214 var selectors = runIfFun(me, h.selectors, args);
215 var act = (h.render || "replace").replace(/\s/g,"");
216 var app = $$(me).app;
218 // $.log("rendering", h.mustache)
219 var newElem = mustachioed(me, h, args);
223 if (act == "replace") {
228 forIn(selectors, function(selector, handlers) {
229 // $.log("selector", selector);
230 // $.log("selected", $(selector, s));
231 $(selector, s).evently(handlers, app, args);
232 // $.log("applied", selector);
236 runIfFun(me, h.after, args);
241 // todo this should return the new element
242 function mustachioed(me, h, args) {
243 var partials = $$(me).partials;
245 runIfFun(me, h.mustache, args),
246 runIfFun(me, h.data, args),
247 runIfFun(me, $.extend(true, partials, h.partials), args)));
250 function runAsync(me, h, args) {
251 // the callback is the first argument
252 funViaString(h.async, me).apply(me, [function() {
254 $.argsToArray(arguments).concat($.argsToArray(args)), false, true);
255 }].concat($.argsToArray(args)));
259 function runQuery(me, h, args) {
260 // $.log("runQuery: args", args)
261 var app = $$(me).app;
262 var qu = runIfFun(me, h.query, args);
264 var viewName = qu.view;
265 var userSuccess = qu.success;
266 // $.log("qType", qType)
269 forIn(qu, function(k, v) {
270 if (["type", "view"].indexOf(k) == -1) {
275 if (qType == "newRows") {
276 q.success = function(resp) {
277 // $.log("runQuery newRows success", resp.rows.length, me, resp)
278 resp.rows.reverse().forEach(function(row) {
279 renderElement(me, h, [row].concat($.argsToArray(args)), true)
281 if (userSuccess) userSuccess(resp);
283 newRows(me, app, viewName, q);
285 q.success = function(resp) {
286 // $.log("runQuery success", resp)
287 renderElement(me, h, [resp].concat($.argsToArray(args)), true);
288 userSuccess && userSuccess(resp);
291 app.view(viewName, q);
295 // this is for the items handler
296 // var lastViewId, highKey, inFlight;
297 // this needs to key per elem
298 function newRows(elem, app, view, opts) {
299 // $.log("newRows", arguments);
300 // on success we'll set the top key
301 var thisViewId, successCallback = opts.success, full = false;
302 function successFun(resp) {
303 // $.log("newRows success", resp)
304 $$(elem).inFlight = false;
305 var JSONhighKey = JSON.stringify($$(elem).highKey);
306 resp.rows = resp.rows.filter(function(r) {
307 return JSON.stringify(r.key) != JSONhighKey;
309 if (resp.rows.length > 0) {
310 if (opts.descending) {
311 $$(elem).highKey = resp.rows[0].key;
313 $$(elem).highKey = resp.rows[resp.rows.length -1].key;
316 if (successCallback) {successCallback(resp, full)};
318 opts.success = successFun;
320 if (opts.descending) {
321 thisViewId = view + (opts.startkey ? JSON.stringify(opts.startkey) : "");
323 thisViewId = view + (opts.endkey ? JSON.stringify(opts.endkey) : "");
325 // $.log(["thisViewId",thisViewId])
326 // for query we'll set keys
327 if (thisViewId == $$(elem).lastViewId) {
328 // we only want the rows newer than changesKey
329 var hk = $$(elem).highKey;
330 if (hk !== undefined) {
331 if (opts.descending) {
333 // opts.inclusive_end = false;
338 // $.log("add view rows", opts)
339 if (!$$(elem).inFlight) {
340 $$(elem).inFlight = true;
341 app.view(view, opts);
345 // $.log("new view stuff")
347 $$(elem).lastViewId = thisViewId;
348 $$(elem).highKey = undefined;
349 $$(elem).inFlight = true;
350 app.view(view, opts);
354 // only start one changes listener per db
355 function followChanges(app) {
356 var dbName = app.db.name, changeEvent = function(resp) {
357 $("body").trigger("evently-changes-"+dbName, [resp]);
359 if (!$.evently.changesDBs[dbName]) {
360 if (app.db.changes) {
361 // new api in jquery.couch.js 1.0
362 app.db.changes(null, $.evently.changesOpts).onChange(changeEvent);
364 // in case you are still on CouchDB 0.11 ;) deprecated.
365 connectToChanges(app, changeEvent);
367 $.evently.changesDBs[dbName] = true;
370 $.evently.followChanges = followChanges;
371 // deprecated. use db.changes() from jquery.couch.js
372 // this does not have an api for closing changes request.
373 function connectToChanges(app, fun, update_seq) {
374 function changesReq(seq) {
375 var url = app.db.uri+"_changes?heartbeat=10000&feed=longpoll&since="+seq;
376 if ($.evently.changesOpts.include_docs) {
377 url = url + "&include_docs=true";
381 contentType: "application/json",
383 complete: function(req) {
384 var resp = $.httpData(req, "json");
386 connectToChanges(app, fun, resp.last_seq);
391 changesReq(update_seq);
393 app.db.info({success: function(db_info) {
394 changesReq(db_info.update_seq);