-
Notifications
You must be signed in to change notification settings - Fork 86
Expand file tree
/
Copy pathnested-dirty.js
More file actions
173 lines (154 loc) · 6.01 KB
/
nested-dirty.js
File metadata and controls
173 lines (154 loc) · 6.01 KB
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
/**
* @mixin NestedDirtyModel
*
* @description Adds the `$dirty` method to a model`s instances. Acts in the same way as the DirtyModel plugin, but supports nested objects.
*/
'use strict';
angular.module('restmod').factory('NestedDirtyModel', ['restmod', function(restmod) {
function isPlainObject(_val) {
return angular.isObject(_val) && !angular.isArray(_val);
}
function convertToSimpleObj(data){
var Model = data.$type, result = {};
data.$each(function(value, key) {
var meta = Model.$$getDescription(key);
if(!meta || !meta.relation) {
// TODO: skip masked properties too?
// TODO: skip masked properties too?
result[key] = angular.copy(value);
} else if (meta) {
if(meta.relation == "belongs_to"){ //This line deals with nested objects that are related by a "BelongsTo". Adds the $pk of nested value to the results object so it can be compared against.
if(value == null){
result[key] = angular.copy(value);
} else if(typeof value.$pk != "undefined") {
result[key] = angular.copy(value.$pk); //This saved the primary key of the related object to the returned object, thereby allowing the plugin to notice when it has changed.
}
} //Could also add an if statement to handle a "belongs_to_many" condition (serialize all $pks as an array)
}
});
return result;
}
function navigate(_target, _keys) {
var key, i = 0;
while((key = _keys[i++])) {
if(_target) {
_target = _target.hasOwnProperty(key) ? _target[key] : null;
}
}
return _target;
}
function hasValueChanged(_model, _original, _keys, _comparator) {
var prop = _keys.pop();
_model = navigate(_model, _keys);
_original = navigate(_original, _keys);
if(angular.isObject(_original) && angular.isObject(_model) && _original.hasOwnProperty(prop)) {
if(typeof _comparator === 'function') {
return !!_comparator(_model[prop], _original[prop]);
} else {
return !angular.equals(_model[prop], _original[prop]);
}
}
return false;
}
function findChangedValues(_model, _original, _keys, _comparator) {
var changes = [], childChanges;
if(_original) {
for(var key in _original) {
if(_original.hasOwnProperty(key)) {
if(isPlainObject(_original[key]) && isPlainObject(_model[key])) {
childChanges = findChangedValues(_model[key], _original[key], _keys.concat([key]), _comparator);
changes.push.apply(changes, childChanges);
} else if(hasValueChanged(_model, _original, [key], _comparator)) {
changes.push(_keys.concat([key]));
}
}
}
}
return changes;
}
function changesAsStrings(_changes) {
for(var i = 0, l = _changes.length; i < l; i++) {
_changes[i] = _changes[i].join('.');
}
return _changes;
}
function restoreValue(_model, _original, _keys) {
var prop = _keys.pop();
_model = navigate(_model, _keys);
_original = navigate(_original, _keys);
if(_original && _model && _original.hasOwnProperty(prop)) {
_model[prop] = angular.copy(_original[prop]);
}
}
return restmod.mixin(function() {
this.on('after-feed', function() {
// store original information in a model's special property
this.$cmStatus = convertToSimpleObj(this);
})
/**
* @method $dirty
* @memberof NestedDirtyModel#
*
* @description Retrieves the model changes
*
* Property changes are determined using the strict equality operator if no comparator
* function is provided.
*
* If given a property name, this method will return true if property has changed
* or false if it has not.
*
* The comparator function can be passed either as the first or second parameter.
* If first, this function will compare all properties using the comparator.
*
* Called without arguments, this method will return a list of changed property names.
*
* @param {string|function} _prop Property to query or function to compare all properties
* @param {function} _comparator Function to compare property
* @return {boolean|array} Property state or array of changed properties
*/
.define('$dirty', function(_prop, _comparator) {
var original = this.$cmStatus;
var model = convertToSimpleObj(this); //Converts model to simple object before comparing (thereby "unnesting" any relation objects and simply comparing against $pks.)
if(_prop && !angular.isFunction(_prop)) {
return hasValueChanged(model, original, _prop.split('.'), _comparator);
} else {
if(angular.isFunction(_prop)) _comparator = _prop;
return changesAsStrings(findChangedValues(model, original, [], _comparator));
}
})
/**
* @method $restore
* @memberof NestedDirtyModel#
*
* @description Restores the model's last fetched values.
*
* Usage:
*
* ```javascript
* bike = Bike.$create({ brand: 'Trek' });
* // later on...
* bike.brand = 'Giant';
* bike.$restore();
*
* console.log(bike.brand); // outputs 'Trek'
* ```
*
* @param {string} _prop If provided, only _prop is restored
* @return {Model} self
*/
.define('$restore', function(_prop) {
return this.$action(function() {
var original = this.$cmStatus;
if(_prop) {
var keys = _prop.split('.');
restoreValue(this, original, keys);
} else {
var changes = findChangedValues(this, original, []);
for(var i = 0, l = changes.length; i < l; i++) {
restoreValue(this, original, changes[i]);
}
}
});
});
});
}]);