Showing posts with label jquery. Show all posts
Showing posts with label jquery. Show all posts

Wednesday, November 2, 2011

Delegated Events in Backbone.js (I'm an Idiot)

‹prev | My Chain | next›

Thanks to astute readers from last night, I now know that I have been operating under a very mistaken assumption for quite some time now. Specifically, jQuery delegated events can prevent propagation.

Revisiting the no-action form from last night:

    var CalendarFilter = Backbone.View.extend({
      template: template(
        '<form>' +
        '<input type="text" name="filter">' +
        '<input type="button" class="filter" value="Filter">' +
        '</form>'
      ),
      render: function() {
        $(this.el).html(this.template());
        return this;
      },
      // ...
    });
It turns out that I can handle this form without explicitly checking for the Enter key:
    var CalendarFilter = Backbone.View.extend({
      // ....
      events: {
        'click .filter':  'filter',
        'keyup input[type=text]': 'filter',
        'submit form': 'filter'
      },
      filter: function(e) {
        var filter = $('input[type=text]', this.el).val();

        // Don't trigger events like this.  See Custom Events in 
        // Recipes with Backbone for more info.
        this.collection.each(function(model) {
          model.trigger('calendar:filter', filter);
        });
        return false;
      }
    });
As any experienced web developer knows, any form with a submit button is submitted when the user hits Enter in a text field. Checking for event.keyCode == 13 and then manually submitting the form is just silly. All browsers (even Internet Explorer) do it for you.

The reason that the above code works where it did not last night is because the filter handler ends with a return false. Returning false from an event handler is jQuery shorthand for
event.stopPropagation();
event.preventDefault();
It is, in fact, the event.preventDefault() that last night's code was missing. I had been relying on the event.stopPropagation() only. As more than one reader reminded my yesterday, form submission is a built-in, default behavior, not an "ordinary" event. Hence the need to prevent the default action.

Part of my confusion has also surrounded the difference between live() events and delegate() events in jQuery. Live events are bound on the document itself. As such, by the time events bubble all the way up there, it is far too late to call event.stopPropagation() or event.preventDefault(). Both would have already fired on any and all DOM elements under document.

When I first experimented with delegated events, I mistakenly interpreted my inability to prevent form submission as being caused by delegated events retaining live events' inability to stopPropagation / preventDefault. It turns out that it was much simpler than that: I was not invoking preventDefault(). In my defense, I still find the delegate() documentation a little hard to read on the subject. Thankfully, the replacement for delegate(), on(), has much better discussion of all of this.

I spend the rest of my time tonight working through my other Backbone.js forms, enabling them to properly handle Enter without resorting to keyCode hackery. And I feel much better for it. Thanks again, to my "Unknown" reader for pointing out my mistake.

I think I have a few old blog posts in need of an update...


Day #183

Tuesday, October 25, 2011

Precipitating Backbone.js Views

‹prev | My Chain | next›

I really need to cut down on my posting to focus on my Recipes with Backbone writing. So far, my co-author Nick Gauthier is doing most of the writing at that's not cool. Still, I must appease the gods of my chain...

I undertook a significant refactoring of some hideous Backbone.js code last night. For the most part, I liked my smaller, focused Backbone views. It certainly beats a huge template full of evaluate-able code.

One thing that bothers me is that my sub-views need to know about the parent view. Per the Precipitation Pattern in Recipes with Backbone, it is OK for higher order objects to reference lower order objects, but not the other way around.

Consider, for example, my calendar:

The lowest level thing in the calendar is the day view. But my day view knows about week view that called it:

        var CalendarMonthDay = Backbone.View.extend({
          tagName: 'td',
          initialize: function(options) {
            this.parent = options.parent;
            this.date = options.date;
          },
          render: function() {
            this.parent.append(this.el);

            this.el.id = Helpers.to_iso8601(this.date);
            var html = '<span class="day-of-month">' + this.date.getDate() + '</span>';
            $(this.el).html(html);

            return this;
          }
        });
Really, my first clue should have been the reference to parent. You should never see a reference to a parent object in Backbone code.

The problem is that both sides of the relationship need to know about each other, compounding the likelihood that something is going to go wrong in one place or the other. If nothing else, it makes for more code than is necessary.

The month/week view in my app needs to know how to render itself into the parent view in addition to knowing that the day sub-views needs a reference to the week view:
        var CalendarMonthWeek = Backbone.View.extend({
           // ...           
           render: function() {
            for (var i=0; i<7; i++) {
              var day = new CalendarMonthDay({
                parent: $(this.el),
                date: date
              });
              day.render();
              $(this.el).append(day.el);

              date = Helpers.dayAfter(date);
            }
          });
That's just silly.

If I refactor my lowest order object, the day-view, so that it is completely unaware of the higher order week view, then it looks like:
        var CalendarMonthDay = Backbone.View.extend({
          tagName: 'td',
          initialize: function(options) {
            this.date = options.date;
          },
          render: function() {
            this.el.id = Helpers.to_iso8601(this.date);
            var html = '<span class="day-of-month">' + this.date.getDate() + '</span>';
            $(this.el).html(html);

            return this;
          }
        });
That is 3 fewer lines of code and one less responsibility (inserting into the parent object's HTML node).

In the week view, I no longer pass the week view's DOM el to the day view and I no longer have to insert the week view's DOM el into the month view's parent el. All I have to do is insert the day view's el (a table td cell) into the week view's tr table row:
        var CalendarMonthWeek = Backbone.View.extend({
          tagName: 'tr',
          initialize: function(options) {
            this.date = options.date;
          },
          render: function() {
            var date = this.date;
            for (var i=0; i<7; i++) {
              var day = new CalendarMonthDay({date: date});
              day.render();
              $(this.el).append(day.el);

              date = Helpers.dayAfter(date);
            }
          }
        });
Single responsibility with an assist from the Precipitation Pattern nets me 4 fewer lines of code. Nice.

I work through the remaining views (in all, there are CalendarMonth, CalendarMonthHeader, CalendarMonthBody, CalendarMonthWeek, and CalendarMonthDay). The only one that gives me any trouble is the CalendarMonthBody because it has no DOM el. Its purpose is to reside a the same abstraction layer as the CalendarMonthHeader and to proxy the week rows back to the CalendarMonth.

The solution there turns out to create a fake el that holds an array of week DOM elements:
        var CalendarMonthBody = Backbone.View.extend({
          initialize: function(options) {
            this.date = options.date;
            this.el = [];
          },
          render: function() {
            // ...
            while (date == firstSunday || date.getMonth() <= month) {
              var week = new CalendarMonthWeek({date: date});
              week.render();
              this.el.push(week.el);

              date = Helpers.weekAfter(date);
            }
          }
        });
I can get away with this because, at each higher order order layer, I am using jQuery's append() to insert the lower layer's DOM element. Conveniently, append accepts an array of elements:
        var CalendarMonth = new (Backbone.View.extend({
          el: $('#calendar'),
          setDate: function(date) {
            this.date = date;
            this.render();
          },
          render: function() {
            $('span.year-and-month', 'h1').html(' (' + this.date + ')');

            $(this.el).html('<table></table>');

            var table = $('table', this.el);

            var header = new CalendarMonthHeader()
            header.render();
            $(table).append(header.el);

            var body = new CalendarMonthBody({date: this.date});
            body.render();
            $(table).append(body.el);

            return this;
          }
        }));
The month-header and month-body insertions look exactly the same: $(table).append(lower_layer_view.el) even though one is a single element and the other is an array of week rows. Some might think of that as a cheat. I choose to think of it as a pattern.

Anyhow, my quick refactoring has netted me a total of 12 new lines of code and 29 deleted lines of code, for a net loss of 17 lines of code that also serves to decrease responsibility through an entire vertical slice of my Backbone app. That is a big win.


Day #185

Friday, August 26, 2011

A Simple Node.js + CouchDB Calendar

‹prev | My Chain | next›

Last night, I got a nice, little node.js and CouchDB app thrown together. The node (really express.js) app serves up simple HTML and also passes through requests to the CouchDB database. Tonight I would like to serve up a static HTML calendar and populate events from the CouchDB store.

For as long as I have been building HTML calendars, I have always put the ISO8601 date on the day cells. In Jade templating this looks like:
...
  tr#week4
    td.sunday
    td.monday 22
    td.tuesday
    td.wednesday
    td#2011-08-25.thursday
    td#2011-08-26.friday
    td.saturday
...
The resultant HTML is then:
And the resultant page looks like:
The benefits of using ISO 8601 are numerous, which is why it is the de facto standard for XML and JSON dates and times. Since CouchDB is returning JSON, I can be pretty sure that it will be returning ISO 8601 (especially since I created the data in the first place). By identifying the date cells by ISO 8601, it will make it easy to tie date records to date cells. Let's have a look at what I mean...

Accessing the /events resource in my app returns:
{"total_rows":2,"offset":0,"rows":[
  {"id":"fdbed27594feb433c74e82eb910015e0",
   "key":"fdbed27594feb433c74e82eb910015e0",
   "value":{"rev":"2-b7c22d428e648a6cdd2978c213f79ec0"},
   "doc":{"_id":"fdbed27594feb433c74e82eb910015e0",
          "_rev":"2-b7c22d428e648a6cdd2978c213f79ec0",
          "startDate":"2011-08-25",
          "title":"create blog post",
          "description":"talk about node and CouchDB"}},
  {"id":"fdbed27594feb433c74e82eb91001f45",
   "key":"fdbed27594feb433c74e82eb91001f45",
   "value":{"rev":"1-2b18432cf6e63b82c6507ff28af9724c"},
   "doc":{"_id":"fdbed27594feb433c74e82eb91001f45",
          "_rev":"1-2b18432cf6e63b82c6507ff28af9724c",
          "startDate":"2011-08-26",
          "title":"blog again",
          "description":"add backbone into the node + couch mix"}}
]}
(this is just a pass-thru to CouchDB's _all_docs?include_docs=true)

To get those events into the calendar, I perform a jQuery getJSON call inside a document-ready:
  $(function() {
    $.getJSON('/events', function(data) {
      $.each(data.rows, function(i, rec) { add_event(rec.doc) });
    });
  });
For each of the rows in the events returned from CouchDB, I extract the document (the event itself) and make a call to add_event().

The add_event() function then exploits the fact that the cells in my calendar are identified with an ISO 8601 date:
  function add_event(event) {
    var date = event.startDate,
        title = event.title,
        description = event.description;

    $('#' + date).html(
      '<span title="' + description + '">' +
        title +
      '</span>'
    );
  }
If the startDate from CouchDB is 2011-08-26, then this function finds the correct cell via a jQuery $('#2011-08-26') selector. If that selector is found, then the inner HTML is replaced with the event's title (and the description in a <span> title attribute). If the calendar event is for a date not currently displayed, no worries, the selector returns an empty wrapped set, in which case the html() has nothing to do.

The result is a rather snappy:
Nice. There is no ability to modify or remove elements just yet, but it was quite easy to get this calendar populated quickly. Up tomorrow, I think I shall begin exploring doing this again, but with Backbone.js.


Day #126

Tuesday, June 22, 2010

Scrolling with jQuery (and Raphaël.js)

‹prev | My Chain | next›

Up tonight, a quick review of what I did last. I got things working last night, but I slapped it together without much thought.

The solution involved document.elementFromPoint, which finds the topmost (in a z-index sense) element at a given point. Like most folks, I glossed over the documentation when I first started using this method and missed this bit:
pixels relative to the upper-left corner of the document's containing window or frame
I did not expect document.elementFromPoint to account for the margin between the raphaël.js "paper" and the top-left of the document. So I had been manually correcting for that margin:
    var c_el = document.elementFromPoint(avatar.attr("cx") + 8,
avatar.attr("cy") + 8);
Instead of manually adding the 8 pixels, I would like to do it a more repeatable fashion (i.e. so I can change the CSS without needing to change my code). Happily, I am using jQuery in addition to raphaël.js in my (fab) game, so I can use offset(), which returns the coordinates of an element relative to the document (just what I need!).

To translate into game coordinates, I need the offset of the room. Trying to get a jQuery matched set for a raphaël.js paper / SVG element results in an empty matched set. Thankfully (and for that reason), I wrapped the room inside a <div>. Thus I can get access to the offset like so:
$(paper.canvas).parent().offset()
Getting back to my poor reading of the document.elementFromPoint documentation, it had not occurred to me that I needed to account for scrolling. As the documentation states, and as I found from bitter experience last night, I do.

I lucked into the scrollTop() method yesterday, but that turns out to be another of jQuery's magical functions that does exactly what I need it to. It, along with its sister method, scrollLeft(), return the offset of an element relative to the browser viewport. Thus, to get my element at the point of collision in my (fab) game, I simply need:
    var c_x = avatar.attr("cx") +
$(self.avatar.paper.canvas).parent().offset().left +
$(document).scrollLeft();

var c_y = avatar.attr("cy") +
$(self.avatar.paper.canvas).parent().offset().top +
$(document).scrollTop();

var c_el = document.elementFromPoint(c_x, c_y);
Nice to know that I hit pretty close to the mark last night. Even better to know that I hit the mark tonight.

Day #142

Monday, June 21, 2010

Not Obvious to Me

‹prev | My Chain | next›

Tonight, I'd like to see if I can figure out what is going on with chatting in my (fab) game. Chatting works, but immediately after I say something, collisions no longer work:



My collision detection is rather convoluted at this point:
    var c_el = document.elementFromPoint(avatar.attr("cx") + 8,
avatar.attr("cy") + 8);

if (!self.initial_walk &&
!self.mid_bounce &&
c_el != self.avatar.node &&
c_el != self.avatar.paper.bottom.node) {
// console.debug(c_el);
// console.debug(self.avatar);
self.stop();
self.bounce_away();
}
So I have a few things to check. In the javascript console, I look my player up in the player_list and test those attributes:



Hrm... Both of those attributes are false, as they ought to be. So why is the player not colliding?

I try approaching the problem from the opposite direction. I am trying to find the colliding element with document.elementFromPoint. I add a player to the room at x-y 250,350 then try document.elementFromPoint(250 + 8, 350 +8) (the 8 is the offset of the room from the top and left of the document). It works before I chat, but fails after I chat.

I try manually simulating the chat event, but it always works. Until I use the chat built into the game. It's maddening.

I don't actually solve the problem until I record the video above. Until then I only scrolled the chat dialog into view when I wanted to chat. When I recorded the view above (at first), I scrolled into view prior to recording... and it always failed to collide players properly. At first I blamed the video recording software, but ultimately I realized the common theme to failure was scrolling. Whenever I chat, I have been scrolling the chat into the view port, which messes with the offset.

I am able to get things working—even with scrolling through judicious use of jQuery's offsetTop and offsetLeft:
    var c_el = document.elementFromPoint(avatar.attr("cx") + 8  - $(document).scrollleft(),
avatar.attr("cy") + 8 - $(document).scrollTop());
Wish I would have thought of that sooner...

Day #141

Sunday, May 9, 2010

Baby Steps on Mother's Day

‹prev | My Chain | next›

It is doubtful that I will make much progress this Mother's day. Nevertheless, even on a special day like this, I want to make some progress and keep my chain unbroken. So here goes...

With my code now well-factored and well-behaved, I want to begin the process of seeing others in the <canvas> room as I move about it. Right now, all of the comet communication from the fab.js server is moving an explicitly referenced character around. If I am to move many named characters around, I need a list of all players with which I can interact. But first, players need to be able to identify themselves. For that, I add a form above the <canvas> element:
 <body>
<form id="login" method="get">
<label>Name
<input type="text" name="player">
</label>
<input type="submit" value="Play">
</form>

<canvas id="canvas" width="500" height="500"></canvas>
</body>
I am not doing anything fancy on the server side to prevent duplicate logins (and I do not even have accounts on the server side). For now, I simply want to identify myself via the form. If I have done so, show the room (and tell the server). If not, show the login form. This bit of jQuery does the trick:
$(function() {
var kv = location.search.substr(1).split(/=/);
if (kv[0] == 'player' && kv[1]) {
$('#login').hide();
new Room($("#canvas")[0], {me:kv[1]});
}
else {
$('#canvas').hide();
}
});
I am actually a bit surprised that jQuery does not make query parameter parsing easier than that (no doubt there is a plugin for that). For my purposes however, splitting the query paramter string (location.search) on the key-value separator (=) is enough. If the play parameter is present, then hide the login form and show the room. Otherwise hide the room and leave the form visible to prompt the user to login.

Now that players can identify themselves, I need a mechanism for fab.js to interact with a list of players on all connected browsers. My first attempt initializes with me (so that the server never tries to move me, only I can move me), but provides an interface for the server access players so that it can inform the client that Player objects have moved:
var PlayerList = function(me) {
this.me = me;
this.others = {};
};

PlayerList.prototype.add_player = function(obj) {
if (!this.others[obj.id] && obj.id != this.me.id) {
this.others[obj.id] = new Player(obj.id, obj);
}
};

PlayerList.prototype.get_player = function(id) {
return this.others[id];
};
With this, I can add others to my board (but I handle me as a special case) and I also provide a mechanism to access each player so that I can inform them that they need to move.

I stop there for tonight. Hopefully tomorrow, I will be able to get other players showing up in my room.

Day #98

Saturday, May 8, 2010

Keeping the Investors Happy with Firefox Support

‹prev | My Chain | next›

Tonight I need to to obtain buy-in from my investors... my kids. Unfortunately, they still use Firefox. I have developed so far on Chrome exclusively. When I tried to show my kids how things are working so far... well, I was a bit embarrassed.

After refactoring a bit, I get to the point that can fix things for Firefox. The view-only version works (the right window below):



The problem only shows up when I try to control the the game from Firefox. So the problem is not with the <canvas> rendering, rather with the handling of the mouse events. Happily, I still have console.debug output that Firebug picks up:
x_diff: NaN, y_diffNaN, angle: NaN        player.js (line 32)
Tracing that back, I find the source of the trouble is in the event handling of my Player:
Player.prototype.notify = function(evt) {
switch(evt.type) {
case "click":
this.stop();
this.walk_to(evt.offsetX, evt.offsetY);
this.notify_server({id:this.id,x:evt.offsetX, y:evt.offsetY});
break;
}
};
The trouble in there is the use of offsetX/offsetY, which varies between browser implementations. jQuery normalizes the location of events on the page in event.pageX and event.pageY. That is not quite what I need. Instead what I want is the coordinate of the event inside my <canvas> room.

To get that, I need the offset() of the <canvas> element relative to the page. If that is offset 8 pixels from the top and 8 pixels from the side of the page, then I can subtract 8 pixels from the event's pageX and pageY to get the <canvas> coordinate.

My worry here is that I do not want to couple the Player and the Room more than necessary. If possible, I want to notify the Player of an event such that the Player need only pull the x-y coordinate from the event in order to act. Given that reasoning, it sounds as if the best place to put this is in the Room:
Room.prototype.decorate_event = function(evt) {
return {
type: evt.type,
x: evt.pageX - $(this.canvas).offset().left,
y: evt.pageY - $(this.canvas).offset().top
};
};
I can then pass that "decorated" event to the Player, who is a subscriber to events on the <canvas> board:
Room.prototype.init_events = function() {
var self = this;
$(this.canvas).click(function(evt) {
var decorated_event = self.decorate_event(evt);
self.subscribers.forEach(
function(subscriber) { subscriber.notify(decorated_event); }
);
});
};
After updating Player to get the x-y coordinate from event.x and event.y, I have the code working in Firefox and Chrome. Hopefully my investors will be pleased.

Day #97

Saturday, May 1, 2010

Connecting Canvas Movements to Fab.js

‹prev | My Chain | next›

Tonight, I continue my exploration of <canvas>, this time with an eye toward hooking it up to fab.js. Last night, I was able to get a small dot representing my character to move about the <canvas> in response to key presses.

That will not work with the "game" my kids have in mind. They'd like a bunch of characters to meander about the board talking to each other. Moving dozens of characters about pixel-by-pixel is something that fab.js/node.js could probably handle, but dealing with network latency and hiccups is probably not worth the effort.

Instead, I will move the character around by having them walk after mouse click. The character will start in the middle of the canvas. A mouse click in the bottom right will result in a animation of my character moving from the middle of the board to the bottom right.

To accomplish this, I publish the clicks from the board to any subscribers interested in being notified. I have no expectation that anything else will be interested in mouse clicks, but pub-sub makes sense here not as a premature generalization, but rather to decouple the game board from my character. So, I establish my <canvas> listeners:
function init() {
var canvas = $("#canvas").get(0);
var ctx = canvas.getContext("2d");

var me = new Player();

canvas_events(canvas, [me]);
draw(ctx, me);
}
My only canvas event:
function canvas_events (canvas, listeners) {
$(canvas).click(function(evt) {
listeners.forEach(
function(listener) { listener.notify(evt) }
);
});
}
(as you may have noticed, I have started using jQuery for my events)

Great, so now I need a Player object that will encapsulate the position of my character and subscribe to the event publications. The instantiation of the class is straight-forward, defaulting to the middle of my 500x500 <canvas>:
var Player = function(options) {
if (!options) options = {};
this.x = options.x || 250;
this.y = options.y || 250;
}
Now, I establish my notify method:
Player.prototype.notify = function(evt) {
switch(evt.type) {
case "click":
this.walk_to(evt.offsetX, evt.offsetY);
break;
}
};
For now, I am only interested in click publications, which initiate a walk:
Player.prototype.walk_to = function(x, y) {
if (this.x < x) this.x = this.x + 1;
if (this.x > x) this.x = this.x - 1;
if (this.y < y) this.y = this.y + 1;
if (this.y > y) this.y = this.y - 1;

if (this.x != x || this.y != y) {
setTimeout(function(){self.walk_to(x,y)}, 25);
}
};
Easy enough!

Now, to tell fab.js what I am up to. I start with my skeleton app:
var puts = require( "sys" ).puts;

with ( require( ".." ) )

( fab )

( listen, 0xFAB )

( 404 );
For now, I am just going to print out the contents of posted location information. I will push that information to other clients later. The fab.js application to do this:
var puts = require( "sys" ).puts;

with ( require( ".." ) )

( fab )

( listen, 0xFAB )

( /move/ )

(
function () {
var out = this;
return function listener( obj ) {
if ( !obj ) out();
else if ( obj.body ) puts(obj.body);
return listener;
};
}
)


( 404 );
To tell my Player class to POST that information, I add a second action in notify():
Player.prototype.notify = function(evt) {
switch(evt.type) {
case "click":
this.walk_to(evt.offsetX, evt.offsetY);
this.notify_server({id:"me",x:evt.offsetX, y:evt.offsetY});
break;
}
};
The server_notify method make a simple jQuery POST:
Player.prototype.notify_server = function(change) {
$.post("http://localhost:4011/move", change);
};
Now, clicking on the board moves my character and tells fab.js:
cstrom@whitefall:~/repos/fab$ node ./play/game.js
id=me&x=322&y=184
id=me&x=364&y=352
id=me&x=199&y=198
id=me&x=350&y=401
Nice! That is a good stopping point for tonight. Tomorrow, I will work on pushing that information to other boards so that others can see my character move.

Day #90