Masonry layout not working with {{#each}}


#1

Hello,
I am quite new to meteor development and I am trying to use masonry to render an image galley. The images are in my public folder and I have created a collection to store the paths to the images. If I render the template with static images it works. but when I try to use it with:

<template name="images"> 
<div class="grid">
  <div class="grid-sizer"></div>
{{#each images}}
<div class="grid-item thumbnail">
<img class=""  src="{{img_src}}"/>
</div>
</div>    
</template>

And this is my Main.js

Template.images.rendered = function () {
    var container = $('#grid').masonry({
        itemSelector: '.item',
        columnWidth: 200
   })
container.imagesLoaded(function () {
        container.masonry();
    });
};


Template.images.helpers({
images:function(){
  return Maquinas.find({}, {sort:{createdOn: -1, rating:-1}});         
},
})

With this code the images are rendered but without the masonry layout.

If I change the html for the code below it will work perfectly. Any ideas of hat am I doing wrong?

<template name="images"> 
<div class="grid">
  <div class="grid-sizer"></div>
  <div class="grid-item">
    <img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/82/orange-tree.jpg" />
  </div>
  <div class="grid-item">
    <img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/82/submerged.jpg" />
  </div>
  <div class="grid-item">
    <img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/82/look-out.jpg" />
  </div>
  <div class="grid-item">
    <img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/82/one-world-trade.jpg" />
  </div>
  <div class="grid-item">
    <img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/82/drizzle.jpg" />
  </div>
  <div class="grid-item">
    <img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/82/cat-nose.jpg" />
  </div>
  <div class="grid-item">
    <img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/82/contrail.jpg" />
  </div>
  <div class="grid-item">
    <img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/82/golden-hour.jpg" />
  </div>
  <div class="grid-item">
    <img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/82/flight-formation.jpg" />
  </div>
</div>
</template>

THANKS


#2

Hey!

This is one of the most tricky things inside meteor. Here is how I’ve made it work:

Create a template for each block that you want to add, so it would look like this:

{{#each images}} {{> imageBlock image=url}} {{/each}}

Inside the imageBlock onRendered callback (use this callback instead of .rendered) you need to call mansory.update() (not sure if this is the correct one, but you need to call the function that updates the mansory grid).

Explaining why

This type of thing happens for a reason: The rendered callback is fired after the template is rendered, and has nothing to do with our helpers. You can still have a helper running but the template was already rendered. In this case, your mansory code is beign fired before the grid is actually built.


#3

Hello moretti,

Thanks for your reply. Still not working. My guess was that something was happening with the timings. I have followed your recommendation and still no luck. My code now looks like

<template name="images"> 
<div class="grid">
  <div class="grid-sizer"></div>
{{#each images}}
{{> imageBlock image=img_src}}
{{/each}}
</div>    
</template>

<template name="imageBlock"> 

<div class="grid-item thumbnail">
<img class=""  src="/{{image}}"/>
</div>
   
</template>

and in main.js I have added onrendered callback on the imageBlock template plus the $grid.masonry(‘reloadItems’); that seems to be the masonry update method.

Template.imageBlock.onRendered = function () {
var $grid = $('.grid').masonry({
  itemSelector: '.grid-item',
  percentPosition: true,
  columnWidth: '.grid-sizer'
});
  
$grid.imagesLoaded().done( function() {
  $grid.masonry();
});
$grid.masonry('reloadItems');

};

    Template.images.helpers({
    images:function(){
      
        return Maquinas.find({}, {sort:{createdOn: -1, rating:-1}});   
       
      
    }
})

Any ideas of what am I doing wrong? Thanks a lot for your prompt response.


#4

There’s a couple of things I would do:

  1. You have the wrong syntax for onRendered - it should be Template.xxx.onRendered(function() {
  2. I think your Template.imageBlock.onRendered should be Template.images.onRendered, since you are setting up Masonry on the .grid which is in the images template. You should note that the onRendered will fire when the non-reactive parts of the template have been rendered. In your case that means everything except the contents of the {{#each}}.

I have to say I am unsure about the relevance or location of $grid.imagesLoaded and reloadItems - I haven’t ever used Masonry :confused:


#5

@morettis explanation is maybe not 100% on target. Let me try to explain what is happening:

onRendered() is called after Meteor has rendered the template the first time. This means Meteor has gone through the template and put all the DOM nodes it generates into the DOM of your page.

But, does that mean that it is necessarily the final result that has been rendered? NO. As a matter of fact it turns out most of the time it is not. Because the #each helper has not yet received all of the data from the subscription.

So, yes, the template has been rendered, but with an empty or half full subscription. As more data arrives in your subscription, the template will be re-rendered, until it achieves its final form. But onRendered() was already run, and will not be run again.

So basically it is wrong to put stuff in onRendered that relies on all data having arrived.

There are two ways to solve this:

  1. The way you find in the Guide: Make a template level subscription and initialize your Masonry after the subscription is ready.

  2. The way I usually do it ;-): The one code you are sure will be run each and every time there is an update to the subscription or a reactive update from the server is the helper method (images() in your case). This function will return the updated set of results to the templating engine, which has already scheduled a re-render of the template (without onRendered() being called, remember?). But we can’t call stuff (like Masonry) that depends on the DOM being complete from here, we need to wait until after the rendering has been done. Meteor.defer() to the rescue. Wrap your Masonry refresh code in a .defer(), and it will only be executed after all scheduled rendering work has been completed. And now it is called every time the template changes, instead of only the first time.


#6

Thanks a lot. I have found the solution so in case it may help somebody else it is below. Not sure it is the one you guys proposed but you certainly gave me the path. Thanks again.

html

<template name="images"> 
<div class="grid">
    {{#each images}}
<div class="grid-item thumbnail">
            <figure class="figure">
              <a href="/image/{{_id}}"><img class=""  src="{{img_src}}"
              alt="{{img_alt}}" /></a>
              <figcaption class="figure-caption">{{img_alt}}</figcaption>
              <figcaption class="figure-caption">{{img_alt}}</figcaption>
            </figure>
</div>
{{/each}}
</div>  

{{#if Template.subscriptionsReady}}
{{mason}}
{{/if}}
</template>

In the main.js

Template.images.onCreated(function(){
var self = this;
self.autorun(function(){
self.subscribe('lists.maquinas')

})


})
Template.images.helpers({

images:function(){

   return Maquinas.find({}, {sort:{createdOn: -1, rating:-1}});
       
  
},
mason:function(){
  var $grid = $('.grid').masonry({
  itemSelector: '.grid-item',
  
  columnWidth: 170
});

  $grid.masonry();
}
})

#7

Nice that you got it working, but have you tried what happens if you add an image after the initial render? For example if another user inserts an image to the collection.

You will either want your masonry to update/rerender or perhaps you’d rather avoid this, if so you would want to turn off reactivity for the data.

Template.images.helpers({
images: function(){
images.find({},{reactive: false});
}
});