diff --git a/backbone.js b/backbone.js index 2f8db3721..f0bb5fb46 100644 --- a/backbone.js +++ b/backbone.js @@ -1985,7 +1985,10 @@ current = this.getHash(this.iframe.contentWindow); } - if (current === this.fragment) return false; + if (current === this.fragment) { + if (!this.matchRoot()) return this.notfound(); + return false; + } if (this.iframe) this.navigate(current); this.loadUrl(); }, @@ -1995,14 +1998,22 @@ // returns `false`. loadUrl: function(fragment) { // If the root doesn't match, no routes can match either. - if (!this.matchRoot()) return false; + if (!this.matchRoot()) return this.notfound(); fragment = this.fragment = this.getFragment(fragment); return _.some(this.handlers, function(handler) { if (handler.route.test(fragment)) { handler.callback(fragment); return true; } - }); + }) || this.notfound(); + }, + + // When no route could be matched, this method is called internally to + // trigger the `'notfound'` event. It returns `false` so that it can be used + // in tail position. + notfound: function() { + this.trigger('notfound'); + return false; }, // Save a fragment into the hash history, or replace the URL state if the diff --git a/index.html b/index.html index af1dfe670..61ba8e348 100644 --- a/index.html +++ b/index.html @@ -1040,6 +1040,7 @@

Backbone.Events

  • "route:[name]" (params) — Fired by the router when a specific route is matched.
  • "route" (route, params) — Fired by the router when any route has been matched.
  • "route" (router, route, params) — Fired by history when any route has been matched.
  • +
  • "notfound" () — Fired by history when no route could be matched.
  • "all" — this special event fires for any triggered event, passing the event name as the first argument followed by all trigger arguments.
  • @@ -2726,6 +2727,9 @@

    Backbone.history

    History serves as a global router (per frame) to handle hashchange events or pushState, match the appropriate route, and trigger callbacks. + It forwards the "route" and "route[name]" events of the + matching router, or "notfound" when no route in any router + matches the current URL. You shouldn't ever have to create one of these yourself since Backbone.history already contains one.

    @@ -2782,8 +2786,10 @@

    Backbone.history

    When called, if a route succeeds with a match for the current URL, - Backbone.history.start() returns true. If no defined - route matches the current URL, it returns false. + Backbone.history.start() returns true and + the "route" and "route[name]" events are triggered. If + no defined route matches the current URL, it returns false + and "notfound" is triggered instead.

    diff --git a/test/router.js b/test/router.js index c60eb7418..9b5bdbdf2 100644 --- a/test/router.js +++ b/test/router.js @@ -1110,4 +1110,89 @@ assert.strictEqual(location.hash, '#' + route); }); + QUnit.test('initial non-matching root triggers notfound event', function(assert) { + assert.expect(1); + location.replace('http://example.com/root#foo'); + Backbone.history.stop(); + Backbone.history = _.extend(new Backbone.History, {location: location}); + Backbone.history.on('notfound', function() { assert.ok(true); }); + var MyRouter = Backbone.Router.extend({ + routes: {foo: function() { assert.ok(false); }} + }); + var myRouter = new MyRouter; + Backbone.history.start({root: 'other'}); + }); + + QUnit.test('later non-matching root triggers notfound event', function(assert) { + assert.expect(2); + location.replace('http://example.com/root#foo'); + Backbone.history.stop(); + Backbone.history = _.extend(new Backbone.History, {location: location}); + Backbone.history.on('notfound', function() { assert.ok(true); }); + var MyRouter = Backbone.Router.extend({ + routes: {foo: function() { assert.ok(true); }} + }); + var myRouter = new MyRouter; + Backbone.history.start({root: 'root'}); + location.replace('http://example.com/other#foo'); + Backbone.history.checkUrl(); + }); + + QUnit.test('initial non-matching route triggers notfound event', function(assert) { + assert.expect(1); + location.replace('http://example.com/root#bar'); + Backbone.history.stop(); + Backbone.history = _.extend(new Backbone.History, {location: location}); + Backbone.history.on('notfound', function() { assert.ok(true); }); + var MyRouter = Backbone.Router.extend({ + routes: {foo: function() { assert.ok(false); }} + }); + var myRouter = new MyRouter; + Backbone.history.start({root: 'root'}); + }); + + QUnit.test('later non-matching route triggers notfound event', function(assert) { + assert.expect(2); + location.replace('http://example.com/root#foo'); + Backbone.history.stop(); + Backbone.history = _.extend(new Backbone.History, {location: location}); + Backbone.history.on('notfound', function() { assert.ok(true); }); + var MyRouter = Backbone.Router.extend({ + routes: {foo: function() { assert.ok(true); }} + }); + var myRouter = new MyRouter; + Backbone.history.start({root: 'root'}); + location.replace('http://example.com/other#bar'); + Backbone.history.checkUrl(); + }); + + QUnit.test('non-matching pushState route triggers notfound event', function(assert) { + assert.expect(2); + location.replace('http://example.com/root/foo'); + Backbone.history.stop(); + Backbone.history = _.extend(new Backbone.History, {location: location}); + Backbone.history.on('notfound', function() { assert.ok(true); }); + var MyRouter = Backbone.Router.extend({ + routes: {foo: function() { assert.ok(true); }} + }); + var myRouter = new MyRouter; + Backbone.history.start({root: 'root', pushState: true}); + location.replace('http://example.com/other/bar'); + Backbone.history.checkUrl(); + }); + + QUnit.test('non-matching navigate triggers notfound event', function(assert) { + assert.expect(2); + location.replace('http://example.com/root#foo'); + Backbone.history.stop(); + Backbone.history = _.extend(new Backbone.History, {location: location}); + Backbone.history.on('notfound', function() { assert.ok(true); }); + var MyRouter = Backbone.Router.extend({ + routes: {foo: function() { assert.ok(true); }} + }); + var myRouter = new MyRouter; + Backbone.history.start({root: 'root'}); + Backbone.history.navigate('http://example.com/other#bar', {trigger: true}); + }); + })(QUnit);