jashkenas / backbone

Give your JS App some Backbone with Models, Views, Collections, and Events
http://backbonejs.org
MIT License
28.09k stars 5.4k forks source link

Single Page Web Applications (View Features) #35

Closed bunchesofdonald closed 13 years ago

bunchesofdonald commented 13 years ago

I've recently been working on making a single-page web application (SPWA), and writing a js framework to do so... that is until I found backbone. I love the way it works, but it's just short of being able to do SPWA, as by default the views have no hide, or destroy, nor do they have the ability to have sub-views. Is this something that you'd be interested in backbone supporting? If so I'd be happy to do the brunt of it... maybe by creating an add-on module?

jashkenas commented 13 years ago

Possibly / Probably.

A great place to start would be to design it as a BackBone plugin. Feel free to extend Views as much as you see fit. That way you'll be able to start using it now, and we can see about merging it in if it seems like a good fit.

bunchesofdonald commented 13 years ago

Awesome! I'll start a repository and get to work, and obviously any input from anyone would be greatly appreciated.

jashkenas commented 13 years ago

We'll leave this ticket open, and you can post examples and code snippets in here, if you like, as you go along, and folks will be able to comment...

jwreagor commented 13 years ago

+1 for sub-views. I'm willing to guess as I continue writing more view classes to wrap presentation elements I'm going to want to stay DRY.

bunchesofdonald commented 13 years ago

Would you mind giving me some idea of how you'd like to see them implemented?

jashkenas commented 13 years ago

I'm afraid that's up to you -- I'm not sure what you have in mind for view enhancements, other than what you mentioned at the top of the ticket, and the first two are pretty easy:

hide: function() {
  $(this.el).hide();
},

destroy: function() {
  $(this.el).remove();
},

Subviews is a whole `nother can of worms, and I don't know too much about what that could or should look like -- sorry.

bunchesofdonald commented 13 years ago

That's cool, I just wanted to make sure that I gave people plenty of room for input.... I should probably just start working on it and then let people critique. Thanks!

jwreagor commented 13 years ago

I actually think sub-views should be rather simple idiom wise. Not sure about implementation.

var FormView = Backbone.View.extend({ form_stuff: function() {} });
var Signup = FormView.extend({ stuff: function() {} });

Thoughts?

jashkenas commented 13 years ago

I think bunchesofdonald is talking about nested child views, not subclasses of general views -- which is something you can already do...

For example, we currently have this heirarchy:

jwreagor commented 13 years ago

Sorry about that, not sure why they aren't working for me. I thought it was rather silly they weren't.

bunchesofdonald commented 13 years ago

Yeah what I intended would probably be better called child-views than subviews. Views that are attached to and controlled by top-level views.

I've been working on this for a couple days now, the repository is up at -http://github.com/bunchesofdonald/backbone-spwa and I have a fully working example application. The application is a very simple contact manager, I'm working on getting the github page up so that you can try it online, but you can always clone it and run it locally.

It does not have child views, but I have implemented a controller class that I think covers some of the ground that child-views would. So it's a step in the right direction.

bunchesofdonald commented 13 years ago

Working example now at: http://bunchesofdonald.github.com/backbone-spwa/examples/contacts/contacts.html

jashkenas commented 13 years ago

Very nice. You could even persist changes to the address book using LocalStorage and Backbone.sync, for a nice refresh-capable demonstration. Also, it would be really neat if the search box filtered the contacts as you type...

As to the SPWA project itself, it seems like the main component is the notion of a controller that routes hash-urls to actions, via hashchange events. This is something that we considered adding to Backbone before the initial release, but decided not to include. On the one hand, URL-routing is usually quite a small part of a client-side application, and there are plenty of existing modules for it. On the other, given the success of Sammy.js, people expect to have URL-routing provided.

Do folks think it would make sense to add a Backbone.Controller, with capabilities similar to Chris' Backbone.SPWA.Controller?

fitzgen commented 13 years ago

Bunchesofdonald, awesome! +1 from me on including something like this in Backbone.

Personally, I would prefer defining routes via regexp or with specifying parameters for interpolation ("/foo/:name/"). I have a project called pineapple[1] that does similar things as Backbone. Ultimately, when Backbone came out, I have found I prefer Backbone, but I have been experimenting with ideas in that project. One thing I think pineapple does pretty well is routing[2](the current implementation might a have a couple bugs, I have been changing things rather liberally and can't remember what state its currently in). Basically, you define routes as having both an enter event and an exit event. I think having the exit event is very nice, but it gets overlooked a lot. Here is what it looks like to define a route:

// Routes are defined with strings that get coerced to RegExp.
// (It is easier than having to escape backslashes all the time \/)
app.defineRoute("^#/foo/(\w+)/$", (function () {

    // Do some private stuff before returning the object

    return {
        enter: function (path, word) {
            // Activation stuff (word is the group match of regexp)
        },
        exit: function () {
            // Clean up stuff
        }
    };

}()));

Of course, Backbone would probably use an OO approach rather than the closure style pineapple does. I think the two important things I want to get across is the awesomeness of regexp routes (and using regexp groups rather than ?foo=bar param style) as well as having entrance and exit hooks.

Ben Alman's jQuery hashchange is a good choice.

More opinons to come when I get a chance to really look through the code :)

[1] http://github.com/fitzgen/pineapple

[2] http://github.com/fitzgen/pineapple/blob/master/src/routes.js

fitzgen commented 13 years ago

Also, being able to have something like app.redirect("/new/path/") is a must.

ghost commented 13 years ago

This is exactly what i've been looking for! I've played around with my own view manager framework and found that backbone complemented it nicely. jQuery Mobile seems to have a nice way of dealing with SPWA. I'm currently evaluating it but it may be worth taking some ideas from this?

As well as sub views, I've found that views that don't leave a history state are usually required. This can be annoying when using the hash change event for navigation. But I agree that the hash change is the most logical and semantically correct method of dealing with nav.

Enter and exit methods would be useful. Allow for setup/cleanup and animations.

jwreagor commented 13 years ago

That's a very nice demo and really fills in some holes as far as common use cases not in the documentation (which was also excellent). I especially like Backbone.SPWA.Controller's hashevent router and those x-ejs templates.

I could definitely see something like this being useful for my apps, however I'd like to see Backbone handling the handleEvent delegation behind the scenes.

bunchesofdonald commented 13 years ago

Fitzgen: I definitely think that regex is the way to go for the routes, as it stands it was more to get it up and running, so some things are the way they are just to have gotten it put together.

I really like the exit idea, that would be much better than the this.hideAll(), as each view could decide what it wanted to do as it lost focus. And we also do need a redirect rather than the location.hash = '/';

owz: I've not really looked at jquery mobile, but I will! Just on a quick glance it looks like it's trying to aim somewhere in-between what I'm doing and something like Cappuccino and SproutCore.

cheapRoc: Thanks! It is a bit kludgy in parts.

bunchesofdonald commented 13 years ago

jashkenas: Those are great ideas for the example! And of course I would be a big +1 to backbone doing something like this.

ghost commented 13 years ago

Having this in a backbone controller would be very useful +1

bunchesofdonald commented 13 years ago

The example is updated with the suggestions from jashkenas, it's much better that way, thanks!

I've been thinking about the router/enter/exit thing, and I think that I've come up with an elegant solution. It's early stages but what would you think of:

Dropping the Controller class in favor of a Router class. Make views so that they can subscribe to router events, via regex, they will get an event both when the hash that the subscribed to is 'entered' as well as 'exited'. The router class would be similar to Backbone events. That saves us from some of the nasty code that is in the controller (this.hideAll(), this.getView, etc). I think this also helps with some of the complexity of the child views, as each view would know the current 'state' of the application and could respond accordingly.

fitzgen commented 13 years ago

Make views so that they can subscribe to router events, via regex, they will get an event both when the hash that the subscribed to is 'entered' as well as 'exited'.

It feels much more natural to me that the routes should manage views based on enter/exit events, rather than have views subscribe to a certain route's events. I think that would make views do too much. I think views are fine as is. How do others feel about this?

eloop commented 13 years ago

It's worth having a look at the way sammy.js handles these issues. Also, this useful snippet was posted a while back by jashkenas - http://gist.github.com/624773.

jashkenas commented 13 years ago

I've been poking around a bit more, and I think I'm still pretty skeptical of including a hashchange router in core Backbone -- here's why:

... so -- it might make more sense to have Backbone.Router (Backbone.Controller, Backbone.History) be a plugin for starters, and we can see how many folks find it to be useful.

bunchesofdonald commented 13 years ago

That's probably a good way of handling it. You're right simple applications, or those for which a page reload here and there is ok, probably wouldn't benefit from this, and at this time, that's probably most sites.

But I also think that single page applications is the way the web is going to go, it makes for a much more desktop-like application. Also Sammy doesn't really provide the data modeling, nor really the views, that backbone does, and I would consider that a huge road-block to SPWA.

tchak commented 13 years ago

I started some work on Backbone.Mapper and Backbone.Controller. This is mostly lots of code borrowed from Sammy, but jquery.hashchange based, more modular and without http verbs stuff that I found mor disturbung then helpful on the client side... I will try to add some tests, docs and a real example application over the week end. tchak/backbone.plugins

For the moment there is routes handling, actions and filters. I plan on add some views integration lately.

chrislloyd commented 13 years ago

Worth reading on topic:

https://developer.mozilla.org/en/DOM/Manipulating_the_browser_history#Adding_and_modifying_history_entries

jashkenas commented 13 years ago

After some more prodding, and the discovery that Sammy does not work (and has never worked) in Internet Explorer < 8, I'm starting on this in earnest. Work so far is here:

https://github.com/documentcloud/backbone/compare/master...controller

fitzgen commented 13 years ago

Why reimplement the logic to make IE < 8 handle back buttons? Ben Alman already has created a jquery plugin to do just this that has been widely adopted and used by the community. Unless I'm missing something (which I will be the first to admit is very possible), I don't see why his can't just be integrated in a build script.

http://benalman.com/projects/jquery-hashchange-plugin/

Also, I have found that it is very common for a view to want to clean up after itself before the next route is entered. Any reason in particular why this is only emitting events on enter, and not exit events right before? Specifically, I think it would be cool to trigger specific route exit events. That way a controller doesn't have to bind to "global" level route events, just exit and enter events for its particular route. I think that is a lot cleaner than having every single route bind to every route change for clean up. I hinted at this in the above conversation. Thoughts?

jashkenas commented 13 years ago

The reason not to include "exit" events is that URLs are supposed to be stateless ... you should be able to navigate to a new URL fragment without knowing anything about what state is currently loaded. Can you share a couple examples of your use of "exit" events from your pineapple project?

Ben Alman's project was a fine resource when he first released it, but I'm not a big fan of it: it's far longer and more complex than necessary, and serializes your state into fairly gross URLs by default. There are more lines of code in jQuery-BBQ than there are in all of Backbone, the hashchange stuff included...

bunchesofdonald commented 13 years ago

I would think that the exit would be more from a perspective of a running application. So for instance an exit could hide the view that was previously showing, or do some clean-up in order to pass the "focus" of the application to the new view.

fitzgen commented 13 years ago

The example I'd give for an exit event is basically this:

Whenever you are going to a new route, I have found that usually, you will be re-rendering a large portion of the page and activating new widgets, etc. At the start of entering any given page, you can globally disable/hide/destroy everything and then replace it with your new stuff, but its overkill. It would be easier to not have every route have to perform this page purging and let each route clean up after itself only. That way you aren't telling widgets which are already disabled to disable themselves: only the ones that need to be disabled will get the message.

Here is some psuedocode:

var myRoute = new Route({
    initialize: function (opts) {
        this.views = opts.views;
        this.main = $("#main");
    },
    enter: function (id) {
        this.views.each(function (v) {
            this.main.append(v.render().el);
        });
    },
    exit: function () {
        this.views.each(function (v) {
            this.main.remove(this.view.disable().el);
        });
    }
});

Re: jquery BBQ, yes BBQ is large and arguably bloated, however Ben Alman also released the jquery hashchange plugin which implements only the logic needed for the back button and is quite a bit smaller. BBQ is built on top of hashchange. Whatever floats the virtual boat, so to speak.

jashkenas commented 13 years ago

Ahh, that's the beauty of mutually-exclusive CSS class names...

Instead of blowing away an entire tree of UI/views when you tab away, and then re-creating them then you tab back over, it's much nicer to just set a CSS class on a high-up node, or even document.body. For example, after loading the workspace, our <body> tag looks something like this:

<body class="logged-in sidebar-tab-entities main-tab-documents paginated">

And if you change tabs, from say "documents" to "help", you'll have a class name of "main-tab-help". It makes for faster tabbing around, because it's a single DOM repaint instead of expensive UI building, and helps keep things stateless -- you don't need to worry about wether "Projects" are being rendered, or "Entities" are: they're both ready to be rendered at all times...

Thoughts?

fitzgen commented 13 years ago

Very clever! I like this technique a lot. The question remains though: where do you add and remove the classes to and from the body? The example just changes like this:

var myRoute = new Route({
    initialize: function (opts) {
        this.main = $("#main");
    },
    enter: function (id) {
        this.main.addClass("foobar");
    },
    exit: function () {
        this.main.removeClass("foobar");
    }
});
jashkenas commented 13 years ago

Naturally, the simple thing is just to removeClass() before you addClass(), but something a bit more sophisticated that handles mutually-exclusive classes was actually almost part of the initial Backbone.View, and got pulled before release. Right now, we have this little extension, in DocumentCloud:

// Makes the view enter a mode. Modes have both a 'mode' and a 'group',
// and are mutually exclusive with any other modes in the same group.
// Setting will update the view's modes hash, as well as set an HTML class
// of *[mode]_[group]* on the view's element. Convenient way to swap styles
// and behavior.
Backbone.View.prototype.setMode = function(mode, group) {
  this.modes || (this.modes = {});
  if (this.modes[group] === mode) return;
  $(this.el).setMode(mode, group);
  this.modes[group] = mode;
};

$.fn.extend({
  // See Backbone.View#setMode...
  setMode : function(state, group) {
    group = group || 'mode';
    var re = new RegExp("\\w+_" + group + "(\\s|$)", 'g');
    var mode = (state === null) ? "" : state + "_" + group;
    this.each(function(){
      this.className = (this.className.replace(re, '') + ' ' + mode).replace(/\s\s/g, ' ');
    });
    return mode;
  }
});

So, in your view, when it opens, you just call:

`this.setMode('entities', 'sidebar');`

... And get an "entities-sidebar" class, blowing away all other "-sidebar" classes. You can also say: this.modes.sidebar, and get back "entities".

Not that I'm advocating this extension -- addClass, removeClass and toggleClass are perfectly sufficient for most needs, but if you're doing a lot of stuff with mutually-exclusive classes, where you can have more than just "on" and "off", it's handy.

jashkenas commented 13 years ago

Backbone.js 0.3.0 is now out, and includes Controllers with hashchange-based routing... Closing this ticket.