可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
I'm getting a collection of records and placing them in a Template, having them rendered {{#each}}
and I want to display a loading icon until the last DOM node is rendered.
My problem is that I have not found a way to query a state/fire a callback on the last item rendered aka the last DOM node to be updated/re-drawn.
It looks a bit like this in my HTML file:
<template name="stuff">
{{#each items}}
<div class="coolView">{{cool_stuff}}</div>
{{/each}}
</template>
And in the client-side JS file:
// returns all records of Stuff belonging to the currently logged-in user
Template.stuff.items = function () {
Session.set('loading_stuff', true);
return Items.find({owner: Meteor.userId()}, {sort: {created_time: -1}});
};
Template.stuff.rendered = function() {
// every time something new is rendered, there will have been loading of the DOM.
// wait a short while after the last render to clear any loading indication.
Session.set('loading_stuff', true);
Meteor.setTimeout(function() {Session.set('loading_stuff', false);}, 300);
};
The Session variable loading_stuff
is queried in a Handlebars helper, returning the name of the loading class (with a loader icon GIF) if it's true
.
The reason I do the awkward Meteor.setTimeout
is because Template.rendered
is called after every single item that was rendered into the Template. So I need to re-confirm that it is still loading, but give it some time to either load the next one or to finish rendering them all, with a little pause until it sets loading_stuff
to false
.
How do I reliably query/callback at the end of all DOM updates (for a particular Template or for the whole page) the proper Meteor way?
Many thanks.
EDIT
The solution with using the subscribe()
onReady()
callback only partially works:
Templates are seemingly called multiple (2-3) times before the rendering even starts, but after the data has returned from the server that the Template relies on to render. This means that that it's 'finished' loading data, but the DOM is still being rendered. I will play around with Meteor.autorun()
and see if I can find a reliable hook somewhere to do this properly. In the meantime, I guess my original question still remains:
How do I know when the entire Template/page has finished rendering?
FINAL EDIT:
At the end of the day, based on how the DOM is structured, it's up to the developer have a rock-solid DOM and attach the appropriate callbacks as best as possible to the elements he/she wants to check the state of. It may seem like an anti-climatic no-brainer, but for me the only way to know the final element of a Template is rendered is to have an element with a special id rendered at the very end of a Template that signals the end of the render and attach a .livequery
callback to it. I hope that Meteor will incorporate a more unified and global support for checking this state in the future.
回答1:
I found that JQuery's livequery plugin doesn't work when waiting for elements from a template to be written to the DOM. This is because the plugin listens on JQuery methods like append, but Meteor doesn't use JQuery. It uses an object called LiveRange
to write to the DOM. I modified my livequery JavaScript file to work with this.
Near the bottom of the file, there is a line that calls registerPlugin()
. I modified it to look like this:
$.livequery.registerPlugin([
{object: $.fn, methods: ['append', 'prepend', 'after', 'before', 'wrap', 'attr', 'removeAttr', 'addClass', 'removeClass', 'toggleClass', 'empty', 'remove', 'html']},
{object: LiveRange.prototype, methods: ['insertBefore', 'insertAfter']}
]);
Then I modified the registerPlugin() method to look like this:
registerPlugin: function(registration) {
for (var i = 0; i < registration.length; i++) {
var object = registration[i].object;
var methods = registration[i].methods;
$.each( methods, function(i,n) {
// Short-circuit if the method doesn't exist
if (!object[n]) return;
// Save a reference to the original method
var old = object[n];
// Create a new method
object[n] = function() {
// Call the original method
var r = old.apply(this, arguments);
// Request a run of the Live Queries
$.livequery.run();
// Return the original methods result
return r;
}
});
}
}
This will check to see if the element exists when insertBefore()
or insertAfter()
is called on the LiveRange
object and will still work with JQuery as it did before.
回答2:
Nesting an item template inside the current one might actually help?
<template name="stuff">
{{#each items}}
{{> itemTemplate}}
{{/each}}
</template>
<template name="itemTemplate">
<div class="coolView">{{cool_stuff}}</div>
</template>
Now the callback on Template.itemTemplate.rendered may run every time new item is updated into the DOM.
回答3:
You can use a jquery plugin like livequery to follow-up dom insertions for a specific selector:
$('.my-rendered-element').livequery(function () {
console.log('I've been added to the DOM');
});
If you also need to make sure images have been loaded and everything is well rendered you can use some other utility like this other plugin imagesLoaded, and do something like:
$('.my-rendered-element').livequery(function () {
$(this).imagesLoaded(function() {
console.log('My images have been loaded');
});
});
It's a workaround and it maybe doesn't integrate well with Meteor, but I found these 2 guys very useful in many situations.
回答4:
In my case, a partial solution is this:
Add the onComplete()
callback to the subscriptions of my Items
collection, which is the official end of any loading process. So, no more hacky setTimeout
s in Template.rendered
, just set loading_stuff
to true
when data is queried (in Template.stuff.items
) and then to false
in the onComplete()
callback in the subscribe function:
Server:
Meteor.publish('items', function (some_param) {
return Items.find({some_field: some_param});
});
and Client:
Meteor.subscribe('items', some_param, function onComplete() {
Session.set('loading_stuff', false);
});
...
Template.stuff.items = function () {
Session.set('loading_stuff', true);
return Items.find({owner: Meteor.userId()}, {sort: {created_time: -1}});
};
It's a partial solution because knowing when the data has arrived from the server and when the DOM has finished rendering are two separate issues.
回答5:
There are a few options:
- As you mentioned in your final comment, you could create your reactive context and handle changes there.
- You could create a special value, ie. _id = "END" and look for that value in the template rendering.
- My favorite: instead of using a collection, populate the template as a result of a
Meteor.call
and Meteor.methods
call pair. You could also put the result in a reactive variable so the template automatically re-renders.
As you may have already discovered, the onReady
event of subscribe()
fires when the publish method calls ready()
, not when all the data has shipped over.
Here's a simple example of #3 above:
In the client:
Meteor.call('get_complete_email',id, function(err,doc) {
if (err === undefined) {
// Note current_compose is reactive
current_compose.set(new _ComposePresenter(action, doc));
} else {
log_error('compose','get complete email ',err);
}
});
In the server:
Meteor.methods({
'get_complete_email': function(id) {
return Emails.findOne({_id:id, user_id:Meteor.userId}, body_message_fields);
}
});
In your presenter or viewer code: (the data
temp variable could be eliminated - it's legacy and hasn't been refactored out yet).
Template.compose_to_cc_bcc_subject.prefilled = function(part) {
if (Current && Current.compose()) {
var data = Current.compose();
if (data == undefined || data[part] == undefined) { return; }
return data[part];
} else {
return;
}
}
Obviously, you'll need to wire up current_compose, Current and your own objects a little differently.
回答6:
I do it yet another DIY way. Map an index/rank onto your cursor and then check that index against a count of the same query in your .rendered
callback:
items: function() {
return Items.find({whatever: whatever}).map(function(item, index) {
item.rank = index + 1;
return item;
});
}
Template.stuff.rendered = function() {
if(this.data.rank === Items.find({whatever: this.data.whatever}).count()) {
// Do something cool like initializing a sweet
// JS plugin now that ALL your template instances
// are all rendered
$("#coolplugindiv").coolJSPluginInit();
}
}
I'm sure it's slightly more taxing on the server, but it works. Thought I'm curious about template.lastNode
and if there's some way to use that to have the same effect.
回答7:
I find it helpful to run a callback in the onRendered
block of the next template:
<template name="stuff">
{{#each items}}
<div class="coolView">{{cool_stuff}}</div>
{{/each}}
{{> didMyStuffLoad}}
</template>
then:
Template.didMyStuffLoad.rendered = function () {
Session.set('loading_stuff', false);
}
Everything will be loaded into the DOM before this onRendered block executes.