backbone-support 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
});
|