eyeballs 0.4.2 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +17 -0
- data/Gemfile +1 -0
- data/Gemfile.lock +14 -2
- data/README.md +17 -41
- data/Rakefile +1 -1
- data/TODO +2 -0
- data/app.rb +40 -2
- data/eyeballs.gemspec +3 -3
- data/lib/eyeballs/app_generator.rb +0 -1
- data/spec/app_generator_spec.rb +1 -1
- data/src/drivers/jquery/adapters/o_O.couchdb.js +2 -2
- data/src/drivers/jquery/adapters/o_O.rest.js +59 -14
- data/src/drivers/jquery/modules/o_O.controller.js +8 -27
- data/src/drivers/jquery/modules/o_O.routes.js +5 -2
- data/src/drivers/jquery/modules/o_O.support.js +20 -2
- data/src/modules/o_O.model.js +38 -11
- data/src/modules/o_O.validations.js +4 -4
- data/templates/app_root/config/initializer.js +2 -1
- data/test/unit/test_controller.html +22 -39
- data/test/unit/test_model.html +5 -3
- data/test/unit/test_model_with_callbacks.html +12 -0
- data/test/unit/test_rest.html +122 -6
- data/test/unit/test_routing.html +3 -3
- metadata +5 -5
- data/dist/jquery/jquery.livequery.js +0 -226
data/CHANGELOG
CHANGED
@@ -1,3 +1,20 @@
|
|
1
|
+
Sat Jul 17
|
2
|
+
- - - - - -
|
3
|
+
- Remove data-controller data-action stuff. "data-bind" is way better.
|
4
|
+
|
5
|
+
Wed Jul 14 2010
|
6
|
+
- - - - - - - -
|
7
|
+
- Add 'url' parameter for destroy
|
8
|
+
- Add errors.on for finding errors
|
9
|
+
|
10
|
+
Fri Jul 9
|
11
|
+
- - - - -
|
12
|
+
- Add 'create' method
|
13
|
+
- Add 'url' parameter for save
|
14
|
+
- Specify whether to use a json_root (for Rails etc.)
|
15
|
+
- Allow specifying the json_root name
|
16
|
+
- Allow specifying a CSRF token
|
17
|
+
|
1
18
|
Wed Jul 7 2010
|
2
19
|
- - - - - - - -
|
3
20
|
- Add namespace and root support in routing
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,5 +1,9 @@
|
|
1
1
|
---
|
2
2
|
dependencies:
|
3
|
+
ruby-debug:
|
4
|
+
group:
|
5
|
+
- :test
|
6
|
+
version: ">= 0"
|
3
7
|
thor:
|
4
8
|
group:
|
5
9
|
- :default
|
@@ -23,6 +27,8 @@ dependencies:
|
|
23
27
|
specs:
|
24
28
|
- activesupport:
|
25
29
|
version: 2.3.8
|
30
|
+
- columnize:
|
31
|
+
version: 0.3.1
|
26
32
|
- json_pure:
|
27
33
|
version: 1.4.3
|
28
34
|
- gemcutter:
|
@@ -33,15 +39,21 @@ specs:
|
|
33
39
|
version: 2.0.4
|
34
40
|
- jeweler:
|
35
41
|
version: 1.4.0
|
42
|
+
- linecache:
|
43
|
+
version: "0.43"
|
36
44
|
- rack:
|
37
45
|
version: 1.2.1
|
38
46
|
- rspec:
|
39
47
|
version: 1.3.0
|
48
|
+
- ruby-debug-base:
|
49
|
+
version: 0.10.3
|
50
|
+
- ruby-debug:
|
51
|
+
version: 0.10.3
|
40
52
|
- sinatra:
|
41
53
|
version: "1.0"
|
42
54
|
- thor:
|
43
|
-
version: 0.13.
|
44
|
-
hash:
|
55
|
+
version: 0.13.8
|
56
|
+
hash: 18ee2e0d3e9a46cd1d8e8c39951ba9414860f401
|
45
57
|
sources:
|
46
58
|
- Rubygems:
|
47
59
|
uri: http://gemcutter.org
|
data/README.md
CHANGED
@@ -118,7 +118,7 @@ Not very exciting.
|
|
118
118
|
However, if you're familiar with Rails, you'll be familiar with the wonderful syntax for adding validations to your models. eyeballs.js lets you add validations to your models as follows:
|
119
119
|
|
120
120
|
o_O('Post', function(post){
|
121
|
-
post.validates_presence_of('title)
|
121
|
+
post.validates_presence_of('title')
|
122
122
|
})
|
123
123
|
|
124
124
|
Now, when you initialize a new Post, you can validate it, nice and easy:
|
@@ -255,52 +255,30 @@ Tasty!
|
|
255
255
|
|
256
256
|
#### Binding actions to events ####
|
257
257
|
|
258
|
-
To bind events to these controller actions, use the data-
|
258
|
+
To bind events to these controller actions, use the data-bind attribute:
|
259
259
|
|
260
|
-
<a href="/posts/new" data-controller="posts
|
260
|
+
<a href="/posts/new" data-controller="posts#new">Click me!</a>
|
261
261
|
|
262
262
|
This binds all clicks on this element to the new action on the PostsController. By default, if you add these attributes to a form, the action is bound to the submit event; to all other elements it binds to a click.
|
263
263
|
|
264
|
-
It also returns false, canceling out the default behavior. If you want the default behavior,
|
264
|
+
It also returns false, canceling out the default behavior. If you want the default behavior, prefix with `+` to "add" the action to the propagation chain:
|
265
265
|
|
266
|
-
<a href="/posts/new" data-controller="posts
|
266
|
+
<a href="/posts/new" data-controller="+posts#new">Click me!</a>
|
267
267
|
|
268
|
-
You can also bind to custom events
|
268
|
+
You can also bind to custom events:
|
269
269
|
|
270
|
-
<a href="/posts/new" data-controller="posts
|
270
|
+
<a href="/posts/new" data-controller="+mouseover:posts#new">Hover over me!</a>
|
271
271
|
|
272
|
-
|
272
|
+
You can bind multiple events and actions to a single element:
|
273
273
|
|
274
|
-
|
275
|
-
|
276
|
-
<a href="/posts/new" data-bind="posts/new">Click me!</a>
|
277
|
-
|
278
|
-
Disable returning false to continue propagating new events:
|
279
|
-
|
280
|
-
<a href="/posts/new" data-bind="+posts/new">Click me!</a>
|
281
|
-
|
282
|
-
Custom events:
|
283
|
-
|
284
|
-
<a href="/posts/new" data-bind="mouseover:posts/new">Click me!</a>
|
285
|
-
|
286
|
-
Finally, in shorthand only, you can bind multiple events to a single element:
|
287
|
-
|
288
|
-
<a href="/posts/new" data-bind="mouseover:posts/preview; click: posts/new">Hover first, then Click me!</a>
|
274
|
+
<a href="/posts/new" data-bind="mouseover:posts#preview; click: posts/new">Hover first, then Click me!</a>
|
289
275
|
|
290
|
-
|
276
|
+
It's called "obtrusive UJS" ... explicit, yet everything has its own place.
|
291
277
|
|
292
278
|
Putting it all together
|
293
279
|
-----------------------
|
294
280
|
|
295
|
-
|
296
|
-
|
297
|
-
There's a small demo app included in this package, a simple app for adding personal reviews. It's a simple Sinatra app, so you can run it with:
|
298
|
-
|
299
|
-
ruby app.rb
|
300
|
-
|
301
|
-
And then visit it in a browser at `localhost:4567`
|
302
|
-
|
303
|
-
It should all "just work" ... in a browser that supports HTML5 local storage.
|
281
|
+
Imagine a simple app for posting reviews. It will comprise a "Review" model, "ReviewsController" and associated views.
|
304
282
|
|
305
283
|
`models/review.js` looks like this:
|
306
284
|
|
@@ -309,9 +287,9 @@ It should all "just work" ... in a browser that supports HTML5 local storage.
|
|
309
287
|
review.validates_presence_of('content');
|
310
288
|
});
|
311
289
|
|
312
|
-
This defines the Review model, allowing us to initialize and save Review objects.
|
290
|
+
This defines the Review model, allowing us to initialize and save Review objects, while ensuring `title` and `content` are included.
|
313
291
|
|
314
|
-
The `create` action in `controllers/reviews_controller.js` looks like this:
|
292
|
+
The `create` action in `controllers/reviews_controller.js` looks like this (using jQuery):
|
315
293
|
|
316
294
|
...
|
317
295
|
create: function(){
|
@@ -331,7 +309,7 @@ The `create` action in `controllers/reviews_controller.js` looks like this:
|
|
331
309
|
|
332
310
|
The form that hooks up to this action is like this:
|
333
311
|
|
334
|
-
<form data-
|
312
|
+
<form data-bind="reviews#create">
|
335
313
|
<label for="review-title">Title</label><br />
|
336
314
|
<input type="text" name="title" value="" data-attribute="title"><br />
|
337
315
|
<label for="review-content">Review</label><br />
|
@@ -339,15 +317,11 @@ The form that hooks up to this action is like this:
|
|
339
317
|
<input type="submit" name="commit" value="Save">
|
340
318
|
</form>
|
341
319
|
|
342
|
-
The main things to note here are the way that the form binds automatically to the create action (using jQuery). Also, field elements have the "data-attribute" attributes ... the o\_O.params() function reads from these, returning a JSON object that can be passed to Review.initialize(...).
|
320
|
+
The main things to note here are the way that the form binds automatically to the create action (using jQuery event delegation). Also, field elements have the "data-attribute" attributes ... the o\_O.params() function reads from these, returning a JSON object that can be passed to Review.initialize(...).
|
343
321
|
|
344
322
|
Notice also `o_O.alert_errors(...)` which displays an alert of all the errors on an invalid review.
|
345
323
|
|
346
324
|
Finally, the o\_O.render function takes a template, which is a Mustache.js template stored in `views/`, the review object and a set of options.
|
347
|
-
|
348
|
-
This is all very early stuff, but please feel free to play and feedback.
|
349
|
-
|
350
|
-
That's all for now!
|
351
325
|
|
352
326
|
|
353
327
|
Running the tests
|
@@ -355,6 +329,8 @@ Running the tests
|
|
355
329
|
|
356
330
|
eyeballs.js uses QUnit for in-browser testing. Just load the files in the test directory in any browser to run them.
|
357
331
|
|
332
|
+
For the REST tests, you can use the included Sinatra app, `app.rb`. The tests expect it to be in a local virtual domain `http://eyeballs`
|
333
|
+
|
358
334
|
About me
|
359
335
|
--------
|
360
336
|
|
data/Rakefile
CHANGED
data/TODO
ADDED
data/app.rb
CHANGED
@@ -11,6 +11,44 @@ post '/reviews' do
|
|
11
11
|
'{"id": "1"}'
|
12
12
|
end
|
13
13
|
|
14
|
+
post '/alternate_reviews' do
|
15
|
+
'{"id": "2"}'
|
16
|
+
end
|
17
|
+
|
18
|
+
post '/string_back' do
|
19
|
+
'some string'
|
20
|
+
end
|
21
|
+
|
22
|
+
post '/test_id' do
|
23
|
+
if params[:alternate_object]
|
24
|
+
if params[:id]
|
25
|
+
'fail'
|
26
|
+
end
|
27
|
+
else
|
28
|
+
'ok'
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
post '/rails_reviews' do
|
33
|
+
if params[:rails_review]
|
34
|
+
'ok'
|
35
|
+
else
|
36
|
+
'fail'
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
post '/alternate_rails_reviews' do
|
41
|
+
if params[:alternate_object]
|
42
|
+
'ok'
|
43
|
+
else
|
44
|
+
'fail'
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
post '/auth_token' do
|
49
|
+
params[:authenticity_token]
|
50
|
+
end
|
51
|
+
|
14
52
|
get '/reviews/:id' do
|
15
53
|
'{"id": "1", "title":"More Magic!"}'
|
16
54
|
end
|
@@ -20,9 +58,9 @@ get '/my/reviews/:id' do
|
|
20
58
|
end
|
21
59
|
|
22
60
|
put '/reviews/:id' do
|
23
|
-
'{"id": "1", "title":"
|
61
|
+
'{"id": "1", "title":"Updated"}'
|
24
62
|
end
|
25
63
|
|
26
64
|
delete '/reviews/:id' do
|
27
|
-
'
|
65
|
+
'Deleted OK'
|
28
66
|
end
|
data/eyeballs.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{eyeballs}
|
8
|
-
s.version = "0.
|
8
|
+
s.version = "0.5.0"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Paul Campbell"]
|
12
|
-
s.date = %q{2010-07-
|
12
|
+
s.date = %q{2010-07-17}
|
13
13
|
s.default_executable = %q{eyeballs}
|
14
14
|
s.email = %q{paul@rslw.com}
|
15
15
|
s.executables = ["eyeballs"]
|
@@ -22,12 +22,12 @@ Gem::Specification.new do |s|
|
|
22
22
|
"Gemfile.lock",
|
23
23
|
"README.md",
|
24
24
|
"Rakefile",
|
25
|
+
"TODO",
|
25
26
|
"app.rb",
|
26
27
|
"bin/eyeballs",
|
27
28
|
"config.ru",
|
28
29
|
"dist/jquery/jquery-1.4.2.min.js",
|
29
30
|
"dist/jquery/jquery.ba-hashchange.js",
|
30
|
-
"dist/jquery/jquery.livequery.js",
|
31
31
|
"dist/mustache/mustache.0.2.3.js",
|
32
32
|
"eyeballs.gemspec",
|
33
33
|
"lib/eyeballs.rb",
|
@@ -18,7 +18,6 @@ module Eyeballs
|
|
18
18
|
def build_the_app
|
19
19
|
directory "templates/app_root", new_app_path
|
20
20
|
copy_file 'dist/jquery/jquery-1.4.2.min.js', "#{new_app_path}/vendor/jquery/jquery-1.4.2.min.js"
|
21
|
-
copy_file 'dist/jquery/jquery.livequery.js', "#{new_app_path}/vendor/jquery/jquery.livequery.js"
|
22
21
|
copy_file 'dist/mustache/mustache.0.2.3.js', "#{new_app_path}/vendor/mustache/mustache.0.2.3.js"
|
23
22
|
directory "src", "#{new_app_path}/vendor/eyeballs/"
|
24
23
|
end
|
data/spec/app_generator_spec.rb
CHANGED
@@ -43,7 +43,7 @@ describe Eyeballs::AppGenerator do
|
|
43
43
|
|
44
44
|
it "should create the app files" do
|
45
45
|
jquery_file.should exist
|
46
|
-
livequery_file.
|
46
|
+
livequery_file.should_not exist
|
47
47
|
mustache_file.should exist
|
48
48
|
index_file.should exist
|
49
49
|
initializer_file.should exist
|
@@ -4,8 +4,8 @@ o_O.couchdb = {
|
|
4
4
|
|
5
5
|
var database = o_O.model.adapter.settings.database;
|
6
6
|
|
7
|
-
var ddoc_id = ['_design', model.table_name].join('/')
|
8
|
-
var view_name = model.table_name + '/all'
|
7
|
+
var ddoc_id = ['_design', model.table_name].join('/');
|
8
|
+
var view_name = model.table_name + '/all';
|
9
9
|
var get_all_documents = function(result){
|
10
10
|
var documents = result.rows;
|
11
11
|
var all_documents = [];
|
@@ -1,5 +1,19 @@
|
|
1
1
|
// REST & Rails, woop!
|
2
2
|
o_O.rest = {
|
3
|
+
figure_url: function(original_callback, object){
|
4
|
+
if(typeof original_callback.url === 'string')
|
5
|
+
{
|
6
|
+
return original_callback.url;
|
7
|
+
}
|
8
|
+
else if(typeof window[object.model_name]['url'] === 'string')
|
9
|
+
{
|
10
|
+
return window[object.model_name]['url'];
|
11
|
+
}
|
12
|
+
else
|
13
|
+
{
|
14
|
+
return '/' + object.table_name;
|
15
|
+
}
|
16
|
+
},
|
3
17
|
all: function(model, callback){
|
4
18
|
$.get('/' + model.table_name, function(response){
|
5
19
|
var documents = JSON.parse(response);
|
@@ -9,47 +23,76 @@ o_O.rest = {
|
|
9
23
|
}
|
10
24
|
})
|
11
25
|
},
|
12
|
-
destroy: function(object, callback){
|
26
|
+
destroy: function(object, callback, original_callback){
|
13
27
|
object.destroyed = true;
|
14
28
|
$.ajax({
|
15
29
|
type: 'DELETE',
|
16
|
-
url:
|
17
|
-
success: function(){
|
30
|
+
url: this.figure_url(original_callback, object) + '/' + object.id,
|
31
|
+
success: function(response){
|
18
32
|
if(typeof callback === 'function')
|
19
33
|
{
|
20
|
-
callback(object);
|
34
|
+
callback(object, response);
|
21
35
|
}
|
22
36
|
}
|
23
37
|
})
|
24
38
|
},
|
25
|
-
save: function(object, callback)
|
39
|
+
save: function(object, callback, original_callback)
|
26
40
|
{
|
27
|
-
var object_to_save = {}
|
41
|
+
var object_to_save = {};
|
42
|
+
var url;
|
28
43
|
for(var i = 0; i < object.attributes.length; i++)
|
29
44
|
{
|
30
45
|
object_to_save[object.attributes[i]] = object[object.attributes[i]];
|
31
46
|
}
|
32
47
|
var respond = function(response){
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
48
|
+
try{
|
49
|
+
var saved_object = JSON.parse(response);
|
50
|
+
for(var attribute in saved_object)
|
51
|
+
{
|
52
|
+
object_to_save[attribute] = saved_object[attribute];
|
53
|
+
}
|
54
|
+
object_to_save.new_record = false;
|
55
|
+
}
|
56
|
+
catch(e){
|
57
|
+
// keep the ID persistent
|
58
|
+
object_to_save.id = object.id;
|
37
59
|
}
|
38
|
-
object_to_save.new_record = false;
|
39
60
|
if(typeof callback === 'function')
|
40
61
|
{
|
41
|
-
callback(object_to_save);
|
62
|
+
callback(object_to_save, response);
|
63
|
+
}
|
64
|
+
}
|
65
|
+
url = this.figure_url(original_callback, object);
|
66
|
+
|
67
|
+
if(window[object.model_name]['include_json_root'] === true)
|
68
|
+
{
|
69
|
+
var object_name;
|
70
|
+
new_object_to_save = {};
|
71
|
+
if(typeof window[object.model_name]['json_root_name'] === 'string')
|
72
|
+
{
|
73
|
+
object_name = window[object.model_name]['json_root_name'];
|
74
|
+
}
|
75
|
+
else
|
76
|
+
{
|
77
|
+
object_name = object.model_name.underscore();
|
42
78
|
}
|
79
|
+
new_object_to_save[object_name] = object_to_save;
|
80
|
+
object_to_save = new_object_to_save;
|
81
|
+
}
|
82
|
+
if(typeof o_O.config.authenticity_token === 'string')
|
83
|
+
{
|
84
|
+
object_to_save['authenticity_token'] = o_O.config.authenticity_token;
|
43
85
|
}
|
44
86
|
if(object.new_record)
|
45
87
|
{
|
46
|
-
$.post(
|
88
|
+
$.post(url, object_to_save, respond);
|
47
89
|
}
|
48
90
|
else
|
49
91
|
{
|
50
92
|
$.ajax({
|
51
93
|
type: 'PUT',
|
52
|
-
|
94
|
+
data: object_to_save,
|
95
|
+
url: url + '/' + object.id,
|
53
96
|
success: respond
|
54
97
|
})
|
55
98
|
}
|
@@ -71,3 +114,5 @@ o_O.rest = {
|
|
71
114
|
})
|
72
115
|
}
|
73
116
|
}
|
117
|
+
|
118
|
+
o_O.config.rest = {}
|
@@ -1,38 +1,19 @@
|
|
1
1
|
o_O.controller = {
|
2
2
|
initialize: function(controller_name, controller){
|
3
|
-
var action_event = function(object){
|
4
|
-
if(object.attr('data-event'))
|
5
|
-
{
|
6
|
-
return object.attr('data-event');
|
7
|
-
}
|
8
|
-
return (object.is('form')) ? 'submit' : 'click';
|
9
|
-
}
|
10
3
|
|
11
4
|
var controller_name = controller_name.replace('Controller', '').toLowerCase();
|
12
5
|
var controller = controller;
|
13
6
|
|
14
7
|
$(function(){
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
var selector = '[data-controller=' + controller_name + '][data-action=' + action + ']';
|
19
|
-
$(selector).livequery(function(){
|
20
|
-
var element = $(this);
|
21
|
-
$(this).bind(action_event(element), controller[element.attr('data-action')]);
|
22
|
-
if(!($(this).attr('data-default')))
|
23
|
-
{
|
24
|
-
$(this).bind(action_event(element), function(){ return false; });
|
25
|
-
}
|
26
|
-
})
|
27
|
-
}
|
28
|
-
$('[data-bind]').livequery(function(){
|
29
|
-
var binders = $(this).attr('data-bind').match(/[\+]?(\s+)?[^ :]?[: ]?[^ #]+[ #]+[^ ;]+[ ;]?/g)
|
8
|
+
var events = 'click submit hover mouseover blur focus change dblclick keydown keypress keyup scroll'
|
9
|
+
$('body').delegate('[data-bind]', events, function(event){
|
10
|
+
var binders = $(this).attr('data-bind').match(/[\+]?(\s+)?[^ :]?[: ]?[^ #]+[ #]+[^ ;]+[ ;]?/g);
|
30
11
|
if(binders != null && binders.length > 0)
|
31
12
|
{
|
32
13
|
for(i = 0; i < binders.length; i++)
|
33
14
|
{
|
34
15
|
var rule = binders[i];
|
35
|
-
var parts = rule.match(/([\+])?(\s+)?(([^ :]+)([: ]+))?([^ #]+)[ #]+([^ ;]+)[ ;]?/)
|
16
|
+
var parts = rule.match(/([\+])?(\s+)?(([^ :]+)([: ]+))?([^ #]+)[ #]+([^ ;]+)[ ;]?/);
|
36
17
|
var default_bit = parts[1];
|
37
18
|
var this_action_event = parts[4];
|
38
19
|
if(this_action_event === undefined)
|
@@ -41,17 +22,17 @@ o_O.controller = {
|
|
41
22
|
}
|
42
23
|
var this_controller_name = parts[6];
|
43
24
|
var this_action = parts[7];
|
44
|
-
if(this_controller_name == controller_name)
|
25
|
+
if(this_controller_name == controller_name && this_action_event === event.type)
|
45
26
|
{
|
46
|
-
|
27
|
+
controller[this_action].apply(this);
|
47
28
|
if(default_bit != '+')
|
48
29
|
{
|
49
|
-
|
30
|
+
return false;
|
50
31
|
}
|
51
32
|
}
|
52
33
|
}
|
53
34
|
}
|
54
|
-
})
|
35
|
+
})
|
55
36
|
});
|
56
37
|
return controller;
|
57
38
|
}
|