-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathdirtyable.js
271 lines (231 loc) · 7.62 KB
/
dirtyable.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
// dirtyable may be freely distributed under the MIT license.
// for `property_will_change()` to work correctly on arrays and objects and
// such, the value stored as the original needs to be a clone of the value
// before the change. This is used for that.
var clone = function(obj) {
if (obj !== Object(obj))
return obj;
if (Array.isArray(obj))
return obj.slice();
var out = {},
prop;
for (prop in obj)
out[prop] = obj[prop];
return out;
};
var extend = function(object, keys) {
//
// ## Initial Setup ##
// Initial type checking. Provide decent error messages here instead of
// confisuing ones later down the line.
//
if (typeof arguments[0] !== 'object')
throw new Error('First argument to dirtyable.extend needs to be an object.');
if (!Array.isArray(arguments[1]))
throw new Error('Second argument to dirtyable.extend needs to be an array.');
//
// `data` is used as a hash of replaced properties to their values. It is
// populated with the original data right before it's replaced.
//
var data = {},
//
// When properties are changed, their original values are stored here
// in the format of `{ property: original }`
//
changed_properties = {};
// Allow use of `object.changed_properties.clear()`
Object.defineProperty(changed_properties, 'clear', {
enumerable: false,
value: function() {
for (var i in changed_properties)
delete changed_properties[i];
}
});
// ## Object-level property getters ##
//
// Handler for `object.isChanged`
//
// This returns true if any tracked values on the object have been changed.
//
var object_isChanged = function() {
return Object.keys(changed_properties).length > 0;
};
//
// Handler for `object.changed`
//
// This returns an array of all changed properties.
// For example:
//
// object.foo = 'made dirty'
// object.changed # => ['foo']
//
var object_changed = function() {
return Object.keys(changed_properties);
};
//
// Handler for `object.changes`
//
// This returns an object that shows the changes for all properties.
// For example:
//
// object.bar # => 'clean'
// object.bar = 'dirty'
// object.changes # => { 'bar': ['clean', 'dirty'] }
//
var object_changes = function() {
var out = {};
for (var i in changed_properties)
out[i] = [changed_properties[i], data[i]];
return out;
}
// ## Property-level property getters (and setter) ##
//
// Handler for `object.property`
//
var property_get = function(property) {
return data[property];
};
//
// Handler for `object.property = value`
//
var property_set = function(property, value) {
if (data[property] == value)
return;
// Setting a value back to its original value "cleans" it.
if (changed_properties[property] == value) {
delete changed_properties[property];
} else {
changed_properties[property] = data[property];
}
data[property] = value;
};
//
// Handler for `object.property_isChanged
//
// like `object.isChanged`, but specific to one property.
//
var property_isChanged = function(property) {
return typeof changed_properties[property] !== 'undefined';
};
//
// Handler for `object.property_was`
//
// Returns the value of the property before it was changed.
//
var property_was = function(property) {
return property_isChanged(property)
? changed_properties[property]
: void(0); // TODO: What does rails do here?
}
//
// Handler for `object.property_change`
//
// Like `object.changes`, but for a single property.
// For example:
//
// object.foo # => 'bar'
// object.foo = baz;
// object.foo_change # => ['bar', 'baz']
//
var property_change = function(property) {
return property_isChanged(property)
? [changed_properties[property], data[property]]
: void(0); // TODO: What does rails do here?
}
//
// Handler for `object.property_will_change`
//
// This is how you mark something like an array as changed. For example:
//
// object.foo # => []
// object.foo.push(1)
// object.foo_isChanged # => false
//
// `foo` has not been marked as changed because `foo` was not set to a new
// value. Rather, the object that is `foo` itself changed. To track changes,
// call property_will_change() before the change happens. For example:
//
// object.foo # => []
// object.foo_will_change()
// object.foo.push(1);
// object.foo_isChanged # => true
// object.foo_change # => [[], [1]]
//
var property_will_change = function(property) {
if (property_isChanged(property))
return;
changed_properties[property] = clone(data[[property]]);
};
//
// Handler for `object.reset_property`
//
var reset_property = function(property) {
if (!property_isChanged(property))
return;
property_set(property, changed_properties[property]);
};
// ## Object modification starts here##
// ### Define object-level properties.
Object.defineProperty(object, 'isChanged', {
enumerable: false,
get: object_isChanged
});
Object.defineProperty(object, 'changed', {
enumerable: false,
get: object_changed
})
Object.defineProperty(object, 'changes', {
enumerable: false,
get: object_changes
});
Object.defineProperty(object, 'changedProperties', {
enumerable: false,
value: changed_properties
})
//
// ### Define property-level properties.
//
// This is the magic part. It wraps all properties on the object
// with getters and setters that keep track of changes.
//
keys.forEach(function(property) {
// Replace the property with getters and setters.
data[property] = object[property];
// Override property with our magic getters and setters.
Object.defineProperty(object, property, {
enumerable: true,
get: property_get.bind(null, property),
set: property_set.bind(null, property)
});
//
// ### Define the prefixed and suffixed properties.
//
// Define `object.property_isChanged`
Object.defineProperty(object, property + '_isChanged', {
enumerable: false,
get: property_isChanged.bind(null, property)
});
// Define `object.property_was`
Object.defineProperty(object, property + '_was', {
enumerable: false,
get: property_was.bind(null, property)
});
// Define `object.property_change`
Object.defineProperty(object, property + '_change', {
enumerable: false,
get: property_change.bind(null, property)
});
// Define `object.property_will_change`
Object.defineProperty(object, property + '_will_change', {
enumerable: false,
value: property_will_change.bind(null, property)
});
// Define `object.property_change`
Object.defineProperty(object, 'reset_' + property, {
enumerable: false,
value: reset_property.bind(null, property)
});
});
}
// Expose the object binder.
exports.extend = extend;