backbone_mvc 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/lib/backbone_mvc/engine.rb +4 -0
  3. data/lib/backbone_mvc/version.rb +3 -0
  4. data/lib/backbone_mvc.rb +4 -0
  5. data/lib/tasks/backbone_mvc_tasks.rake +4 -0
  6. data/test/backbone_mvc_test.rb +7 -0
  7. data/test/dummy/README.rdoc +28 -0
  8. data/test/dummy/Rakefile +6 -0
  9. data/test/dummy/app/assets/javascripts/application.js +13 -0
  10. data/test/dummy/app/assets/stylesheets/application.css +13 -0
  11. data/test/dummy/app/controllers/application_controller.rb +5 -0
  12. data/test/dummy/app/helpers/application_helper.rb +2 -0
  13. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  14. data/test/dummy/bin/bundle +3 -0
  15. data/test/dummy/bin/rails +4 -0
  16. data/test/dummy/bin/rake +4 -0
  17. data/test/dummy/config/application.rb +23 -0
  18. data/test/dummy/config/boot.rb +5 -0
  19. data/test/dummy/config/database.yml +25 -0
  20. data/test/dummy/config/environment.rb +5 -0
  21. data/test/dummy/config/environments/development.rb +29 -0
  22. data/test/dummy/config/environments/production.rb +80 -0
  23. data/test/dummy/config/environments/test.rb +36 -0
  24. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  25. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  26. data/test/dummy/config/initializers/inflections.rb +16 -0
  27. data/test/dummy/config/initializers/mime_types.rb +5 -0
  28. data/test/dummy/config/initializers/secret_token.rb +12 -0
  29. data/test/dummy/config/initializers/session_store.rb +3 -0
  30. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  31. data/test/dummy/config/locales/en.yml +23 -0
  32. data/test/dummy/config/routes.rb +56 -0
  33. data/test/dummy/config.ru +4 -0
  34. data/test/dummy/public/404.html +58 -0
  35. data/test/dummy/public/422.html +58 -0
  36. data/test/dummy/public/500.html +57 -0
  37. data/test/dummy/public/favicon.ico +0 -0
  38. data/test/test_helper.rb +15 -0
  39. data/vendor/assets/javascripts/backbonemvc.js +542 -0
  40. metadata +142 -0
@@ -0,0 +1,57 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>We're sorry, but something went wrong (500)</title>
5
+ <style>
6
+ body {
7
+ background-color: #EFEFEF;
8
+ color: #2E2F30;
9
+ text-align: center;
10
+ font-family: arial, sans-serif;
11
+ }
12
+
13
+ div.dialog {
14
+ width: 25em;
15
+ margin: 4em auto 0 auto;
16
+ border: 1px solid #CCC;
17
+ border-right-color: #999;
18
+ border-left-color: #999;
19
+ border-bottom-color: #BBB;
20
+ border-top: #B00100 solid 4px;
21
+ border-top-left-radius: 9px;
22
+ border-top-right-radius: 9px;
23
+ background-color: white;
24
+ padding: 7px 4em 0 4em;
25
+ }
26
+
27
+ h1 {
28
+ font-size: 100%;
29
+ color: #730E15;
30
+ line-height: 1.5em;
31
+ }
32
+
33
+ body > p {
34
+ width: 33em;
35
+ margin: 0 auto 1em;
36
+ padding: 1em 0;
37
+ background-color: #F7F7F7;
38
+ border: 1px solid #CCC;
39
+ border-right-color: #999;
40
+ border-bottom-color: #999;
41
+ border-bottom-left-radius: 4px;
42
+ border-bottom-right-radius: 4px;
43
+ border-top-color: #DADADA;
44
+ color: #666;
45
+ box-shadow:0 3px 8px rgba(50, 50, 50, 0.17);
46
+ }
47
+ </style>
48
+ </head>
49
+
50
+ <body>
51
+ <!-- This file lives in public/500.html -->
52
+ <div class="dialog">
53
+ <h1>We're sorry, but something went wrong.</h1>
54
+ </div>
55
+ <p>If you are the application owner check the logs for more information.</p>
56
+ </body>
57
+ </html>
File without changes
@@ -0,0 +1,15 @@
1
+ # Configure Rails Environment
2
+ ENV["RAILS_ENV"] = "test"
3
+
4
+ require File.expand_path("../dummy/config/environment.rb", __FILE__)
5
+ require "rails/test_help"
6
+
7
+ Rails.backtrace_cleaner.remove_silencers!
8
+
9
+ # Load support files
10
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
11
+
12
+ # Load fixtures from the engine
13
+ if ActiveSupport::TestCase.method_defined?(:fixture_path=)
14
+ ActiveSupport::TestCase.fixture_path = File.expand_path("../fixtures", __FILE__)
15
+ end
@@ -0,0 +1,542 @@
1
+ //BackboneMVC 1.0
2
+
3
+ //Copyright 2012 Changsi An
4
+
5
+ //This file is part of Backbone-MVC.
6
+ //
7
+ //Backbone-MVC is free software: you can redistribute it and/or modify
8
+ //it under the terms of the GNU Lesser General Public License as published by
9
+ //the Free Software Foundation, either version 3 of the License, or
10
+ //(at your option) any later version.
11
+ //
12
+ //Backbone-MVC is distributed in the hope that it will be useful,
13
+ //but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ //MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ //GNU Lesser General Public License for more details.
16
+ //
17
+ //You should have received a copy of the GNU Lesser General Public License
18
+ //along with Backbone-MVC. If not, see <http://www.gnu.org/licenses/>.
19
+ //------------------------------------------------------------------------------
20
+ //Quick Start
21
+ //
22
+ //This software requires Backbone.js and Underscore.js to work correctly.
23
+ //
24
+ //Usage:
25
+ //To create a controller, use :
26
+
27
+ //BackboneMVC.Controller.extend({
28
+ // name:'controller1', //mandatory field
29
+ //
30
+ // //defined once, will be invoked before each action method
31
+ // beforeFilter:function () {
32
+ // },
33
+ //
34
+ // //defined once, will be invoked after each action method
35
+ // afterRender:function () {
36
+ // },
37
+ //
38
+ // //used with secure methods, expect true/false or Deferred Object.
39
+ // checkSession:function () {
40
+ // },
41
+ //
42
+ // //action method
43
+ // action1:function () {
44
+ // this._privateMethod("Hello");
45
+ // },
46
+ //
47
+ // //secure method, checkSession method will be invoked first
48
+ // user_action2:function () {
49
+ // },
50
+ //
51
+ // //a private method starts with _
52
+ // _privateMethod:function (message) {
53
+ // alert(message);
54
+ // }
55
+ //})
56
+
57
+
58
+ //------------------------------------------------------------------------------
59
+ (function () {
60
+ 'use strict';
61
+ var PRODUCT_NAME = 'BackboneMVC';
62
+
63
+ //check prerequisites
64
+ if (typeof Backbone === 'undefined' || typeof _ === 'undefined') {
65
+ return;
66
+ }
67
+
68
+ /**
69
+ * @namesapce BackboneMVC
70
+ */
71
+ var BackboneMVC = window[PRODUCT_NAME] = {};
72
+
73
+ /**
74
+ * This is the base prototype of the Controller classes.
75
+ * The inheriting classes only expand the prototype so the trouble of handling
76
+ * private constructor is saved.
77
+ * It makes sense that each controller is a singleton. The cases that a
78
+ * controller's state need to be shared across the application are more than the
79
+ * cases that the states need to be kept independently. It also helps the user
80
+ * logic shares the same controller state as the one the Router uses.
81
+ * However, if independent states are vital, one can extend a controller with
82
+ * empty members or define their method statelessly.
83
+ * @type {Class}
84
+ */
85
+ var ControllerSingleton = (function () {
86
+ function BaseController() {
87
+ this._created = (new Date()).getTime(); //this can be useful for development
88
+ }
89
+
90
+ _.extend(BaseController.prototype, {
91
+ _created:null
92
+ });
93
+
94
+ BaseController.extend = function (properties) {
95
+ var instance;
96
+ var klass = function Controller() {
97
+ if (instance !== undefined) { //try to simulate Singleton
98
+ return instance;
99
+ }
100
+ BaseController.apply(this, arguments);
101
+
102
+ //'initialize()' method works as explicit constructor, if it is defined,
103
+ // then run it
104
+ if (this.initialize !== undefined) {
105
+ this.initialize.apply(this, arguments);
106
+ }
107
+
108
+ instance = this;
109
+ return instance;
110
+ };
111
+
112
+ klass.prototype = new BaseController();
113
+ _.extend(klass.prototype, properties);
114
+
115
+ klass.prototype.constructor = klass;
116
+ klass.prototype.classId = _.uniqueId('controller_');
117
+
118
+ return klass;
119
+ };
120
+ return BaseController;
121
+ }());
122
+
123
+ _.extend(BackboneMVC, {
124
+ /**
125
+ * A utility method used to create namespace object levels
126
+ * @param {String} namespaceString levels in namespaces
127
+ * @example "Mammalia.Cetacea.Delphinidae.Dolphin"
128
+ */
129
+ namespace:function (namespaceString) {
130
+ var components = namespaceString.split('.');
131
+ var node = window;
132
+ for (var i = 0, l = components.length; i < l; i++) {
133
+ if (node[components[i]] === undefined) {
134
+ node[components[i]] = {};
135
+ }
136
+ node = node[components[i]];
137
+ }
138
+ },
139
+
140
+ /**
141
+ * Backbone MVC Controller class
142
+ * @type {Class} BackboneMVC.Controller
143
+ */
144
+ //some default implementations for the methods are listed here:
145
+ Controller:{
146
+ beforeFilter:function () {
147
+ return (new $.Deferred()).resolve();
148
+ },
149
+
150
+ afterRender:function () {
151
+ return (new $.Deferred()).resolve();
152
+ },
153
+
154
+ checkSession:function () {
155
+ //if not defined, then always succeed
156
+ return (new $.Deferred()).resolve(true);
157
+ },
158
+
159
+ 'default':function () {
160
+ //TODO: this function will list all the actions of the controller
161
+ //intend to be overridden in most of the cases
162
+ return true;
163
+ }
164
+ },
165
+
166
+ /**
167
+ * This is the automatic Router class, it is an implementation of Backbone.Router.
168
+ * It must not be further customized, or the automatic routing feature cannot function.
169
+ * @class BackboneMVC.Router
170
+ */
171
+ Router:(function(){
172
+ var _inherentRouterProperties = {
173
+ _history:[],
174
+
175
+ routes:{
176
+ "*components":'dispatch' // route everything to 'dispatch' method
177
+ },
178
+
179
+ dispatch:function (actionPath) {
180
+ var components = (actionPath || '').replace(/\/+$/g, '').split('/');
181
+ var controllerName;
182
+
183
+ //look for controllers
184
+ if (ControllersPool[components[0]]) {
185
+ controllerName = components[0];
186
+ } else if (typeof ControllersPool[camelCased(components[0])] !== 'undefined') {
187
+ controllerName = camelCased(components[0]);
188
+ } else if (typeof ControllersPool[underscored(components[0])] !== 'undefined') {
189
+ controllerName = underscored(components[0]);
190
+ }
191
+
192
+ //test if the controller exists, if not, return a deferred object and reject it.
193
+ if (typeof controllerName === 'undefined') {
194
+ return this['404'](); //no such controller, reject
195
+ }
196
+
197
+ var controller = new ControllersPool[controllerName]();
198
+ //if the action is omitted, it is 'default'.
199
+ var action = components.length > 1 ? components[1] : 'default';
200
+
201
+ if (typeof controller._actions[action] !== 'function') {
202
+ return this['404'](); //no such action, reject
203
+ }
204
+
205
+ //the URL components after the 2nd are passed to the action method
206
+ var _arguments = components.length > 2 ? _.rest(components, 2) : [];
207
+
208
+ addHistoryEntry(this, controllerName, action, _arguments);
209
+ return invokeAction(controllerName, action, _arguments);
210
+ },
211
+
212
+ '404':function () {
213
+ //do nothing, expect overriding
214
+ },
215
+
216
+ /**
217
+ * Return the last invoked action
218
+ * @return {object} the last action being invoked and it's parameters
219
+ */
220
+ getLastAction:function () {
221
+ return _.last(this._history, 1)[0];
222
+ },
223
+
224
+ /**
225
+ * Make navigate() returns a deferred object
226
+ * @param fragment
227
+ * @param options may contain trigger and replace options.
228
+ * @return {*} Deferred
229
+ */
230
+ navigate: function(fragment, options){
231
+ if (!options || options === true) {
232
+ options = {trigger: options};
233
+ }
234
+ var _options = _.extend({}, options);
235
+ _options.trigger = false; //too hard to port Backbone's mechanism without much refactory,
236
+ // but such logical flaw can be exploited. The goal is to not modify Backbone.js at all
237
+
238
+ Backbone.Router.prototype.navigate.call(this, fragment, _options);
239
+ if(options.trigger){
240
+ return this.dispatch(fragment);
241
+ }else{
242
+ return (new $.Deferred()).resolve();
243
+ }
244
+ }
245
+ };
246
+
247
+ function extend(properties){
248
+ var _routes = _.extend(properties.routes || {}, _inherentRouterProperties.routes );
249
+ return Backbone.Router.extend(_.extend(properties, _inherentRouterProperties, { routes: _routes }));
250
+ }
251
+
252
+ var RouterClass = Backbone.Router.extend(
253
+ _.extend({ extend: extend }, _inherentRouterProperties)
254
+ );
255
+ RouterClass.extend = extend;
256
+ return RouterClass;
257
+ })(),
258
+
259
+ /**
260
+ * An extension of BackboneMVC.Model, add events of 'read' and 'error' to
261
+ * a model, which will be triggered upon loading data from server.
262
+ *
263
+ * This class assumes the returned json packet contains both 'error' and 'data' fields
264
+ * as root properties, which is commonly seen in modern web service APIs. If you business
265
+ * logic cannot comply this standard. Then this model class might not fit.
266
+ * @class BackboneMVC.Model
267
+ * @example //TODO
268
+ */
269
+ Model:{
270
+ extend:function (properties) {
271
+ properties = _.extend({
272
+ __fetchSuccessCallback:null,
273
+ __fetchErrorCallback:null,
274
+
275
+ fetch:function (options) {
276
+ options = options || {};
277
+ //wrap the success callback, so we get a chance of triggering 'read' event
278
+ //by taking over the '__fetchSuccessCallback()' defined in 'parse()'
279
+ var success = options.success;
280
+ options.success = function (model, resp) {
281
+ if (success) {
282
+ success(model, resp);
283
+ }
284
+ if (model.__fetchSuccessCallback) {
285
+ var tmp = model.__fetchSuccessCallback;
286
+ model.__fetchSuccessCallback = null; //remove the temporary method after use
287
+ tmp.apply(model);
288
+ }
289
+ };
290
+ //wrap the error callback, so we get a chance of triggering 'error' event
291
+ var error = options.error;
292
+ options.error = function (model, resp) {
293
+ if (error){
294
+ error(model, resp);
295
+ }
296
+ model.trigger('error', error);
297
+ };
298
+ Backbone.Model.prototype.fetch.apply(this, [options].concat(_.rest(arguments)));
299
+ },
300
+
301
+ /**
302
+ * Intercept the data return from server and see if there is any error.
303
+ * Overriding is discouraged.
304
+ * @param {object} response the returned and parsed json object
305
+ * @return {*}
306
+ */
307
+ parse:function (response) {
308
+ this.__fetchSuccessCallback = null;
309
+ this.__fetchErrorCallback = null;
310
+
311
+ if (!response || response.error) {
312
+ //if response contains a non-null 'error' field, still trigger 'error' event
313
+ this.trigger('error', (response && response.error) || response);
314
+ return {};
315
+ }
316
+ this.__fetchSuccessCallback = function () {
317
+ this.trigger('read', response.data);
318
+ }.bind(this);
319
+ return response.data;
320
+ }
321
+ }, properties);
322
+
323
+ return Backbone.Model.extend(properties);
324
+ }
325
+ }
326
+ });
327
+
328
+ //_extendMethodGenerator is used to create a closure that can store class members(fields and members)
329
+ //in the ancestry, so as to provide a basis for the children controller to further derive
330
+ BackboneMVC.Controller.extend = _extendMethodGenerator(BackboneMVC.Controller, {});
331
+
332
+ //internal variables
333
+ var ControllersPool = {}; //hashmap, keeps a record of defined Controllers with their names as keys
334
+
335
+ //if a Controller class defines these actions, then they will not be treated as action methods
336
+ var systemActions = ['initialize', 'beforeFilter', 'afterRender', 'checkSession'];
337
+
338
+ //internal functions
339
+ /**
340
+ *
341
+ * @param {Class} klass the current klass object
342
+ * @param {object} _inheritedMethodsDefinition store all inherited methods from the ancestors(in closure only)
343
+ * @return {Function}
344
+ * @private
345
+ */
346
+ function _extendMethodGenerator(klass, _inheritedMethodsDefinition) {
347
+ //create closure
348
+ return function (properties) {
349
+ var name = properties.name;
350
+ if (typeof name === 'undefined') {
351
+ throw '\'name\' property is mandatory ';
352
+ }
353
+
354
+ // also inherits the methods from ancestry
355
+ properties = _.extend({}, _inheritedMethodsDefinition, properties);
356
+
357
+ //special handling of method override in inheritance
358
+ var tmpControllerProperties = _.extend({}, BackboneMVC.Controller);
359
+
360
+ var actionMethods = {}, secureActions = {};
361
+ //try to pick out action methods
362
+ _.each(properties, function (value, propertyName) {
363
+ tmpControllerProperties[propertyName] = value; // transfer the property, which will be later
364
+ //filter the non-action methods
365
+ if (typeof value !== 'function' || propertyName[0] === '_' ||
366
+ _.indexOf(systemActions, propertyName) >= 0) {
367
+ return false;
368
+ }
369
+
370
+ actionMethods[propertyName] = value;
371
+ if (propertyName.match(/^user_/i)) { //special handling to secure methods
372
+ secureActions[propertyName] = value;
373
+ // even though secure methods start with 'user_', it's useful if they can be invoked without
374
+ // that prefix
375
+ var shortName = propertyName.replace(/^user_/i, '');
376
+ if (typeof properties[shortName] !== 'function') {
377
+ // if the shortname function is not defined separately, also account it for a secure method
378
+ secureActions[shortName] = value;
379
+ actionMethods[shortName] = value;
380
+ }
381
+ }
382
+ });
383
+
384
+ //_actions and _secureActions are only used to tag those two types of methods, the action methods
385
+ //are still with the controller
386
+ _.extend(tmpControllerProperties, actionMethods, {
387
+ _actions:actionMethods,
388
+ _secureActions:secureActions
389
+ });
390
+ //remove the extend method if there is one, so it doesn't stay in the property history
391
+ if ('extend' in tmpControllerProperties) {
392
+ delete tmpControllerProperties.extend;
393
+ }
394
+ //get around of singleton inheritance issue by using mixin
395
+ var _controllerClass = ControllerSingleton.extend(tmpControllerProperties);
396
+ //special handling for utility method of inheritance
397
+ _.extend(_controllerClass, {
398
+ extend:_extendMethodGenerator(_controllerClass, _.extend({}, _inheritedMethodsDefinition, properties))
399
+ });
400
+
401
+ //Register Controller
402
+ ControllersPool[name] = _controllerClass;
403
+
404
+ return _controllerClass;
405
+ };
406
+ }
407
+
408
+ function _d(a) {
409
+ console.log(a);
410
+ }
411
+
412
+ /**
413
+ * use duck-typing to check if an object is a Deferred Object.
414
+ * @param suspiciousObject
415
+ * @return {boolean}
416
+ */
417
+ function isDeferred(suspiciousObject) {
418
+ //duck-typing
419
+ return _.isObject(suspiciousObject) && suspiciousObject.promise &&
420
+ typeof suspiciousObject.promise === 'function';
421
+ }
422
+
423
+ /**
424
+ * Convert a non-deferred object ot deferred object, and resolve or reject the deferred object based on the value
425
+ * of the non-deferred object.
426
+ * @param deferred
427
+ * @param result
428
+ * @return {object} a Deferred Object
429
+ */
430
+ function assertDeferredByResult(deferred, result) {
431
+ if (typeof result === 'undefined') {
432
+ result = true;
433
+ }
434
+ return deferred[result ? 'resolve' : 'reject'](result);
435
+ }
436
+
437
+ /**
438
+ * convert to CamelCased form
439
+ * @param string the non-camel-cased form
440
+ * @return {string}
441
+ */
442
+ function camelCased(string) {
443
+ if (typeof string !== 'string') {
444
+ return null;
445
+ }
446
+ string = string.replace(/\s{2,}/g, ' ');
447
+
448
+ return (_.map(string.split(' '), function (entry) {
449
+ return entry.replace(/(^|_)[a-z]/gi,function (match) {
450
+ return match.toUpperCase();
451
+ }).replace(/_/g, '');
452
+ })).join(' ');
453
+
454
+ }
455
+
456
+ /**
457
+ * convert to underscored form
458
+ * @param string the non-underscored form
459
+ * @return {string}
460
+ */
461
+ function underscored(string) {
462
+ if (typeof string !== 'string') {
463
+ return null;
464
+ }
465
+ string = string.replace(/\s{2,}/g, ' ');
466
+
467
+ return (_.map(string.split(' '), function (entry) {
468
+ return entry.replace(/^[A-Z]/g, function (match) {
469
+ return match.toLowerCase();
470
+ })
471
+ .replace(/([a-z])([A-Z])/g, function ($, $1, $2) {
472
+ return $1 + '_' + $2.toLowerCase();
473
+ });
474
+ })).join(' ');
475
+ }
476
+
477
+ /**
478
+ * Invoke the action method under a controller, also takes care of event callbacks and session checking method
479
+ * on the call chain.
480
+ * @param controllerName the controller name
481
+ * @param {string} action action method name
482
+ * @param {Array} _arguments the parameters sent ot the action method
483
+ * @return {*}
484
+ */
485
+ function invokeAction(controllerName, action, _arguments) {
486
+ var controller = new ControllersPool[controllerName]();
487
+
488
+ var hooksParameters = [action].concat(_arguments);
489
+ var deferred = $.Deferred();
490
+ //do beforeFilter
491
+ var result = controller.beforeFilter.apply(controller, hooksParameters);
492
+ if (isDeferred(result)) {
493
+ deferred = result;
494
+ } else {
495
+ assertDeferredByResult(deferred, result);
496
+ }
497
+
498
+ //check if secure method
499
+ if (typeof controller._secureActions[action] === 'function') {
500
+ //do session checking
501
+ deferred = deferred.pipe(function () {
502
+ var result = controller.checkSession.apply(controller, _arguments);
503
+
504
+ if (isDeferred(result)) {
505
+ return result;
506
+ } else {
507
+ return assertDeferredByResult(new $.Deferred(), result);
508
+ }
509
+ });
510
+ }
511
+
512
+ //invoke the action
513
+ deferred = deferred.pipe(function () {
514
+ var result = controller[action].apply(controller, _arguments);
515
+
516
+ if (isDeferred(result)) {
517
+ return result;
518
+ } else {
519
+ return assertDeferredByResult(new $.Deferred(), result);
520
+ }
521
+ });
522
+
523
+ //do afterRender
524
+ deferred = deferred.pipe(function () {
525
+ var result = controller.afterRender.apply(controller, hooksParameters);
526
+ if (isDeferred(result)) {
527
+ return result;
528
+ } else {
529
+ return assertDeferredByResult(new $.Deferred(), result);
530
+ }
531
+ });
532
+
533
+ return deferred;
534
+ }
535
+
536
+ function addHistoryEntry(router, controller_name, action, _arguments) {
537
+ if (router._history.length > 888) {
538
+ router._history = _.last(router._history, 888);
539
+ }
540
+ router._history.push([controller_name, action, _arguments]);
541
+ }
542
+ })();