Spacebars #each access to sibling


#1

Hello everyone,

I am currently bulding a chat app with Meteor, and i am facing a problem to display the messages with the #each statement. I want to group the message by user and add dividers if the messages dates are changing.

Screenshot
screenschot app

Actually, i am creating a new multi-dimensionnal array from the cursor of messages. This is not very good, and when i load more previous messages with and infinite scroll system, the UI become very slow when i send a new message.

The relevant part of the process is to access previous/sibling elements of a message because i need to compare if the sender of the messages differ, or if the dates of two messages are teh same day to display dividers or to group messages when the UI render them.

I could’nt find a way to access sibling elements with a minimongo cursor.

Do you know a way ?

Regards,

William


Blaze.getData or jQuery DOM
#2

Just use cursor.fetch(). Then you have an array of your messages an you can access siblings. I would recommend to do the grouping in JavaScript and then just send a data structure to the template that can be easily rendered as you want. I think its an array of message groups. Each message group has metadata like post time and author. And it has also a list of messages that are part of the message group.

I think the challenge is to only load and render the data that is in the viewport.


#3

Hello Sanjo.

This is more or less what i am actually doing. I group messages of the same day in an array, and then each of those array elements contains blocks of messages of the same user. It allow me to render nicely the viewport. The problem is when i increment the message limit for the reverse infinite scrolling (loading more older messages). The helper that transform my cursor to a multi-dimensionnal array does not take so long, but when i use this debugger package, i see that a lot of my helpers are rerun, even for a small amount of message.

And of course, when a user add a new message to the chat, the UI is slow if there is a lot of messages (i am doing other tasks like scrolling to the bottom of the viewport, saving the scrollTop position of the viewport).

Edit: My javascript helper that do the grouping use forEach on the cursor instead of fetch(). This mean i create the multi-dimensionnal array on the fly by iterating the cursor.


#4

How many messages do you render when you get this performance issues?
You can use the Chrome Web Tools profiler to see if the helper execution is slow or if it is just the rerendering performance that is the bottle neck.

I’ve read in another thread that there is a performance issue when you render another template inside the {{#each}} block and that it doesn’t happen when you directly embed the HTML in the {{#each}} block.


#5

I start with a limit of 50 messages on the initial viewport with the subscription, then i increment by 50. It start to be slow when the limit is around 200-300 messages.

Regarding the performances issues for the #each statement you are talking to, sadly i have to use subTemplate to if i want to use onRendered and onCreated callbacks.

In your opinion, is it better to do the grouping task of the messages in a Javascript Helper, or should i in the #each statement look at the sibling element and decide what to render ?


#6

While fetch() forces a total re-render for all templates on screen, creating a helper that groups the data you want and returns a cursor does not.

That’s what I do in my app, and after installing the debugger package you posted I confirmed that changes in the database do NOT force all helpers to rerun.

So, you can have a helper that brings you all the “parent” messages, and in the template do something like “Display all parents, and for each parent call the helper that returns a cursor of it’s children”


#7

Hello loupax.

Interresting stuuf. I am wondering what is your data model in this case.

In my case, the messages have no relations between them. They just have the id of the conversation they came from, and i sort them by date with a limit.

I could’ve get all messages from the same day with a minimongo query for sure (and then iterate on those - it is easy with this process to add the days dividers), but after that, i don’t understand how to group the messages based on the sender id, and returning a cursor.

Could you provide a code hint for this, it would be really appreciated.


#8

You can use css for this :slight_smile:

for example, you can do something like:

<li class="message" data-date={{date}}>{{message}}</li>

where date is the message timestamp in d (day of week) format (you can use moment or a momentjs helpers package to achieve that easily, more at http://momentjs.com/docs/#/displaying/format/)

and in your css

li.message {
  border-top-style: solid
}
li[data-date="0"]+li[data-date="0"],
li[data-date="1"]+li[data-date="1"],
li[data-date="2"]+li[data-date="2"],
li[data-date="3"]+li[data-date="3"],
li[data-date="4"]+li[data-date="4"],
li[data-date="5"]+li[data-date="5"],
li[data-date="6"]+li[data-date="6"] {
  border-top-style: none
}

Therefore, you won’t need to change anything in your subscriptions or template blocks. You’ll just sort the messages and display them. This makes by default every message to have a top border, but if two consecutive messages have the same data-date attribute (that means the same day), there is no top border.

Fast, easy and scalable.

Enjoy :wink:


#9

Well, my use case my messages get grouped the moment they are stored in the database. My data looks like this:

{
    _id
    is_threadhead
    message
    thread_id
    response_to
}

So when I store a new message to the database, I check if it’s response to another message. If it is, I make sure they share the same thread_id (normally the _id of the first post of the thread, but that’s not really important)

So my helpers would look like this:

Template.messages.helpers({
    messageRoots: function(){
        return Messages.find({is_thread_head: false});
    },
    messageThread: function(parent){
        return Messages.find({thread_id: parent.thread_id});
    }
});

And my templates like this:

<template name="thread">
    {{#each messageRoots }}
        <div>{{this.message}}</div>
        {{#each messageThread this}}
         <div>{{> messageTemplate}}</div>
        {{/each}}
    {{/each}}
</template>

<template name="messageTemplate">
    <div>{{this.message}}</div>
</template>

Of course this works for me because I create discussion threads, instead of chat rooms. You could also do something like that, for example by using a timestamp-generated value as a thread_id. Of course this means that you won’t be able to re-group your data in a different manner the moment you select them. Bummer.

You could also try aggregating your data on the server, but after a small research, I ended up empty. No native support for aggregate yet. Maybe some package like meteorhacks:aggregate? Oh it’s no reactive. Double bummer for a chat app.

While I was typing this thing @serkandurusoy gave an pretty cool answer that looks it might work much better for your case :smile: Nice one!


#10

Hi serkandurusoy

This is interessting, however, in my case, the grouping of the message has to be done by sender id.
And with tour solution, what about messages from the same day but from a different week ?

Edit: I guess, if the day change from a sunday to a next week monday, the data-date will change from 6 to 0 and it will be fine. However, like i said, i’m not reaaly grouping messages based on the day. I am inserting separators if the day change from a message to another. The grouping is done by the sender id and this is more complex since this is not a know variable i can harcode in css.


#11

Same day different week does not make any practical impact unless one person posts a message and nobody else posts another message until the same day next week :smile:

But yes, you are right that grouping by sender id poses a problem I have overlooked too early :frowning:

In that case, @loupax’s answer makes more sense:

Of course this works for me because I create discussion threads, instead of chat rooms. You could also do something like that, for example by using a timestamp-generated value as a thread_id.

But there may still be some chance to keep this behave without extra data, using some helpers to create client side timestamps and id’s and grouping reordering with a library like https://atmospherejs.com/isotope/isotope

We can brainstorm on the idea if you like.

EDIT:

I think we can work it without an external library. You can set another property like data-sender="xxx" use jquery to check consecutive selectors, and add/remove an extra border class depending on that.


#12

@serkandurusoy @loupax @rafaelfaria @Sanjo

I was thinking of another way to bypass my problem. After querying all my messages with the proper sort based on the date field of my messages, i could’ve just render them, and for each message, i use the onRendered callback to get the previous element in the DOM + get the data context, compare the id of the sender and if it differ, add a specific class to the message first node (the class could show the username and the avatar, like in the screenshot i provided).

I could be the same thing for the “days separators”. If the date of the two messages differs then i can insert a new separator template between those two messages with the Blaze API.

It seems to me that the #each statement would remain reactive (i forgot to mention that message can be edited, but this is juste basic reactivity consideration), and the conditions like (is the message is from the same sender than the previous, and is the message from the same day than the previous) would be evaluated only one time a rendering.

EDIT
I am wondering what would be the bahavior with my infinite scrolling system if i load more previous messages by increasing the messages limit. Maybe i could add an autorun in the onRendered callback of the viewport template, and each time the cursor change, i relaunch the process.

@serkandurusoy
I’ll take a look a this package, i don’t know it. And any help, ideas would be really appreciated. I think my usecase is not so specific and we should be able to find the best way in terms of performances.


#13

This is more or less the same thing i just wrote in my last message.
Advice me please. Is it better to use a data-x attribute, or should i go like i said, with Blaze.getData to get the data context of the DOM node ?


#14

I was just about to ask the same question.

Is it more costly (memory/cpu) to lookup the data context with blaze or the dom with jquery?

I guess it should be tested.


#15

I asked the question : Blaze.getData or jQuery DOM
Maybe someone know. And i’ll give a try.


#16

@serkandurusoy @wi2l well, if you manage to run the helper function once per data update, is it really worth it to try another optimisation?


#17

@loupax The helpers does not seems to run once. Each time a user send a new message to the conversation, the helper that do grouping rerun, and so the rendering logic behind. This is why i think my code is fondamentaly broken at the moment.


#18

Oh, I thought we passed this problem after you proposed returning a cursor and handling the styling inside the onRendered callback


#19

Oh ok ! You were speaking about this “Blaze.getData or jQuery DOM” i guess ?
My bad i misunderstood you. Indeed, this is not very relevant in term of performances, but this is just for pure knowledge :wink:


#20

http://vignette1.wikia.nocookie.net/wingsoffirefanon/images/c/c2/Alrighty_then.jpg/revision/latest?cb=20141012013530