Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Idea: Route events #93

Closed
addyosmani opened this issue Aug 22, 2012 · 13 comments
Closed

Idea: Route events #93

addyosmani opened this issue Aug 22, 2012 · 13 comments

Comments

@addyosmani
Copy link
Member

From the last catch-up @dustinboston and I had about the project:

  • Routes that publish events
  • For example, a navigation widget gets clicked, modifies URL, e.g. /about
  • Route responds to URL change by publishing an event
@bobholt
Copy link
Contributor

bobholt commented Aug 29, 2012

I've been using History.js for my Aura routing. I tried augmenting Backbone's built-in routing, but it turned into kind of a rabbit hole I didn't want to dive down at the time.

I think if we could get that to work with Aura, it would be ideal, so I may sit down again and see if I can get it up and running.

@addyosmani
Copy link
Member Author

@dustinboston In order for routes to publish events, wouldn't we have to either generalize the routing to outside a framework or only implement this in the backbone-aura extension? Did you have a POC that describes this concept better in mind that you might be able to share?

@sindresorhus
Copy link
Contributor

Better fit as an extension or use of a routing lib directly IMHO.

@kloy
Copy link

kloy commented Sep 26, 2012

@addyosmani Would it not make more sense to treat routes as another widget? This prevents the use case of two widgets implementing the same route potentially as well as centralizing all routing. You also get the described feature of Routes having events.

@tony tony mentioned this issue Sep 26, 2012
@tony
Copy link
Contributor

tony commented Sep 26, 2012

So what are routers in front end js.

Let's be good scholars and dig in :D

  1. They monitor hashchange.
      var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7));

      if (oldIE) {
        this.iframe = $('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo('body')[0].contentWindow;
        this.navigate(fragment); // ancient ie just navigates to fragments.
      }

      // cross-browser events against window's popstate and hashchange
      if (this._hasPushState) {
        $(window).bind('popstate', this.checkUrl);
      } else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) {
        $(window).bind('hashchange', this.checkUrl);
      } else if (this._wantsHashChange) {
        this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
      }

Insight: Backbone.History doesn't store a collection of previous hashes, instead it lets the browser handle back and forward between hashes.

  1. Routing

This is where it gets complicated.

We have 3 components:

Backbone.History, which is often seen as Backbone.history. Adding a route to a new Backbone.Router will start up Backbone.history automatically.

Backbone.history handles the hash changes.

      // cross-browser events against window's popstate and hashchange
      if (this._hasPushState) {
        $(window).bind('popstate', this.checkUrl);
      } else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) {
        $(window).bind('hashchange', this.checkUrl);
      }

and poll with window.setInterval:

   else if (this._wantsHashChange) {
        this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
      }

Backbone.history will checkUrl:

    checkUrl: function(e) {
      var current = this.getFragment();
      if (current == this.fragment && this.iframe) current = this.getFragment(this.getHash(this.iframe));
      if (current == this.fragment) return false;
      if (this.iframe) this.navigate(current);
      this.loadUrl() || this.loadUrl(this.getHash());
    },

Backbone.history.checkUrl finds the route changed from this.getFragment(), Backbone.history.loadUrl is ran:

    loadUrl: function(fragmentOverride) {
      var fragment = this.fragment = this.getFragment(fragmentOverride);
      var matched = _.any(this.handlers, function(handler) {
        if (handler.route.test(fragment)) {
          handler.callback(fragment); // it's hitting a callback function
          return true;
        }
      });
      return matched;
    },

this.handlers (of Backbone.history.handlers) is from Backbone.Router.prototype.route. It's ran against every route when you do a normal router initialization:

Where does the Router come in to play?

var Workspace = Backbone.Router.extend({

  routes: {
    "help":                 "help",    // #help
    "search/:query":        "search",  // #search/kiwis
    "search/:query/p:page": "search"   // #search/kiwis/p7
  },

  help: function() {
    ...
  }
  search: function(query, page) {
    ...
  }

Backbone.Router sifts through this.routes with Backbone.Router.prototype._bindRoutes and pumps it to Backbone.Router.prototype.route instance one by one. Here's what happens:

    route: function(route, name, callback) {
      Backbone.history || (Backbone.history = new History);
      if (!_.isRegExp(route)) route = this._routeToRegExp(route);
      if (!callback) callback = this[name];
      Backbone.history.route(route, _.bind(function(fragment) {
        var args = this._extractParameters(route, fragment);
        callback && callback.apply(this, args);
        this.trigger.apply(this, ['route:' + name].concat(args));
        Backbone.history.trigger('route', this, name, args);
      }, this));
      return this;
    },

So as of Backbone 0.9.2, the router verifies initialization of Backbone.History into Backbone.history. If it's not initialized, it does it now.

Then it will use underscore method _.isRegExp to see if the route is a RegExp object.

The !callback confuses me, as this[name] and the argument from _bindRoutes's iterator passes in the same thing, this.route(routes[i][0], routes[i][1], this[routes[i][1]]). I thought it may been being like Marionette's AppRouter and perhaps allowing a more complex routing possibility but nah.

It will use Backbone.history.route which accepts the RegExp of the route and an anonymous function, let's zoom in:

_.bind(function(fragment) {
        var args = this._extractParameters(route, fragment);
        callback && callback.apply(this, args);
        this.trigger.apply(this, ['route:' + name].concat(args));
        Backbone.history.trigger('route', this, name, args);
      }, this)

This dance is wrapping the callback to accept the parameter arguments and trigger a history event. And making sure to bind it against Backbone.Router's instance context - as it's being accessed from Backbone.history. With the context binded,_extractParameters can be utilized against the reference to fragment, which is just Backbone.history.getFragment().

Backbone.history.route adds this to this.handlers, being sure to pass callback in the hash for loadUrl().

    route: function(route, callback) {
      this.handlers.unshift({route: route, callback: callback});
    },

So when Backbone.history detects a hashchange, with all of the parameters extracted as an argument, which are then passed in via callback.apply.

passes out

edit: fixes.

@Francisc
Copy link

hands glass of water to Tony

Are you better?
Awesome post.

@tony
Copy link
Contributor

tony commented Sep 27, 2012

POC for routing in branch https://github.com/tony/aura/tree/history.

#view/month

#view/month is calendar:changeView:basicWeek

Trigger this.calendar.fullCalendar and pass changeView and basicWeek in as arguments.

#view/:year/:month/:date

Above: #view/:year/:month/:day to calendar:gotoDate.

this.calendar.fullCalendar must pass gotoDate as an action. This case must accept parameters.

The widget is called router. It has no DOM object on the page being used, but one is being passed to prevent error.

The lambda in Backbone.Router.prototype.route is reused. It will only accept a route with a controller endpoint and have extracted args ready.

sandbox.publish('router', name, args); publishes what would normally be a function prototype being extended into Router. Instead of using a callback function, the router triggers sandbox.publish.

The Calendar widget subscribes and then does stuff.

#view/:year/:month and #view/:year will default to current date arguments when not filled. Entering a date takes you to a date view.

@addyosmani
Copy link
Member Author

I can't believe I almost missed this. That's really awesome work @tony :) I also really appreciate all of the research work you put into figuring out a good routing strategy.

If we could add a routing support example for the Todo widget as well (e.g https://github.com/addyosmani/todomvc/tree/gh-pages/architecture-examples/backbone) I'd be happy with us merging your POC in :)

@tony
Copy link
Contributor

tony commented Oct 7, 2012

thank you @addyosmani . sounds like a plan.

@tony
Copy link
Contributor

tony commented Oct 10, 2012

@addyosmani just a pulse, router poc is alive using router pattern from that repo. will pr tomorrow when todo is wired in.

https://github.com/tony/aura/tree/history

@addyosmani
Copy link
Member Author

Awesome! :)

@addyosmani
Copy link
Member Author

@tony is there anything else we're waiting on for this issue or is it good to close? :)

@addyosmani
Copy link
Member Author

Closing due to age and relevance to direction in master.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants