Skip to content

Rendering template with array of data in SammyJS

Mon 18th August 2014

In my first play around with SammyJS, I was severely restricted for time, so just had to hack it to work. However, there were a couple of things I couldn’t work out how to do that bothered me, so I thought I’d figure out what was going on, and maybe try to fix anything that’s broken (be it buggy code, or lacking documentation).

My main concern was that, after having added things to the DOM using SammyJS, I couldn’t seem to select those things using jQuery. I was also concerned that if I ran the same loop twice (to print different bits of the array) the output was unintentionally interleaved. Finally, I couldn’t really get my head around how to insert some HTML into the DOM, then perform my main loop. It turns out these were a all a little interrelated 🙂

What I Was Doing

In my initial hack, I had a number of routes that looked a bit like this:

this.get('#/drunkbeers', function(context) {
	context.app.swap('');

	context.render('browse-header.template', {category: 'Drunk Beers'}).appendTo(context.$element());

	context.render('link.csv.template').appendTo('#accordion');

	// Display drunk
	$.each(this.items, function(i, item) {
		if(localStorage[item.id+'_drunk'] == "true") {
			var id = item.id;
			
			var wantclass = 'btn-default';
			if(localStorage[id+'_want'] == "true")
				wantclass = 'btn-primary';

			var drunkclass = 'btn-default';
			if(localStorage[id+'_drunk'] == "true")
				drunkclass = 'btn-success';

			var unavailableclass = 'btn-default';
			if(localStorage[id+'_unavailable'] == "true")
				unavailableclass = 'btn-warning';

			var notes = localStorage[id+'_notes'];
			if(notes == undefined) notes = "";

			context.render('beer.template', {beer: item, wantclass: wantclass, drunkclass: drunkclass, unavailableclass: unavailableclass, notes: notes})
				//.appendTo(context.$element());
				.appendTo('#accordion');
		}
	});

});

This first uses two very simple templates to render some header information, then iterates over all the items in an array (loaded elsewhere from a JSON file), and if that item is marked as “drunk”, in localStorage, it renders that item using a template. Some issues here are that if I code a jQuery select after the iteration, it doesn’t find any of the things added to the DOM, and because I used $.each() to go through all the items in the array, I can’t use .then() afterwards to force ordering on the actions, which I believe is what was causing the interleaving of two array outputs.

How to use renderEach()

In my investigations for how to do this right, I was noticed that example on the front page and its line this.load('posts.json').renderEach('post.mustache').swap(); The important thing here is renderEach(), which we assume is probably rendering the template post.mustache for every item in posts.json. But what if we’ve already loaded posts.json and stored in a variable (as recommended in the tutorial)? SammyJS certainly doesn’t attach .renderEach() to the Array prototype (though maybe it should?)

Let’s take a look at the documentation for renderEach():

sammyjs - renderEach documentation

 

Well, the example sort of makes sense. Run renderEach(template_location, array_of_data) and it will render the template for each item in the array. But that doesn’t sync with the arguments listed. What is name for? And why isn’t it passed in the examples?

Also, what about that this.load('posts.json').renderEach('post.mustache')? How does it get the data when nothing is passed as the 2nd nor 3rd argument? And what if your array isn’t associative: what variable name is each item put in (important for referring to in the template)?

Previous Data

It turns out, that whenever you call one of these functions in a RenderContext, it pushes the results from the previous function out of the content property and into the previous_content property. If data is not set, and previous_content is an array, then it will use previous_content in .renderEach(). This is how the this.load('posts.json') gets into .renderEach().

Reference Variable Name

If the array (either from previous_content or data) is just pure values (e.g. [“pete”, 15, true, “hey”, “there”, 22]) then it’s unclear what variable name this will be passed in as to the templating engine, so it will be hard to refer to this value in the template. If this is the case, .renderEach() lets you provide a string name for this variable, in the 2nd argument, name. If you set this, make sure you use the same name as in your template 🙂

Order of Arguments

If your array already has a key for each value (e.g. because it’s an array of objects – [{x: “pete”}, {x: 15}, {x: true}, {x: “hey”}, {x: “there”}, {x: 22}]), then you may omit the name argument. SammyJS detects this (because argument 2 is now an array, instead of a string), and shifts all the arguments along one. It’s times like these that you wish JavaScript had named parameters!

Updated DOM

Interestingly, now that we’re using .renderEach() to render the items of the array, instead of $.each(), it means we can tag a .then() on the end. Now, inside the callback provided to .then(), the DOM is up-to-date and any jQuery select you perform will work. Hooray!

Also, there seems to be another way to at least attach event handlers to the dynamically added elements, called event delegation:

$('#main').on('click', 'li', function() {
	console.log(this);
});

However, this doesn’t really work for other jQuery actions (not really sure why), so it’s easier to just use .then() where possible.

Printing to Document

Using templates is excellent for situations where you need to repeat the same format of information lots of times, but when you just want to print a line of HTML, it’s a bit laborious. However, when you realise that this.$element() (or context.$element(), not sure which is more appropriate when), works just like the jQuery function $, and therefore you can call regular jQuery functions, such as .append(), it becomes a little easier:

context.$element().append('<a href="#">bob</a><ul></ul>');

Confusingly, you can’t use .then() after this, because it’s a jQuery function. However, it seems you don’t need to, presumably as this has its effect and returns immediately (whereas the rendering operations must work asynchronously).

Contribution

I made an amendment to the documentation of EventContext.renderEach(), and submitted a pull request, so hopefully this very powerful function will be a little clearer to future users (if it gets accepted and merged in)!

Lessons

What were the main things I’ve learned here?

  • You can treat this.$element or context.$element like $ or jQuery (use it to select things).
  • .renderEach() is tricksy but powerful once you understand what the arguments do!
  • If you’re generating parts of the DOM through SammyJS rendering, you must use .then() to ensure your code sees the latest version of the DOM, else your jQuery selections may occur before the rendering has completed.
Leave a Comment

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.