diff --git a/packages/liveui/liveui.js b/packages/liveui/liveui.js index 213f8a44d4..0f9925027b 100644 --- a/packages/liveui/liveui.js +++ b/packages/liveui/liveui.js @@ -169,29 +169,36 @@ Sky.ui.render = function (render_func, events, event_data) { /// options to include: /// selector: minimongo selector (default: {}) /// sort: minimongo sort specification (default: natural order) -/// render: render function (from object to element) +/// render: render function (as in render(), but takes a document) /// .. plus optionally +/// render_empty: render function for content to show when query empty. +/// still gets same event bindings. /// events: vaguely backbone-style live event specification /// {'click #selector #path' : function (obj) { } } /// /// returns an object with: /// stop(): stop updating, tear everything down and let it get GC'd -/// -/// XXX rewrite using Sky.ui.render, and new GC semantics, and make it -/// return a fragment rather than plopping its results into a -/// container -Sky.ui.renderList = function (collection, options) { +/// XXX consider making render an argument rather than an option +/// +/// XXX what package should this go in? depends on both liveui and minimongo.. +/// +/// XXX what can now be a collection, or the handle of an existing +/// findlive. messy. +Sky.ui.renderList = function (what, options) { var frag = document.createDocumentFragment(); var context = new Sky.deps.Context; - var start = document.createComment("renderList " + collection._name); - var end = document.createComment("end " + collection._name); + var is_handle = what instanceof Collection.LiveResultsSet; + var name = (is_handle ? what.collection : what)._name; + var start = document.createComment("renderList " + name); + var end = document.createComment("end " + name); start._used = end._used = true; start._context = context; frag.appendChild(start); frag.appendChild(end); + var empty_shown = false; var entry_starts = []; var entry_end = function (idx) { return (entry_starts[idx + 1] || end).previousSibling; @@ -206,14 +213,26 @@ Sky.ui.renderList = function (collection, options) { return this_start; }; - var query = collection.findLive(options.selector, { - sort: options.sort, + var maybe_show_empty = function () { + if (!entry_starts.length && options.render_empty) { + start.parentNode.insertBefore( + Sky.ui.render(options.render_empty, options.events), end); + empty_shown = true; + } + }; + + var query_opts = { added: function (doc, before_idx) { + if (empty_shown) { + Sky.ui._remove(start.nextSibling, end.previousSibling); + empty_shown = false; + } entry_starts.splice(before_idx, 0, insert_entry(doc, before_idx)); }, removed: function (id, at_idx) { Sky.ui._remove(entry_starts[at_idx], entry_end(at_idx)); entry_starts.splice(at_idx, 1); + maybe_show_empty(); }, changed: function (doc, at_idx) { var this_start = insert_entry(doc, at_idx); @@ -228,7 +247,16 @@ Sky.ui.renderList = function (collection, options) { entry_starts.splice(old_idx, 1); entry_starts.splice(new_idx, 0, this_start); } - }); + }; + + if (is_handle) { + var query = what; + query.reconnect(query_opts); + } else { + query_opts.sort = options.sort; + var query = what.findLive(options.selector || {}, query_opts); + } + maybe_show_empty(); context.on_invalidate(function (old_context) { query.stop(); diff --git a/packages/minimongo/minimongo.js b/packages/minimongo/minimongo.js index 50a1860d89..46d6c3c8f4 100644 --- a/packages/minimongo/minimongo.js +++ b/packages/minimongo/minimongo.js @@ -111,12 +111,13 @@ Collection.prototype.find = function (selector, options) { // - removed (id, at_index) // * sort: sort descriptor // -// functions available on returned query handle: +// attributes available on returned query handle: // * stop(): end updates // * indexOf(id): return current index of object in result set, or -1 // * reconnect({}): replace added, changed, moved, removed, from the // arguments, and call added to deliver the current state of the // query (XXX ugly hack to support templating) +// * collection: the collection this query is querying // // iff x is a returned query handle, (x instanceof // Collection.LiveResultsSet) is true @@ -127,6 +128,7 @@ Collection.prototype.find = function (selector, options) { // XXX maybe support limit/skip // XXX it'd be helpful if removed got the object that just left the // query, not just its id +// XXX document that initial results will definitely be delivered before we return [do, add to asana] Collection.LiveResultsSet = function () {}; Collection.prototype.findLive = function (selector, options) { @@ -165,7 +167,8 @@ Collection.prototype.findLive = function (selector, options) { return i; return -1; }, - reconnect: connect + reconnect: connect, + collection: this }); return handle; }; diff --git a/packages/templating/deftemplate.js b/packages/templating/deftemplate.js index b9b2d27922..32bb7b28cc 100644 --- a/packages/templating/deftemplate.js +++ b/packages/templating/deftemplate.js @@ -17,14 +17,9 @@ if (typeof Sky === "undefined") Sky = {}; Sky._pending_partials = null; // id -> element Sky._pending_partials_idx_nonce = 0; -// XXX another disgusting hack -- we reach into handlebars and -// extend #each to know how to cooperate with pending_partials and -// minimongo findlive. -// -// XXX XXX XXX the garbage collection implications are terrible.. we -// don't even pretend to call stop on the findlive, so every time -// we're rerendered, we kick off another findlive that runs -// .. forever! +// XXX another messy hack -- we reach into handlebars and extend #each +// to know how to cooperate with pending_partials and minimongo +// findlive. Sky._hook_handlebars_each = function () { Sky._hook_handlebars_each = function(){}; // install the hook only once @@ -33,80 +28,15 @@ Sky._hook_handlebars_each = function () { if (!(context instanceof Collection.LiveResultsSet)) return orig(context, options); - // XXX inserts an intermediate DIV!! that is lame and should be - // fixed. besides general hygiene/pride, we really need to - // support - var element = document.createElement("div"); - - var trim = function (markup) { - // Consider {{#each items}\n{{> item}}\n{{/each}} - // - // In that case, options.fn will return HTML that parses into - // three nodes: whitespace, the partial, whitespace. That - // won't work. I can't reconcile this logically at the moment, - // but "do what you mean" and strip the whitespace. - // - // XXX fails if a {{#if}..{{else}}..{{/if}} is at toplevel in - // a template? - var match = markup.match(/^\s*(<[\s\S]+>)\s*$/); - if (match) - markup = match[1]; - return markup; - } - - var render = Sky._def_template(null, function (obj) { - return trim(options.fn(obj)); - }); - - var renderElse = Sky._def_template(null, function () { - return trim(options.inverse({})); - }); - - // XXX sort of lame that we always end up rendering this even if - // the query returns results 100% of the time .. - var is_empty = true; - element.appendChild(renderElse()); - - // XXX copied code from Sky.ui.renderList.. bleh - // (with addition of is_empty / renderElse) - context.reconnect({ - added: function (obj, before_idx) { - if (is_empty) { - element.removeChild(element.childNodes[0]); - is_empty = false; - } - if (before_idx === element.childNodes.length) - element.appendChild(render(obj)); - else - element.insertBefore(render(obj), element.childNodes[before_idx]); - Sky.ui._tryFocus(); - }, - removed: function (id, at_idx) { - element.removeChild(element.childNodes[at_idx]); - if (element.childNodes.length === 0) { - is_empty = true; - element.appendChild(renderElse()); - } - }, - changed: function (obj, at_idx) { - element.insertBefore(render(obj), element.childNodes[at_idx]); - element.removeChild(element.childNodes[at_idx + 1]); - Sky.ui._tryFocus(); - }, - moved: function (obj, old_idx, new_idx) { - var elt = element.removeChild(element.childNodes[old_idx]); - if (new_idx === element.childNodes.length) - element.appendChild(elt); - else - element.insertBefore(elt, element.childNodes[new_idx]); - } - }); - var id = Sky._pending_partials_idx_nonce++; - Sky._pending_partials[id] = element; + Sky._pending_partials[id] = Sky.ui.renderList(context, { + render: Sky._def_template(null, options.fn), + render_empty: Sky._def_template(null, _.bind(options.inverse, null, {})) + }); + return "
"; - } + }; }; // XXX namespacing