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

allow route to be defined but not require a callback method on the router #659

Closed
wants to merge 1 commit into from

Conversation

mxriverlynn
Copy link
Contributor

I'm using a router without using callback methods in an example app that I'm building, and I don't want to be required to build a callback method on my router.

for example, i want this to work:

MyRouter = Backbone.Router.extend({
  routes: {
    "foo": "bar",
    "baz/:id": "bazIt"
  }
});

without defining a bar and bazIt function on the router, this would cause errors because the callback is undefined.

this patch adds support for using a router like this, which then allows me to bind to the router events independently of the router configuration:

myRouter = new MyRouter();

myRouter.bind("route:bar", function(){ ... });

this patch includes the test and implementation to cover this.

@yuchi
Copy link

yuchi commented Oct 8, 2011

If you're using jQuery you can also do the following, which IMO is better because you are explicitly saying "I do not need a real callback". Or even better, while you have Underscore for sure, use _.identity.

  events: {
    "/idonothaveacallback" : $.noop
  }

@mxriverlynn
Copy link
Contributor Author

@yuchi - i must be misunderstanding what you're suggesting...

there's no support for events directly on routers. the events that are raised come from route callback names.

  routes: {
  "/foo/bar": "foobar"
  }

this raises a route:foobar event, which is what i'm binding to.

@yuchi
Copy link

yuchi commented Oct 9, 2011

Oh, sorry. I misunderstood your needs. You are actually right.

@paulyoder
Copy link

+1

1 similar comment
@ncammarata
Copy link

+1

@wookiehangover
Copy link
Collaborator

Is there any advantage to defining the routes only to attach a handler via bind? I guess I don't see the difference between what you're describing and defining a route from another class with router.route()...

@mxriverlynn
Copy link
Contributor Author

@wookiehangover

it's all about coupling.

I'm building a composite application right now. There's a treeview on the left, a grid on the top and a form on the bottom. Each of these areas has their own distinct set of functionality, and are separated into their own JavaScript modules.

When building the routes to manage this app, I need each of my areas to respond correctly to the route being fired. I can either do this by coupling all three of my areas directly to the router and have the router callback method call all three of the modules to do something, or I can decouple everything through events. When the router fires an event for the route, each of the modules can listen to that event and separately respond in order to put themselves into the correct state and display.

Right now, I have to put empty callback methods in my router to make this work. It's ugly and extra code that does nothing.

I'm hoping that, since the change I made is only 1 if-statement around the callback being called, that this will make it in to the next release.

@ghost ghost assigned wookiehangover Oct 29, 2011
@wookiehangover
Copy link
Collaborator

What about associating the routing relationships through a generic (tho non-evented) interface, such as a generic match in your router code? That's how I've handled similar situations in the past:

Backbone.router.extend({
  routes: [
    ':section':       'openSection',
  ],

  openSection: function( section ){
    try {
      App[ section ].open();
    } catch ( routing_error ) {
      console.error( '404 - ' + section + ' not found' );
    }
  }
})

...

var BaseView = Backbone.View.extend({
  _open: function(){
    // shared code in here...
  }
});

var TodoView = BaseView.extend({
  open: function(){
    // custom stuff to this view instance

    // then the generic, application wide _open()
    this._open();
  }
});

And then attach the instance of any singleton View directly to your application namespace, named after the route that matches it.

The point being that you can use the router as-is to establish routes to decoupled modules. Happy to discuss different patterns for achieving this... cc/ @tbranyen

@mxriverlynn
Copy link
Contributor Author

sure, there are dozens of ways to decouple things. i'm saying the change i made is the only way. i'm only saying that this one little if statement is such a small change for such a large benefit, that it should be pulled in.

@wookiehangover
Copy link
Collaborator

This may seem simliar to @yuchi's comment, but can't your event binding approach be achieve by binding the dummy routes?

Backbone.router.extend({
  routes: [
    'foo/bar':       'foobar',
  ],

  "foobar": $.noop
});

IMO this patch seems to serve a very specific edge case...

@mxriverlynn
Copy link
Contributor Author

What does "edge case" have to do with anything, though? This is a non-breaking change that simplifies things for a particular scenario.

The $.noop suggestion is functional, yes. But i'm not looking for a functional workaround - I have plenty of those. I like my code to be clean and in situations where I want to use an event instead of a callback, I would prefer to not provide extraneous no-op callbacks.

@wookiehangover
Copy link
Collaborator

@derickbailey is there a use case where assigning the route to a NoOp like in my example above is insufficient?

@jashkenas
Copy link
Owner

I'm afraid the proposed change here would lead to really nasty bugs for a number of folks. The contract with binding events, like the OP's:

MyRouter = Backbone.Router.extend({
  routes: {
    "foo": "bar",
    "baz/:id": "bazIt"
  }
});

... is that the bar and bazIt functions exist. If you've mistyped your function name, you want that to error as early as possible, instead of silently failing later on. The clean way to write this is:

MyRouter = Backbone.Router.extend({
  routes: {
    "foo": "noop",
    "baz/:id": "noop"
  },
  noop: function(){},
});

@jashkenas jashkenas closed this Oct 29, 2011
@mxriverlynn
Copy link
Contributor Author

It's not about "insufficient". All of the workarounds that I am currently using, and the ones you have provides, work. There's no questioning that. Here's a much more detailed scenario, via code, illustrating why I want this. This is a simplification of an app that I wrote, showing an evented architecture (note that I'm writing this directly into the issues list so it probably won't run as is, but it should get the point across).

Image = Backbone.Model.extend({
  select: function(){
    this.collection.select(this);
  }
});

ImageCollection = Backbone.Collection.extend({
  model: Image,

  select: function(image){
    if (this.selectedImage){
      this.selectedImage.unset("selected");
    }
    image.set({selected: "selected"});
    this.selectedImage = image;
    this.trigger("image:selected", image);
  }
});

MainView = function(images){
  this.images = images;

  this.el = $("#image-view");

  this.loadImageById = function(id){
    var image = this.images.get(id);
    this.showImage(image);
  };

  this.showImage = function(image){
    var imageView = new ImageView({model: image});
    imageView.render();

    if (this.currentImageView){
      this.currentImageView.close();
    }

    this.el.html(imageView.el);
    this.currentImageView = imageView;
  }
}

ImageRouter = Backbone.Router.extend({
  routes: {
    "/images/:id": "loadImageById"
  },

  initialize: function(options){
    this.mainView = options.mainView;
  },

  loadImageById: function(id){
    this.mainView.loadImageById(id);
  }
});

ImageGalleryApp = function(){
  var images = new ImageCollection();

  var mainView = new MainView(images);

  var router = new ImageRouter({mainView: mainView});

  images.bind("image:selected", function(image){
    mainView.showImage(image);
    router.navigate("/images/" + image.id);
  });

  Backbone.history.start();
}

When a user clicks on an image in an ImagePreview, I call this.model.select() to select the image fire off the "image:selected" event on the collection. This allows the user to view the full size version of the image. It also sets the URL's hash to the correct route so that the user can bookmark/hit refresh/etc.

This code works... my app works this way. But, in this version of the code, my router is directly coupled to an instance of MainView. This is less desirable than what I want.

I would prefer to write this version of the router / app objects instead:

ImageRouter = Backbone.Router.extend({
  routes: {
    "/images/:id": "loadImageById"
  }
});

ImageGalleryApp = function(){
  var images = new ImageCollection();

  var mainView = new MainView(images);

  var router = new ImageRouter();

  router.bind("loadImageById", mainView.loadImageById);

  images.bind("image:selected", function(image){
    mainView.showImage(image);
    router.navigate("/images/" + image.id);
  });

  Backbone.history.start();
}

It's a few lines of code cleaner in this version, using events instead of coupling the objects together directly. As the application grows and additional areas are added to the screen, the benefit becomes larger.

@jashkenas
Copy link
Owner

Alright -- thanks for the persistence and persuasion.

@jashkenas jashkenas reopened this Oct 29, 2011
@mxriverlynn
Copy link
Contributor Author

(deleted most recent comment... i sent it in after Jeremy had re-opened this. thanks, jeremy!)

@jashkenas jashkenas closed this in 5ebbeb0 Oct 31, 2011
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants