6bd3073f by Adam Heath

First pass of monkey-patching nested model support. No tests yet, but

it passes jshint and build targets.
1 parent 40915018
1 {
2 "directory": "src/lib"
3 }
1 .*.swp
2 .tmp/
3 bin/coverage/
4 dist/
5 node_modules/
6 src/lib/
7 .grunt/
8 _SpecRunner.html
1 // Generated on 2014-02-06 using generator-webapp 0.4.7
2 /* global module */
3
4 // # Globbing
5 // for performance reasons we're only matching one level down:
6 // 'test/spec/{,*/}*.js'
7 // use this if you want to recursively match all subfolders:
8 // 'test/spec/**/*.js'
9
10 module.exports = function (grunt) {
11 /* global require */
12 'use strict';
13
14 var jasmineRequirejsTemplateOptions = function(withInstanbul) {
15 /* global requirejs */
16 var callback;
17 if (withInstanbul) {
18 callback = function() {
19 var oldLoad = requirejs.load;
20 requirejs.load = function (context, moduleName, url) {
21 //console.log('context=' + JSON.stringify(arguments), 'moduleName=' + moduleName, 'url=' + url);
22 var parts = url.split('/');
23 for (var i = 0; i < parts.length; ) {
24 var part = parts[i];
25 if (part === '.') {
26 parts.splice(i, 1);
27 } else if (part === '') {
28 parts.splice(i, 1);
29 } else if (part === '..') {
30 if (i > 0) {
31 i--;
32 parts.splice(i, 2);
33 } else {
34 parts.splice(i, 1);
35 }
36 } else {
37 i++;
38 }
39 }
40 url = parts.join('/');
41 if (url.indexOf('src/scripts/') === 0) {
42 url = './.grunt/grunt-contrib-jasmine/' + url;
43 }
44 //console.log('url=' + url);
45 return oldLoad.apply(this, [context, moduleName, url]);
46 };
47 };
48 }
49 return {
50 requireConfigFile: '<%= yeoman.src %>/scripts/config.js',
51 requireConfig: {
52 baseUrl: '<%= yeoman.src %>/scripts',
53 callback: callback
54 }
55 };
56 };
57
58 var jasmineInstanbulTemplateOptions = function(nestedTemplate, nestedOptions) {
59 return {
60 coverage: 'bin/coverage/coverage.json',
61 report: 'bin/coverage',
62 replace: false,
63 template: require(nestedTemplate),
64 templateOptions: nestedOptions
65 };
66 };
67
68 // Load grunt tasks automatically
69 require('load-grunt-tasks')(grunt);
70
71 // Time how long tasks take. Can help when optimizing build times
72 require('time-grunt')(grunt);
73
74 // Define the configuration for all the tasks
75 grunt.initConfig({
76 bower: {
77 target: {
78 options: {
79 exclude: [
80 'requirejs',
81 ],
82 transitive: true,
83 },
84 rjsConfig: '<%= yeoman.src %>/scripts/config.js'
85 }
86 },
87
88 // Project settings
89 yeoman: {
90 // Configurable paths
91 app: 'app',
92 dist: 'dist',
93 src: 'src',
94 },
95
96 // Watches files for changes and runs tasks based on the changed files
97 watch: {
98 js: {
99 files: ['<%= yeoman.src %>/scripts/{,*/}*.js'],
100 tasks: ['jshint'],
101 },
102 jstest: {
103 files: ['test/spec/{,*/}*.js'],
104 tasks: ['test:watch']
105 },
106 gruntfile: {
107 files: ['Gruntfile.js']
108 },
109 styles: {
110 files: ['<%= yeoman.src %>/styles/{,*/}*.css'],
111 tasks: ['newer:copy:styles', 'autoprefixer']
112 }
113 },
114
115 // The actual grunt server settings
116 connect: {
117 options: {
118 port: 9000,
119 // Change this to '0.0.0.0' to access the server from outside
120 hostname: 'localhost'
121 },
122 app: {
123 options: {
124 open: false,
125 base: [
126 '.tmp',
127 '<%= yeoman.src %>'
128 ]
129 }
130 },
131 test: {
132 options: {
133 port: 9001,
134 base: [
135 '.tmp',
136 'test',
137 '<%= yeoman.src %>'
138 ]
139 }
140 },
141 dist: {
142 options: {
143 open: false,
144 base: '<%= yeoman.dist %>',
145 }
146 }
147 },
148
149 // Empties folders to start fresh
150 clean: {
151 dist: {
152 files: [{
153 dot: true,
154 src: [
155 '.tmp',
156 '<%= yeoman.dist %>/*',
157 '!<%= yeoman.dist %>/.git*'
158 ]
159 }]
160 },
161 server: '.tmp'
162 },
163
164 // Make sure code styles are up to par and there are no obvious mistakes
165 jshint: {
166 options: {
167 browser: true,
168 esnext: true,
169 bitwise: true,
170 camelcase: true,
171 curly: true,
172 eqeqeq: true,
173 immed: true,
174 indent: 4,
175 latedef: true,
176 newcap: true,
177 noarg: true,
178 quotmark: 'single',
179 undef: true,
180 unused: true,
181 strict: true,
182 trailing: true,
183 smarttabs: true,
184 jquery: true,
185 reporter: require('jshint-stylish')
186 },
187 all: [
188 'Gruntfile.js',
189 ],
190 scripts: {
191 options: {
192 globals: {
193 define: false,
194 }
195 },
196 files: {
197 src: [
198 '<%= yeoman.src %>/scripts/**/*.js',
199 '!<%= yeoman.src %>/scripts/vendor/*',
200 ]
201 }
202 },
203 specs: {
204 options: {
205 globals: {
206 define: false,
207 describe: false,
208 expect: false,
209 it: false,
210 }
211 },
212 files: {
213 src: [
214 'test/specs/**/*.spec.js'
215 ]
216 }
217 }
218 },
219
220 jasmine: {
221 all: {
222 src: '<%= yeoman.src %>/scripts/{,**/}*.js',
223 options: {
224 specs: 'test/specs/**/*.spec.js',
225 template: require('grunt-template-jasmine-istanbul'),
226 templateOptions: jasmineInstanbulTemplateOptions('grunt-template-jasmine-requirejs', jasmineRequirejsTemplateOptions(true))
227 }
228 }
229 },
230
231 // Mocha testing framework configuration options
232 mocha: {
233 all: {
234 options: {
235 run: true,
236 urls: ['http://<%= connect.test.options.hostname %>:<%= connect.test.options.port %>/index.html']
237 }
238 }
239 },
240
241 // Add vendor prefixed styles
242 autoprefixer: {
243 options: {
244 browsers: ['last 1 version']
245 },
246 dist: {
247 files: [{
248 expand: true,
249 cwd: '.tmp/styles/',
250 src: '{,*/}*.css',
251 dest: '.tmp/styles/'
252 }]
253 }
254 },
255
256 // Automatically inject Bower components into the HTML file
257 'bower-install': {
258 app: {
259 html: '<%= yeoman.src %>/index.html',
260 ignorePath: '<%= yeoman.src %>/'
261 }
262 },
263
264 // Renames files for browser caching purposes
265 rev: {
266 dist: {
267 files: {
268 src: [
269 '<%= yeoman.dist %>/scripts/*/**/*.js',
270 '<%= yeoman.dist %>/scripts/!(config)*.js',
271 '<%= yeoman.dist %>/styles/{,*/}*.css',
272 '<%= yeoman.dist %>/images/{,*/}*.{gif,jpeg,jpg,png,webp}',
273 '<%= yeoman.dist %>/styles/fonts/{,*/}*.*'
274 ]
275 }
276 },
277 requireconfig: {
278 files: {
279 src: [
280 '<%= yeoman.dist %>/scripts/config.js'
281 ]
282 }
283 }
284 },
285
286 requirejs: {
287 dist: {
288 options: {
289 done: function(done) {
290 var requireModules = grunt.config('requireModules') || {};
291 var lines = [
292 'require.bundles = (function(bundles) {',
293 ];
294 for (var key in requireModules) {
295 var keyS = JSON.stringify(key);
296 var value = requireModules[key];
297 var included = [];
298 for (var i = 0; i < value.included.length; i++) {
299 var file = value.included[i];
300 if (file.match(/\.js$/)) {
301 included.push(file.substring(0, file.length - 3));
302 }
303 }
304 lines.push('bundles[' + keyS + '] = ' + JSON.stringify(included) + ';');
305 }
306 lines.push('return bundles;');
307 lines.push('})(require.bundles || {});');
308 grunt.file.write('.tmp/scripts/bundles.js', lines.join('\n'));
309 done();
310 },
311 baseUrl: '<%= yeoman.src %>/scripts',
312 mainConfigFile: '<%= yeoman.src %>/scripts/config.js',
313 wrapShim: true,
314 dir: '<%= yeoman.dist %>/scripts',
315 optimize: 'none',
316 removeCombined: true,
317 onModuleBundleComplete: function(data) {
318 if (data.name.slice(0, 'bundles/'.length) === 'bundles/') {
319 var requireModules = grunt.config('requireModules') || {};
320 requireModules[data.name] = data;
321 grunt.config('requireModules', requireModules);
322 }
323 },
324 }
325 },
326 },
327
328 // Reads HTML for usemin blocks to enable smart builds that automatically
329 // concat, minify and revision files. Creates configurations in memory so
330 // additional tasks can operate on them
331 useminPrepare: {
332 options: {
333 dest: '<%= yeoman.dist %>'
334 },
335 html: '<%= yeoman.src %>/index.html'
336 },
337
338 // Performs rewrites based on rev and the useminPrepare configuration
339 usemin: {
340 options: {
341 assetsDirs: ['<%= yeoman.dist %>']
342 },
343 html: ['<%= yeoman.dist %>/{,*/}*.html'],
344 css: ['<%= yeoman.dist %>/styles/{,*/}*.css']
345 },
346
347 // The following *-min tasks produce minified files in the dist folder
348 imagemin: {
349 dist: {
350 files: [{
351 expand: true,
352 cwd: '<%= yeoman.src %>/images',
353 src: '{,*/}*.{gif,jpeg,jpg,png}',
354 dest: '<%= yeoman.dist %>/images'
355 }]
356 }
357 },
358 svgmin: {
359 dist: {
360 files: [{
361 expand: true,
362 cwd: '<%= yeoman.src %>/images',
363 src: '{,*/}*.svg',
364 dest: '<%= yeoman.dist %>/images'
365 }]
366 }
367 },
368 htmlmin: {
369 dist: {
370 options: {
371 collapseBooleanAttributes: true,
372 collapseWhitespace: true,
373 removeAttributeQuotes: true,
374 removeCommentsFromCDATA: true,
375 removeEmptyAttributes: true,
376 removeOptionalTags: true,
377 removeRedundantAttributes: true,
378 useShortDoctype: true
379 },
380 files: [{
381 expand: true,
382 cwd: '<%= yeoman.dist %>',
383 src: '{,*/}*.html',
384 dest: '<%= yeoman.dist %>'
385 }]
386 }
387 },
388
389 // By default, your `index.html`'s <!-- Usemin block --> will take care of
390 // minification. These next options are pre-configured if you do not wish
391 // to use the Usemin blocks.
392 // cssmin: {
393 // dist: {
394 // files: {
395 // '<%= yeoman.dist %>/styles/main.css': [
396 // '.tmp/styles/{,*/}*.css',
397 // '<%= yeoman.src %>/styles/{,*/}*.css'
398 // ]
399 // }
400 // }
401 // },
402 // uglify: {
403 // dist: {
404 // files: {
405 // '<%= yeoman.dist %>/scripts/scripts.js': [
406 // '<%= yeoman.dist %>/scripts/scripts.js'
407 // ]
408 // }
409 // }
410 // },
411 // concat: {
412 // dist: {}
413 // },
414
415 concat: {
416 requireconfig: {
417 }
418 },
419
420 uglify: {
421 dist: {
422 },
423 requireconfig: {
424 files: {
425 '<%= yeoman.dist %>/scripts/config.js': [
426 '<%= yeoman.dist %>/scripts/config.js',
427 '.tmp/scripts/config.js',
428 ],
429 }
430 }
431 },
432
433 // Copies remaining files to places other tasks can use
434 copy: {
435 dist: {
436 files: [{
437 expand: true,
438 dot: true,
439 cwd: '<%= yeoman.src %>',
440 dest: '<%= yeoman.dist %>',
441 src: [
442 '*.{ico,png,txt}',
443 '.htaccess',
444 'images/{,*/}*.webp',
445 '{,*/}*.html',
446 'styles/fonts/{,*/}*.*'
447 ]
448 }]
449 },
450 styles: {
451 expand: true,
452 dot: true,
453 cwd: '<%= yeoman.src %>/styles',
454 dest: '.tmp/styles/',
455 src: '{,*/}*.css'
456 }
457 },
458
459
460 // Run some tasks in parallel to speed up build process
461 concurrent: {
462 server: [
463 'copy:styles'
464 ],
465 test: [
466 'copy:styles'
467 ],
468 dist: [
469 'copy:styles',
470 'imagemin',
471 'svgmin'
472 ]
473 }
474 });
475
476 grunt.loadNpmTasks('grunt-bower-requirejs');
477
478 grunt.registerTask('revconfig', function () {
479 var prefix = grunt.template.process('<%= yeoman.dist %>/scripts/');
480 var pattern = prefix + '**/*.{js,html}';
481 var files = grunt.file.expand(pattern);
482 var lines = [];
483 grunt.util._.each(files, function(file) {
484 file = file.substring(prefix.length);
485 var res = file.match(/^(.*\/)?([0-9a-f]+)\.([^\/]+)\.([^\.]+)$/);
486 if (!res) {
487 return;
488 }
489 //grunt.log.oklns(JSON.stringify(res));
490 var dir = res[1] || '';
491 //var hash = res[2];
492 var base = res[3];
493 var ext = res[4];
494 var id;
495 if (ext === 'js') {
496 id = dir + base;
497 file = file.substring(0, file.length - ext.length - 1);
498 } else if (ext === 'html') {
499 id = 'text!' + dir + base + '.' + ext;
500 }
501 grunt.log.oklns('map: ' + id + ' -> ' + file);
502 lines.push('require.paths[' + JSON.stringify(id) + ']=' + JSON.stringify(file) + ';\n');
503 });
504 grunt.file.write('.tmp/scripts/config.js', lines.join(''));
505 });
506
507 grunt.registerTask('serve', function (target) {
508 if (target === 'dist') {
509 return grunt.task.run(['build', 'connect:dist:keepalive']);
510 }
511
512 grunt.task.run([
513 'clean:server',
514 'concurrent:server',
515 'autoprefixer',
516 'connect:app',
517 'watch'
518 ]);
519 });
520
521 grunt.registerTask('server', function () {
522 grunt.log.warn('The `server` task has been deprecated. Use `grunt serve` to start a server.');
523 grunt.task.run(['serve']);
524 });
525
526 grunt.registerTask('test', function(target) {
527 if (target !== 'watch') {
528 grunt.task.run([
529 'clean:server',
530 'concurrent:test',
531 'autoprefixer',
532 ]);
533 }
534
535 grunt.task.run([
536 'connect:test',
537 'mocha'
538 ]);
539 });
540
541 grunt.registerTask('build', [
542 'clean:dist',
543 'useminPrepare',
544 'requirejs',
545 'concurrent:dist',
546 'autoprefixer',
547 'concat',
548 // 'cssmin',
549 'uglify:dist',
550 'copy:dist',
551 // 'rev:dist',
552 'revconfig',
553 'uglify:requireconfig',
554 // 'rev:requireconfig',
555 'usemin',
556 'htmlmin'
557 ]);
558
559 grunt.registerTask('dist', [
560 'bower',
561 'newer:jshint',
562 // 'test',
563 'build'
564 ]);
565 grunt.registerTask('default', []);
566 };
1 {
2 "name": "backbone-nested-models",
3 "version": "0.0.0",
4 "authors": [
5 "Adam Heath <doogie@brainfood.com>"
6 ],
7 "private": true,
8 "ignore": [
9 "**/.*",
10 "node_modules",
11 "src/lib",
12 "test"
13 ],
14 "dependencies": {
15 "underscore": "~1.6.0",
16 "backbone": "~1.1.0",
17 "backbone-validation": "0.9.1",
18 "requirejs": "~2.1.10"
19 }
20 }
1 {
2 "name": "backbone-nested-models",
3 "version": "0.0.0",
4 "main": [
5 "src/scripts/backbone-nested-models.js"
6 ],
7 "dependencies": {
8 "backbone": "~1.1.0",
9 "backbone-validation": "0.9.1",
10 "requirejs": "~2.1.10"
11 },
12 "devDependencies": {
13 "bower-requirejs": "~0.9.2",
14 "grunt": "~0.4.1",
15 "grunt-contrib-copy": "~0.4.1",
16 "grunt-contrib-concat": "~0.3.0",
17 "grunt-contrib-uglify": "~0.2.0",
18 "grunt-contrib-jshint": "~0.7.0",
19 "grunt-contrib-cssmin": "~0.7.0",
20 "grunt-contrib-connect": "~0.5.0",
21 "grunt-contrib-clean": "~0.5.0",
22 "grunt-contrib-htmlmin": "~0.1.3",
23 "grunt-bower-install": "~0.7.0",
24 "grunt-contrib-imagemin": "~0.2.0",
25 "grunt-contrib-watch": "~0.5.2",
26 "grunt-rev": "~0.1.0",
27 "grunt-autoprefixer": "~0.5.0",
28 "grunt-usemin": "~0.1.10",
29 "grunt-mocha": "~0.4.0",
30 "grunt-newer": "~0.6.0",
31 "grunt-svgmin": "~0.2.0",
32 "grunt-concurrent": "~0.4.0",
33 "load-grunt-tasks": "~0.2.0",
34 "time-grunt": "~0.2.0",
35 "jshint-stylish": "~0.1.3",
36 "grunt-contrib-requirejs": "~0.4.0",
37 "grunt-bower-requirejs": "~0.8.4",
38 "grunt-template-jasmine-istanbul": "~0.2.6",
39 "grunt-template-jasmine-requirejs": "~0.1.10",
40 "grunt-contrib-jasmine": "~0.5.3"
41 },
42 "engines": {
43 "node": ">=0.8.0"
44 }
45 }
46
1 define(
2 [
3 'underscore',
4 'backbone',
5 'backbone-validation',
6 ],
7 function(
8 _,
9 Backbone
10 ) {
11 'use strict';
12
13 function validateNestedValue(attrValue, attrName) {
14 attrValue.validate();
15 var isValid = attrValue.isValid();
16 return isValid ? null : (attrName + ' is invalid');
17 }
18
19 function updateValidation(model) {
20 var oldValidation = model.validation;
21 var allKeys = _.uniq(model.keys().concat(_.keys(oldValidation)));
22 var validation = _.extend({}, oldValidation);
23 var found;
24 var f = function(value) {
25 if (value === validateNestedValue) {
26 found = true;
27 }
28 };
29 for (var i = 0; i < allKeys.length; i++) {
30 var key = allKeys[i];
31 var value = model.get(key);
32 var validators = validation[key];
33 if (validators) {
34 if(_.isArray(validators)) {
35 validators = validators.concat();
36 } else {
37 validators = [validators];
38 }
39 } else {
40 validators = [];
41 }
42 validation[key] = validators;
43 if (value instanceof Backbone.Model) {
44 found = false;
45 _.each(validators, f);
46 if (!found) {
47 validators.push(validateNestedValue);
48 }
49 }
50 }
51 model.validation = validation;
52 return oldValidation;
53 }
54
55 function wrapValidationFunction(modelClass, methodName) {
56 var originalMethod = modelClass.prototype[methodName];
57 modelClass.prototype[methodName] = function() {
58 var oldValidation = updateValidation(this);
59 try {
60 if (originalMethod) {
61 return originalMethod.apply(this, arguments);
62 } else {
63 return modelClass.__super__[methodName].apply(this, arguments);
64 }
65 } finally {
66 this.validation = oldValidation;
67 }
68 };
69 }
70
71 function wrapSetFunction(modelClass) {
72 var originalMethod = modelClass.prototype.set;
73 modelClass.prototype.set = function(key, val, options) {
74 var attr, attrs, curVal, nestedOptions, newVal;
75 if (key === null) {
76 return this;
77 }
78
79 if (typeof key === 'object') {
80 attrs = key;
81 options = val;
82 } else {
83 (attrs = {})[key] = val;
84 }
85 if (options && options.merge) {
86 nestedOptions = {silent: false, merge: true};
87 for (attr in attrs) {
88 curVal = this.get(attr);
89 newVal = attrs[attr];
90 if (curVal instanceof Backbone.Model && newVal instanceof Backbone.Model) {
91 delete attrs[attr];
92 curVal.set(newVal.attributes, nestedOptions);
93 }
94 }
95 }
96 if (originalMethod) {
97 return originalMethod.call(this, attrs, options);
98 } else {
99 return modelClass.__super__.set.call(this, attrs, options);
100 }
101 };
102 }
103
104 function wrapToJSONFunction(modelClass) {
105 var originalMethod = modelClass.prototype.toJSON;
106 modelClass.prototype.toJSON = function(options) {
107 var result;
108 if (originalMethod) {
109 result = originalMethod.apply(this, arguments);
110 } else {
111 result = modelClass.__super__.toJSON.apply(this, arguments);
112 }
113 if (options && options.deep) {
114 _.each(result, function(value, key) {
115 if (value instanceof Backbone.Model) {
116 result[key] = value.toJSON(options);
117 }
118 });
119 }
120 return result;
121 };
122 }
123
124 var NestedModels = {
125 validateNestedValue: validateNestedValue,
126
127 wrapSetFunction: wrapSetFunction,
128 wrapToJSONFunction: wrapToJSONFunction,
129 wrapValidationFunction: wrapValidationFunction,
130
131 mixin: function(modelClass) {
132 wrapSetFunction(modelClass);
133 wrapToJSONFunction(modelClass);
134 wrapValidationFunction(modelClass, 'isValid');
135 wrapValidationFunction(modelClass, 'validate');
136 wrapValidationFunction(modelClass, 'preValidate');
137 return modelClass;
138 },
139 };
140
141 return NestedModels;
142
143 }
144 );
1 /* global require:true */
2 var require;
3 require = (function() {
4 'use strict';
5
6 var require = {
7 baseUrl: 'scripts',
8 shim: {
9
10 },
11 paths: {
12 'backbone-validation': '../lib/backbone-validation/dist/backbone-validation-amd',
13 backbone: '../lib/backbone/backbone',
14 underscore: '../lib/underscore/underscore'
15 }
16 };
17
18 return require;
19 })();
1 define([], {});
1 /* global require */
2 require(
3 [],
4 function() {
5 'use strict';
6 }
7 );