I am working on a Meteor project that has come custom Pagination using Sessions. The template rendering the contents of said items is using ellipsis.js and highlight.js to do some DOM formatting. The code looks something like thus:
if (Meteor.isClient) {
Meteor.startup(function () {
Session.setDefault("homePageSize", 10);
Session.setDefault("homePageStart", 0);
});
}
Template.home.articlesPaginated = function() {
return Articles.find({published: true}, {sort: {post_date: -1}, skip: Session.get("homePageStart"), limit: Session.get("homePageSize")});
}
Template.home.rendered = function() {
// Setup ellipsis
$('.ellipsis').dotdotdot({
ellipsis: '...',
wrap: 'word',
fallbackToLetter: true,
after: $('a.blog_continue')
});
// Setup highlight.js
$('pre code').each(function(i, block) {
hljs.highlightBlock(block);
});
}
Template.home.events({
'click .next': function(event) {
var offset = Session.get("homePageStart") + Session.get("homePageSize");
if (offset < 0) {
offset = 0;
}
Session.set("homePageStart", offset);
},
'click .prev': function(event) {
var offset = Session.get("homePageStart") - Session.get("homePageSize");
if (offset < 0) {
offset = 0;
}
Session.set("homePageStart", offset);
}
});
Pagination is working just fine, but as soon as the Template re-renders I loose all the ellipsis.js and highlight.js formatting. I know the obvious reason is that the DOM has changed, and since the Template.render only runs once up-front and doesn't happen when the Template re-renders the DOM updates are not being applied. So, what is the best way to trigger ellipsis.js and highlight.js after the Template is done such that it gets re-called everytime the Template re-renders?
Basically you need to listen for changes in your Articles collection, which is a client-side subset of the server database clipped to contain only the currently visible paginated articles.
When you detect a change in the articles subset, you'll need to retrigger initialization of ellipsis.js and highlight.js.
You could reorganize your code as follow :
First, we define the cursor declaration as a separate function on his own because we need to use it twice :
function articlesPaginated(){
return Articles.find({
published: true
}, {
sort: {
post_date: -1
},
skip: Session.get("homePageStart"),
limit: Session.get("homePageSize")
});
}
Template.home.helpers({
articlesPaginated:articlesPaginated
});
Then in the rendered callback, we need to setup a reactive computation that will depend on this cursor, so whenever the articles subset is updated to a new page, our computation will rerun.
But we need to be aware that the helper we defined on the home template returns the same cursor so it's going to be invalidated and trigger DOM refresh AT THE SAME TIME... JavaScript is single-threaded and the Tracker.Computation manual states that the order of execution of concurrently invalidated computations is unpredictable.
So we cannot just trigger the ellipsis/highlight initialization code in the computation because this setup code assumes that the DOM is ready, however at this precise moment we don't know if DOM manipulation has just happened before or is going to happen immediately after.
Fortunately there is a Tracker.afterFlush method which allows us to execute code after concurrent computations are done so we are sure that by that time DOM state is OK.
Having understand all these implications, we can write the following rendered callback :
Template.home.rendered=function(){
// declare a template managed Deps.Computation
this.autorun(function(){
// have this reactive computation depend on the SAME cursor
// that triggers DOM rerendering
var articles=articlesPaginated();
// forEach is actually the method that triggers a dependency on the cursor in this computation
articles.forEach(function(article){
// you can manipulate the model here if needed
});
// setup a callback to execute your DOM alteration code after
// it is actually rerendered by Blaze
Tracker.afterFlush(function(){
// your ellipsis/highlight initialization code goes here
});
});
};
If you can put your Articles into another template, then you could apply formatting individually as they are inserted.
Template.article.rendered = function () {
// Setup ellipsis
this.$('.ellipsis').dotdotdot({
ellipsis: '...',
wrap: 'word',
fallbackToLetter: true,
after: $('a.blog_continue')
});
// Setup highlight.js
this.$('pre code').each(function(i, block) {
hljs.highlightBlock(block);
});
};
Assuming your template looks something like this.
<template name="home">
...
{{#each articlesPaginated}}
{{> article}}
{{/each}}
{{> paginationControls}}
...
</template>
This has the added benefit of scoping the formatting to just the articles, rather than the entire DOM.
Related
I have a template that subscribes to a document. Everything works fine in the DOM and Blaze updates as soon as an attribute used in the template helpers is changed.
I also have some custom logic that doesn't appears in the DOM and depends on the document attributes. How can I call a function to change that logic when an attribute is updated?
I'm looking for something like this.data.attr.onChanged where this would refer to the template and this.data is the data send to the template, as usual; or a Meteor function that is rerun on change where I could put my callback in.
I hoped that template.onRendered would be recalled, but that's not the case.
I've read a lot about reactive variables, but could not find how they could be useful here.
[edit] the change is coming from the server that is communicating with another service
I've tried Tracker.autorun like this:
Template.editItem.onRendered(function() {
var self = this;
Tracker.autorun(function () {
console.log("tracker", self.data.item.socketId);
});
});
And the corresponding route is:
Router.route('editItem', {
path: '/edit/:_id',
waitOn: function () {
var sub = Meteor.subscribe('item', this.params._id);
return [sub];
},
data: function () {
return {item: Items.findOne(this.params._id)};
},
action: function () {
if (this.ready())
this.render();
}
});
At some point, the property socketId gets removed from the corresponding document by the server and I'm sure of that since I've checked in the shell, but the tracker doesn't rerun.
Use Template.currentData().item.socketId instead of self.data.item.socketId, this will give you reactivity.
And in templates generally, use self.autorun instead of Tracker.autorun (unlike Tracker.autorun, this will ensure that the autorun is stopped when the template is destroyed). Likewise, if you want to subscribe in a template, use self.subscribe instead of Meteor.subscribe.
Code to see if Template.currentData() works for you:
Template.editItem.onRendered(function() {
var self = this;
self.autorun(function () {
console.log("tracker", Template.currentData().item.socketId);
});
});
I'm not sure if I got you right, you just want to observe your html inputs and apply the new value to your helper method(s) on change?!
If so, you could use session variables to store your temporary UI state:
// observe your input
Template.yourTemplate.events({
"change #inputA": function (event) {
if(event.target.value != "") {
Session.set("valueA", event.target.value);
}
}
}
// apply the changed value on your helper function
Template.yourTemplate.helpers({
getSomeData: function() {
var a = Session.get("valueA");
// do something with a ..
}
}
In meteor's official todo app tutorial this concept is also used.
If you need to re-run something which is not part of DOM/helper, you can use Tracker.autorun. According to meteor docs, Run a function now and rerun it later whenever its dependencies change.
here's the docs link
Try moving the subscription into Tracker.autorun
Template.editItem.onRendered(function() {
var self = this;
Tracker.autorun(function () {
Meteor.subscribe('item', this.params._id);
console.log("tracker", self.data.item.socketId);
});
});
Of course you can't use this.params there so you can store this as a Session variable
<template name="FrameItems">
<div class="frame-items">
{{#each frames}}
{{> FrameItem}}
{{/each}}
</div>
</template>
In the above example, I want to know when all FrameItem templates inside FrameItems template have been rendered. I thought onRendered of the parent would be invoked when all the child templates have been rendered, but it was just called right away. What's the conventional way of making sure all the child templates are rendered?
One way to do it is to use a counter and increment it until it reaches a certain value.
Here the counter would in Session and incremented until it reaches the length of your Frames iterable thing:
Template.FrameItems.onRendered(function() {
Session.set('frameCounter', 0);
});
Template.FrameItem.onRendered(function() {
Session.set('frameCounter', Session.get('frameCounter') + 1);
});
Then you simply use a tracker:
//Where template is your template instance, for example 'this' in an onCreated callback
template.autorun(function doStuffWhenFramesRendered(computation) {
if(Session.get('frameCounter') === template.frames.length) {
doStuff();
//Stop observing
computation.stop();
}
});
Note that it takes into account the fact that FrameItem may render at weird times (avoiding race conditions if any), but it doesn't take into account new frames. To take those into account you would not stop the computation.
Here is how I would proceed:
You create a pageSession reactive variable or reactive dictionary entry. Let's call it lastRendered.
You update it in the onRendered function of your FrameItem template using the _id of the related frames item. This way, each time a FrameItem template is rendered, you now which one it is.
You create an helper in your parent template watching your lastRendered reactive variable and checking if it matches your last frames item. It could look like that (untested code):
lastFrameIsRendered: function() {
var lastId = frames.find().limit(1).sort({$natural:-1}).fetch()._id;
return pageSession.get ("lastRendered") === lastId;
},
Alternatively, if you need to get a feedback in your parent template onRendered function, you can wrap this code into a this.autorun(function() { (tracker) like this:
var lastId = frames.find().limit(1).sort({$natural:-1}).fetch()._id;
this.autorun(function() {
if (pageSession.get ("lastRendered") === lastId) {
//do your stuff
}
});
It will be executed each time there is a change in your parent template.
I have a route like this:
Router.route('/box', function () {
this.render('boxCanvasTpl');
},{
name: 'box',
layoutTemplate: 'appWrapperLoggedInTpl',
waitOn: function() {
console.log("Box route ran ok.");
return [
Meteor.subscribe('item_ownership_pub', function() {
console.log("subscription 'item_ownership_pub' is ready.");
}),
Meteor.subscribe('my_items', function() {
console.log("subscription 'my_items' is ready.");
})
];
}
});
... and I am clicking a link in a Template like this:
My Link
I receive the 'Box route ran ok.' message, but some reason the page does not navigate to the given URL. I have added console.log code in the funciton that is run when the 'boxCanvasTpl' is rendered, but these aren't showing in the browser console. It seems that something inbetween is stopping the templkate from re-rendering, but can't put my finger on it - any ideas?
There are some properties of Iron Router that you need to be aware of.
Say that the user is currently already on /boxes and there is a box template that renders for that path. If you:
click on a link Click Me
or
click on a link Click Me
Iron Router will NOT re-render the template because it already exists on the page. It will also NOT re-render the template if the box template happens to be a partial template that is already rendered on the page that you're on and also exists on the page that you want to navigate to.
Since it doesn't re-render, any code you have inside Template.box.onRendered will also not run again.
This behavior is most common in your layout, header, and footer templates. For many users, these templates are used for all of a website's pages, regardless of path. Because the layout, header, and footer template is rendered on a person's first visit to the site, they won't be re-rendered ever again if the user decides to navigate to other parts of the site using the same templates, so the code inside Template.layout/header/footer.onRendered won't fire.
Also note - even if a reactive Spacebars helper changes the physical look of the layout / header / footer, it doesn't qualify as an actual render, so reactive updates to the template do not trigger the onRendered callback.
The lack of re-rendering is what gives Meteor that "snappy" feel.
EDIT
Try to code in a reactive, event-driven style. Try not to think too much in a render / re-render sense.
You go to /box
You click on a link for /box?box=2342
Get your params or query in Iron Router
https://github.com/iron-meteor/iron-router/blob/devel/Guide.md#route-parameters
In Iron Router use the data from the params or query to set the data context for the template.
Grab stuff from the data context as needed inside of the template's .onRendered, .events, and .helpers callbacks.
Set Session vars as necessary and use them in helpers to give reactive changes to the page without having to re-render a template. Also use events to trigger updates to the session vars to, again, trigger reactive changes to the page.
Try this:
afterwards, go to /test?BUNNIES=lalalala
check out the console logs
test.html
<template name="test">
{{myData}}
</template>
test.js
Template.test.helpers({
myData: function() {
console.log("data context accessed from test.helpers: ", this);
console.log("this.BUNNIES accessed from test.helpers: ", this.BUNNIES);
return this.BUNNIES;
}
});
Template.test.onRendered(function() {
console.log("data context accessed from test.onRendered: ", this.data);
});
Template.test.events({
'click': function(){
console.log("data accessed from test.events: ", this);
}
});
router.js
Router.route('/test', function() {
console.log("routed!");
this.render('test');
}, {
name: 'test',
data: function(){
//here I am setting the data context
// for /test?BUNNIES=1234
var query = this.params.query;
console.log("query: ", query);
return query;
},
waitOn: function() {
console.log("waitOn is running (should see this message once for each subscription)");
return [
Meteor.subscribe('item_ownership_pub'),
Meteor.subscribe('my_items')
];
}
});
way cleaner way of writing router
Router.route('/test', {
waitOn: function() {
console.log("waitOn is running (should see this message once for each subscription");
return [
Meteor.subscribe('item_ownership_pub'),
Meteor.subscribe('my_items')
];
},
data: function(){
var query = this.params.query;
console.log("query: ", query);
return query;
},
action: function(){
console.log("this will re-render if url params changed");
this.render();
}
})
I have the following template:
<template name="modalTest">
{{session "modalTestNumber"}} <button id="modalTestIncrement">Increment</button>
</template>
That session helper simply is a go-between with the Session object. I have that modalTestNumber initialized to 0.
I want this template to be rendered, with all of it's reactivity, into a bootbox modal dialog. I have the following event handler declared for this template:
Template.modalTest.events({
'click #modalTestIncrement': function(e, t) {
console.log('click');
Session.set('modalTestNumber', Session.get('modalTestNumber') + 1);
}
});
Here are all of the things I have tried, and what they result in:
bootbox.dialog({
message: Template.modalTest()
});
This renders the template, which appears more or less like 0 Increment (in a button). However, when I change the Session variable from the console, it doesn't change, and the event handler isn't called when I click the button (the console.log doesn't even happen).
message: Meteor.render(Template.modalTest())
message: Meteor.render(function() { return Template.modalTest(); })
These both do exactly the same thing as the Template call by itself.
message: new Handlebars.SafeString(Template.modalTest())
This just renders the modal body as empty. The modal still pops up though.
message: Meteor.render(new Handlebars.SafeString(Template.modalTest()))
Exactly the same as the Template and pure Meteor.render calls; the template is there, but it has no reactivity or event response.
Is it maybe that I'm using this less packaging of bootstrap rather than a standard package?
How can I get this to render in appropriately reactive Meteor style?
Hacking into Bootbox?
I just tried hacked into the bootbox.js file itself to see if I could take over. I changed things so that at the bootbox.dialog({}) layer I would simply pass the name of the Template I wanted rendered:
// in bootbox.js::exports.dialog
console.log(options.message); // I'm passing the template name now, so this yields 'modalTest'
body.find(".bootbox-body").html(Meteor.render(Template[options.message]));
body.find(".bootbox-body").html(Meteor.render(function() { return Template[options.message](); }));
These two different versions (don't worry they're two different attempts, not at the same time) these both render the template non-reactively, just like they did before.
Will hacking into bootbox make any difference?
Thanks in advance!
I am giving an answer working with the current 0.9.3.1 version of Meteor.
If you want to render a template and keep reactivity, you have to :
Render template in a parent node
Have the parent already in the DOM
So this very short function is the answer to do that :
renderTmp = function (template, data) {
var node = document.createElement("div");
document.body.appendChild(node);
UI.renderWithData(template, data, node);
return node;
};
In your case, you would do :
bootbox.dialog({
message: renderTmp(Template.modalTest)
});
Answer for Meteor 1.0+:
Use Blaze.render or Blaze.renderWithData to render the template into the bootbox dialog after the bootbox dialog has been created.
function openMyDialog(fs){ // this can be tied to an event handler in another template
<! do some stuff here, like setting the data context !>
bootbox.dialog({
title: 'This will populate with content from the "myDialog" template',
message: "<div id='dialogNode'></div>",
buttons: {
do: {
label: "ok",
className: "btn btn-primary",
callback: function() {
<! take some actions !>
}
}
}
});
Blaze.render(Template.myDialog,$("#dialogNode")[0]);
};
This assumes you have a template defined:
<template name="myDialog">
Content for my dialog box
</template>
Template.myDialog is created for every template you're using.
$("#dialogNode")[0] selects the DOM node you setup in
message: "<div id='dialogNode'></div>"
Alternatively you can leave message blank and use $(".bootbox-body") to select the parent node.
As you can imagine, this also allows you to change the message section of a bootbox dialog dynamically.
Using the latest version of Meteor, here is a simple way to render a doc into a bootbox
let box = bootbox.dialog({title:'',message:''});
box.find('.bootbox-body').remove();
Blaze.renderWithData(template,MyCollection.findOne({_id}),box.find(".modal-body")[0]);
If you want the dialog to be reactive use
let box = bootbox.dialog({title:'',message:''});
box.find('.bootbox-body').remove();
Blaze.renderWithData(template,function() {return MyCollection.findOne({_id})},box.find(".modal-body")[0]);
In order to render Meteor templates programmatically while retaining their reactivity you'll want to use Meteor.render(). They address this issue in their docs under templates.
So for your handlers, etc. to work you'd use:
bootbox.dialog({
message: Meteor.render(function() { return Template.modalTest(); })
});
This was a major gotcha for me too!
I see that you were really close with the Meteor.render()'s. Let me know if it still doesn't work.
This works for Meteor 1.1.0.2
Assuming we have a template called changePassword that has two fields named oldPassword and newPassword, here's some code to pop up a dialog box using the template and then get the results.
bootbox.dialog({
title: 'Change Password',
message: '<span/>', // Message can't be empty, but we're going to replace the contents
buttons: {
success: {
label: 'Change',
className: 'btn-primary',
callback: function(event) {
var oldPassword = this.find('input[name=oldPassword]').val();
var newPassword = this.find('input[name=newPassword]').val();
console.log("Change password from " + oldPassword + " to " + newPassword);
return false; // Close the dialog
}
},
'Cancel': {
className: 'btn-default'
}
}
});
// .bootbox-body is the parent of the span, so we can replace the contents
// with our template
// Using UI.renderWithData means we can pass data in to the template too.
UI.insert(UI.renderWithData(Template.changePassword, {
name: "Harry"
}), $('.bootbox-body')[0]);
I am currently using iron-router and this is my very first attempt to try out the Meteor platform. I has been running into issues where most of the jquery libraries failed to initialized properly because the of the way Meteor renders html, $(document).ready() fires before any templates are rendered. I am wondering is there any callbacks from Meteor/iron-router that allows me to replace the jQuery's dom ready?
Also, how should I (easily and properly) handle the live update of the dom elements if some of them are customized by jQuery/javascript?
This is what i am currently doing, i feel like it is very hackish and probably would run into issues if the elements got updated after the initialization.
var jsInitalized = false;
Router.map(function () {
this.route('', {
path: '/',
layoutTemplate: 'default',
after: function(){
if(!jsInitalized){
setTimeout(function(){
$(document).ready( function() { $$$(); });
}, 0);
jsInitalized = true;
}
}
});
}
With Meteor you generally want to think about when a template is ready, not when the dom is ready.
For example, let's say you want to use the jQuery DataTables plugin to add sorting to a table element that's created by a template. You would listen to the template's rendered event and bind the plugin to the dom:
HTML:
<template name="data_table">
<table class="table table-striped" id="tblData">
</table>
</template>
JavaScript:
Template.data_table.rendered = function () {
$('#tblData').dataTable();
};
Now anytime the template is re-rendered (for example, if the data changes), your handler will be called and you can bind the jQuery plugin to the dom again.
This is the general approach. For a complete example (that includes populating the table with rows) see this answer.
Try making a separate .js file, call it rendered.js if you'd like. and then;
Template.layout.rendered = function ()
{
$(document).ready(function(){console.log('ready')});
}
I use template layout, but you can do Template.default.rendered. I hope that helps.
Also take a look at this part of documentation, especially the Template.events; http://docs.meteor.com/#templates_api
I use Meteor v0.8.0 with Iron Router (under Windows 7) and here is how I handle 'DOM ready':
When I want to modify the DOM after a specific template has been rendered:
I use Template.myTemplateName.rendered on the client side :
Template.blog.rendered = function()
{
$('#addPost').click(function()
{
...
});
}
When I want to modify the DOM after any new path has been rendered:
I use Router.onAfterAction, but there seems to be a trick:
Router.onAfterAction(function()
{
setTimeout(function()
{
$('.clickable').click(function()
{
...
});
}, 0);
});
Notice the setTimeout(..., 0), it doesn't work for me otherwise (DOM empty).
Notice that you can use onAfterAction on specific path, but most of the time I think it is redundant with the Template.myTemplateName.rendered method above.
What seems to be missing:
A way to modify the DOM after any template has been rendered.