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 +5 -0
- data/Gemfile.lock +38 -0
- data/LICENSE +22 -0
- data/README.md +176 -0
- data/Rakefile +4 -0
- data/backbone-support.gemspec +20 -0
- data/lib/assets/javascripts/backbone-support.js +2 -0
- data/lib/assets/javascripts/backbone-support/composite_view.js +48 -0
- data/lib/assets/javascripts/backbone-support/support.js +2 -0
- data/lib/assets/javascripts/backbone-support/swapping_router.js +17 -0
- data/lib/backbone-support.rb +7 -0
- data/lib/backbone-support/engine.rb +5 -0
- data/lib/backbone-support/version.rb +3 -0
- data/spec/javascripts/composite_view_spec.js +133 -0
- data/spec/javascripts/helpers/helpers.js +19 -0
- data/spec/javascripts/helpers/sinon-1.1.1.js +2821 -0
- data/spec/javascripts/support/jasmine.yml +51 -0
- data/spec/javascripts/support/jasmine_config.rb +23 -0
- data/spec/javascripts/support/jasmine_runner.rb +32 -0
- data/spec/javascripts/swapping_router_spec.js +120 -0
- data/vendor/backbone.js +1158 -0
- data/vendor/jquery.js +18 -0
- data/vendor/underscore.js +839 -0
- metadata +110 -0
data/Gemfile
ADDED
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,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,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,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,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
|
+
});
|