rasputin 0.12.1 → 0.13.1

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rasputin (0.11.3)
4
+ rasputin (0.12.2)
5
5
  actionpack (~> 3.1.0)
6
6
  jquery-rails (~> 1.0)
7
7
  railties (~> 3.1.0)
@@ -34,9 +34,9 @@ GEM
34
34
  jquery-rails (1.0.19)
35
35
  railties (~> 3.0)
36
36
  thor (~> 0.14)
37
- json (1.6.3)
37
+ json (1.6.4)
38
38
  multi_json (1.0.4)
39
- rack (1.3.5)
39
+ rack (1.3.6)
40
40
  rack-cache (1.1)
41
41
  rack (>= 0.4)
42
42
  rack-mount (0.8.3)
@@ -53,7 +53,7 @@ GEM
53
53
  rdoc (~> 3.4)
54
54
  thor (~> 0.14.6)
55
55
  rake (0.9.2.2)
56
- rdoc (3.11)
56
+ rdoc (3.12)
57
57
  json (~> 1.4)
58
58
  sprockets (2.0.3)
59
59
  hike (~> 1.2)
data/README.md CHANGED
@@ -3,20 +3,13 @@ Rasputin
3
3
 
4
4
  This is a gem for integration of Ember.js with Rails 3.1 assets pipeline.
5
5
 
6
- It provide direct requires for official ember packages :
6
+ It provide direct requires for following ember packages :
7
7
 
8
8
  * ember
9
- * ember-datetime
10
9
  * ember-data
11
- * ember-touch
12
- * ember-routing
13
-
14
- And it also provides one unnoficial package :
15
-
16
- * ember-i18n (integration with i18n-js gem)
17
10
 
18
11
  Rasputin also provide sprockets engine for handlebars templates. Any template in your
19
- javascript assets folder with extention handlebars will be availabel in sproutcore.
12
+ javascript assets folder with extention `handlebars` or `hbs` will be availabel in ember.
20
13
 
21
14
  Examples :
22
15
 
@@ -33,6 +26,7 @@ The new default is '/'
33
26
  Precompilation :
34
27
 
35
28
  Starting with 0.9.0 release, Rasputin will precompile your handlebars templates.
29
+ Starting with 0.12.1 release, default behavior is to precompile templates only in production environment.
36
30
  If you do not want this behavior you can tourn it off in your rails configuration block :
37
31
 
38
32
  config.rasputin.precompile_handlebars = false
@@ -48,6 +42,20 @@ It will be translated as :
48
42
  {{view Ember.Button}}OK{{/view}}
49
43
  </script>
50
44
 
45
+ Preprocessor :
46
+
47
+ If you chouse to use "javascript native require" your application.js file will look like this :
48
+
49
+ require('jquery');
50
+ require('ember');
51
+ require('ember-data');
52
+ require('app/**/*');
53
+
54
+ Ther is two new settings :
55
+
56
+ config.rasputin.use_javascript_require = true
57
+ config.rasputin.strip_javascript_require = true
58
+
51
59
  Install
52
60
  -------
53
61
 
@@ -62,11 +70,7 @@ In your javascript asset manifest (app/assets/javascripts/application.js) add th
62
70
 
63
71
  And any of the following you want to include:
64
72
 
65
- //= require ember-datetime
66
73
  //= require ember-data
67
- //= require ember-touch
68
- //= require ember-routing
69
- //= require ember-i18n
70
74
 
71
75
  In your stylesheet asset manifest (app/assets/stylesheets/application.css) add the following:
72
76
 
@@ -77,6 +81,22 @@ In your stylesheet asset manifest (app/assets/stylesheets/application.css) add t
77
81
  ChangeLog
78
82
  ----------
79
83
 
84
+ 0.13.1
85
+
86
+ * fix to ensure rasputin is initialized in all groups (thanks @chrisconley)
87
+ * update ember-data
88
+
89
+ 0.13.0
90
+
91
+ * new preprocessor for "javascript native require" (WIP)
92
+ * remove legacy packages
93
+
94
+ 0.12.1
95
+
96
+ * new precompiler (borrowed from @keithpitt)
97
+ * default behavior is to precompil only in production environment
98
+ * haml filter (thanks @ootoovak)
99
+
80
100
  0.12.0
81
101
 
82
102
  * replace ember-datastore with ember-data
data/lib/rasputin.rb CHANGED
@@ -8,13 +8,19 @@ require "rasputin/handlebars/template"
8
8
  require "rasputin/slim" if defined? Slim
9
9
  require "rasputin/haml" if defined? Haml
10
10
 
11
+ require "rasputin/require_preprocessor"
12
+
11
13
  module Rasputin
12
14
  class Engine < ::Rails::Engine
13
15
  config.rasputin = ActiveSupport::OrderedOptions.new
14
16
  config.rasputin.precompile_handlebars = Rails.env.production?
15
17
  config.rasputin.template_name_separator = '/'
16
18
 
17
- initializer :setup_rasputin do |app|
19
+ config.rasputin.use_javascript_require = true
20
+ config.rasputin.strip_javascript_require = true
21
+
22
+ initializer :setup_rasputin, :group => :all do |app|
23
+ app.assets.register_preprocessor 'application/javascript', Rasputin::RequirePreprocessor
18
24
  app.assets.register_engine '.handlebars', Rasputin::HandlebarsTemplate
19
25
  app.assets.register_engine '.hbs', Rasputin::HandlebarsTemplate
20
26
  end
@@ -0,0 +1,188 @@
1
+
2
+ module Rasputin
3
+ class RequirePreprocessor < Tilt::Template
4
+
5
+ REQUIRE_PATTERN = /require\(\s*['"]([^\)]+)['"]\s*\)\s*;?\s*/
6
+ HEADER_PATTERN = /
7
+ \A (
8
+ (?m:\s*) (
9
+ (\/\* (?m:.*?) \*\/) |
10
+ (\#\#\# (?m:.*?) \#\#\#) |
11
+ (\/\/ .* \n?)+ |
12
+ (\# .* \n?)+ |
13
+ (#{REQUIRE_PATTERN} \n?)+
14
+ )
15
+ )+
16
+ /x
17
+ DIRECTIVE_PATTERN = /^\s*#{REQUIRE_PATTERN}$/
18
+ TREE_PATTERN = /\*\*\/\*$/
19
+ DIRECTORY_PATTERN = /\*$/
20
+
21
+ attr_reader :pathname
22
+ attr_reader :header, :body
23
+
24
+ def prepare
25
+ @pathname = Pathname.new(file)
26
+
27
+ @use_javascript_require = Rails.configuration.rasputin.use_javascript_require
28
+ @strip_javascript_require = Rails.configuration.rasputin.strip_javascript_require
29
+
30
+ if @use_javascript_require
31
+ @header = data[HEADER_PATTERN, 0] || ""
32
+ @body = $' || data
33
+ # Ensure body ends in a new line
34
+ @body += "\n" if @body != "" && @body !~ /\n\Z/m
35
+ else
36
+ @body = data
37
+ end
38
+ end
39
+
40
+ def evaluate(context, locals, &block)
41
+ if @use_javascript_require
42
+ @context = context
43
+ process_directives
44
+
45
+ processed_source
46
+ else
47
+ body
48
+ end
49
+ end
50
+
51
+ def processed_header
52
+ if @use_javascript_require && @strip_javascript_require
53
+ lineno = 0
54
+ @processed_header ||= header.lines.map { |line|
55
+ lineno += 1
56
+ # Replace directive line with a clean break
57
+ directives.assoc(lineno) ? "\n" : line
58
+ }.join.chomp
59
+ else
60
+ @processed_header ||= header.chomp
61
+ end
62
+ end
63
+
64
+ def processed_source
65
+ @processed_source ||= processed_header + "\n" + body
66
+ end
67
+
68
+ def directives
69
+ @directives ||= header.lines.each_with_index.map { |line, index|
70
+ if line =~ DIRECTIVE_PATTERN
71
+ name, path = detect_directive($1)
72
+ if respond_to?("process_#{name}_directive")
73
+ [index + 1, name, path]
74
+ end
75
+ end
76
+ }.compact
77
+ end
78
+
79
+ protected
80
+
81
+ attr_reader :context
82
+
83
+ def process_directives
84
+ directives.each do |line_number, name, path|
85
+ context.__LINE__ = line_number
86
+ send("process_#{name}_directive", path)
87
+ context.__LINE__ = nil
88
+ end
89
+ end
90
+
91
+ def detect_directive(path)
92
+ if path =~ TREE_PATTERN
93
+ return :require_tree, absolute_path_to_directory(path, TREE_PATTERN)
94
+ elsif path =~ DIRECTORY_PATTERN
95
+ return :require_directory, absolute_path_to_directory(path, DIRECTORY_PATTERN)
96
+ else
97
+ return :require_file, path
98
+ end
99
+ end
100
+
101
+ ###
102
+ # Directives implementation
103
+ ###
104
+
105
+ # `path`
106
+ def process_require_file_directive(path)
107
+ if relative?(path)
108
+ # The path must be absolute.
109
+ raise ArgumentError, "require argument must be absolute path"
110
+ else
111
+ context.require_asset(path)
112
+ end
113
+ end
114
+
115
+ # `path/*`
116
+ def process_require_directory_directive(path)
117
+ if relative?(path)
118
+ # The path must be absolute.
119
+ raise ArgumentError, "require_directory argument must be absolute path"
120
+ else
121
+ root = Pathname.new(path)
122
+
123
+ unless (stats = stat(root)) && stats.directory?
124
+ raise ArgumentError, "require_directory argument must be a directory"
125
+ end
126
+
127
+ context.depend_on(root)
128
+
129
+ entries(root).each do |pathname|
130
+ pathname = root.join(pathname)
131
+ if pathname.to_s == self.file
132
+ next
133
+ elsif context.asset_requirable?(pathname)
134
+ context.require_asset(pathname)
135
+ end
136
+ end
137
+ end
138
+ end
139
+
140
+ # `path/**/*`
141
+ def process_require_tree_directive(path)
142
+ if relative?(path)
143
+ # The path must be absolute.
144
+ raise ArgumentError, "require_tree argument must be absolute path"
145
+ else
146
+ root = Pathname.new(path)
147
+
148
+ unless (stats = stat(root)) && stats.directory?
149
+ raise ArgumentError, "require_tree argument must be a directory"
150
+ end
151
+
152
+ context.depend_on(root)
153
+
154
+ each_entry(root) do |pathname|
155
+ if pathname.to_s == self.file
156
+ next
157
+ elsif stat(pathname).directory?
158
+ context.depend_on(pathname)
159
+ elsif context.asset_requirable?(pathname)
160
+ context.require_asset(pathname)
161
+ end
162
+ end
163
+ end
164
+ end
165
+
166
+ private
167
+
168
+ def absolute_path_to_directory(path, pattern)
169
+ File.join(context.root_path, path.gsub(pattern, ''))
170
+ end
171
+
172
+ def relative?(path)
173
+ path =~ /^\.($|\.?\/)/
174
+ end
175
+
176
+ def stat(path)
177
+ context.environment.stat(path)
178
+ end
179
+
180
+ def entries(path)
181
+ context.environment.entries(path)
182
+ end
183
+
184
+ def each_entry(root, &block)
185
+ context.environment.each_entry(root, &block)
186
+ end
187
+ end
188
+ end
@@ -1,3 +1,3 @@
1
1
  module Rasputin
2
- VERSION = "0.12.1"
2
+ VERSION = "0.13.1"
3
3
  end
@@ -9,33 +9,33 @@ window.DS = SC.Namespace.create();
9
9
  DS.Adapter = SC.Object.extend({
10
10
  commit: function(store, commitDetails) {
11
11
  commitDetails.updated.eachType(function(type, array) {
12
- this.updateMany(store, type, array.slice());
12
+ this.updateRecords(store, type, array.slice());
13
13
  }, this);
14
14
 
15
15
  commitDetails.created.eachType(function(type, array) {
16
- this.createMany(store, type, array.slice());
16
+ this.createRecords(store, type, array.slice());
17
17
  }, this);
18
18
 
19
19
  commitDetails.deleted.eachType(function(type, array) {
20
- this.deleteMany(store, type, array.slice());
20
+ this.deleteRecords(store, type, array.slice());
21
21
  }, this);
22
22
  },
23
23
 
24
- createMany: function(store, type, models) {
24
+ createRecords: function(store, type, models) {
25
25
  models.forEach(function(model) {
26
- this.create(store, type, model);
26
+ this.createRecord(store, type, model);
27
27
  }, this);
28
28
  },
29
29
 
30
- updateMany: function(store, type, models) {
30
+ updateRecords: function(store, type, models) {
31
31
  models.forEach(function(model) {
32
- this.update(store, type, model);
32
+ this.updateRecord(store, type, model);
33
33
  }, this);
34
34
  },
35
35
 
36
- deleteMany: function(store, type, models) {
36
+ deleteRecords: function(store, type, models) {
37
37
  models.forEach(function(model) {
38
- this.deleteModel(store, type, model);
38
+ this.deleteRecord(store, type, model);
39
39
  }, this);
40
40
  },
41
41
 
@@ -84,10 +84,10 @@ DS.ModelArray = SC.ArrayProxy.extend({
84
84
  },
85
85
 
86
86
  arrayDidChange: function(array, index, removed, added) {
87
- this._super(array, index, removed, added);
88
-
89
87
  var modelCache = get(this, 'modelCache');
90
88
  modelCache.replace(index, 0, Array(added));
89
+
90
+ this._super(array, index, removed, added);
91
91
  },
92
92
 
93
93
  arrayWillChange: function(array, index, removed, added) {
@@ -145,6 +145,196 @@ DS.AdapterPopulatedModelArray = DS.ModelArray.extend({
145
145
  })({});
146
146
 
147
147
 
148
+ (function(exports) {
149
+ var get = Ember.get, set = Ember.set, getPath = Ember.getPath, fmt = Ember.String.fmt;
150
+
151
+ var OrderedSet = SC.Object.extend({
152
+ init: function() {
153
+ this.clear();
154
+ },
155
+
156
+ clear: function() {
157
+ this.set('presenceSet', {});
158
+ this.set('list', SC.NativeArray.apply([]));
159
+ },
160
+
161
+ add: function(obj) {
162
+ var guid = SC.guidFor(obj),
163
+ presenceSet = get(this, 'presenceSet'),
164
+ list = get(this, 'list');
165
+
166
+ if (guid in presenceSet) { return; }
167
+
168
+ presenceSet[guid] = true;
169
+ list.pushObject(obj);
170
+ },
171
+
172
+ remove: function(obj) {
173
+ var guid = SC.guidFor(obj),
174
+ presenceSet = get(this, 'presenceSet'),
175
+ list = get(this, 'list');
176
+
177
+ delete presenceSet[guid];
178
+ list.removeObject(obj);
179
+ },
180
+
181
+ isEmpty: function() {
182
+ return getPath(this, 'list.length') === 0;
183
+ },
184
+
185
+ forEach: function(fn, self) {
186
+ get(this, 'list').forEach(function(item) {
187
+ fn.call(self, item);
188
+ });
189
+ },
190
+
191
+ toArray: function() {
192
+ return get(this, 'list').slice();
193
+ }
194
+ });
195
+
196
+ /**
197
+ A Hash stores values indexed by keys. Unlike JavaScript's
198
+ default Objects, the keys of a Hash can be any JavaScript
199
+ object.
200
+
201
+ Internally, a Hash has two data structures:
202
+
203
+ `keys`: an OrderedSet of all of the existing keys
204
+ `values`: a JavaScript Object indexed by the
205
+ Ember.guidFor(key)
206
+
207
+ When a key/value pair is added for the first time, we
208
+ add the key to the `keys` OrderedSet, and create or
209
+ replace an entry in `values`. When an entry is deleted,
210
+ we delete its entry in `keys` and `values`.
211
+ */
212
+
213
+ var Hash = SC.Object.extend({
214
+ init: function() {
215
+ set(this, 'keys', OrderedSet.create());
216
+ set(this, 'values', {});
217
+ },
218
+
219
+ add: function(key, value) {
220
+ var keys = get(this, 'keys'), values = get(this, 'values');
221
+ var guid = Ember.guidFor(key);
222
+
223
+ keys.add(key);
224
+ values[guid] = value;
225
+
226
+ return value;
227
+ },
228
+
229
+ remove: function(key) {
230
+ var keys = get(this, 'keys'), values = get(this, 'values');
231
+ var guid = Ember.guidFor(key), value;
232
+
233
+ keys.remove(key);
234
+
235
+ value = values[guid];
236
+ delete values[guid];
237
+
238
+ return value;
239
+ },
240
+
241
+ fetch: function(key) {
242
+ var values = get(this, 'values');
243
+ var guid = Ember.guidFor(key);
244
+
245
+ return values[guid];
246
+ },
247
+
248
+ forEach: function(fn, binding) {
249
+ var keys = get(this, 'keys'), values = get(this, 'values');
250
+
251
+ keys.forEach(function(key) {
252
+ var guid = Ember.guidFor(key);
253
+ fn.call(binding, key, values[guid]);
254
+ });
255
+ }
256
+ });
257
+
258
+ DS.Transaction = Ember.Object.extend({
259
+ init: function() {
260
+ set(this, 'dirty', {
261
+ created: Hash.create(),
262
+ updated: Hash.create(),
263
+ deleted: Hash.create()
264
+ });
265
+ },
266
+
267
+ createRecord: function(type, hash) {
268
+ var store = get(this, 'store');
269
+
270
+ return store.createRecord(type, hash, this);
271
+ },
272
+
273
+ add: function(model) {
274
+ var modelTransaction = get(model, 'transaction');
275
+ ember_assert("Models cannot belong to more than one transaction at a time.", !modelTransaction);
276
+
277
+ set(model, 'transaction', this);
278
+ },
279
+
280
+ modelBecameDirty: function(kind, model) {
281
+ var dirty = get(get(this, 'dirty'), kind),
282
+ type = model.constructor;
283
+
284
+ var models = dirty.fetch(type);
285
+
286
+ models = models || dirty.add(type, OrderedSet.create());
287
+ models.add(model);
288
+ },
289
+
290
+ modelBecameClean: function(kind, model) {
291
+ var dirty = get(get(this, 'dirty'), kind),
292
+ type = model.constructor;
293
+
294
+ var models = dirty.fetch(type);
295
+ models.remove(model);
296
+
297
+ set(model, 'transaction', null);
298
+ },
299
+
300
+ commit: function() {
301
+ var dirtyMap = get(this, 'dirty');
302
+
303
+ var iterate = function(kind, fn, binding) {
304
+ var dirty = get(dirtyMap, kind);
305
+
306
+ dirty.forEach(function(type, models) {
307
+ if (models.isEmpty()) { return; }
308
+
309
+ models.forEach(function(model) { model.willCommit(); });
310
+ fn.call(binding, type, models.toArray());
311
+ });
312
+ };
313
+
314
+ var commitDetails = {
315
+ updated: {
316
+ eachType: function(fn, binding) { iterate('updated', fn, binding); }
317
+ },
318
+
319
+ created: {
320
+ eachType: function(fn, binding) { iterate('created', fn, binding); }
321
+ },
322
+
323
+ deleted: {
324
+ eachType: function(fn, binding) { iterate('deleted', fn, binding); }
325
+ }
326
+ };
327
+
328
+ var store = get(this, 'store');
329
+ var adapter = get(store, '_adapter');
330
+ if (adapter && adapter.commit) { adapter.commit(store, commitDetails); }
331
+ else { throw fmt("Adapter is either null or do not implement `commit` method", this); }
332
+ }
333
+ });
334
+
335
+ })({});
336
+
337
+
148
338
  (function(exports) {
149
339
  var get = SC.get, set = SC.set, getPath = SC.getPath, fmt = SC.String.fmt;
150
340
 
@@ -245,13 +435,15 @@ DS.Store = SC.Object.extend({
245
435
  set(this, 'models', []);
246
436
  set(this, 'modelArrays', []);
247
437
  set(this, 'modelArraysByClientId', {});
248
- set(this, 'updatedTypes', OrderedSet.create());
249
- set(this, 'createdTypes', OrderedSet.create());
250
- set(this, 'deletedTypes', OrderedSet.create());
438
+ set(this, 'defaultTransaction', DS.Transaction.create({ store: this }));
251
439
 
252
440
  return this._super();
253
441
  },
254
442
 
443
+ transaction: function() {
444
+ return DS.Transaction.create({ store: this });
445
+ },
446
+
255
447
  modelArraysForClientId: function(clientId) {
256
448
  var modelArrays = get(this, 'modelArraysByClientId');
257
449
  var ret = modelArrays[clientId];
@@ -287,23 +479,29 @@ DS.Store = SC.Object.extend({
287
479
  // . CREATE NEW MODEL .
288
480
  // ....................
289
481
 
290
- create: function(type, hash) {
482
+ createRecord: function(type, hash, transaction) {
291
483
  hash = hash || {};
292
484
 
293
485
  var id = hash[getPath(type, 'proto.primaryKey')] || null;
294
486
 
295
- var model = type.create({ data: hash || {}, store: this });
487
+ var model = type.create({
488
+ data: hash || {},
489
+ store: this,
490
+ transaction: transaction
491
+ });
492
+
296
493
  model.adapterDidCreate();
297
494
 
298
495
  var data = this.clientIdToHashMap(type);
299
496
  var models = get(this, 'models');
300
497
 
301
498
  var clientId = this.pushHash(hash, id, type);
302
- this.updateModelArrays(type, clientId, hash);
303
499
 
304
500
  set(model, 'clientId', clientId);
305
501
 
306
- get(this, 'models')[clientId] = model;
502
+ models[clientId] = model;
503
+
504
+ this.updateModelArrays(type, clientId, hash);
307
505
 
308
506
  return model;
309
507
  },
@@ -312,8 +510,8 @@ DS.Store = SC.Object.extend({
312
510
  // . DELETE MODEL .
313
511
  // ................
314
512
 
315
- deleteModel: function(model) {
316
- model.deleteModel();
513
+ deleteRecord: function(model) {
514
+ model.deleteRecord();
317
515
  },
318
516
 
319
517
  // ...............
@@ -466,126 +664,27 @@ DS.Store = SC.Object.extend({
466
664
  this.updateModelArrays(type, clientId, hash);
467
665
  },
468
666
 
469
-
470
- // Internally, the store keeps two data structures representing
471
- // the dirty models.
472
- //
473
- // It holds an OrderedSet of all of the dirty types and a Hash
474
- // keyed off of the guid of each type.
475
- //
476
- // Assuming that Ember.guidFor(Person) is 'sc1', guidFor(Place)
477
- // is 'sc2', and guidFor(Thing) is 'sc3', the structure will look
478
- // like:
479
- //
480
- // store: {
481
- // updatedTypes: [ Person, Place, Thing ],
482
- // updatedModels: {
483
- // sc1: [ person1, person2, person3 ],
484
- // sc2: [ place1 ],
485
- // sc3: [ thing1, thing2 ]
486
- // }
487
- // }
488
- //
489
- // Adapters receive an iterator that they can use to retrieve the
490
- // type and array at the same time:
491
- //
492
- // adapter: {
493
- // commit: function(store, commitDetails) {
494
- // commitDetails.updated.eachType(function(type, array) {
495
- // // this callback will be invoked three times:
496
- // //
497
- // // 1. Person, [ person1, person2, person3 ]
498
- // // 2. Place, [ place1 ]
499
- // // 3. Thing, [ thing1, thing2 ]
500
- // }
501
- // }
502
- // }
503
- //
504
- // This encapsulates the internal structure and presents it to the
505
- // adapter as if it was a regular Hash with types as keys and dirty
506
- // models as values.
507
- //
508
- // Note that there is a pair of *Types and *Models for each of
509
- // `created`, `updated` and `deleted`. These correspond with the
510
- // commitDetails passed into the adapter's commit method.
511
-
512
- modelBecameDirty: function(kind, model) {
513
- var dirtyTypes = get(this, kind + 'Types'), type = model.constructor;
514
- dirtyTypes.add(type);
515
-
516
- var dirtyModels = this.typeMap(type)[kind + 'Models'];
517
- dirtyModels.add(model);
518
- },
519
-
520
- modelBecameClean: function(kind, model) {
521
- var dirtyTypes = get(this, kind + 'Types'), type = model.constructor;
522
-
523
- var dirtyModels = this.typeMap(type)[kind + 'Models'];
524
- dirtyModels.remove(model);
525
-
526
- if (dirtyModels.isEmpty()) {
527
- dirtyTypes.remove(type);
528
- }
529
- },
530
-
531
- eachDirtyType: function(kind, fn, self) {
532
- var types = get(this, kind + 'Types'), dirtyModels;
533
-
534
- types.forEach(function(type) {
535
- dirtyModels = this.typeMap(type)[kind + 'Models'];
536
- fn.call(self, type, get(dirtyModels, 'list'));
537
- }, this);
538
- },
539
-
540
667
  // ..............
541
668
  // . PERSISTING .
542
669
  // ..............
543
670
 
544
671
  commit: function() {
545
- var self = this;
546
-
547
- var iterate = function(kind, fn, binding) {
548
- self.eachDirtyType(kind, function(type, models) {
549
- models.forEach(function(model) {
550
- model.willCommit();
551
- });
552
-
553
- fn.call(binding, type, models);
554
- });
555
- };
556
-
557
- var commitDetails = {
558
- updated: {
559
- eachType: function(fn, binding) { iterate('updated', fn, binding); }
560
- },
561
-
562
- created: {
563
- eachType: function(fn, binding) { iterate('created', fn, binding); }
564
- },
565
-
566
- deleted: {
567
- eachType: function(fn, binding) { iterate('deleted', fn, binding); }
568
- }
569
- };
570
-
571
- var adapter = get(this, '_adapter');
572
- if (adapter && adapter.commit) { adapter.commit(this, commitDetails); }
573
- else { throw fmt("Adapter is either null or do not implement `commit` method", this); }
672
+ get(this, 'defaultTransaction').commit();
574
673
  },
575
674
 
576
- didUpdateModels: function(array, hashes) {
675
+ didUpdateRecords: function(array, hashes) {
577
676
  if (arguments.length === 2) {
578
677
  array.forEach(function(model, idx) {
579
- this.didUpdateModel(model, hashes[idx]);
678
+ this.didUpdateRecord(model, hashes[idx]);
580
679
  }, this);
581
680
  } else {
582
681
  array.forEach(function(model) {
583
- this.didUpdateModel(model);
682
+ this.didUpdateRecord(model);
584
683
  }, this);
585
684
  }
586
685
  },
587
686
 
588
- didUpdateModel: function(model, hash) {
687
+ didUpdateRecord: function(model, hash) {
589
688
  if (arguments.length === 2) {
590
689
  var clientId = get(model, 'clientId');
591
690
  var data = this.clientIdToHashMap(model.constructor);
@@ -597,17 +696,17 @@ DS.Store = SC.Object.extend({
597
696
  model.adapterDidUpdate();
598
697
  },
599
698
 
600
- didDeleteModels: function(array) {
699
+ didDeleteRecords: function(array) {
601
700
  array.forEach(function(model) {
602
701
  model.adapterDidDelete();
603
702
  });
604
703
  },
605
704
 
606
- didDeleteModel: function(model) {
705
+ didDeleteRecord: function(model) {
607
706
  model.adapterDidDelete();
608
707
  },
609
708
 
610
- didCreateModels: function(type, array, hashes) {
709
+ didCreateRecords: function(type, array, hashes) {
611
710
  var id, clientId, primaryKey = getPath(type, 'proto.primaryKey');
612
711
 
613
712
  var idToClientIdMap = this.idToClientIdMap(type);
@@ -629,7 +728,7 @@ DS.Store = SC.Object.extend({
629
728
  }
630
729
  },
631
730
 
632
- didCreateModel: function(model, hash) {
731
+ didCreateRecord: function(model, hash) {
633
732
  var type = model.constructor;
634
733
 
635
734
  var id, clientId, primaryKey = getPath(type, 'proto.primaryKey');
@@ -650,6 +749,10 @@ DS.Store = SC.Object.extend({
650
749
  model.adapterDidUpdate();
651
750
  },
652
751
 
752
+ recordWasInvalid: function(record, errors) {
753
+ record.wasInvalid(errors);
754
+ },
755
+
653
756
  // ................
654
757
  // . MODEL ARRAYS .
655
758
  // ................
@@ -753,10 +856,7 @@ DS.Store = SC.Object.extend({
753
856
  idToCid: {},
754
857
  idList: [],
755
858
  cidList: [],
756
- cidToHash: {},
757
- updatedModels: OrderedSet.create(),
758
- createdModels: OrderedSet.create(),
759
- deletedModels: OrderedSet.create()
859
+ cidToHash: {}
760
860
  });
761
861
  }
762
862
  },
@@ -931,7 +1031,8 @@ DS.State = SC.State.extend({
931
1031
  isSaving: stateProperty,
932
1032
  isDeleted: stateProperty,
933
1033
  isError: stateProperty,
934
- isNew: stateProperty
1034
+ isNew: stateProperty,
1035
+ isValid: stateProperty
935
1036
  });
936
1037
 
937
1038
  var cantLoadData = function() {
@@ -939,6 +1040,99 @@ var cantLoadData = function() {
939
1040
  throw "You cannot load data into the store when its associated model is in its current state";
940
1041
  };
941
1042
 
1043
+ var isEmptyObject = function(obj) {
1044
+ for (var prop in obj) {
1045
+ if (!obj.hasOwnProperty(prop)) { continue; }
1046
+ return false;
1047
+ }
1048
+
1049
+ return true;
1050
+ };
1051
+
1052
+ var setProperty = function(manager, context) {
1053
+ var key = context.key, value = context.value;
1054
+
1055
+ var model = get(manager, 'model'), type = model.constructor;
1056
+ var store = get(model, 'store');
1057
+ var data = get(model, 'data');
1058
+
1059
+ data[key] = value;
1060
+
1061
+ if (store) { store.hashWasUpdated(type, get(model, 'clientId')); }
1062
+ };
1063
+
1064
+ // several states share extremely common functionality, so we are factoring
1065
+ // them out into a common class.
1066
+ var DirtyState = DS.State.extend({
1067
+ // these states are virtually identical except that
1068
+ // they (thrice) use their states name explicitly.
1069
+ //
1070
+ // child classes implement stateName.
1071
+ stateName: null,
1072
+ isDirty: true,
1073
+ willLoadData: cantLoadData,
1074
+
1075
+ enter: function(manager) {
1076
+ var stateName = get(this, 'stateName'),
1077
+ model = get(manager, 'model');
1078
+
1079
+ model.withTransaction(function (t) {
1080
+ t.modelBecameDirty(stateName, model);
1081
+ });
1082
+ },
1083
+
1084
+ exit: function(manager) {
1085
+ var stateName = get(this, 'stateName'),
1086
+ model = get(manager, 'model');
1087
+
1088
+ this.notifyModel(model);
1089
+
1090
+ model.withTransaction(function (t) {
1091
+ t.modelBecameClean(stateName, model);
1092
+ });
1093
+ },
1094
+
1095
+ setProperty: setProperty,
1096
+
1097
+ willCommit: function(manager) {
1098
+ manager.goToState('saving');
1099
+ },
1100
+
1101
+ saving: DS.State.extend({
1102
+ isSaving: true,
1103
+
1104
+ didUpdate: function(manager) {
1105
+ manager.goToState('loaded');
1106
+ },
1107
+
1108
+ wasInvalid: function(manager, errors) {
1109
+ var model = get(manager, 'model');
1110
+
1111
+ set(model, 'errors', errors);
1112
+ manager.goToState('invalid');
1113
+ }
1114
+ }),
1115
+
1116
+ invalid: DS.State.extend({
1117
+ isValid: false,
1118
+
1119
+ setProperty: function(manager, context) {
1120
+ setProperty(manager, context);
1121
+
1122
+ var stateName = getPath(this, 'parentState.stateName'),
1123
+ model = get(manager, 'model'),
1124
+ errors = get(model, 'errors'),
1125
+ key = context.key;
1126
+
1127
+ delete errors[key];
1128
+
1129
+ if (isEmptyObject(errors)) {
1130
+ manager.goToState(stateName);
1131
+ }
1132
+ }
1133
+ })
1134
+ });
1135
+
942
1136
  var states = {
943
1137
  rootState: SC.State.create({
944
1138
  isLoaded: false,
@@ -947,6 +1141,7 @@ var states = {
947
1141
  isDeleted: false,
948
1142
  isError: false,
949
1143
  isNew: false,
1144
+ isValid: true,
950
1145
 
951
1146
  willLoadData: cantLoadData,
952
1147
 
@@ -988,16 +1183,7 @@ var states = {
988
1183
  willLoadData: SC.K,
989
1184
 
990
1185
  setProperty: function(manager, context) {
991
- var key = context.key, value = context.value;
992
-
993
- var model = get(manager, 'model'), type = model.constructor;
994
- var store = get(model, 'store');
995
- var data = get(model, 'data');
996
-
997
- data[key] = value;
998
-
999
- if (store) { store.hashWasUpdated(type, get(model, 'clientId')); }
1000
-
1186
+ setProperty(manager, context);
1001
1187
  manager.goToState('updated');
1002
1188
  },
1003
1189
 
@@ -1005,83 +1191,21 @@ var states = {
1005
1191
  manager.goToState('deleted');
1006
1192
  },
1007
1193
 
1008
- created: DS.State.create({
1194
+ created: DirtyState.create({
1195
+ stateName: 'created',
1009
1196
  isNew: true,
1010
- isDirty: true,
1011
-
1012
- enter: function(manager) {
1013
- var model = get(manager, 'model');
1014
- var store = get(model, 'store');
1015
-
1016
- if (store) { store.modelBecameDirty('created', model); }
1017
- },
1018
-
1019
- exit: function(manager) {
1020
- var model = get(manager, 'model');
1021
- var store = get(model, 'store');
1022
1197
 
1198
+ notifyModel: function(model) {
1023
1199
  model.didCreate();
1024
-
1025
- if (store) { store.modelBecameClean('created', model); }
1026
- },
1027
-
1028
- setProperty: function(manager, context) {
1029
- var key = context.key, value = context.value;
1030
-
1031
- var model = get(manager, 'model'), type = model.constructor;
1032
- var store = get(model, 'store');
1033
- var data = get(model, 'data');
1034
-
1035
- data[key] = value;
1036
-
1037
- if (store) { store.hashWasUpdated(type, get(model, 'clientId')); }
1038
- },
1039
-
1040
- willCommit: function(manager) {
1041
- manager.goToState('saving');
1042
- },
1043
-
1044
- saving: DS.State.create({
1045
- isSaving: true,
1046
-
1047
- didUpdate: function(manager) {
1048
- manager.goToState('loaded');
1049
- }
1050
- })
1200
+ }
1051
1201
  }),
1052
1202
 
1053
- updated: DS.State.create({
1054
- isDirty: true,
1055
-
1056
- willLoadData: cantLoadData,
1057
-
1058
- enter: function(manager) {
1059
- var model = get(manager, 'model');
1060
- var store = get(model, 'store');
1061
-
1062
- if (store) { store.modelBecameDirty('updated', model); }
1063
- },
1064
-
1065
- willCommit: function(manager) {
1066
- manager.goToState('saving');
1067
- },
1068
-
1069
- exit: function(manager) {
1070
- var model = get(manager, 'model');
1071
- var store = get(model, 'store');
1203
+ updated: DirtyState.create({
1204
+ stateName: 'updated',
1072
1205
 
1206
+ notifyModel: function(model) {
1073
1207
  model.didUpdate();
1074
-
1075
- if (store) { store.modelBecameClean('updated', model); }
1076
- },
1077
-
1078
- saving: DS.State.create({
1079
- isSaving: true,
1080
-
1081
- didUpdate: function(manager) {
1082
- manager.goToState('loaded');
1083
- }
1084
- })
1208
+ }
1085
1209
  })
1086
1210
  }),
1087
1211
 
@@ -1098,8 +1222,11 @@ var states = {
1098
1222
 
1099
1223
  if (store) {
1100
1224
  store.removeFromModelArrays(model);
1101
- store.modelBecameDirty('deleted', model);
1102
1225
  }
1226
+
1227
+ model.withTransaction(function(t) {
1228
+ t.modelBecameDirty('deleted', model);
1229
+ });
1103
1230
  },
1104
1231
 
1105
1232
  willCommit: function(manager) {
@@ -1115,9 +1242,10 @@ var states = {
1115
1242
 
1116
1243
  exit: function(stateManager) {
1117
1244
  var model = get(stateManager, 'model');
1118
- var store = get(model, 'store');
1119
1245
 
1120
- store.modelBecameClean('deleted', model);
1246
+ model.withTransaction(function(t) {
1247
+ t.modelBecameClean('deleted', model);
1248
+ });
1121
1249
  }
1122
1250
  }),
1123
1251
 
@@ -1149,11 +1277,15 @@ DS.Model = SC.Object.extend({
1149
1277
  isDeleted: retrieveFromCurrentState,
1150
1278
  isError: retrieveFromCurrentState,
1151
1279
  isNew: retrieveFromCurrentState,
1280
+ isValid: retrieveFromCurrentState,
1152
1281
 
1153
1282
  clientId: null,
1154
1283
 
1284
+ // because unknownProperty is used, any internal property
1285
+ // must be initialized here.
1155
1286
  primaryKey: 'id',
1156
1287
  data: null,
1288
+ transaction: null,
1157
1289
 
1158
1290
  didLoad: Ember.K,
1159
1291
  didUpdate: Ember.K,
@@ -1168,6 +1300,12 @@ DS.Model = SC.Object.extend({
1168
1300
  stateManager.goToState('empty');
1169
1301
  },
1170
1302
 
1303
+ withTransaction: function(fn) {
1304
+ var transaction = get(this, 'transaction') || getPath(this, 'store.defaultTransaction');
1305
+
1306
+ if (transaction) { fn(transaction); }
1307
+ },
1308
+
1171
1309
  setData: function(data) {
1172
1310
  var stateManager = get(this, 'stateManager');
1173
1311
  stateManager.send('setData', data);
@@ -1178,11 +1316,16 @@ DS.Model = SC.Object.extend({
1178
1316
  stateManager.send('setProperty', { key: key, value: value });
1179
1317
  },
1180
1318
 
1181
- "deleteModel": function() {
1319
+ deleteRecord: function() {
1182
1320
  var stateManager = get(this, 'stateManager');
1183
1321
  stateManager.send('delete');
1184
1322
  },
1185
1323
 
1324
+ destroy: function() {
1325
+ this.deleteRecord();
1326
+ this._super();
1327
+ },
1328
+
1186
1329
  loadingData: function() {
1187
1330
  var stateManager = get(this, 'stateManager');
1188
1331
  stateManager.send('loadingData');
@@ -1213,6 +1356,11 @@ DS.Model = SC.Object.extend({
1213
1356
  stateManager.send('didDelete');
1214
1357
  },
1215
1358
 
1359
+ wasInvalid: function(errors) {
1360
+ var stateManager = get(this, 'stateManager');
1361
+ stateManager.send('wasInvalid', errors);
1362
+ },
1363
+
1216
1364
  unknownProperty: function(key) {
1217
1365
  var data = get(this, 'data');
1218
1366
 
@@ -1332,7 +1480,7 @@ DS.attr.transforms = {
1332
1480
 
1333
1481
  if (type === "string" || type === "number") {
1334
1482
  return new Date(serialized);
1335
- } else if (serialized == null) {
1483
+ } else if (serialized === null || serialized === undefined) {
1336
1484
  // if the value is not present in the data,
1337
1485
  // return undefined, not null.
1338
1486
  return serialized;