Before I write a post about my most recent project, I thought it would be a good idea to write about the thing I did before that. It had a much smaller scope, and I made it so long ago now that I'm in danger of forgetting absolutely everything what I did.
Before I start the explanation, I should just provide a link to the site - http://captioncapta.in. Glad that's out of the way! So Caption Captain is a game that picks one image and three titles at random from reddit, and the goal is to pick the title that the image was submitted with. With a subreddit like Reaction GIFs this is a pretty fun game and an entertaining way of browsing the latest submissions. The game also works with other subreddits, but obviously the ones that are not image-heavy will not work too well, and the captions are sometimes pretty obvious for subreddits like funny or gifs.
The main reason I wanted to make this was both to have a bit of practice with Dojo and Bootstrap, and to see what I could do with a site when I limited myself to having a minimal infrastructure - I wanted to implement everything in pure HTML and JavaScript and have no server-side logic. That means no database, no server to access remote URLs, and no URL routing. The site could be served as a single HTML file, but I split it up a bit for clarity. It's currently running on Amazon S3 so it has minimal cost and maintenance.
This post will probably be light on the code, but I've put up all the source on GitHub so you can follow along by going there.
The first problem I encountered was due to my technology choices. Bootstrap (the JavaScript part at least) is generally written to be used alongside jQuery instead of Dojo. I would also prefer to use jQuery, but I knew I had to use Dojo for another thing I was working on, so I wanted to get at least a little bit of practice with it. Thankfully, some kind soul has done the hard work and re-written Bootstrap's JavaScript modules in a format that works well with Dojo and they creatively named it Dojo Bootstrap. All I wanted to use was Bootstrap's Modal functionality, and with Dojo Bootstrap this was as easy as require-ing the Modal module and putting the right tags into the HTML.
The next problem to solve was thinking about how to get subreddit data. Usually I'd just write some code into the server to query reddit, then return that as part of a site or ajax request, but since I didn't want to actually use a server this was not possible. Luckily, reddit's JSON API supports the usage of JSONP, which means we can call it on the client side and use the response inside a JavaScript function. Using this I made a function that would attempt to get the next image and set of captions, and if there were not left it used JSONP to get some more images and captions from reddit before returning one of them. All of the image loading logic is in imageLoader.js but to summarize - getNextImage is the main function, this will return an image if we have it, otherwise it calls _loadImages. _loadImages calls a set of query functions asynchronously, one for a few different sort orders of the subreddit, and that function (_getDataFromReddit) is a fairly generic JSONP call. Once all of these queries finish, the results are filtered to make sure that the link is an image and that its score isn't too low, then it's separated into a set of images and captions that are ready for use.
The rest of the code and HTML (mainly in index.html and controller.js) is pretty standard scaffolding code so I won't go through most of it, but the last thing I wanted to mention was the solution to the problem of URL routing when not using a server. I wanted to support the selection of different subreddits, but the usual solution of having a unique URL for each subreddit and using URL routes to detect the subreddit would not work in a plain HTML page. However, JavaScript does have access to the full URL, including query string, so all we need to do is redirect to index.html with e.g. ?subreddit=reactiongifs, detect that on page load, and the problem is solved.
So that's it, the main thing I learnt is that I don't really like using Dojo as much as jQuery, at least for the "utility" functionality. I still think it's pretty good for UI work, and the rest of it has improved a lot recently, but it's hard to beat jQuery for things like AJAX and async calls. I also quite enjoyed the challenge of making a single-HTML page, and if I ever think of something else that would work in that form I'll have another go at it. It's just lovely to be able to shove something on S3 and forget about it while it keeps running happily in the background.
Showing posts with label dojo. Show all posts
Showing posts with label dojo. Show all posts
Saturday, April 27, 2013
Sunday, March 3, 2013
Secondary sort in dgrid
In my never ending attempt to harvest google search clicks help people in need, today I'm going to explain a way to get a secondary sort working in dgrid, something my google searches could not find a good solution for. I needed this because I was making a grid that could be sorted by several headings as well as being filtered. Everything worked fine for small data sets, but I guess due to the way that dgrid's in-memory store works, sometimes I would apply a filter and the order of the rows would get switched up a bit. The results were still sorted correctly, but the order of the rows that all contained identical values for the column being sorted changed randomly. For example, suppose I'm sorting a movie list by genre:
Then I apply a filter for movies made in Nigeria, the results would still be sorted by genre, but the order of all the movies in the same genre got mixed up:
You can see that in the original list, the Nigerian Action movies showed up in the order 43-8-4-13, but after the results were filtered, the order was 4-8-13-43. The best solution I thought to fix this would be to sort primarily by genre, and secondarily by ID. Since the ID is unique across all the rows, the order of movies that contain identical genres will stay the same.
Let's start with the some HTML and JavaScript to create the filterable grid, there are several tutorials online about how to get this far, so I won't go into any details.
Although it's not too easy to find in the API, it is possible to sort a grid by multiple columns. For example, we could add this code to set up a default sort which would use a secondary sort on ID to retain the order of rows after filtering:
This works for the default sort, but if we try to sort by country and filter by genre, we run into the same problems. Luckily, dgrid allows you to intercept the sort event and override it with your own implementation by using:
We can then prevent the usual sort for happening by calling preventDefault/stopPropagation on the event passed to our sort function, and then use the sort information from the event, but add our own secondary sort option:
And there you go, sort order maintained, mission accomplished!
Here is the full code:
Then I apply a filter for movies made in Nigeria, the results would still be sorted by genre, but the order of all the movies in the same genre got mixed up:
You can see that in the original list, the Nigerian Action movies showed up in the order 43-8-4-13, but after the results were filtered, the order was 4-8-13-43. The best solution I thought to fix this would be to sort primarily by genre, and secondarily by ID. Since the ID is unique across all the rows, the order of movies that contain identical genres will stay the same.
Let's start with the some HTML and JavaScript to create the filterable grid, there are several tutorials online about how to get this far, so I won't go into any details.
<html>
<head>
<title>dgrid secondary filtering</title>
<script data-dojo-config="async: true" src="js/dojo/dojo.js"></script>
<script>
require([
"dgrid/OnDemandGrid",
"dojo/store/Memory",
"dojo/on",
"dojo/dom",
"dojo/domReady!"
], function(Grid, Memory, On, Dom) {
var genres = ['Comedy','Action','Drama', 'Musical','Doco'];
var countries = ['Australia','USA','Japan','England','Nigeria'];
var data = [];
for (var i=1; i<50; i++) {
var title = "Movie " + i;
var genre = genres[Math.floor(Math.random() * genres.length)];
var country = countries[Math.floor(Math.random() *
countries.length)];
data.push({id:i, title:title, genre:genre, country:country});
}
store = new Memory({ data: data });
grid = new Grid({
store: store,
query: filterData,
columns: {id:"ID",title:"Title",genre:"Genre",country:"Country"}
}, "grid");
grid.set('sort', [
{ attribute : 'genre', descending : false },
{ attribute : 'id', descending : false }
]);
On(Dom.byId("country-select"), "change", function(evt) {
grid.refresh();
});
On(Dom.byId("genre-select"), "change", function(evt) {
grid.refresh();
});
function filterData(item, index, items) {
var countrySel = Dom.byId("country-select");
var country = countrySel.options[countrySel.selectedIndex].value;
var genreSel = Dom.byId("genre-select");
var genre = genreSel.options[genreSel.selectedIndex].value;
return (!country || country == item.country) &&
(!genre || genre == item.genre);
}
});
</script>
<style>
body { font-family: sans-serif; font-size: 11px; }
.claro .ui-state-default.dgrid-row-odd { background: #EEE; }
</style>
</head>
<body class="claro">
<div>
<label for="genre-select">Genre:</label>
<select id="genre-select">
<option value="">All</option>
<option value="Comedy">Comedy</option>
<option value="Action">Action</option>
<option value="Drama">Drama</option>
<option value="Musical">Musical</option>
<option value="Doco">Doco</option>
</select>
<label for="country-select">Country:</label>
<select id="country-select">
<option value="">All</option>
<option value="Australia">Australia</option>
<option value="England">England</option>
<option value="India">India</option>
<option value="Japan">Japan</option>
<option value="Nigeria">Nigeria</option>
<option value="USA">USA</option>
</select>
</div>
<div id="grid">
</div>
</body>
</html>
Although it's not too easy to find in the API, it is possible to sort a grid by multiple columns. For example, we could add this code to set up a default sort which would use a secondary sort on ID to retain the order of rows after filtering:
grid.set('sort', [
{ attribute : 'genre', descending : false },
{ attribute : 'id', descending : false }
]);
This works for the default sort, but if we try to sort by country and filter by genre, we run into the same problems. Luckily, dgrid allows you to intercept the sort event and override it with your own implementation by using:
grid.on("dgrid-sort", onSort);
We can then prevent the usual sort for happening by calling preventDefault/stopPropagation on the event passed to our sort function, and then use the sort information from the event, but add our own secondary sort option:
function onSort(event) {
// Stop the normal sort event/bubbling
event.preventDefault();
event.stopPropagation();
updateSort(event.sort[0]);
}
function updateSort(sortType) {
// Always sort secondarily by ID so that a definite order is kept
grid.set('sort', [
sortType,
{ attribute : 'id', descending : false }
]);
}
And there you go, sort order maintained, mission accomplished!
Here is the full code:
<html>
<head>
<title>dgrid secondary filtering</title>
<script data-dojo-config="async: true" src="js/dojo/dojo.js"></script>
<script>
require([
"dgrid/OnDemandGrid",
"dojo/store/Memory",
"dojo/on",
"dojo/dom",
"dojo/domReady!"
], function(Grid, Memory, On, Dom) {
var genres = ['Comedy','Action','Drama', 'Musical','Doco'];
var countries = ['Australia','USA','Japan','England','Nigeria'];
var data = [];
for (var i=1; i<50; i++) {
var title = "Movie " + i;
var genre = genres[Math.floor(Math.random() * genres.length)];
var country = countries[Math.floor(Math.random() *
countries.length)];
data.push({id:i, title:title, genre:genre, country:country});
}
store = new Memory({ data: data });
grid = new Grid({
store: store,
query: filterData,
columns: {id:"ID",title:"Title",genre:"Genre",country:"Country"}
}, "grid");
grid.on("dgrid-sort", onSort);
updateSort({ attribute : 'genre', descending : false });
On(Dom.byId("country-select"), "change", function(evt) {
grid.refresh();
});
On(Dom.byId("genre-select"), "change", function(evt) {
grid.refresh();
});
function filterData(item, index, items) {
var countrySel = Dom.byId("country-select");
var country = countrySel.options[countrySel.selectedIndex].value;
var genreSel = Dom.byId("genre-select");
var genre = genreSel.options[genreSel.selectedIndex].value;
return (!country || country == item.country) &&
(!genre || genre == item.genre);
}
function onSort(event) {
// Stop the normal sort event/bubbling
event.preventDefault();
event.stopPropagation();
updateSort(event.sort[0]);
}
function updateSort(sortType) {
// Always sort secondarily by ID so that a definite order is kept
grid.set('sort', [
sortType,
{ attribute : 'id', descending : false }
]);
}
});
</script>
<style>
body { font-family: sans-serif; font-size: 11px; }
.claro .ui-state-default.dgrid-row-odd { background: #EEE; }
</style>
</head>
<body class="claro">
<div>
<label for="genre-select">Genre:</label>
<select id="genre-select">
<option value="">All</option>
<option value="Comedy">Comedy</option>
<option value="Action">Action</option>
<option value="Drama">Drama</option>
<option value="Musical">Musical</option>
<option value="Doco">Doco</option>
</select>
<label for="country-select">Country:</label>
<select id="country-select">
<option value="">All</option>
<option value="Australia">Australia</option>
<option value="England">England</option>
<option value="India">India</option>
<option value="Japan">Japan</option>
<option value="Nigeria">Nigeria</option>
<option value="USA">USA</option>
</select>
</div>
<div id="grid">
</div>
</body>
</html>
Subscribe to:
Posts (Atom)