Pagination with meteor and iron router not re-rendering - meteor

I'm really struggling to understand what's going on here. I have a template
<template name="gallery">
<div>
123
</div>
{{#each art}}
{{name}}
{{/each}}
</template>
Then I have routes like this
Router.map(function() {
this.route('/', {
template: 'gallery',
data: function() {
var page = 1;
return {
page: page,
};
},
});
this.route('/gallery/:_page', {
template: 'gallery',
data: function() {
var page = parseInt(this.params._page);
return {
page: page,
};
},
});
});
Following this article I'm using template subscriptions like this
var G_PAGE_SIZE = 10;
Template.gallery.onCreated(function() {
var instance = this;
var route = Router.current();
instance.loaded = new ReactiveVar(0);
instance.autorun(function() {
var pageId = parseInt(route.params._page);
var page = pageId - 1;
var skip = page * G_PAGE_SIZE;
var subscription = instance.subscribe('artForGrid', skip, G_PAGE_SIZE);
if (subscription.ready()) {
instance.loaded.set(1);
}
});
instance.art = function() {
return Art.find({});
};
});
Template.gallery.helpers({
art: function() {
return Template.instance().art();
},
});
It works but when I click one of the links changing the route the page doesn't re-render
So ... some other answer on here said that's because there's no reactive connection between data changing on the route through the router and the template.
So I tried use route.state which as a ReactiveDict (not sure where that came from). If it's part of iron:router or if it's part of reactive-var
Anyway I changed the code to
Router.map(function() {
this.route('/', {
template: 'gallery',
data: function() {
var page = 1;
this.state.set('page', page); // <------------ADDED
return {
page: page,
};
},
});
this.route('/gallery/:_page', {
template: 'gallery',
data: function() {
var page = parseInt(this.params._page);
this.state.set('page', page); // <------------ADDED
return {
page: page,
};
},
});
});
In the onCreated
Template.gallery.onCreated(function() {
var instance = this;
var route = Router.current();
instance.loaded = new ReactiveVar(0);
instance.autorun(function() {
var pageId = route.state.get('page'); // <--------- CHANGED
var page = pageId - 1;
var skip = page * G_PAGE_SIZE;
var subscription = instance.subscribe('artForGrid', skip, G_PAGE_SIZE);
if (subscription.ready()) {
instance.loaded.set(1);
}
});
instance.art = function() {
return Art.find({});
};
});
Template.gallery.helpers({
art: function() {
return Template.instance().art();
},
});
That doesn't work either except sometimes. In the interest of debugging I added a console.log to the route
Router.map(function() {
this.route('/', {
template: 'gallery',
data: function() {
var page = 1;
this.state.set('page', page);
console.log("/root"); // <------------ADDED
return {
page: page,
};
},
});
this.route('/gallery/:_page', {
template: 'gallery',
data: function() {
var page = parseInt(this.params._page);
this.state.set('page', page);
console.log("/gallery/" + page); // <------------ADDED
return {
page: page,
};
},
});
});
Suddenly it starts working !???!#!? I remove the console.logs and it stops
I also tried adding actions without the console.logs
Router.map(function() {
this.route('/', {
template: 'gallery',
data: function() {
var page = 1;
this.state.set('page', page);
return {
page: page,
};
},
action: function() { // <- added
var page = 1; // <- added
this.state.set('page', page); // <- added
this.render(); // <- added
}, // <- added
});
this.route('/gallery/:_page', {
template: 'gallery',
data: function() {
var page = parseInt(this.params._page);
this.state.set('page', page);
return {
page: page,
};
},
action: function() { // <- added
var page = parseInt(this.params._page); // <- added
this.state.set('page', page); // <- added
this.render(); // <- added
}, // <- added
});
});
That doesn't work either.
update
So using Router.current().data().??? seems to make it work. It's now this
Router.map(function() {
this.route('/', {
template: 'gallery',
data: function() {
var page = 1;
// <--- removed this.state() stuff
return {
page: page,
};
},
// <---- removed action stuff
});
this.route('/gallery/:_page', {
template: 'gallery',
data: function() {
var page = parseInt(this.params._page);
// <--- removed this.state() stuff
return {
page: page,
};
},
// <---- removed action stuff
});
});
helper
Template.gallery.onCreated(function() {
var instance = this;
instance.loaded = new ReactiveVar(0);
instance.autorun(function() {
var route = Router.current(); // <----- Moved from 3 lines up
var pageId = route.data().page; // <--------- CHANGED
var page = pageId - 1;
var skip = page * G_PAGE_SIZE;
var subscription = instance.subscribe('artForGrid', skip, G_PAGE_SIZE);
if (subscription.ready()) {
instance.loaded.set(1);
}
});
instance.art = function() {
return Art.find({});
};
});
Template.gallery.helpers({
art: function() {
return Template.instance().art();
},
});
No idea if this is now correct or if I'm just getting lucky like with the console.log editions

My guess is that your autorun function is not being re-run because you are not using any reactive variables. I'm not sure if Router.current() or Router.current().data() are reactive, but if they're not, then this explains the issue clearly for me. To test this, try putting some console logging in your autorun function.
Now, I would proposed some revisions that may work for you.
Routing code
Redirect to '/' URL to '/gallery/1' so you can re-use routing code
Router.route('/', function(){
this.redirect('/gallery/1');
});
Sending the page number in the data object to your template is important so that we can use this object in our autorun function
Router.route('/gallery/:_page', {
template: 'gallery',
data: function() {
return {
page: this.params._page,
};
}
});
Also, you don't need to use parseInt method unless you are trying to re-route or handle that exception in your routing code.
Gallery onCreated and helpers
There is a Template.subscriptionsReady helper provided by Meteor so you don't have to manually check if the subscription is loaded unless you really want it. Here's the Template.subscriptionsReady documentation
Here's a simplified version of your onCreated method using the template.data object which was built by the routing code above. Using the autorun wrapper is important so that it will re-run when template.data is changed by your router.
var G_PAGE_SIZE = 10;
Template.gallery.onCreated(function() {
var instance = this;
instance.autorun(function() {
// access the data object built by your route code
var pageId = instance.data.page;
var page = pageId - 1;
var skip = page * G_PAGE_SIZE;
instance.subscribe('artForGrid', skip, G_PAGE_SIZE);
});
});
Define access to your reactive data sources using the template.helpers method.
Template.gallery.helpers({
art: function() {
return Art.find({});
}
});
Note: Not necessary to define your helper in the onCreated and template.helpers

Related

onRendered data not appearing unless I hit back button

I have some functionality in a onRendered event that I want to run on every render.
Template.DashboardCoachPost.onRendered(function () {
var post, postId;
this.autorun(function() {
postId = Router.current().params.query.p;
reactive.isSubmitted.set(false);
if (Template.instance().subscriptionsReady()) {
post = Posts.find({_id: postId}).fetch()[0];
reactive.post.set(post);
if (post) {
reactive.isSubmitted.set(true);
}
}
});
});
In my RouterController I have:
DashboardCoachPostController = RouteController.extend({
subscriptions: function() {
this.postId = Router.current().params.query.p;
if (this.handle) {
this.handle.stop();
}
if (this.postId) {
this.handle = this.subscribe('posts', { postId: this.postId });
}
}
});
and my route:
Router.route('/dashboard/coach/post', {
name: 'dashboardCoachPost',
controller: DashboardCoachPostController,
where: 'client',
layoutTemplate: 'LegalLayout'
});
I have a feeling that I am not handling subscriptions properly, but I can't figure out how to get my post without this method.
Template.instance().subscriptionsReady() is used when you subscribe in a template instance. But you are subscribing in the router.
You have to choose if you let the template instance handle the subscription or the router.

Create a universal helper variable

Is there a way to create a variable at the top of template helpers to remove duplication.
In this particular situation I'm using var candidate = FlowRouter.getParam('id'); and I have to create the variable in each helper. I assume there is a better way.
professionalOverview: function() {
var candidate = FlowRouter.getParam('id');
return ProfessionalOverview.findOne({ candidateUserId: candidate });
},
candidateImg: function() {
var candidateUserId = FlowRouter.getParam('id');
return Files.findOne({ userId: candidateUserId });
},
EDIT
Template.talentProfileNew.onCreated(function() {
var self = this;
self.autorun(function(){
this.candidateUserId = new ReactiveVar(FlowRouter.getParam('id'));
}
});
Template.talentProfileNew.helpers({
candidate: function() {
console.log(Template.instance().candidateUserId.get());
return Meteor.users.findOne({_id: Template.instance().candidateUserId.get()});
}
});
you could read it once in onCreated() and put it in a reactive var. e.g.
Template.Foo.onCreated(function() {
this.candidateUserId = new ReactiveVar(FlowRouter.getParam('id'));
});
Template.Foo.helpers({
candidateImg() {
return ProfessionalOverview.findOne({ userId: Template.instance().candidateUserId.get()});
}
});

How do you do client side routing when saving via methods?

I want to save some data and show it in a view template. So I want to do like the example below but using methods.
Template.postSubmit.events({
'submit form': function(e) {
e.preventDefault();
var post = {
url: $(e.target).find('[name=url]').val(),
title: $(e.target).find('[name=title]').val()
};
post._id = Posts.insert(post);
Router.go('postPage', post);
}
});
I tried this:
'insertClubData': function(clubname, capacity, description, homepage){
var currentUserId = Meteor.userId();
var club = {
clubname: clubname,
description: description,
capacity: parseInt(capacity),
homepage: homepage,
createdAt: new Date(),
visitors: 0,
occupancy: 0,
trend: "club-1",
createdBy: currentUserId
}
club._id = clubs.insert(club);
Router.go('club', club);
},
but I get the error:
Exception while invoking method 'insertClubData' TypeError: Object
function router(req, res, next) { I20160425-14:04:55.724(2)? //XXX
this assumes no other routers on the parent stack which we should
probably fix
I understand that this is because Router.go is a client side method. But I also understand that you should avoid server side routing. So what's the most elegant solution?
This is my route:
Router.route('/club/:_id', {
name: 'club',
template: 'club',
data: function(){
return clubs.findOne({_id: this.params._id})
}
});
How about, you call the method from the client and in the callback on success you do the routing. For example:
Template.postSubmit.events({
'submit form': function(e) {
e.preventDefault();
var post = {
url: $(e.target).find('[name=url]').val(),
title: $(e.target).find('[name=title]').val()
};
Meteor.call('insertPost', post, function(error, id) {
if (error) {
alert(error)
} else {
Router.go('postPage', {_id: id});
}
});
}
});
and on the server
Meteor.methods({
insertPost: function(post) {
// do checks
id = Posts.insert(post);
return id;
}
});
Does that work for you?

Adding collection items as routes in Meteor

I have a meteor project where all my users have their own profile page setup in this way using routes:
Routes code:
Router.route('/#:username', {
name: 'profile',
controller: 'ProfileController'
});
ProfileController = RouteController.extend({
template: 'profile',
waitOn: function() {
return Meteor.subscribe('userProfile', this.params.username);
},
data: function() {
var username = Router.current().params.username;
return Meteor.users.findOne({
username: username
});
}
});
Server code:
Meteor.publish('users', function() {
return Meteor.users.find({}, {fields: {username: 1, emails: 1, profile: 1, roles: 1}});
});
Meteor.publish('userProfile', function(username) {
// Try to find the user by username
var user = Meteor.users.findOne({
username: username
});
// If we can't find it, mark the subscription as ready and quit
if (!user) {
this.ready();
return;
}
// If the user we want to display the profile is the currently logged in user
if(this.userId === user._id) {
// Then we return the curresonding full document via a cursor
return Meteor.users.find(this.userId);
} else {
return Meteor.users.find(user._id, {
fields: {
profile: 0
}
});
}
});
I want to do something similar with a pages collection that I've set up. Creating the collection works and the collection page has an _id field that is made upon creation.
Right now the program works nicely for users where mysite.com/# works. Now I want the same thing to work for mysite.com/&
I've basically attempted to do the exact same thing as I did in the above code with the user name but it wasn't working. I've checked to make sure my creation of the collection items are working and they are. But somehow I can't figure out how to do this same thing with collections since I'm relatively new to using routes.
This is what I've attempted:
Here's my routes:
var pageRoute = '/&:_id';
Router.route(pageRoute, {
name: 'page',
controller: 'PageController'
});
PageController = RouteController.extend({
template: 'page',
waitOn: function() {
return Meteor.subscribe('Page', this.params._id);
},
data: function() {
var _id = Router.current().params._id;
return Meteor.pages.findOne({
_id: _id
});
}
});
Server code:
Meteor.publish('pages', function() {
return Pages.find({});
});
Meteor.publish('Page', function(_id) {
// Try find the page by _id
var page = Meteor.pages.findOne({
_id: _id
});
// If we can't find it, mark the subscription as ready and quit
if (!page) {
this.ready();
return;
}
// If the page we want to display is not claimed, display it
if(true) {
return Meteor.pages.find(this._id);
} else {
// Redirect to the page
}
});
The Schema of the Page Collection:
_id: ,
createdAt: ,
CreatedBy: ,
claimedAt: ,
claimedBy: ,
Update:
I've scoped it down to this problem, I get the following error in the console server-side:
I20160202-11:16:24.644(2)? Exception from sub qrPage id 2kY6RKCTuCpBDbuzm TypeError: Cannot call method 'findOne' of undefined
I20160202-11:16:24.645(2)? at [object Object].process.env.MAIL_URL [as _handler] (server/ecclesia.life_server.js:40:33)
I20160202-11:16:24.645(2)? at maybeAuditArgumentChecks (livedata_server.js:1698:12)
I20160202-11:16:24.645(2)? at [object Object]._.extend._runHandler (livedata_server.js:1023:17)
I20160202-11:16:24.645(2)? at [object Object]._.extend._startSubscription (livedata_server.js:842:9)
I20160202-11:16:24.646(2)? at [object Object]._.extend.protocol_handlers.sub (livedata_server.js:614:12)
I20160202-11:16:24.646(2)? at livedata_server.js:548:43
This error occurs whenever I try to direct to mysite.com/&<_id>
Based on this website: https://perishablepress.com/stop-using-unsafe-characters-in-urls/
It looks like # is considered an unsafe character to use in a URL string. On the web page above, it looks like there are several symbols you could use instead as safe characters.
I just tried this on my own machine, and I don't think Meteor plays nicely when the # is introduced in the URL.
This got it working...
Publications:
Meteor.publish('qrpages', function() {
return QRPages.find({});
});
Meteor.publish('qrPage', function(id) {
// Try find the qrpage by _id
var qrpage = QRPages.find({_id: id});
// If we can't find it, mark the subscription as ready and quit
if (!qrpage) {
this.ready();
return;
}
return qrpage;
});
Routes:
var qrpageRoute = '/$:_id';
Router.route(qrpageRoute, {
name: 'qrpage',
controller: 'QRController'
});
QRController = RouteController.extend({
template: 'qrpage',
waitOn: function() {
var id = this.params._id;
return Meteor.subscribe('qrPage', id);
},
data: function() {
var id = this.params._id;
return QRPages.findOne({
_id: id
});
}
});

Change components with ajax. (Ractivejs)

views.list = Ractive.extend({/*...*/});
var Content = Ractive.extend({
template: '#template-content',
components: {
//view: views['list']
},
onrender: function() {
/* call this.request */
},
request: function(route) {
var self = this;
$.ajax({
url: route,
type: 'GET',
data: {},
dataType: 'json',
success: function(data){
self.components.view = views[data.view];
}
});
}
});
Ajax request returns {view:'list'}. I write in the console: "Content.components.view" and see:
function ( options ) {
initialise( this, options );
}
But "Content.findComponent('view')" returns "null".
It will work if I uncomment the line "//view: views['list']" in the components. How to dynamically change the component?
Update 1:
I thought I can do this:
var Content = Ractive.extend({
components: {
// Aaaand add listener for data.view?
view: function(data) {
return views[data.view];
// But no, it is not updated when changes data.view ...
}
},
data: {},
oninit: function() {
var self = this;
self.set('view', 'list');
},
});
Not work.. Why then need "data" argument?
I won :)
// Create component globally
Ractive.components.ListView = Ractive.extend({});
// Create ractive instance
var Content = Ractive.extend({
template: '#template-content',
partials: {
view: 'Loading...' // So... default
},
oninit: function() {
var self = this;
$.ajax({
...
success: function(data){
// Hardcore inject component
self.partials.view = '<'+data.view+'/>';
// Little hack for update partial in template
self.set('toggle', true);
self.set('toggle', false);
}
});
},
});
In Content template:
{{^toggle}}{{>view}}{{/}}
It's works-fireworks!

Resources