Meteor - How does Telescope load more posts without re-rendering? - javascript

I'm trying to figure out how to implement the load more functionality like Telescope. This is what I have originally:
// Iron Router
Router.route('/posts', {
name: 'posts.index',
waitOn: function () {
return Meteor.subscribe('posts', Session.get('postsLimit');
},
data: function () {
return { posts: Posts.find({}, { sort: { createdAt: -1 } }) };
},
action: function () {
this.render();
}
});
// client/views/posts_list.html
<ul>
{{#each posts}}
<li>{{ title }}</li>
{{/each}}
</ul>
<a href"#" class="load-more">Load more</a>
// client/views/posts_list.js
var POSTS_INCREMENT = 3;
Session.setDefault('postsLimit', POSTS_INCREMENT);
Template.PostsIndex.events({
'click .load-more': function (e, tmpl) {
Session.set('postsLimit', Session.get('postsLimit') + POSTS_INCREMENT);
return false;
}
}
});
It makes sense that Meteor will rerender the list when the postsLimit changes. I'm just curious how Telescope did it without re-rendering the list and only render the new posts. From what I see from the code, instead of storing the limit in the Session, the author uses the route top/:limit? and instead of using waitOn, they use onBeforeAction. It's hard to pinpoint which part of the code helps prevent re-rendering the list. Could someone please help explain in detail how they did it?

The part that triggers the re-rendering is actually waitOn. By using waitOn, you're telling Iron Router to redirect you to the loading template while you wait, which is what triggers the re-rendering and sticks you back up at the top of the page.
This is waitOn's job, and it works great when going from page to page, but is obviously not ideal when re-rendering the same page.
By the way, note that the new subscriptions option can also trigger the same behavior (if you've set a global loading template).
So this is why we're using onBeforeAction in this specific case. This pattern is explained in more details in Discover Meteor, by the way.

Don't know if this is helpful but to load more posts all you have to do is add {{> UI.dynamic template=postsLoadMore}} in the postList template.
<template name="posts_list">
{{> UI.dynamic template=postsListIncoming data=incoming}}
<div class="posts-wrapper grid grid-module">
<div class="posts list">
{{#each posts}}
{{> UI.dynamic template=post_item}}
{{/each}}
</div>
</div>
{{> UI.dynamic template=postsLoadMore}}
</template>

Related

Vue slot not working - child elements show as empty array

I have the following code where I am trying to pass individual tab components into a tabs component via slots. However, the tabs do not seem to be getting passed. Putting {{ tabs }} on the template only shows an empty array.
<template>
<article>
{{ tabs }} // empty array
<header class="tabs">
<ul>
<li v-for="(tab, index) in tabs" :key="index">
<div class="nav-item"
:class="{ 'is-active': tab.isActive }"
#click="selectTab(tab)">
{{ tab.name }}
</div>
</li>
</ul>
</header>
<section class="tabs-details">
<slot></slot>
</section>
</article>
</template>
<script>
export default {
data: () => {
return {
tabs: []
}
},
methods: {
selectTab(selectedTab) {
this.tabs.forEach(tab => {
tab.isActive = tab.name === selectedTab.name;
});
}
},
created() {
console.log('created[Tabs.vue]-> ', this.$children) // nothing here
this.tabs = this.$children; // not working
}
}
</script>
I have my components registered in the parent like so:
import Tab from '#/components/Tab'
import Tabs from '#/components/Tabs'
export default {
components: {
Datepicker,
Tab,
Tabs
},
...
The template is pretty straight forward. Note, the contents of the individual tabs display fine when selected="true"
<Tabs>
<Tab name="hours" selected="true">Contents here...</Tab>
<Tab name="pay"></Tab>
</Tabs>
I have checked in the browser console and, although the nav-item <li> elements are being created, they have no content.
I'm new to slots so maybe I am missing something obvious or the code syntax I am using is outdated. Any help is much appreciated.
Try to use this.$slots :
this.tabs = this.$slots.default();//returns vnodes, that can be rendered via render function
$children has been deprecated in Vue 3:
In 3.x, the $children property is removed and no longer supported. Instead, if you need to access a child component instance, we recommend using template refs.
If you're trying to get children of <slot></slot>, use this.$slots.default(), as suggested in Boussadjra's answer.
In Vue2, the syntax for getting the default slot contents is this.$slots.default (not a function).
Notes:
you'll get back an array of VNodes, not actual DOM,
you can only access $slots after component has been mounted (e.g: in mounted() hook)
If you want specific data from the vNodes, you'll have to log them and see where exactly it is in your version of Vue (vNodes have had minor changes over time - they're considered internal API).
My advice would be to upgrade to a newer version of 2. If not 2.7.10, at least 2.6.14. Some of the available documentation on Vue 2 describes features which were not yet added in 2.2.3. $slots had a major revamp in 2.6.
For example, I was using slots at the time, but if you ask me now how they looked like, I couldn't tell you with certainty.

Add a loading indicator or spinner for rendering delay (Not for subscription ready)

My meteor app template is taking a 1 second delay between creation and rendering. The delay is after the data subscription is available, so it seems that the 1 second is the time required by Blaze to read the data locally and draw all the objects in DOM (Perhaps most of them are in cache).
The question is: There is a way to add a spinner in the loaded template to cover the delay between myTemplate.OnCreated and myTemplate.onRendered?
Thanks in advance.
There are a couple of ways to achieve what you are after and its a little difficult to pick the right one for your case without seeing more of your code.
But if you are not performing any specific logic in your template definition once your subscription is ready then you can use the Blaze Template.subscriptionsReady helper.
<template name="notifications">
{{#if Template.subscriptionsReady}}
<!-- put your view code here -->
{{else}}
<!-- put loading indicator here -->
{{/if}}
</template>
If you are doing something special to your data once your subscription is ready, or if you need to wait until all the data is fully loaded then you can control when the Template renders using a ReactiveVar.
Template definition:
<template name="notifications">
{{#if isReady }}
<!-- put your view code here -->
{{else}}
<!-- put loading indicator here -->
{{/if}}
</template>
Template logic:
Template.notifications.onCreated(function() {
this.isReady = new ReactiveVar(false);
});
Template.notifications.onRendered(function() {
this.subscribe('activeNotifications', () => {
this.isReady.set(true);
});
});
Template.notifications.helpers({
isReady: function() {
return Template.instance().isReady.get();
},
});
I usually implement my loading indicator logic in its own Template so that I can re-use it across the site.
The solution was very similar in logic to #jordanwillis solution, just was needed to add a timeout on template created:
<template name="contactList">
{{#if contactListRendered }}
<!-- Here show the contact list -->
{{else}}
<!-- show loading indicator or spinner -->
{{/if}}
</template>
Logic:
Template.contactList.onCreated( function() {
Session.set("contactListRendered", false);
});
Template.contactList.onRendered( function() {
setTimeout(function(){Session.set("contactListRendered", true);}, 1);
});
Template.contactList.helpers({
contactListRendered: function() {
return Session.get("contactListRendered");
}});

Meteor: how to paginate simple-todos demo?

I am looking at Meteor pagination today.
I am interested in this repo:
https://github.com/alethes/meteor-pages
The initial code shown looks simple:
this.Pages = new Meteor.Pagination("collection-name");
and:
<body>
{{> collection-name}}
</body>
<template name="collection-name">
{{> pages}}
{{> pagesNav}} <!--Bottom navigation-->
</template>
I want to paginate this demo:
https://github.com/meteor/simple-todos
The code I see there simplifies to this:
Tasks = new Mongo.Collection("tasks");
if (Meteor.isServer) {
// This code only runs on the server
Meteor.publish("tasks", function () {
return Tasks.find({})})}
if (Meteor.isClient) {
// This code only runs on the client
Meteor.subscribe("tasks");
// ...
}
and:
<body>
<ul>
{{#each tasks}}
{{> task}}
{{/each}}
</ul>
</body>
<template name="task">
<li>
{{text}}
</li>
</template>
Perhaps my brain is a bit slow today.
It is not obvious to me how to paginate the above code.
How do I use
github.com/alethes/meteor-pages
to paginate the above code from simple-todos?
Been a while since I've used meteor-pages, but you should be able to just do replace Tasks = new Mongo.Collection("tasks"); with this.Tasks = new Meteor.Pagination("tasks"); - common code between client and server.
Basically meteor pages just creates a wrapper around a mongo collection and applies search and filter criteria.
If you're familiar with coffeescript make sure to check out their /examples directory in the repo.
Also, the settings https://github.com/alethes/meteor-pages#settings will help explain some of the defaults like items per page etc.

How to execute a callback after an #each is done?

I'm having trouble with a callback after the #each has finished. I have a template named "content":
<template name="content">
{{#if Template.subscriptionsReady}}
{{#each currentData}}
<div data-cid="{{this._id}}"></div>
{{/each}}
{{else}}
Loading...
{{/if}}
</template>
At first I wait for a subscription, when this is available, I iterate through my Collection with {{#each}} and append the div. What I need is a sort of callback for when the for-each loop is done (in other words DOM ready).
Template.content.onRendered()
-> triggers to early
I also tried appending an image after the {{each}} and fire a function in its onload like this:
<img style="height:0;width:0" src="*mysource*" onload="callback()">
-> did work sometimes but not reliable somehow
Is there a way to get this callback? I do not fear to change the structure of this template, if that brings the solution.
There's no easy way to get notified when a Spacebars {{#each}} block has done rendering into the DOM every item getting iterated over.
The best solution is to use another reactive computation (Tracker.autorun) to observe your (reactive) current data.
Everytime your current data (which is likely a cursor) is modified, you can run arbitrary code after every other reactive computations are done performing whatever their job is, using Tracker.afterFlush.
The {{#each}} block is one of those computations, whose role is to listen to the reactive data source you give it as argument and rerender its Template.contentBlock as many times as items fetched from the source being iterated over, with the current item as current data context.
By listening to the exact same reactive data source as the {{#each}} block helper and running your code AFTER it has finished its own reactive computation, you can get the actual requested behavior without relying on some weird tricks.
Here is the full implementation of this pattern :
JS
Template.content.helpers({
currentData: function(){
return Template.currentData();
}
});
Template.content.onRendered(function(){
this.autorun(function(){
var cursor = Template.currentData();
// we need to register a dependency on the number of documents returned by the
// cursor to actually make this computation rerun everytime the count is altered
var count = cursor.count();
//
Tracker.afterFlush(function(){
// assert that every items have been rendered
console.log(this.$("[data-cid]") == count);
}.bind(this));
}.bind(this));
});
I had a similar problem and after a lot of searching found the following solution. I tried using Tracker, onRendered and other tricks, none of them worked. This could be considered more of a hack, but works. Unfortunately can't remember where I found this solution initially.
Start with your template, but add an template tag after your each.
<template name="content">
{{#if Template.subscriptionsReady}}
{{#each currentData}}
<div data-cid="{{this._id}}"></div>
{{/each}}
{{doneTrigger}}
{{else}}
Loading...
{{/if}}
</template>
Then define a helper that returns null.
Template.content.helpers({
doneTrigger: function() {
Meteor.defer(function() {
// do what you need to do
});
return null;
}
});
You can read more about Meteor.defer() here, but it is equivalent to using a 0 millisecond setTimeout.
You can also use sub-templates and count the number of sub-templates rendered. If this number is the number of items in the collection, then all are rendered.
<template name="content">
{{#if Template.subscriptionsReady}}
{{#each currentData}}
{{> showData}}
{{/each}}
{{else}}
Loading...
{{/if}}
</template>
<template name="currentData">
<div data-cid="{{this._id}}"></div>
</template>
With that, initialize a reactive variable and track it:
var renderedCount = new ReactiveVar(0);
Tracker.autorun(function checkIfAllRendered() {
if(renderedCount.get() === currentData.count() && renderedCount.get() !== 0) {
//allDataRendered();
}
});
When the currentData template is rendered, increment it, and decrement it when it is destroyed.
Template.currentData.onRendered(function() {
renderedCount.set(++renderedCount.curValue);
});
Template.currentData.onDestroyed(function() {
renderedCount.set(--renderedCount.curValue);
});
A possibly simpler approach to consider - create a template around your #each block and then get an onRendered event afterwards:
html:
<template name="content">
{{#if Template.subscriptionsReady}}
{{> eachBlock}}
{{else}}
Loading...
{{/if}}
</template>
<template name="eachBlock">
{{#each currentData}}
<div data-cid="{{this._id}}"></div>
{{/each}}
</template>
js:
Template.eachBlock.onRendered(function(){
console.log("My each block should now be fully rendered!");
});

Load part of page separately after page load in meteor

I want to create complex blog page with meteor. I have a template with two sub template as below:
<template name="singlePostPage">
{{> post }}
{{#each comments}}
{{> comment}}
{{/each}}
</template>
I want to load post when page is loading and after post load completely then load comments. (like disqus comment system that load after entire page load)
Please guide me how to do this and what packages may useful for this scenario?
You can set a session variable or reactive variable when the post template has been rendered, and then have a handlebars {{#if}} to show the comments based on the value of that variable. Something like below should work.
.js file (Sessions)
Template.post.onRendered(function() {
Session.set("postRendered", true);
});
Template.singlePostPage.helpers({
postRendered: function() {
return Session.get("postRendered");
}
});
Just make sure that the postRendered Session variable is set to false or null when you first load the singlePostPage template. If you want to use a reactive variable:
Add this package:
meteor add reactive-var
And use this .js code:
var postRendered = new ReactiveVar(false);
Template.post.onRendered(function() {
postRendered.set(true);
});
Template.singlePostPage.helpers({
postRendered: function() {
return postRendered.get();
}
});
For both examples you can use this .html file
<template name="singlePostPage">
{{> post }}
{{#if postRendered}}
{{#each comments}}
{{> comment}}
{{/each}}
{{/if}}
</template>

Categories

Resources