backbone-support 0.0.1

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/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source :rubygems
2
+
3
+ gem 'jasmine'
4
+ gem 'headless'
5
+ gem 'rake'
data/Gemfile.lock ADDED
@@ -0,0 +1,38 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ childprocess (0.2.0)
5
+ ffi (~> 1.0.6)
6
+ diff-lcs (1.1.2)
7
+ ffi (1.0.9)
8
+ headless (0.1.0)
9
+ jasmine (1.0.2.1)
10
+ json_pure (>= 1.4.3)
11
+ rack (>= 1.1)
12
+ rspec (>= 1.3.1)
13
+ selenium-webdriver (>= 0.1.3)
14
+ json_pure (1.5.3)
15
+ rack (1.3.2)
16
+ rake (0.9.2)
17
+ rspec (2.6.0)
18
+ rspec-core (~> 2.6.0)
19
+ rspec-expectations (~> 2.6.0)
20
+ rspec-mocks (~> 2.6.0)
21
+ rspec-core (2.6.4)
22
+ rspec-expectations (2.6.0)
23
+ diff-lcs (~> 1.1.2)
24
+ rspec-mocks (2.6.0)
25
+ rubyzip (0.9.4)
26
+ selenium-webdriver (2.3.2)
27
+ childprocess (>= 0.1.9)
28
+ ffi (>= 1.0.7)
29
+ json_pure
30
+ rubyzip
31
+
32
+ PLATFORMS
33
+ ruby
34
+
35
+ DEPENDENCIES
36
+ headless
37
+ jasmine
38
+ rake
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2011 thoughtbot
2
+
3
+ Permission is hereby granted, free of charge, to any person
4
+ obtaining a copy of this software and associated documentation
5
+ files (the "Software"), to deal in the Software without
6
+ restriction, including without limitation the rights to use,
7
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the
9
+ Software is furnished to do so, subject to the following
10
+ conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,176 @@
1
+ Helper and utility classes that fill out Backbone for serious development.
2
+
3
+ Inspired by our projects and the Backbone.js on Rails book:
4
+ http://workshops.thoughtbot.com/backbone-js-on-rails
5
+
6
+ The book contains complete instructions and in-depth coverage of the internals
7
+ of CompositeView and Swappingrouter, and an example application that shows
8
+ their usage
9
+
10
+ ### SwappingRouter
11
+
12
+ A Router subclass the provides a standard way to swap one view for another.
13
+
14
+ This introduces a convention that all views have a `leave()` function,
15
+ responsible for unbinding and cleaning up the view. And the convention that
16
+ all actions underneath the same `Router` share the same root element, and
17
+ define it as `el` on the router.
18
+
19
+ Now, a `SwappingRouter` can take advantage of the `leave()` function, and
20
+ clean up any existing views before swapping to a new one. It swaps into a new
21
+ view by rendering that view into its own `el`:
22
+
23
+ swap: function(newView) {
24
+ if (this.currentView && this.currentView.leave) {
25
+ this.currentView.leave();
26
+ }
27
+
28
+ this.currentView = newView;
29
+ this.currentView.render();
30
+ $(this.el).empty().append(this.currentView.el);
31
+ }
32
+
33
+ An example SwappingRouter would look as follows:
34
+
35
+ Trajectory.Routers.Stories = Support.SwappingRouter.extend({
36
+ initialize: function(options) {
37
+ this.el = $("div.primary_content");
38
+ },
39
+ routes: {
40
+ "stories": "index",
41
+ "stories/new": "newStory"
42
+ }
43
+ index: function(story_id) {
44
+ var view = new Trajectory.Views.StoriesIndex();
45
+ this.swap(view);
46
+ },
47
+ newStory: function() {
48
+ var view = new Trajectory.Views.StoryNew({ model: new Story() });
49
+ this.swap(view);
50
+ }
51
+ }
52
+
53
+ ### CompositeView
54
+
55
+ CompositeView provides a convention for a parent view that has one or more
56
+ child views.
57
+
58
+ This introduces a convention that all views have a `leave()` function,
59
+ responsible for unbinding and cleaning up the view.
60
+
61
+ `CompositeView` provides methods for adding and removing children from the
62
+ parent view.
63
+
64
+ `CompositeView` maintains an array of its immediate children as
65
+ `this.children`. With this reference in place, a parent view's `leave()`
66
+ method can invoke `leave()` on its children, ensuring that an entire tree of
67
+ composed views is cleaned up properly.
68
+
69
+ For child views that can dismiss themselves, such as dialog boxes, children
70
+ maintain a back-reference at `this.parent`. This is used to reach up and call
71
+ `this.parent.removeChild(this)` for these self-dismissing views.
72
+
73
+ ## Dependencies
74
+
75
+ You'll need these, but chances are you already have them in your app:
76
+
77
+ * jQuery
78
+ * Underscore
79
+ * Backbone
80
+
81
+ ## Development
82
+
83
+ First:
84
+
85
+ bundle
86
+
87
+ While TDD'ing:
88
+
89
+ bundle exec rake jasmine
90
+
91
+ To not open tests a browser window:
92
+
93
+ bundle exec rake jasmine:ci
94
+
95
+ ## Installation
96
+
97
+ The recommended usage is with Rails 3.1.
98
+
99
+ ### With Rails 3.1
100
+
101
+ Add the gem to your Gemfile
102
+
103
+ gem "backbone-support"
104
+
105
+ And then `bundle install`
106
+
107
+ In your application.js, or in whatever file your backbone.js assets are
108
+ required in, add the following:
109
+
110
+ //= require backbone-support
111
+
112
+ This should be _above_ any usage of SwappingController or SwappingRouter, but
113
+ below the inclusion of Backbone.js, Underscore, and jQuery.
114
+
115
+ If you do not wish to include all of backbone-support, you can include
116
+ individual pieces. First, require the main support file:
117
+
118
+ //= require backbone-support/support
119
+
120
+ Then require the individual assets you wish to use:
121
+
122
+ //= require backbone-support/swapping_router
123
+ //= require backbone-support/composite_view
124
+
125
+ ### With Jammit
126
+
127
+ First off:
128
+
129
+ rails plugin install git@github.com:thoughtbot/backbone-support.git
130
+
131
+ In your `config/application.rb`:
132
+
133
+ ``` ruby
134
+ config.middleware.use Rack::Static,
135
+ :urls => ['/vendor/plugins/backbone-support/lib/assets/javascripts']
136
+ ```
137
+
138
+ And in your `config/assets.yml`:
139
+
140
+ ``` yaml
141
+ javascripts:
142
+ common:
143
+ - public/javascripts/vendor/underscore.js
144
+ - public/javascripts/vendor/backbone.js
145
+ - vendor/plugins/backbone-support/lib/assets/**/*.js
146
+ ```
147
+
148
+ If you have evergreen tests, you will need to change your `require` statements
149
+ since these files live in `vendor/`. Change `config/evergreen.rb` to be this:
150
+
151
+ ``` ruby
152
+ Evergreen.configure do |config|
153
+ config.public_dir = '.'
154
+ end
155
+ ```
156
+
157
+ Your individual specs will then need the full root path in `require`. For
158
+ example:
159
+
160
+
161
+ ``` js
162
+ requirePublic = function(path) {
163
+ require("/public/javascripts/" + path);
164
+ };
165
+
166
+ requirePublic("vendor/underscore.js");
167
+ requirePublic("vendor/backbone.js");
168
+
169
+ require("/vendor/plugins/backbone-support/lib/assets/javascripts/backbone-support.js");
170
+ require("/vendor/plugins/backbone-support/lib/assets/javascripts/backbone-support/composite_view.js");
171
+ require("/vendor/plugins/backbone-support/lib/assets/javascripts/backbone-support/swapping_router.js");
172
+ ```
173
+
174
+ ## License
175
+
176
+ Copyright 2011 thoughtbot. Please check LICENSE for more details.
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ require 'jasmine'
2
+ load 'jasmine/tasks/jasmine.rake'
3
+
4
+ task :default => "jasmine:ci"
@@ -0,0 +1,20 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "backbone-support/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'backbone-support'
7
+ s.version = BackboneSupport::VERSION.dup
8
+ s.authors = ['Chad Pytel', 'Joe Ferris', 'Jason Morrison', 'Nick Quaranto']
9
+ s.email = ['support@thoughtbot.com']
10
+ s.homepage = 'http://github.com/thoughtbot/backbone-support'
11
+ s.summary = 'SwappingController and CompositeView for Backbone.js'
12
+ s.description = 'SwappingController and CompositeView for Backbone.js'
13
+
14
+ s.files = `git ls-files`.split("\n")
15
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
16
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
17
+ s.require_paths = ["lib"]
18
+
19
+ s.add_development_dependency('jasmine')
20
+ end
@@ -0,0 +1,2 @@
1
+ //= require backbone-support/support
2
+ //= require_tree ./backbone-support
@@ -0,0 +1,48 @@
1
+ Support.CompositeView = function(options) {
2
+ this.children = _([]);
3
+ Backbone.View.apply(this, [options]);
4
+ };
5
+
6
+ _.extend(Support.CompositeView.prototype, Backbone.View.prototype, {
7
+ leave: function() {
8
+ this.unbind();
9
+ this.remove();
10
+ this._leaveChildren();
11
+ this._removeFromParent();
12
+ },
13
+
14
+ renderChild: function(view) {
15
+ view.render();
16
+ this.children.push(view);
17
+ view.parent = this;
18
+ },
19
+
20
+ appendChild: function(view) {
21
+ this.renderChild(view);
22
+ $(this.el).append(view.el);
23
+ },
24
+
25
+ renderChildInto: function(view, container) {
26
+ this.renderChild(view);
27
+ $(container).empty().append(view.el);
28
+ },
29
+
30
+ _leaveChildren: function() {
31
+ this.children.chain().clone().each(function(view) {
32
+ if (view.leave)
33
+ view.leave();
34
+ });
35
+ },
36
+
37
+ _removeFromParent: function() {
38
+ if (this.parent)
39
+ this.parent._removeChild(this);
40
+ },
41
+
42
+ _removeChild: function(view) {
43
+ var index = this.children.indexOf(view);
44
+ this.children.splice(index, 1);
45
+ }
46
+ });
47
+
48
+ Support.CompositeView.extend = Backbone.View.extend;
@@ -0,0 +1,2 @@
1
+ Support = {};
2
+ Support.VERSION = "0.0.1";
@@ -0,0 +1,17 @@
1
+ Support.SwappingRouter = function(options) {
2
+ Backbone.Router.apply(this, [options]);
3
+ };
4
+
5
+ _.extend(Support.SwappingRouter.prototype, Backbone.Router.prototype, {
6
+ swap: function(newView) {
7
+ if (this.currentView && this.currentView.leave) {
8
+ this.currentView.leave();
9
+ }
10
+
11
+ this.currentView = newView;
12
+ this.currentView.render();
13
+ $(this.el).empty().append(this.currentView.el);
14
+ }
15
+ });
16
+
17
+ Support.SwappingRouter.extend = Backbone.Router.extend;
@@ -0,0 +1,7 @@
1
+ module BackboneSupport
2
+ if defined?(Rails)
3
+ class Engine < ::Rails::Engine
4
+ require 'backbone-support/engine'
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,5 @@
1
+ module BackboneSupport
2
+ class Engine < Rails::Engine
3
+ # auto wire
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ module BackboneSupport
2
+ VERSION = '0.0.1'.freeze
3
+ end
@@ -0,0 +1,133 @@
1
+ describe("Support.CompositeView", function() {
2
+ var orangeView = Support.CompositeView.extend({
3
+ render: function() {
4
+ var text = this.make("span", {}, "Orange!");
5
+ $(this.el).append(text);
6
+ }
7
+ });
8
+
9
+ var blankView = Support.CompositeView.extend({
10
+ render: function() {
11
+ }
12
+ });
13
+
14
+ var normalView = Backbone.View.extend({
15
+ render: function() {
16
+ var text = this.make("span", {}, "Normal!");
17
+ $(this.el).append(text);
18
+ }
19
+ });
20
+
21
+ beforeEach(function() {
22
+ Helpers.setup();
23
+ Helpers.append("test1");
24
+ Helpers.append("test2");
25
+ });
26
+
27
+ afterEach(function() {
28
+ Helpers.teardown();
29
+ });
30
+
31
+ describe("#renderChild", function() {
32
+ it("renders children views", function() {
33
+ var view = new blankView();
34
+ view.renderChild(new orangeView({el: "#test1"}));
35
+ view.renderChild(new orangeView({el: "#test2"}));
36
+
37
+ expect($("#test1").text()).toEqual("Orange!");
38
+ expect($("#test2").text()).toEqual("Orange!");
39
+ });
40
+ });
41
+
42
+ describe("#appendChild", function() {
43
+ it("renders and appends children views", function() {
44
+ var view = new blankView({el: "#test"});
45
+ view.appendChild(new orangeView());
46
+ view.appendChild(new orangeView());
47
+
48
+ expect($("#test").text()).toEqual("Orange!Orange!");
49
+ });
50
+ });
51
+
52
+ describe("#renderChildInto", function() {
53
+ it("renders child into the given element and replaces content there", function() {
54
+ $("#test1").text("Replace this!");
55
+
56
+ var view = new blankView({el: "#test"});
57
+ view.renderChildInto(new orangeView(), "#test1");
58
+
59
+ expect($("#test").text()).toEqual("");
60
+ expect($("#test1").text()).toEqual("Orange!");
61
+ });
62
+ });
63
+
64
+ describe("#leave", function() {
65
+ it("removes elements and events when leave() is called", function() {
66
+ var view = new orangeView();
67
+ var spy = sinon.spy(view, "unbind");
68
+
69
+ runs(function() {
70
+ view.render();
71
+ $("#test").append(view.el);
72
+ });
73
+
74
+ Helpers.sleep();
75
+
76
+ runs(function() {
77
+ expect($("#test").text()).toEqual("Orange!");
78
+ });
79
+
80
+ Helpers.sleep();
81
+
82
+ runs(function() {
83
+ view.leave();
84
+ });
85
+
86
+ Helpers.sleep();
87
+
88
+ runs(function() {
89
+ expect($("#test").text()).toEqual("");
90
+ expect(spy.called).toBeTruthy();
91
+ });
92
+ });
93
+
94
+ it("removes children views on leave", function() {
95
+ var view = new blankView();
96
+ view.renderChild(new orangeView({el: "#test1"}));
97
+ view.renderChild(new orangeView({el: "#test2"}));
98
+
99
+ view.leave();
100
+
101
+ expect($("#test1").size()).toEqual(0);
102
+ expect($("#test2").size()).toEqual(0);
103
+ });
104
+
105
+ it("doesn't fail on normal backbone views that may be children", function() {
106
+ var view = new blankView();
107
+ view.renderChild(new orangeView({el: "#test1"}));
108
+ view.renderChild(new normalView({el: "#test2"}));
109
+
110
+ view.leave();
111
+
112
+ expect($("#test1").size()).toEqual(0);
113
+ expect($("#test2").size()).toEqual(1);
114
+ });
115
+
116
+ it("removes self from parent if invoked on a child view", function() {
117
+ var view = new blankView();
118
+ var childView = new orangeView({el: "#test1"});
119
+ view.renderChild(childView)
120
+ view.renderChild(new orangeView({el: "#test2"}));
121
+
122
+ expect($("#test1").size()).toEqual(1);
123
+ expect($("#test2").size()).toEqual(1);
124
+ expect(view.children.size()).toEqual(2);
125
+
126
+ childView.leave();
127
+
128
+ expect($("#test1").size()).toEqual(0);
129
+ expect($("#test2").size()).toEqual(1);
130
+ expect(view.children.size()).toEqual(1);
131
+ });
132
+ });
133
+ });