rails-backbone 0.2.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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"