booster 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.
Files changed (72) hide show
  1. data/.gitignore +5 -0
  2. data/Gemfile +7 -0
  3. data/Gemfile.lock +106 -0
  4. data/MIT-LICENSE +20 -0
  5. data/README.md +174 -0
  6. data/Rakefile +7 -0
  7. data/booster.gemspec +24 -0
  8. data/booster.tmbundle/Snippets/untitled.tmSnippet +14 -0
  9. data/booster.tmbundle/Syntaxes/Booster.tmLanguage +46 -0
  10. data/booster.tmbundle/info.plist +10 -0
  11. data/lib/assets/javascripts/booster-core.js +5 -0
  12. data/lib/assets/javascripts/booster-support.js +1 -0
  13. data/lib/assets/javascripts/booster.js +2 -0
  14. data/lib/assets/javascripts/booster/collection.js.boost +1 -0
  15. data/lib/assets/javascripts/booster/model.js.boost +30 -0
  16. data/lib/assets/javascripts/booster/router.js.boost +107 -0
  17. data/lib/assets/javascripts/booster/support/binding.js.boost +1 -0
  18. data/lib/assets/javascripts/booster/support/helpers.js.boost +109 -0
  19. data/lib/assets/javascripts/booster/support/i18n.js.boost +136 -0
  20. data/lib/assets/javascripts/booster/support/observer.js.boost +81 -0
  21. data/lib/assets/javascripts/booster/support/schema.js.boost +117 -0
  22. data/lib/assets/javascripts/booster/view.js.boost +45 -0
  23. data/lib/assets/javascripts/booster/views/composite.js.boost +59 -0
  24. data/lib/assets/javascripts/booster/views/layout.js.boost +94 -0
  25. data/lib/booster.rb +7 -0
  26. data/lib/booster/engine.rb +8 -0
  27. data/lib/booster/handlebars.rb +50 -0
  28. data/lib/booster/template.rb +59 -0
  29. data/lib/booster/version.rb +3 -0
  30. data/test/booster/tilt_test.rb +35 -0
  31. data/test/dummy/Rakefile +7 -0
  32. data/test/dummy/app/assets/javascripts/application.js.boost +18 -0
  33. data/test/dummy/app/assets/javascripts/booster/support/helpers_spec.js.boost +69 -0
  34. data/test/dummy/app/assets/javascripts/booster/support/i18n_spec.js.boost +0 -0
  35. data/test/dummy/app/assets/javascripts/booster/support/observer_spec.js.boost +56 -0
  36. data/test/dummy/app/assets/javascripts/booster/support/router_spec.js.boost +19 -0
  37. data/test/dummy/app/assets/javascripts/booster/support/schema_spec.js.boost +79 -0
  38. data/test/dummy/app/assets/javascripts/booster/views/layout_spec.js.boost +30 -0
  39. data/test/dummy/app/controllers/application_controller.rb +7 -0
  40. data/test/dummy/app/views/layouts/application.html.erb +29 -0
  41. data/test/dummy/config.ru +4 -0
  42. data/test/dummy/config/application.rb +30 -0
  43. data/test/dummy/config/boot.rb +10 -0
  44. data/test/dummy/config/environment.rb +5 -0
  45. data/test/dummy/config/environments/development.rb +27 -0
  46. data/test/dummy/config/environments/production.rb +29 -0
  47. data/test/dummy/config/environments/test.rb +34 -0
  48. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  49. data/test/dummy/config/initializers/inflections.rb +10 -0
  50. data/test/dummy/config/initializers/mime_types.rb +5 -0
  51. data/test/dummy/config/initializers/secret_token.rb +7 -0
  52. data/test/dummy/config/initializers/session_store.rb +8 -0
  53. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  54. data/test/dummy/config/locales/en.yml +5 -0
  55. data/test/dummy/config/routes.rb +3 -0
  56. data/test/dummy/public/404.html +26 -0
  57. data/test/dummy/public/422.html +26 -0
  58. data/test/dummy/public/500.html +26 -0
  59. data/test/dummy/public/favicon.ico +0 -0
  60. data/test/dummy/script/rails +6 -0
  61. data/test/test_helper.rb +4 -0
  62. data/vendor/assets/javascripts/backbone.js +1158 -0
  63. data/vendor/assets/javascripts/handlebars-helpers.js +56 -0
  64. data/vendor/assets/javascripts/handlebars-vm.js +191 -0
  65. data/vendor/assets/javascripts/handlebars.js +1561 -0
  66. data/vendor/assets/javascripts/jasmine-html.js +190 -0
  67. data/vendor/assets/javascripts/jasmine-jquery.js +288 -0
  68. data/vendor/assets/javascripts/jasmine.js +2471 -0
  69. data/vendor/assets/javascripts/stitch.js +57 -0
  70. data/vendor/assets/javascripts/underscore.js +981 -0
  71. data/vendor/assets/stylesheets/jasmine.css +166 -0
  72. metadata +172 -0
@@ -0,0 +1,107 @@
1
+ /** @type {Backbone.Router} The one and only application router */
2
+ var router = exports.router = new Backbone.Router();
3
+
4
+ /** @type {Object.<string, function(Object, function)} ... */
5
+ var params = { };
6
+
7
+ /** @type {RegExp} Regular expression for finding named parameters in path */
8
+ var namedParam = /[:\*]([\w\d]+)/g;
9
+
10
+ /**
11
+ *
12
+ * @param {string} name The name of the path parameter to catch
13
+ * @param {function(Object, function)} callback The callback to invoke when handling
14
+ * routes containing parameters with the
15
+ * given `name`. This should have the
16
+ * same signature as regular route middleware
17
+ * and invoke `next` when done.
18
+ */
19
+
20
+ exports.param = function(name, callback) {
21
+ (params[name] || (params[name] = [])).push(callback);
22
+ }
23
+
24
+ /**
25
+ * @param {string} path The route path, passed as-is to `Backbone.Router`
26
+ * @param {...function(Object, function)} middleware
27
+ */
28
+
29
+ exports.route = function(path, middleware) {
30
+ var parameters = extractParameters(path);
31
+ var middleware = Array.prototype.slice.call(arguments, 1);
32
+
33
+ // The actual route function registered with Backbone. This creates a closure
34
+ // for the nested `next` function which will be invoked asynchronously as the
35
+ // application drives it through the middleware stack.
36
+
37
+ router.route(path, middleware[middleware.length -1].name, function() {
38
+ var iter = 0;
39
+ var run = [];
40
+ var req = {
41
+ params: parameters ? normalize(parameters, arguments) : arguments,
42
+ path: path
43
+ };
44
+
45
+ // Start with any registered parameter middleware. TODO: Determine if it is
46
+ // OK to move this to the outer `route` function, in which case it would
47
+ // only include the parameter middleware registered **before** the route
48
+ // was registered (i.e. no dynamically added param middleware at runtime).
49
+ _.each(parameters, function(param) {
50
+ if (params[param]) {
51
+ run = run.concat(params[param]);
52
+ }
53
+ });
54
+
55
+ // And append the rest of the middleware registered for this route.
56
+ run = run.concat(middleware);
57
+
58
+ /**
59
+ * Invokes the middleware, driven by the application asynchronously.
60
+ * This function is recursive, but there won't be that many middleware
61
+ * for a route such that the stack will suffer significantly.
62
+ */
63
+
64
+ var next = function() {
65
+ run[iter++](req, iter < run.length ? next : undefined);
66
+ }
67
+
68
+ next();
69
+ });
70
+ }
71
+
72
+ /**
73
+ * Extracts the named parameters in the given path.
74
+ *
75
+ * @param {string} path A URL path that may contain named parameters in the
76
+ * form of `:name` or `*name`.
77
+ * @return {Array.<string>} An array of all the named parameters found in the
78
+ * path, without `:` and `*`.
79
+ */
80
+
81
+ function extractParameters(path) {
82
+ var match, result = [];
83
+ while (match = namedParam.exec(path)) {
84
+ result.push(match[1]);
85
+ };
86
+ return result;
87
+ }
88
+
89
+ /**
90
+ * Creates a normalized `params` hash that will go into the request argument
91
+ * to all the route middleware.
92
+ *
93
+ * @param {Array.<string>} parameters The named parameters from the URL path that
94
+ * will be combined with `args` into a hash.
95
+ * @param {Array.<string>} arguments The arguments from Backbone which has extracted
96
+ * them from the location hash.
97
+ * @return {Object.<string, string>} A hash where the arguments are keyed by the
98
+ * parameters.
99
+ */
100
+
101
+ function normalize(parameters, args) {
102
+ var result = { };
103
+ for (var arg = 0; arg < args.length; ++arg) {
104
+ result[parameters[arg]] = args[arg];
105
+ }
106
+ return result;
107
+ }
@@ -0,0 +1,109 @@
1
+
2
+ /**
3
+ * Returns a {Handlebars.SafeString} with HTML for an `<input>` element with name and value based
4
+ * on the given `attribute` and the current context (`this`) which is expected to be a {Backbone.Model}.
5
+ *
6
+ * @param {string} attribute The name of the attribute to use as the name and value for the textarea
7
+ * @param {Object.<String, *>} options Object containing a mandatory `hash` object to be transformed into attributes.
8
+ * @return {Handlebars.SafeString} A safe string that will not be escaped when used in templates
9
+ */
10
+
11
+ exports.input = function(attribute, options) {
12
+ options = options.hash;
13
+ options.name = attribute;
14
+ options.value = this.get(attribute);
15
+ return tag('input', options);
16
+ }
17
+
18
+ /**
19
+ * Returns a {Handlebars.SafeString} with HTML for a `<select>` element with name and content based
20
+ * on the given `attribute` int the current context (`this`) which is expected to be a {Backbone.Model}.
21
+ *
22
+ * @param {string} attribute The name of the attribute to use as the name and value for the textarea
23
+ * @param {Object.<String, *>} options Object containing a mandatory `hash` object to be transformed into attributes.
24
+ * @return {Handlebars.SafeString} A safe string that will not be escaped when used in templates
25
+ */
26
+
27
+ exports.textarea = function(attribute, options) {
28
+ options = options.hash;
29
+ options.name = attribute;
30
+ return tag('textarea', options, this.get(attribute));
31
+ }
32
+
33
+ /**
34
+ * @param {string} attribute The name of the attribute to use as the name and selected option for the select element.
35
+ * @param {Object|Array} selectOptions The options to choose from in the select element. This can either be an array
36
+ * where the key and value will be the same for an option, or an Object where
37
+ * attribute keys will become __option values__ and the attribute values will
38
+ * be __presented to the user__.
39
+ * @param {Object.<String, *>} options Object containing a mandatory `hash` object to be transformed into attributes.
40
+ * @return {Handlebars.SafeString} A safe string that will not be escaped when used in templates
41
+ */
42
+
43
+ exports.select = function(attribute, selectOptions, options) {
44
+ options = options.hash;
45
+ options.name = attribute;
46
+
47
+ var value = this.get(attribute);
48
+
49
+ if (_.isArray(selectOptions)) {
50
+ selectOptions = _.map(selectOptions, function(option) { // Intentional non-strict comparison below
51
+ return '<option value="#{option}" #{value == option ? "selected" : ""}>#{option}</option>';
52
+ });
53
+ } else {
54
+ selectOptions = _.map(_.keys(selectOptions), function(key) { // Intentional non-strict comparison below
55
+ return '<option value="#{key}" #{value == key ? "selected" : ""}>#{selectOptions[key]}</option>';
56
+ });
57
+ }
58
+
59
+ return tag('select', options, '\n' + selectOptions.join('\n') + '\n');
60
+ }
61
+
62
+ /**
63
+ * Returns a {Handlebars.SafeString} with HTML for an `<input type="radio">` element with name and selected state
64
+ * based on the given `attribute` and the current context (`this`) which is expected to be a {Backbone.Model}.
65
+ *
66
+ * @param {string} attribute The name of the attribute to use as the name and checked state for the radio button
67
+ * @param {*} value The value of the radio button -- if the value of the attribute is the same as this value
68
+ * the radio button will be checked. Correspondingly, checking this radio button will set the
69
+ * model attribute to this value.
70
+ * @param {Object.<String, *>} options Object containing a mandatory `hash` object to be transformed into attributes.
71
+ * @return {Handlebars.SafeString} A safe string that will not be escaped when used in templates
72
+ */
73
+
74
+ exports.radio = function(attribute, value, options) {
75
+ options = options.hash;
76
+ options.name = attribute;
77
+ options.value = value;
78
+ options.type = 'radio';
79
+
80
+ if (this.get(attribute) === value) {
81
+ options.checked = true;
82
+ }
83
+
84
+ return tag('input', options);
85
+ }
86
+
87
+ /**
88
+ * Generic helper for creating HTML tags.
89
+ *
90
+ * @param {string} name The tag name, for instance `"input"` or `"textarea"`
91
+ * @param {Object.<String,*>} options A set of options that will be converted into HTML attributes. The values will
92
+ * have `toString` invoked on them
93
+ * @param {*=} content Tag content, if any. The result of `content.toString()` will be placed inside the tag.
94
+ */
95
+
96
+ function tag(name, options, content) {
97
+ var attributes = [];
98
+ _.each(options, function(value, attribute) {
99
+ attributes.push('#{attribute}="#{value}"');
100
+ });
101
+ return new Handlebars.SafeString('<#{name} #{attributes.join(" ")}>#{content || ""}</#{name}>');
102
+ }
103
+
104
+ // Initialization code for this module; every function exported as a helper is
105
+ // registered as a handlebars-helper so that they can be used directly from
106
+ // Handlebars templates, in addition from JavaScript.
107
+ _.each(exports, function(helper, name) {
108
+ Handlebars.registerHelper(name, helper);
109
+ });
@@ -0,0 +1,136 @@
1
+ /** @type {RegExp} Regular expression used for capturing string interpolations in the translations **/
2
+ var matcher = /#\{([A-Za-z_0-9]*)\}/gm;
3
+
4
+ /**
5
+ * Hash where translations go. The application needs to fill this
6
+ * out using potentially nested objects where each key corresponds
7
+ * to a translation (string) or whatever type you like for the key.
8
+ *
9
+ * @type {Object.<string, Object>}
10
+ */
11
+
12
+ exports.translations = { };
13
+
14
+ /**
15
+ * Returns the translation corresponding to the given key. If the
16
+ * translation is a string containing interpolations, these are resolved
17
+ * using the keys in the given options hash.
18
+ *
19
+ * The translations need not necessarily be strings and can be any
20
+ * type you like. For instance, it might be a good idea to store complete
21
+ * hashes with select options to be used in select elements directly
22
+ * under a key in the translations.
23
+ *
24
+ * var I18n = require('booster/support/i18n');
25
+ * var translation = I18n.t('views.discussion.edit.status_options')
26
+ *
27
+ * @param {String} the (composite) key to use when locating translations
28
+ * @param {Object} options currently only used for string interpolations
29
+ */
30
+
31
+ exports.translate = exports.t = function(key, options) {
32
+ var translation = _.reduce(key.split('.'), function(memo, key) {
33
+ return memo ? memo[key] : undefined;
34
+ }, exports.translations);
35
+
36
+ // Interpolate string translation, replacing %{attribut} with option value
37
+ if (typeof translation === 'string') {
38
+ return _.reduce(translation.match(this.matcher), function(memo, match) {
39
+ key = match.replace(I18n.matcher, '$1');
40
+ return memo.replace(match, (options && options[key]) || ('[Missing option: ' + key + ']'));
41
+ }, translation)
42
+ }
43
+
44
+ return translation || (options && options.silent ? '' : ('[Missing translation: ' + key + ']'));
45
+ }
46
+
47
+ /**
48
+ * A model mixin that makes it easier for models to expose internationalized
49
+ * attribute names to be used as form labels, for instance. It estabslishes a
50
+ * convention for naming keys in the translations hash exported from this module,
51
+ * and introduces shortcut functions for reading those translations.
52
+ *
53
+ * **Example:**
54
+ *
55
+ * var I18n = require('booster/support/i18n);
56
+ *
57
+ * I18n.translations = {
58
+ * modelNames: {
59
+ * user: 'User'
60
+ * },
61
+ *
62
+ * attributeNames: {
63
+ * user: {
64
+ * firstName: 'First name',
65
+ * lastName: 'Last name',
66
+ * }
67
+ * },
68
+ *
69
+ * valueNames: {
70
+ * user: {
71
+ * status: {
72
+ * 0: 'Inactive',
73
+ * 1: 'Active'
74
+ * }
75
+ * }
76
+ * }
77
+ *
78
+ * This mixin is intended to be applied to model constructor functions
79
+ * and not model constructor prototypes to allow the functions to be invoked
80
+ * directly on a constructor (App.Models.Customer.modelName() for instance) as
81
+ * well as on individual instances (someCustomer.constructor.modelName()).
82
+ *
83
+ * **Example:**
84
+ *
85
+ * exports.Model = Backbone.Model.extend({
86
+ * ...
87
+ * });
88
+ *
89
+ * _.extend(exports.Model, require('booster/support/i18n').mixin('user'));
90
+ *
91
+ * The namespace argument is required to indicate the namespace used in the `exports.translations`
92
+ * object where the current translations can be found since the name of the model can't be inferred.
93
+ *
94
+ * @param {String} basically the name of the model which gets used when doing translation lookups
95
+ */
96
+
97
+ exports.mixin = function(namespace) {
98
+
99
+ return {
100
+
101
+ /**
102
+ * Returns the translated name for the model "class" based on the namespace used when mixed in.
103
+ *
104
+ * @return {string} The translated name of the model.
105
+ */
106
+
107
+ modelName: function() {
108
+ return exports.t('modelNames.' + namespace);
109
+ },
110
+
111
+ /**
112
+ * Returns the translated name for the given attribute.
113
+ *
114
+ * @param {string} attribute The name of the model attribute to translate.
115
+ * @return {string}
116
+ */
117
+
118
+ attributeName: function(attribute) {
119
+ return exports.t('attributeNames.' + namespace + '.' + attribute);
120
+ },
121
+
122
+ /**
123
+ * Returns the translated name for the given attribute and value. This is mostly
124
+ * used for attributes of Number type, for instance a `status` attribute that stores
125
+ * Numbers that should be translated to "Open", "Closed", "Pending", for instance, based
126
+ * on a fixed set of possible values for the attribute.
127
+ *
128
+ * @param {string} attribute The name of the attribute the value applies to
129
+ * @param {*} attribute The attribute value to translate. Must respond to `toString()`.
130
+ */
131
+
132
+ attributeValueName: function(attribute, value) {
133
+ return exports.t('valueNames.' + namespace + '.' + attribute + '.' + value);
134
+ }
135
+ }
136
+ }
@@ -0,0 +1,81 @@
1
+
2
+ /**
3
+ * Exposes the local `mixin` as a function to allow it to be parameterized in the
4
+ * future without breakage, and to align it with other mixins that already are
5
+ * parameterized. The actual mixin object is stored outside of the function
6
+ * to prevent unnecessary creation of Function objects.
7
+ *
8
+ * @return {Object} A mixin object that can be applied to any {Backbone.Model}
9
+ */
10
+
11
+ exports.mixin = function() {
12
+ return mixin;
13
+ }
14
+
15
+ /**
16
+ * A generic mixin that can be applied to any object that wants to track all the
17
+ * event bindings it makes to any {Backbone.Events} source during its lifetime.
18
+ * This is achieved by introducing the `observe` and `unobserve` functions that
19
+ * forwards to the `bind` and `unbind` functions, respectively, of the subject that
20
+ * they are invoked on.
21
+ *
22
+ * As an example, this mixin is applied to the Booster View implementation to track
23
+ * all the bindings inheriting views do to any models, collections, and other
24
+ * event sources so that they can be automatically unbound when the view leaves the
25
+ * DOM and its parent view.
26
+ *
27
+ * Instead of using the traditional `this.model.bind("change", this.render)` the
28
+ * observe function can be used; `this.observe(this.model, "change", this.render)`.
29
+ * All `observe` calls ever made by "`this`" can be undone by invoking `unobserve`,
30
+ * which is what the Booster View implementation does in its `leave` function.
31
+ *
32
+ * `observe` automatically makes `this` the context of the callback so that
33
+ * when using `this.observe(this.model, "change", this.render)`, `render` will be
34
+ * invoked such that `this` refers to the same object on which `observe` was invoked.
35
+ */
36
+
37
+ var mixin = {
38
+
39
+ /**
40
+ * Binds `this` to the given `event` and `subject` using the {Backbone.Events} API.
41
+ * The binding is recorded internally so that it can be easily undone later in a
42
+ * generic manner.
43
+ *
44
+ * @param {Backbone.Events} subject Any object that mixes in {Backbone.Event}
45
+ * @param {string} event The name of the event to subscribe to
46
+ * @param {function(Backbone.Events)} callback Function that will be invoked when
47
+ * the event is triggered. The `subject` will
48
+ * be passed as an argument. `this` will refer
49
+ * to the object on which `observe` was invoked.
50
+ */
51
+ observe: function(subject, event, callback) {
52
+ subject.bind(event, callback, this);
53
+ this._subjects || (this._subjects = []);
54
+ this._subjects.push([subject, event, callback]);
55
+ },
56
+
57
+ /**
58
+ * Unbinds the given `event` and `callback` from `subject`. If no arguments are given,
59
+ * all events ever bound to, on any subject, will be removed. This essentially undos every
60
+ * `observer` call ever made to on this object.
61
+ *
62
+ * @param {Backbone.Events=} subject A subject observed earlier with `observe` (optional)
63
+ * @param {string=} event The name of and event observed earlier with `observe` (optional)
64
+ * @param {function(Backbone.Events)=} callback A callback registered earlier with `observe` (optional)
65
+ */
66
+
67
+ unobserve: function(subject, event, callback) {
68
+ if (subject === undefined) {
69
+ _.each(this._subjects, function(subject) {
70
+ subject[0].unbind(subject[1], subject[2]);
71
+ });
72
+ delete this._subjects;
73
+ } else {
74
+ subject.unbind(event, callback);
75
+ this._subjects = _.reject(this._subjects, function(subject) {
76
+ return subject[0] === subject && subject[1] === event;
77
+ });
78
+ }
79
+ }
80
+
81
+ }
@@ -0,0 +1,117 @@
1
+
2
+ /**
3
+ * Exposes the local `mixin` as a function to allow it to be parameterized in the
4
+ * future without breakage, and to align it with other mixins that already are
5
+ * parameterized. The actual mixin object is stored outside of the function
6
+ * to prevent unnecessary creation of Function objects.
7
+ *
8
+ * @return {Object} A mixin object that can be applied to any {Backbone.Model}
9
+ */
10
+
11
+ exports.mixin = function() {
12
+ return mixin;
13
+ }
14
+
15
+ /**
16
+ * A {Backbone.Model} mixin that introduces support for declaratively specifying that
17
+ * one or more nested attributes are to be wrapped in {Backbone.Model}s,
18
+ * {Backbone.Collection}s, or any other type (via constructor) when accessed via the regular
19
+ * `get()` accessor.
20
+ *
21
+ * @type {Object}
22
+ */
23
+
24
+ var mixin = {
25
+
26
+ /**
27
+ * Overrides the {Backbone.Model} `get` function to allow attributes of object or array type
28
+ * to be lazily converted into collections and models which are memoized inside the model instance.
29
+ * This is an alternative to, for instance, instantiating these nested objects in the
30
+ * `initialize` function.
31
+ *
32
+ * @param {string} attribute The name of the attribute to retrieve.
33
+ * @override
34
+ */
35
+
36
+ get: function(attribute) {
37
+
38
+ // Fast case; the attribute has been accessed before for this model and has already been transformed
39
+ // using the schema type constructor. We optimize for this and return the memoized value immediately.
40
+ if (this._memoized && this._memoized[attribute]) {
41
+ return this._memoized[attribute];
42
+ }
43
+
44
+ // This is the actual internal attribute value which we may need to convert or return as-is.
45
+ var value = this.attributes[attribute];
46
+
47
+ // If the model prototype has specified a schema with an entry for this particular attribute
48
+ // we go ahead and process it.
49
+ if (this.schema && this.schema[attribute]) {
50
+ var type = this.schema[attribute];
51
+ if (typeof type === 'object') {
52
+ type = type.type;
53
+ }
54
+
55
+ if (value.constructor !== type) {
56
+ value = new type(value);
57
+ value.parent = this;
58
+
59
+ // Determine if the value should be memoized for performance reasons. Some types of values
60
+ // are memoized by default and some by marking the attribute as `{memoized: true}` in the schema.
61
+ // Memoized values override the internal attribute value so that can be removed from the model
62
+ // to preserve memory.
63
+ if (shallMemoize(type)) {
64
+ this._memoized || (this._memoized = {});
65
+ this._memoized[attribute] = value
66
+ delete this.attributes[attribute];
67
+ }
68
+ }
69
+ }
70
+
71
+ return value;
72
+ },
73
+
74
+ /**
75
+ * Overrides {Backbone.Model}.toJson to not only return the internal attributes for the
76
+ * model, but also any mapped nested models and collections. Nested collections and models are
77
+ * based on the attributes in a model on instantiation, but manage their own dataset which
78
+ * needs to be merged back into the model when serialized.
79
+ *
80
+ * @return {Object} An object with the internal attributes merged with the attributes of any
81
+ * nested models and collections, recursively. Note that this is not a true
82
+ * JSON string as per the default behavior in {Backbone.Model}
83
+ * @override
84
+ */
85
+
86
+ toJSON: function() {
87
+ var self = this;
88
+ var json = _.clone(this.attributes);
89
+
90
+ _.each(this.schema, function(definition, attribute) {
91
+ if (definition.serialize === false) {
92
+ delete json[attribute];
93
+ } else if (_.isFunction(definition.serialize)) {
94
+ json[attribute] = definition.serialize(self.get(attribute));
95
+ } else {
96
+ var value = self.get(attribute);
97
+ json[attribute] = value.toJSON ? value.toJSON() : value;
98
+ }
99
+ });
100
+
101
+ return json;
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Returns true if the conversion of an attribute to the given `type` should be
107
+ * memoized inside the model instance.
108
+ *
109
+ * @type {function} The type constructor to evaluate.
110
+ */
111
+
112
+ function shallMemoize(type) {
113
+ return type.prototype instanceof Backbone.Model ||
114
+ type.prototype instanceof Backbone.Collection ||
115
+ type === Backbone.Model ||
116
+ type === Backbone.Collection;
117
+ }