Writing Maintainable JavaScript
- 7. Day 31
var trip = Gowalla.trip;
$.each(trip.spots, function(i, spot) {
var marker = new GMarker(
new GLatLng(spot.lat, spot.lng), {
icon: Gowalla.createLetterIcon(i),
title: h(spot.name)
}
);
GEvent.addListener(marker, "click", function() {
marker.openInfoWindowHtml('<div class="map-bubble"><img src="' +
spot.image_url + '" width="50" height="50" /><b><a href="' +
spot.url + '" style="color: #37451e;">' + h(spot.name) +
'</a></b></div>');
return false;
});
Gowalla.map.addOverlay(marker);
});
Gowalla.zoomAndCenter(trip.spots);
- 8. Day 90
options = options || {};
var params = this.getSearchParams(options);
Paginator.currentPage = 1;
Paginator.handler = Gowalla.displaySpots;
Paginator.paginate('/spots', params);
if (Gowalla.filterOptions["l"] || Gowalla.filterOptions["sw"] ||
Gowalla.filterOptions["lat"]) {
$('#map-wrapper').show();
$('#spots_search_l').removeClass('off');
if (options.l) $('#spots_search_l').val(unescape(options.l));
} else {
$('#map-wrapper').hide();
}
if (Gowalla.mapVisible()) $('#map-placeholder').show();
$('#heading').hide();
$('#featured_spots').hide();
$('#new_spots').hide();
$.getJSON('/spots', this.getSearchParams(options), function(spots) {
if (spots.length > 0) {
$('.paging').show();
$('#filter').show();
$('#results').show();
$('#map-placeholder').hide();
if (Gowalla.mapVisible() && !Gowalla.map) {
$('#map-placeholder').addClass("transparent");
Gowalla.createMap();
GEvent.addListener(Gowalla.map, "dragend", function() {
var sw = this.getBounds().getSouthWest().toString();
var ne = this.getBounds().getNorthEast().toString();
Gowalla.searchSpots({sw:sw, ne:ne, limit:'150'});
});
}
}
Gowalla.displaySpots(spots);
});
- 17. WISH:
Code that accomplishes a single task
should all live together in one place.
THEREFORE:
Divide your codebase into components,
placing each in its own file.
- 19. WISH:
We should be able to rewrite a component
without breaking things elsewhere.
THEREFORE:
A component should be whatever size is
necessary to isolate its details from other code.
- 20. A “component” is
something you could
rewrite from scratch
without affecting other stuff.
- 25. Gowalla.Flash
handles the display of
transient status messages.
Gowalla.Flash.success("Your settings were updated.");
- 29. WISH:
We should be able to rewrite a component
without breaking things elsewhere.
THEREFORE:
We should standardize the way
components talk to one another.
- 40. Dojo
(“pub-sub”)
dojo.subscribe('some-event', function(data) {
// stuff
});
dojo.publish('some-event', someData);
- 41. A custom event is an interface that
publisher and subscriber adhere to.
- 42. As long as the interface
remains the same, either part
can be safely rewritten.
- 43. “So I should replace
all my method calls
with custom events?
Fat chance.”
- 45. It’s OK for a subscriber
to call methods on a broadcaster,
but not vice-versa.
- 48. The auto-completer knows
about the menu…
var menu = new S2.UI.Menu();
menu.addChoice("Foo");
menu.addChoice("Bar");
someElement.insert(menu);
menu.open();
- 49. …but the menu doesn’t know
about the auto-completer
menu.observe('ui:menu:selected', function(event) {
console.log('user clicked on:', event.memo.element);
});
- 51. Instead of:
function showNearbySpotsInMenu() {
$.ajax({
url: '/spots',
params: { lat: someLat, lng: someLng },
success: function(spots) {
var html = $.map(spots, function(spot) {
return '<li id="spot-"' + spot.id + '>' + spot.name + '</li>';
});
$('#spot_menu').html(html.join(''));
}
});
}
- 53. And this:
function renderNearbySpots(event, spots) {
var html = $.map(spots, function(spot) {
return '<li id="spot-"' + spot.id + '>' + spot.name + '</li>';
});
$('#spot_menu').html(html.join(''));
}
$(document).bind('nearby-spots-received', renderNearbySpots);
- 54. Or, if you prefer…
function getNearbySpotsFromServer(lat, lng) {
$.ajax({
url: '/spots',
params: { lat: lat, lng: lng },
success: function(spots) {
renderNearbySpots(spots);
}
});
}
function renderNearbySpots(spots) {
var html = $.map(spots, function(spot) {
return '<li id="spot-"' + spot.id + '>' + spot.name + '</li>';
});
$('#spot_menu').html(html.join(''));
}
- 63. use of the URL hash for routing/permalinking
(or HTML5 history management).
- 66. Models
define a model class window.Todo = Backbone.Model.extend({
EMPTY: "new todo...",
property access wrapped in set/get methods initialize: function() {
if (!this.get('content'))
this.set({ 'content': this.EMPTY });
},
toggle: function() {
this.set({ done: !this.get('done') });
},
triggered when the object is saved validate: function(attributes) {
if (!attributes.content.test(/S/))
return "content can't be empty";
},
// ...
});
- 67. Views
define a view class window.Todo.View = Backbone.View.extend({
tagName: 'li',
bind events to pieces of the view events: {
'dblclick div.todo-content' : 'edit',
'keypress .todo-input' : 'updateOnEnter'
},
initialize: function() {
map to a model object; re-render when it changes this.model.bind('change', this.render);
},
set the view’s contents render: function() {
// ...
},
// ...
});
- 68. Synchronization
Backbone.sync = function(method, model, yes, no) {
determine the HTTP verb to use for this action var type = methodMap[method];
serialize the object to JSON var json = JSON.stringify(model.toJSON());
send the data to the server $.ajax({
url: getUrl(model),
type: type,
data: json,
processData: false,
contentType: 'application/json',
dataType: 'json',
success: yes,
error: no
});
};
- 69. Other options:
SproutCore
(http://sproutcore.com/)
Cappuccino
(http://cappuccino.org/)
JavaScriptMVC
(http://javascriptmvc.com/)
- 72. One strategy:
Write new code to conform to your architecture.
Improve old code little by little as you revisit it.
- 74. Questions?
✍ PLEASE FILL OUT
AN EVALUATION FORM
Andrew Dupont
http://andrewdupont.net