rails-backbone 0.2.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Backbone-Rails
2
2
 
3
- Easily setup and use backbone.js with rails 3.1
3
+ Easily setup and use backbone.js (0.5.0) with rails 3.1
4
4
 
5
5
  ## Rails 3.1 setup
6
6
  This gem requires the use of rails 3.1, coffeescript and the new rails asset pipeline provided by sprockets.
@@ -22,7 +22,7 @@ Then run the following commands:
22
22
 
23
23
  Running `rails g backbone:install` will create the following directory structure under `app/assets/javascripts/backbone`:
24
24
 
25
- controllers/
25
+ routers/
26
26
  models/
27
27
  templates/
28
28
  views/
@@ -30,7 +30,8 @@ Running `rails g backbone:install` will create the following directory structure
30
30
  It will also create a toplevel app_name.coffee file to setup namespacing and setup initial requires.
31
31
 
32
32
  ## Generators
33
- backbone-rails provides 3 simple generators to help get you started using bacbone.js with rails 3.1
33
+ backbone-rails provides 3 simple generators to help get you started using bacbone.js with rails 3.1.
34
+ The generators will only create client side code (javascript).
34
35
 
35
36
  ### Model Generator
36
37
 
@@ -38,14 +39,44 @@ backbone-rails provides 3 simple generators to help get you started using bacbon
38
39
 
39
40
  This generator creates a backbone model and collection inside `app/assets/javascript/backbone/models` to be used to talk to the rails backend.
40
41
 
41
- ### Controller
42
+ ### Routers
42
43
 
43
- rails g backbone:controller
44
+ rails g backbone:router
44
45
 
45
- This generator creates a backbone controller with corresponding views and templates for the given actions provided.
46
+ This generator creates a backbone router with corresponding views and templates for the given actions provided.
46
47
 
47
48
  ### Scaffolding
48
49
 
49
50
  rails g backbone:scaffold
50
51
 
51
- This generator creates a controller, views, templates, model and collection to create a simple crud single page app
52
+ This generator creates a router, views, templates, model and collection to create a simple crud single page app
53
+
54
+ ## Example Usage
55
+
56
+ Say we have just created a new rails 3.1 application called `blog`. Edit your gemfile and add `gem rails-backbone`.
57
+
58
+ Install the gem and generate scaffolding.
59
+
60
+ bundle install
61
+ rails g backbone:install
62
+ rails g scaffold Post title:string content:string
63
+ rake db:migrate
64
+ rails g backbone:scaffold Post title:string content:string
65
+
66
+ You now have installed the backbone-rails gem, setup a default directory structure for your frontend backbone code.
67
+ Then you generated the usual rails server side crud scaffolding and finally generated backbone.js code to provide a simple single page crud app.
68
+ You have one last step:
69
+
70
+ Edit your posts index view `app/views/posts/index.html.erb` with the following contents:
71
+
72
+ <div id="posts"></div>
73
+
74
+ <script type="text/javascript">
75
+ $(function() {
76
+ window.router = new Blog.Routers.PostsRouter({posts: <%= @posts.to_json.html_safe -%>});
77
+ Backbone.history.start();
78
+ });
79
+ </script>
80
+
81
+ Now start your server `rails s` and browse to [localhost:3000/posts](http://localhost:3000/posts)
82
+ You should now have a fully functioning single page crud app for Post models.
@@ -15,14 +15,14 @@ module Backbone
15
15
  end
16
16
 
17
17
  def create_dir_layout
18
- %W{controllers models views templates}.each do |dir|
18
+ %W{routers models views templates}.each do |dir|
19
19
  empty_directory "app/assets/javascripts/backbone/#{dir}"
20
20
  create_file "app/assets/javascripts/backbone/#{dir}/.gitkeep" unless options[:skip_git]
21
21
  end
22
22
  end
23
23
 
24
24
  def create_app_file
25
- template "app.coffee", "app/assets/javascripts/backbone/#{application_name}.coffee"
25
+ template "app.coffee", "app/assets/javascripts/backbone/#{application_name}.js.coffee"
26
26
  end
27
27
 
28
28
  protected
@@ -2,10 +2,10 @@
2
2
  #= require_tree ./templates
3
3
  #= require_tree ./models
4
4
  #= require_tree ./views
5
- #= require_tree ./controllers
5
+ #= require_tree ./routers
6
6
 
7
7
  window.<%= application_name.capitalize %> =
8
8
  Models: {}
9
9
  Collections: {}
10
- Controllers: {}
10
+ Routers: {}
11
11
  Views: {}
@@ -11,7 +11,7 @@ module Backbone
11
11
  argument :attributes, :type => :array, :default => [], :banner => "field:type field:type"
12
12
 
13
13
  def create_backbone_model
14
- template "model.coffee", "#{backbone_path}/models/#{file_name}.coffee"
14
+ template "model.coffee", "#{backbone_path}/models/#{file_name}.js.coffee"
15
15
  end
16
16
 
17
17
  end
@@ -18,8 +18,8 @@ module Backbone
18
18
  [application_name.capitalize, "Views", plural_name.capitalize].join(".")
19
19
  end
20
20
 
21
- def controller_namespace
22
- [application_name.capitalize, "Controllers", plural_name.capitalize].join(".")
21
+ def router_namespace
22
+ [application_name.capitalize, "Routers", plural_name.capitalize].join(".")
23
23
  end
24
24
 
25
25
  def jst(action)
@@ -2,11 +2,11 @@ require 'generators/backbone/resource_helpers'
2
2
 
3
3
  module Backbone
4
4
  module Generators
5
- class ControllerGenerator < Rails::Generators::NamedBase
5
+ class RouterGenerator < Rails::Generators::NamedBase
6
6
  include Backbone::Generators::ResourceHelpers
7
7
 
8
8
  source_root File.expand_path("../templates", __FILE__)
9
- desc "This generator creates a backbone controller with views and templates for the provided actions"
9
+ desc "This generator creates a backbone router with views and templates for the provided actions"
10
10
 
11
11
  argument :actions, :type => :array, :default => [], :banner => "action action"
12
12
 
@@ -24,14 +24,14 @@ module Backbone
24
24
  end
25
25
  end
26
26
 
27
- def create_controller_files
28
- template 'controller.coffee', File.join(backbone_path, "controllers", class_path, "#{file_name}_controller.coffee")
27
+ def create_router_files
28
+ template 'router.coffee', File.join(backbone_path, "routers", class_path, "#{file_name}_router.js.coffee")
29
29
  end
30
30
 
31
31
  def create_view_files
32
32
  actions.each do |action|
33
33
  @action = action
34
- @view_path = File.join(backbone_path, "views", plural_name, "#{action}_view.coffee")
34
+ @view_path = File.join(backbone_path, "views", plural_name, "#{action}_view.js.coffee")
35
35
  @jst_path = File.join(backbone_path,"templates", plural_name, "#{action}.jst.ejs")
36
36
 
37
37
  template "view.coffee", @view_path
@@ -1,4 +1,4 @@
1
- class <%= controller_namespace %>Controller extends Backbone.Controller
1
+ class <%= router_namespace %>Router extends Backbone.Router
2
2
  initialize: (options) ->
3
3
 
4
4
  routes:
@@ -7,17 +7,17 @@ module Backbone
7
7
  source_root File.expand_path("../templates", __FILE__)
8
8
  desc "This generator creates the client side crud scaffolding"
9
9
 
10
- def create_controller_files
11
- template 'controller.coffee', File.join(backbone_path, "controllers", class_path, "#{plural_name}_controller.coffee")
10
+ def create_router_files
11
+ template 'router.coffee', File.join(backbone_path, "routers", class_path, "#{plural_name}_router.js.coffee")
12
12
  end
13
13
 
14
14
  def create_view_files
15
15
  available_views.each do |view|
16
- template "views/#{view}_view.coffee", File.join(backbone_path, "views", plural_name, "#{view}_view.coffee")
16
+ template "views/#{view}_view.coffee", File.join(backbone_path, "views", plural_name, "#{view}_view.js.coffee")
17
17
  template "templates/#{view}.jst", File.join(backbone_path, "templates", plural_name, "#{view}.jst.ejs")
18
18
  end
19
19
 
20
- template "views/model_view.coffee", File.join(backbone_path, "views", plural_name, "#{singular_name}_view.coffee")
20
+ template "views/model_view.coffee", File.join(backbone_path, "views", plural_name, "#{singular_name}_view.js.coffee")
21
21
  template "templates/model.jst", File.join(backbone_path, "templates", plural_name, "#{singular_name}.jst.ejs")
22
22
  end
23
23
 
@@ -1,7 +1,7 @@
1
- class <%= controller_namespace %>Controller extends Backbone.Controller
1
+ class <%= router_namespace %>Router extends Backbone.Router
2
2
  initialize: (options) ->
3
3
  @<%= plural_name %> = new <%= collection_namespace %>Collection()
4
- @<%= plural_name %>.refresh options.<%= plural_name %>
4
+ @<%= plural_name %>.reset options.<%= plural_name %>
5
5
 
6
6
  routes:
7
7
  "/new": "new<%= class_name %>"
@@ -6,7 +6,7 @@ class <%= view_namespace %>.IndexView extends Backbone.View
6
6
  initialize: () ->
7
7
  _.bindAll(this, 'addOne', 'addAll', 'render');
8
8
 
9
- @options.<%= plural_name %>.bind('refresh', this.addAll);
9
+ @options.<%= plural_name %>.bind('reset', this.addAll);
10
10
 
11
11
  addAll: () ->
12
12
  @options.<%= plural_name %>.each(this.addOne)
@@ -1,4 +1,4 @@
1
- // Backbone.js 0.3.3
1
+ // Backbone.js 0.5.0
2
2
  // (c) 2010 Jeremy Ashkenas, DocumentCloud Inc.
3
3
  // Backbone may be freely distributed under the MIT license.
4
4
  // For all details and documentation:
@@ -25,7 +25,7 @@
25
25
  }
26
26
 
27
27
  // Current version of the library. Keep in sync with `package.json`.
28
- Backbone.VERSION = '0.3.3';
28
+ Backbone.VERSION = '0.5.0';
29
29
 
30
30
  // Require Underscore, if we're on the server, and it's not already present.
31
31
  var _ = root._;
@@ -70,7 +70,7 @@
70
70
  // Passing `"all"` will bind the callback to all events fired.
71
71
  bind : function(ev, callback) {
72
72
  var calls = this._callbacks || (this._callbacks = {});
73
- var list = this._callbacks[ev] || (this._callbacks[ev] = []);
73
+ var list = calls[ev] || (calls[ev] = []);
74
74
  list.push(callback);
75
75
  return this;
76
76
  },
@@ -103,13 +103,13 @@
103
103
  // same arguments as `trigger` is, apart from the event name.
104
104
  // Listening for `"all"` passes the true event name as the first argument.
105
105
  trigger : function(eventName) {
106
- var list, calls, ev, callback, args, i, l;
106
+ var list, calls, ev, callback, args;
107
107
  var both = 2;
108
108
  if (!(calls = this._callbacks)) return this;
109
109
  while (both--) {
110
110
  ev = both ? eventName : 'all';
111
111
  if (list = calls[ev]) {
112
- for (i = 0, l = list.length; i < l; i++) {
112
+ for (var i = 0, l = list.length; i < l; i++) {
113
113
  if (!(callback = list[i])) {
114
114
  list.splice(i, 1); i--; l--;
115
115
  } else {
@@ -143,7 +143,7 @@
143
143
  this._changed = false;
144
144
  this._previousAttributes = _.clone(this.attributes);
145
145
  if (options && options.collection) this.collection = options.collection;
146
- this.initialize(attributes, options);
146
+ this.initialize.apply(this, arguments);
147
147
  };
148
148
 
149
149
  // Attach all inheritable methods to the Model prototype.
@@ -204,6 +204,10 @@
204
204
  // Check for changes of `id`.
205
205
  if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];
206
206
 
207
+ // We're about to start triggering change events.
208
+ var alreadyChanging = this._changing;
209
+ this._changing = true;
210
+
207
211
  // Update attributes.
208
212
  for (var attr in attrs) {
209
213
  var val = attrs[attr];
@@ -216,7 +220,8 @@
216
220
  }
217
221
 
218
222
  // Fire the `"change"` event, if the model has been changed.
219
- if (!options.silent && this._changed) this.change(options);
223
+ if (!alreadyChanging && !options.silent && this._changed) this.change(options);
224
+ this._changing = false;
220
225
  return this;
221
226
  },
222
227
 
@@ -248,6 +253,7 @@
248
253
  // to silence it.
249
254
  clear : function(options) {
250
255
  options || (options = {});
256
+ var attr;
251
257
  var old = this.attributes;
252
258
 
253
259
  // Run validation.
@@ -299,10 +305,11 @@
299
305
  return (this.sync || Backbone.sync).call(this, method, this, options);
300
306
  },
301
307
 
302
- // Destroy this model on the server. Upon success, the model is removed
308
+ // Destroy this model on the server if it was already persisted. Upon success, the model is removed
303
309
  // from its collection, if it has one.
304
310
  destroy : function(options) {
305
311
  options || (options = {});
312
+ if (this.isNew()) return this.trigger('destroy', this, this.collection, options);
306
313
  var model = this;
307
314
  var success = options.success;
308
315
  options.success = function(resp) {
@@ -333,10 +340,9 @@
333
340
  return new this.constructor(this);
334
341
  },
335
342
 
336
- // A model is new if it has never been saved to the server, and has a negative
337
- // ID.
343
+ // A model is new if it has never been saved to the server, and lacks an id.
338
344
  isNew : function() {
339
- return !this.id;
345
+ return this.id == null;
340
346
  },
341
347
 
342
348
  // Call this method to manually fire a `change` event for this model.
@@ -391,7 +397,7 @@
391
397
  var error = this.validate(attrs);
392
398
  if (error) {
393
399
  if (options.error) {
394
- options.error(this, error);
400
+ options.error(this, error, options);
395
401
  } else {
396
402
  this.trigger('error', this, error, options);
397
403
  }
@@ -410,14 +416,11 @@
410
416
  // its models in sort order, as they're added and removed.
411
417
  Backbone.Collection = function(models, options) {
412
418
  options || (options = {});
413
- if (options.comparator) {
414
- this.comparator = options.comparator;
415
- delete options.comparator;
416
- }
419
+ if (options.comparator) this.comparator = options.comparator;
417
420
  _.bindAll(this, '_onModelEvent', '_removeReference');
418
421
  this._reset();
419
- if (models) this.refresh(models, {silent: true});
420
- this.initialize(models, options);
422
+ if (models) this.reset(models, {silent: true});
423
+ this.initialize.apply(this, arguments);
421
424
  };
422
425
 
423
426
  // Define the Collection's inheritable methods.
@@ -485,7 +488,7 @@
485
488
  options || (options = {});
486
489
  if (!this.comparator) throw new Error('Cannot sort a set without a comparator');
487
490
  this.models = this.sortBy(this.comparator);
488
- if (!options.silent) this.trigger('refresh', this, options);
491
+ if (!options.silent) this.trigger('reset', this, options);
489
492
  return this;
490
493
  },
491
494
 
@@ -495,27 +498,27 @@
495
498
  },
496
499
 
497
500
  // When you have more items than you want to add or remove individually,
498
- // you can refresh the entire set with a new list of models, without firing
499
- // any `added` or `removed` events. Fires `refresh` when finished.
500
- refresh : function(models, options) {
501
+ // you can reset the entire set with a new list of models, without firing
502
+ // any `added` or `removed` events. Fires `reset` when finished.
503
+ reset : function(models, options) {
501
504
  models || (models = []);
502
505
  options || (options = {});
503
506
  this.each(this._removeReference);
504
507
  this._reset();
505
508
  this.add(models, {silent: true});
506
- if (!options.silent) this.trigger('refresh', this, options);
509
+ if (!options.silent) this.trigger('reset', this, options);
507
510
  return this;
508
511
  },
509
512
 
510
- // Fetch the default set of models for this collection, refreshing the
513
+ // Fetch the default set of models for this collection, resetting the
511
514
  // collection when they arrive. If `add: true` is passed, appends the
512
- // models to the collection instead of refreshing.
515
+ // models to the collection instead of resetting.
513
516
  fetch : function(options) {
514
517
  options || (options = {});
515
518
  var collection = this;
516
519
  var success = options.success;
517
520
  options.success = function(resp, status, xhr) {
518
- collection[options.add ? 'add' : 'refresh'](collection.parse(resp, xhr), options);
521
+ collection[options.add ? 'add' : 'reset'](collection.parse(resp, xhr), options);
519
522
  if (success) success(collection, resp);
520
523
  };
521
524
  options.error = wrapError(options.error, collection, options);
@@ -524,19 +527,15 @@
524
527
 
525
528
  // Create a new instance of a model in this collection. After the model
526
529
  // has been created on the server, it will be added to the collection.
530
+ // Returns the model, or 'false' if validation on a new model fails.
527
531
  create : function(model, options) {
528
532
  var coll = this;
529
533
  options || (options = {});
530
- if (!(model instanceof Backbone.Model)) {
531
- var attrs = model;
532
- model = new this.model(null, {collection: coll});
533
- if (!model.set(attrs, options)) return false;
534
- } else {
535
- model.collection = coll;
536
- }
534
+ model = this._prepareModel(model, options);
535
+ if (!model) return false;
537
536
  var success = options.success;
538
537
  options.success = function(nextModel, resp, xhr) {
539
- coll.add(nextModel);
538
+ coll.add(nextModel, options);
540
539
  if (success) success(nextModel, resp, xhr);
541
540
  };
542
541
  model.save(null, options);
@@ -556,7 +555,7 @@
556
555
  return _(this.models).chain();
557
556
  },
558
557
 
559
- // Reset all internal state. Called when the collection is refreshed.
558
+ // Reset all internal state. Called when the collection is reset.
560
559
  _reset : function(options) {
561
560
  this.length = 0;
562
561
  this.models = [];
@@ -564,21 +563,32 @@
564
563
  this._byCid = {};
565
564
  },
566
565
 
566
+ // Prepare a model to be added to this collection
567
+ _prepareModel: function(model, options) {
568
+ if (!(model instanceof Backbone.Model)) {
569
+ var attrs = model;
570
+ model = new this.model(attrs, {collection: this});
571
+ if (model.validate && !model._performValidation(attrs, options)) model = false;
572
+ } else if (!model.collection) {
573
+ model.collection = this;
574
+ }
575
+ return model;
576
+ },
577
+
567
578
  // Internal implementation of adding a single model to the set, updating
568
579
  // hash indexes for `id` and `cid` lookups.
580
+ // Returns the model, or 'false' if validation on a new model fails.
569
581
  _add : function(model, options) {
570
582
  options || (options = {});
571
- if (!(model instanceof Backbone.Model)) {
572
- model = new this.model(model, {collection: this});
573
- }
574
- var already = this.getByCid(model);
583
+ model = this._prepareModel(model, options);
584
+ if (!model) return false;
585
+ var already = this.getByCid(model) || this.get(model);
575
586
  if (already) throw new Error(["Can't add the same model to a set twice", already.id]);
576
587
  this._byId[model.id] = model;
577
588
  this._byCid[model.cid] = model;
578
- if (!model.collection) {
579
- model.collection = this;
580
- }
581
- var index = this.comparator ? this.sortedIndex(model, this.comparator) : this.length;
589
+ var index = options.at != null ? options.at :
590
+ this.comparator ? this.sortedIndex(model, this.comparator) :
591
+ this.length;
582
592
  this.models.splice(index, 0, model);
583
593
  model.bind('all', this._onModelEvent);
584
594
  this.length++;
@@ -640,16 +650,16 @@
640
650
  };
641
651
  });
642
652
 
643
- // Backbone.Controller
653
+ // Backbone.Router
644
654
  // -------------------
645
655
 
646
- // Controllers map faux-URLs to actions, and fire events when routes are
656
+ // Routers map faux-URLs to actions, and fire events when routes are
647
657
  // matched. Creating a new one sets its `routes` hash, if not set statically.
648
- Backbone.Controller = function(options) {
658
+ Backbone.Router = function(options) {
649
659
  options || (options = {});
650
660
  if (options.routes) this.routes = options.routes;
651
661
  this._bindRoutes();
652
- this.initialize(options);
662
+ this.initialize.apply(this, arguments);
653
663
  };
654
664
 
655
665
  // Cached regular expressions for matching named param parts and splatted
@@ -658,8 +668,8 @@
658
668
  var splatParam = /\*([\w\d]+)/g;
659
669
  var escapeRegExp = /[-[\]{}()+?.,\\^$|#\s]/g;
660
670
 
661
- // Set up all inheritable **Backbone.Controller** properties and methods.
662
- _.extend(Backbone.Controller.prototype, Backbone.Events, {
671
+ // Set up all inheritable **Backbone.Router** properties and methods.
672
+ _.extend(Backbone.Router.prototype, Backbone.Events, {
663
673
 
664
674
  // Initialize is an empty function by default. Override it with your own
665
675
  // initialization logic.
@@ -674,17 +684,16 @@
674
684
  route : function(route, name, callback) {
675
685
  Backbone.history || (Backbone.history = new Backbone.History);
676
686
  if (!_.isRegExp(route)) route = this._routeToRegExp(route);
677
- Backbone.history.route(route, _.bind(function(hash) {
678
- var args = this._extractParameters(route, hash);
687
+ Backbone.history.route(route, _.bind(function(fragment) {
688
+ var args = this._extractParameters(route, fragment);
679
689
  callback.apply(this, args);
680
690
  this.trigger.apply(this, ['route:' + name].concat(args));
681
691
  }, this));
682
692
  },
683
693
 
684
- // Simple proxy to `Backbone.history` to save a fragment into the history,
685
- // without triggering routes.
686
- saveLocation : function(hash) {
687
- Backbone.history.saveLocation(hash);
694
+ // Simple proxy to `Backbone.history` to save a fragment into the history.
695
+ navigate : function(fragment, triggerRoute) {
696
+ Backbone.history.navigate(fragment, triggerRoute);
688
697
  },
689
698
 
690
699
  // Bind all defined routes to `Backbone.history`. We have to reverse the
@@ -712,8 +721,8 @@
712
721
 
713
722
  // Given a route, and a URL fragment that it matches, return the array of
714
723
  // extracted parameters.
715
- _extractParameters : function(route, hash) {
716
- return route.exec(hash).slice(1);
724
+ _extractParameters : function(route, fragment) {
725
+ return route.exec(fragment).slice(1);
717
726
  }
718
727
 
719
728
  });
@@ -721,7 +730,7 @@
721
730
  // Backbone.History
722
731
  // ----------------
723
732
 
724
- // Handles cross-browser history management, based on URL hashes. If the
733
+ // Handles cross-browser history management, based on URL fragments. If the
725
734
  // browser does not support `onhashchange`, falls back to polling.
726
735
  Backbone.History = function() {
727
736
  this.handlers = [];
@@ -744,33 +753,67 @@
744
753
  // twenty times a second.
745
754
  interval: 50,
746
755
 
747
- // Get the cross-browser normalized URL fragment.
748
- getHash : function(loc) {
749
- return (loc || window.location).hash.replace(hashStrip, '');
756
+ // Get the cross-browser normalized URL fragment, either from the URL,
757
+ // the hash, or the override.
758
+ getFragment : function(fragment, forcePushState) {
759
+ if (fragment == null) {
760
+ if (this._hasPushState || forcePushState) {
761
+ fragment = window.location.pathname;
762
+ var search = window.location.search;
763
+ if (search) fragment += search;
764
+ if (fragment.indexOf(this.options.root) == 0) fragment = fragment.substr(this.options.root.length);
765
+ } else {
766
+ fragment = window.location.hash;
767
+ }
768
+ }
769
+ return fragment.replace(hashStrip, '');
750
770
  },
751
771
 
752
772
  // Start the hash change handling, returning `true` if the current URL matches
753
773
  // an existing route, and `false` otherwise.
754
- start : function() {
774
+ start : function(options) {
775
+
776
+ // Figure out the initial configuration. Do we need an iframe?
777
+ // Is pushState desired ... is it available?
755
778
  if (historyStarted) throw new Error("Backbone.history has already been started");
756
- var hash = this.getHash();
757
- var docMode = document.documentMode;
758
- var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7));
779
+ this.options = _.extend({}, {root: '/'}, this.options, options);
780
+ this._wantsPushState = !!this.options.pushState;
781
+ this._hasPushState = !!(this.options.pushState && window.history && window.history.pushState);
782
+ var fragment = this.getFragment();
783
+ var docMode = document.documentMode;
784
+ var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7));
759
785
  if (oldIE) {
760
786
  this.iframe = $('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo('body')[0].contentWindow;
761
- this.saveLocation(hash);
787
+ this.navigate(fragment);
762
788
  }
763
- if ('onhashchange' in window && !oldIE) {
789
+
790
+ // Depending on whether we're using pushState or hashes, and whether
791
+ // 'onhashchange' is supported, determine how we check the URL state.
792
+ if (this._hasPushState) {
793
+ $(window).bind('popstate', this.checkUrl);
794
+ } else if ('onhashchange' in window && !oldIE) {
764
795
  $(window).bind('hashchange', this.checkUrl);
765
796
  } else {
766
797
  setInterval(this.checkUrl, this.interval);
767
798
  }
768
- this.hash = hash;
799
+
800
+ // Determine if we need to change the base url, for a pushState link
801
+ // opened by a non-pushState browser.
802
+ this.fragment = fragment;
769
803
  historyStarted = true;
770
- return this.loadUrl();
804
+ var started = this.loadUrl() || this.loadUrl(window.location.hash);
805
+ var atRoot = window.location.pathname == this.options.root;
806
+ if (this._wantsPushState && !this._hasPushState && !atRoot) {
807
+ this.fragment = this.getFragment(null, true);
808
+ window.location = this.options.root + '#' + this.fragment;
809
+ } else if (this._wantsPushState && this._hasPushState && atRoot && window.location.hash) {
810
+ this.navigate(window.location.hash);
811
+ } else {
812
+ return started;
813
+ }
771
814
  },
772
815
 
773
- // Add a route to be tested when the hash changes. Routes added later may
816
+ // Add a route to be tested when the fragment changes. Routes added later may
774
817
  // override previous routes.
775
818
  route : function(route, callback) {
776
819
  this.handlers.unshift({route : route, callback : callback});
@@ -778,23 +821,22 @@
778
821
 
779
822
  // Checks the current URL to see if it has changed, and if it has,
780
823
  // calls `loadUrl`, normalizing across the hidden iframe.
781
- checkUrl : function() {
782
- var hash = this.getHash();
783
- if (hash == this.hash && this.iframe) hash = this.getHash(this.iframe.location);
784
- if (hash == this.hash || hash == decodeURIComponent(this.hash)) return false;
785
- if (this.iframe) this.saveLocation(hash);
786
- this.hash = hash;
787
- this.loadUrl();
824
+ checkUrl : function(e) {
825
+ var current = this.getFragment();
826
+ if (current == this.fragment && this.iframe) current = this.getFragment(this.iframe.location.hash);
827
+ if (current == this.fragment || current == decodeURIComponent(this.fragment)) return false;
828
+ if (this.iframe) this.navigate(current);
829
+ this.loadUrl() || this.loadUrl(window.location.hash);
788
830
  },
789
831
 
790
832
  // Attempt to load the current URL fragment. If a route succeeds with a
791
833
  // match, returns `true`. If no defined routes matches the fragment,
792
834
  // returns `false`.
793
- loadUrl : function() {
794
- var hash = this.hash;
835
+ loadUrl : function(fragmentOverride) {
836
+ var fragment = this.fragment = this.getFragment(fragmentOverride);
795
837
  var matched = _.any(this.handlers, function(handler) {
796
- if (handler.route.test(hash)) {
797
- handler.callback(hash);
838
+ if (handler.route.test(fragment)) {
839
+ handler.callback(fragment);
798
840
  return true;
799
841
  }
800
842
  });
@@ -804,14 +846,22 @@
804
846
  // Save a fragment into the hash history. You are responsible for properly
805
847
  // URL-encoding the fragment in advance. This does not trigger
806
848
  // a `hashchange` event.
807
- saveLocation : function(hash) {
808
- hash = (hash || '').replace(hashStrip, '');
809
- if (this.hash == hash) return;
810
- window.location.hash = this.hash = hash;
811
- if (this.iframe && (hash != this.getHash(this.iframe.location))) {
812
- this.iframe.document.open().close();
813
- this.iframe.location.hash = hash;
849
+ navigate : function(fragment, triggerRoute) {
850
+ fragment = (fragment || '').replace(hashStrip, '');
851
+ if (this.fragment == fragment || this.fragment == decodeURIComponent(fragment)) return;
852
+ if (this._hasPushState) {
853
+ var loc = window.location;
854
+ if (fragment.indexOf(this.options.root) != 0) fragment = this.options.root + fragment;
855
+ this.fragment = fragment;
856
+ window.history.pushState({}, document.title, loc.protocol + '//' + loc.host + fragment);
857
+ } else {
858
+ window.location.hash = this.fragment = fragment;
859
+ if (this.iframe && (fragment != this.getFragment(this.iframe.location.hash))) {
860
+ this.iframe.document.open().close();
861
+ this.iframe.location.hash = fragment;
862
+ }
814
863
  }
864
+ if (triggerRoute) this.loadUrl(fragment);
815
865
  }
816
866
 
817
867
  });
@@ -826,7 +876,7 @@
826
876
  this._configure(options || {});
827
877
  this._ensureElement();
828
878
  this.delegateEvents();
829
- this.initialize(options);
879
+ this.initialize.apply(this, arguments);
830
880
  };
831
881
 
832
882
  // Element lookup, scoped to DOM elements within the current view.
@@ -899,10 +949,11 @@
899
949
  if (!(events || (events = this.events))) return;
900
950
  $(this.el).unbind('.delegateEvents' + this.cid);
901
951
  for (var key in events) {
902
- var methodName = events[key];
952
+ var method = this[events[key]];
953
+ if (!method) throw new Error('Event "' + events[key] + '" does not exist');
903
954
  var match = key.match(eventSplitter);
904
955
  var eventName = match[1], selector = match[2];
905
- var method = _.bind(this[methodName], this);
956
+ method = _.bind(method, this);
906
957
  eventName += '.delegateEvents' + this.cid;
907
958
  if (selector === '') {
908
959
  $(this.el).bind(eventName, method);
@@ -950,7 +1001,7 @@
950
1001
 
951
1002
  // Set up inheritance for the model, collection, and view.
952
1003
  Backbone.Model.extend = Backbone.Collection.extend =
953
- Backbone.Controller.extend = Backbone.View.extend = extend;
1004
+ Backbone.Router.extend = Backbone.View.extend = extend;
954
1005
 
955
1006
  // Map from CRUD to HTTP for our default `Backbone.sync` implementation.
956
1007
  var methodMap = {
@@ -1076,7 +1127,7 @@
1076
1127
 
1077
1128
  // Throw an error when a URL is needed, and none is supplied.
1078
1129
  var urlError = function() {
1079
- throw new Error("A 'url' property or function must be specified");
1130
+ throw new Error('A "url" property or function must be specified');
1080
1131
  };
1081
1132
 
1082
1133
  // Wrap an optional error callback with a fallback error event.
@@ -1092,7 +1143,7 @@
1092
1143
 
1093
1144
  // Helper function to escape a string for HTML rendering.
1094
1145
  var escapeHTML = function(string) {
1095
- return string.replace(/&(?!\w+;|#\d+;|#x[\da-f]+;)/gi, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
1146
+ return string.replace(/&(?!\w+;|#\d+;|#x[\da-f]+;)/gi, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#x27').replace(/\//g,'&#x2F;');
1096
1147
  };
1097
1148
 
1098
1149
  }).call(this);
@@ -22,7 +22,11 @@
22
22
  var params = _.extend({
23
23
  type: type,
24
24
  dataType: 'json',
25
- processData: false
25
+ processData: false,
26
+ beforeSend: function( xhr ) {
27
+ var token = $('meta[name="csrf-token"]').attr('content');
28
+ if (token) xhr.setRequestHeader('X-CSRF-Token', token);
29
+ }
26
30
  }, options);
27
31
 
28
32
  if (!params.url) {
@@ -55,8 +55,7 @@
55
55
  module.exports = _;
56
56
  _._ = _;
57
57
  } else {
58
- // Exported as a string, for Closure Compiler "advanced" mode.
59
- root['_'] = _;
58
+ root._ = _;
60
59
  }
61
60
 
62
61
  // Current version.
@@ -74,7 +73,7 @@
74
73
  obj.forEach(iterator, context);
75
74
  } else if (_.isNumber(obj.length)) {
76
75
  for (var i = 0, l = obj.length; i < l; i++) {
77
- if (i in obj && iterator.call(context, obj[i], i, obj) === breaker) return;
76
+ if (iterator.call(context, obj[i], i, obj) === breaker) return;
78
77
  }
79
78
  } else {
80
79
  for (var key in obj) {
@@ -252,16 +251,6 @@
252
251
  }), 'value');
253
252
  };
254
253
 
255
- // Groups the object's values by a criterion produced by an iterator
256
- _.groupBy = function(obj, iterator) {
257
- var result = {};
258
- each(obj, function(value, index) {
259
- var key = iterator(value, index);
260
- (result[key] || (result[key] = [])).push(value);
261
- });
262
- return result;
263
- };
264
-
265
254
  // Use a comparator function to figure out at what index an object should
266
255
  // be inserted so as to maintain order. Uses binary search.
267
256
  _.sortedIndex = function(array, obj, iterator) {
@@ -513,7 +502,7 @@
513
502
  var funcs = slice.call(arguments);
514
503
  return function() {
515
504
  var args = slice.call(arguments);
516
- for (var i = funcs.length - 1; i >= 0; i--) {
505
+ for (var i=funcs.length-1; i >= 0; i--) {
517
506
  args = [funcs[i].apply(this, args)];
518
507
  }
519
508
  return args[0];
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: rails-backbone
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 0.2.0
5
+ version: 0.5.0
6
6
  platform: ruby
7
7
  authors:
8
8
  - Ryan Fitzgerald
@@ -11,7 +11,7 @@ autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
13
 
14
- date: 2011-06-19 00:00:00 -04:00
14
+ date: 2011-07-04 00:00:00 -04:00
15
15
  default_executable:
16
16
  dependencies:
17
17
  - !ruby/object:Gem::Dependency
@@ -21,7 +21,7 @@ dependencies:
21
21
  requirements:
22
22
  - - ~>
23
23
  - !ruby/object:Gem::Version
24
- version: 3.1.0.beta1
24
+ version: "3.1"
25
25
  type: :runtime
26
26
  prerelease: false
27
27
  version_requirements: *id001
@@ -32,7 +32,7 @@ dependencies:
32
32
  requirements:
33
33
  - - ~>
34
34
  - !ruby/object:Gem::Version
35
- version: 2.2.0
35
+ version: "2.2"
36
36
  type: :runtime
37
37
  prerelease: false
38
38
  version_requirements: *id002
@@ -58,18 +58,18 @@ extra_rdoc_files: []
58
58
 
59
59
  files:
60
60
  - lib/backbone-rails.rb
61
- - lib/generators/backbone/controller/controller_generator.rb
62
- - lib/generators/backbone/controller/templates/controller.coffee
63
- - lib/generators/backbone/controller/templates/template.jst
64
- - lib/generators/backbone/controller/templates/view.coffee
65
61
  - lib/generators/backbone/install/install_generator.rb
66
62
  - lib/generators/backbone/install/templates/app.coffee
67
63
  - lib/generators/backbone/model/model_generator.rb
68
64
  - lib/generators/backbone/model/templates/model.coffee
69
65
  - lib/generators/backbone/resource_helpers.rb
66
+ - lib/generators/backbone/router/router_generator.rb
67
+ - lib/generators/backbone/router/templates/router.coffee
68
+ - lib/generators/backbone/router/templates/template.jst
69
+ - lib/generators/backbone/router/templates/view.coffee
70
70
  - lib/generators/backbone/scaffold/scaffold_generator.rb
71
- - lib/generators/backbone/scaffold/templates/controller.coffee
72
71
  - lib/generators/backbone/scaffold/templates/model.coffee
72
+ - lib/generators/backbone/scaffold/templates/router.coffee
73
73
  - lib/generators/backbone/scaffold/templates/templates/edit.jst
74
74
  - lib/generators/backbone/scaffold/templates/templates/index.jst
75
75
  - lib/generators/backbone/scaffold/templates/templates/model.jst
@@ -103,7 +103,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
103
103
  requirements:
104
104
  - - ">="
105
105
  - !ruby/object:Gem::Version
106
- hash: 1804076716045259291
106
+ hash: -730083928370135301
107
107
  segments:
108
108
  - 0
109
109
  version: "0"
@@ -112,7 +112,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
112
112
  requirements:
113
113
  - - ">="
114
114
  - !ruby/object:Gem::Version
115
- hash: 1804076716045259291
115
+ hash: -730083928370135301
116
116
  segments:
117
117
  - 0
118
118
  version: "0"