Angular deep compare objects except for specific properties - javascript

Use case:
$scope.$watch('settings', function (newVal, oldVal) {
if (!angular.equals(oldVal, newVal)) {
setDirty();
}
}, true);
Now this is practically the same as
$scope.$watch('settings', function (newVal, oldVal) {
if (oldVal) {
setDirty();
}
}, true);
Since the $watch already compares the two.
However, there is one property that even though it changes i don't want to setDirty().
This is my working (hacky) solution so far:
$scope.$watch('settings', function (newVal, oldVal) {
if (!oldVal) return;
var editedOldVal = angular.copy(oldVal, {});
var editedNewVal = angular.copy(newVal, {});
delete editedOldVal.propertyIDontWannaWatch;
delete editedNewVal.propertyIDontWannaWatch;
if (!angular.equals(editedOldVal, editedNewVal)) {
setDirty();
}
}, true);
Is there a cleaner way to make angular.equals() or the $watch ignore specific properties?
EDIT:
This answer does not solve my problem since option 1 is not a solution at all, says that if the property that i don't want to watch hasn't changed - do nothing, but this is exactly the property i want to ignore (don't care if it changed or not). option 2 makes that property not comparable at all, i only want to ignore it on this specific case and not make it uncomparable by definition.

You can prefix properties with a $ and it will not be included in the equals comparison. The angular.equals method ignores properties that start with a $.
From the Angular.js code:
if (key.charAt(0) === '$' || isFunction(o1[key])) continue;
For example:
var object1 = {propertyOne: '1', $myCustomHiddenField: 'something'};
var object2 = {propertyOne: '1', $myCustomHiddenField: 'something else'};
var result = angular.equals(object1, object2);//this is true
The equals method can be referenced here.

Related

Binding to arbitrarily deep properties on an object based on rules

(I'm sorry if my question title isn't very good, I couldn't think of a better one. Feel free to suggest better options.)
I'm trying to create a reusable "property grid" in Angular, where one can bind an object to the grid, but in such a way that presentation of the object can be customized somewhat.
This is what the directive template looks like (the form-element isn't important to my question, so I'll leave it out):
<div ng-repeat="prop in propertyData({object: propertyObject})">
<div ng-switch on="prop.type">
<div ng-switch-when="text">
<form-element type="text"
label-translation-key="{{prop.key}}"
label="{{prop.key}}"
name="{{prop.key}}"
model="propertyObject[prop.key]"
focus-events-enabled="false">
</form-element>
</div>
</div>
</div>
and, the directive code:
angular.module("app.shared").directive('propertyGrid', ['$log', function($log) {
return {
restrict: 'E',
scope: {
propertyObject: '=',
propertyData: '&'
}
templateUrl: 'views/propertyGrid.html'
};
}]);
Here's an example usage:
<property-grid edit-mode="true"
property-object="selectedSite"
property-data="getSitePropertyData(object)">
</property-grid>
And the getSitePropertyData() function that goes with it:
var lastSite;
var lastSitePropertyData;
$scope.getSitePropertyData = function (site) {
if (site == undefined) return null;
if (site == lastSite)
return lastSitePropertyData;
lastSite = site;
lastSitePropertyData = [
{key:"SiteName", value:site.SiteName, editable: true, type:"text"},
//{key:"Company.CompanyName", value:site.Company.CompanyName, editable: false, type:"text"},
{key:"Address1", value:site.Address1, editable: true, type:"text"},
{key:"Address2", value:site.Address2, editable: true, type:"text"},
{key:"PostalCode", value:site.PostalCode, editable: true, type:"text"},
{key:"City", value:site.City, editable: true, type:"text"},
{key:"Country", value:site.Country, editable: true, type:"text"},
{key:"ContactName", value:site.ContactName, editable: true, type:"text"},
{key: "ContactEmail", value: site.ContactEmail, editable: true, type:"email"},
{key: "ContactPhone", value: site.ContactPhone, editable: true, type:"text"},
{key: "Info", value: site.Info, editable: true, type:"text"}
];
return lastSitePropertyData;
};
The reason I'm going through such a "property data" function and not just binding directly to properties on the object is that I need to control the order of the properties, as well as whether they should even be shown to the user at all, and also what kind of property it is (text, email, number, date, etc.) for the sake of presentation.
At first, as you can tell from the value property remnant in the getSitePropertyData() function, I first tried providing the values directly from this function, but that wouldn't bind to the object, so changes either in the object or form the property grid didn't sync back and forth. Next up, then, was using the key idea, which lets me do this: propertyObject[prop.key]—which works great for direct properties, but as you can see, I had to comment out the "Company" field, because it's a property of a property, and propertyObject["a.b"] doesn't work.
I'm struggling to figure out what to do here. I need the bindings to work, and I need to be able to use arbitrarily deep properties in my bindings. I know this kind of thing is theoretically possible; I've seen it done for instance in UI Grid, but such projects have so much code that I would probably spend days finding out how they do it.
Am I getting close, or am I going about this all wrong?
You want to run an arbitrary Angular expression on an object. That is exactly the purpose of $parse (ref). This service can well... parse an Angular expression and return a getter and setter. The following example is an oversimplified implementation of your formElement directive, demonstrating the use of $parse:
app.directive('formElement', ['$parse', function($parse) {
return {
restrict: 'E',
scope: {
label: '#',
name: '#',
rootObj: '=',
path: '#'
},
template:
'<label>{{ label }}</label>' +
'<input type="text" ng-model="data.model" />',
link: function(scope) {
var getModel = $parse(scope.path);
var setModel = getModel.assign;
scope.data = {};
Object.defineProperty(scope.data, 'model', {
get: function() {
return getModel(scope.rootObj);
},
set: function(value) {
setModel(scope.rootObj, value);
}
});
}
};
}]);
I have altered slightly the way the directive is used, hopefully without changing the semantics:
<form-element type="text"
label-translation-key="{{prop.key}}"
label="{{prop.key}}"
name="{{prop.key}}"
root-obj="propertyObject"
path="{{prop.key}}"
focus-events-enabled="false">
Where root-obj is the top of the model and path is the expression to reach the actual data.
As you can see, $parse creates the getter and setter function for the given expression, for any root object. In the model.data property, you apply the accessor functions created by $parse to the root object. The entire Object.defineProperty construct could be replaced by watches, but that would only add overhead to the digest cycle.
Here is a working fiddle: https://jsfiddle.net/zb6cfk6y/
By the way, another (more terse and idiomatic) way to write the get/set would be:
Object.defineProperty(scope.data, 'model', {
get: getModel.bind(null, scope.rootObj),
set: setModel.bind(null, scope.rootObj)
});
If you are using lodash you can use the _.get function to achieve this.
You can store _.get in the controller of your property-grid and then use
model="get(propertyObject,prop.key)"
in your template. If you need this functionality in multiple places in your application (and not just in property-grid) you could write a filter for this.
The problem with this is that you can't bind your model this way and thus you can't edit the values. You can use the _.set function and an object with a getter and a setter to make this work.
vm.modelize = function(obj, path) {
return {
get value(){return _.get(obj, path)},
set value(v){_.set(obj, path,v)}
};
}
You can then use the function in the template:
<div ng-repeat="prop in propertyData({object: propertyObject})">
<input type="text"
ng-model="ctrl.modelize(propertyObject,prop.key).value"
ng-model-options="{ getterSetter: true }"></input>
</div>
For a reduced example see this Plunker.
If you don't use lodash you can use this simplified version of the _.get function that I extracted from lodash.
function getPath(object, path) {
path = path.split('.')
var index = 0
var length = path.length;
while (object != null && index < length) {
object = object[path[index++]];
}
return (index && index == length) ? object : undefined;
}
This function makes sure that you won't get any Cannot read property 'foo' of undefined errors. This is useful especially if you have long chains of properties where there might be an undefined value. If you want to be able to use more advanced paths (like foo.bar[0]) you have to use the full _.get function from lodash.
And here is a simplified version of _.set also extracted form lodash:
function setPath(object, path, value) {
path = path.split(".")
var index = -1,
length = path.length,
lastIndex = length - 1,
nested = object;
while (nested != null && ++index < length) {
var key = path[index]
if (typeof nested === 'object') {
var newValue = value;
if (index != lastIndex) {
var objValue = nested[key];
newValue = objValue == null ?
((typeof path[index + 1] === 'number') ? [] : {}) :
objValue;
}
if (!(hasOwnProperty.call(nested, key) && (nested[key] === value)) ||
(value === undefined && !(key in nested))) {
nested[key] = newValue;
}
}
nested = nested[key];
}
return object;
}
Keep in mind that these extracted functions ignore some edge cases that lodash handles. But they should work in most cases.
When you creating the lastSitePropertyData you can create the object in this way to not hardcode it
function createObject (){
for(var key in site){
lastSitePropertyData.push({key:key, value:site[key], editable: true, type:"text"});
} }
And later use function to get data something like this
function getKey(prop){
if(typeof prop.value === 'object'){
return prop.value.key; //can run loop create a go deep reccursive method - thats upto u
}
else return prop.key;
}
function getValue(prop){
if(typeof prop === 'object'){
return prop.value.value; //have tp run loop get value from deep reccursive method - thats upto u
}
else return prop.value;
}
That way can be use in html {{getKey(prop)}} and {{getValue(prop}}
For working demo please have look this link - https://jsfiddle.net/718px9c2/4/
Note: Its just idea for accessing json data in better way, I am not using angular in demo.
Another idea is to do smth like this.
If you wont to avoid making object.proto dirty (this is always good idea) just move this functionality into the other module.
(function () {
'use strict';
if (Object.hasOwnProperty('getDeep')) {
console.error('object prototype already has prop function');
return false;
}
function getDeep(propPath) {
if (!propPath || typeof propPath === 'function') {
return this;
}
var props = propPath.split('.');
var result = this;
props.forEach(function queryProp(propName) {
result = result[propName];
});
return result;
}
Object.defineProperty(Object.prototype, 'getDeep', {
value: getDeep,
writable: true,
configurable: true,
enumerable: false
});
}());
I have something similar used to show data in grids, those grids may show the same objects yet no the same columns. however I don't handle that in one go.
I have a type service where I declare my types and some default configuration
I have a grid service which generates the grid definition options according to what I specified.
In the controller I instantiate the grid using the grid service, specifying the ordering of the columns and some specific configurations, which override the default ones. The grid service itself generate appropriate configuration for filtering, ordering using the type definition of the fields.

Ember - Custom Computed Property to check if all dependent fields exists

I am creating a form and I am trying to find a simple, elegant way of handling to see if all inputs exist.
Form = Ember.Object.extend({
// section 1
name: null,
age: null,
isABoolean: null,
// section 2
job: null,
numberOfSiblings: null,
isComplete: Ember.computed.and('_isSection1Complete', '_isSection2Complete'),
_isSection1Complete: function() {
var isPresent = Ember.isPresent;
return isPresent(this.get('name')) && isPresent(this.get('age')) && isPresent(this.get('isABoolean'));
}.property('name', 'age', 'isABoolean'),
_isSection2Complete: function() {
var isPresent = Ember.isPresent;
return isPresent(this.get('job')) && isPresent(this.get('numberOfSiblings'));
}.property('job', 'numberOfSiblings')
});
However, this doesn't seem to scale. My actual application will have many sections (over 20 sections).
I am looking into trying to create a re-usable computed property that fits my needs. Take for example the code of what I am going for:
Form = Ember.Object.extend({
// properties...
isComplete: Ember.computed.and('_isSection1Complete', '_isSection2Complete'),
_isSection1Complete: Ember.computed.allPresent('name', 'age', 'isABoolean'),
_isSection2Complete: Ember.computed.allPresent('job', 'numberOfSiblings')
});
I feel that this is a common case, but I'm failing to find the correct computed properties on how to execute this, so I would like to make my own.
Two questions:
Where's the best place to define the custom computed property? Can I just attach a function to Ember.computed?
Is there an easier way to solve this? I feel like I'm overlooking something simple.
As for Question #1,
You can define a custom computed helper in the App namespace. In this example, I created a new computed helper called allPresent that checks each property passed in against Ember.isPresent.
App.computed = {
allPresent: function (propertyNames) {
// copy the array
var computedArgs = propertyNames.slice(0);
computedArgs.push(function () {
return propertyNames.map(function (propertyName) {
// get the value for each property name
return this.get(propertyName);
}, this).every(Ember.isPresent);
});
return Ember.computed.apply(Ember.computed, computedArgs);
}
};
It can be used like this, per your example code:
_isSection2Complete: App.computed.allPresent(['job', 'numberOfSiblings'])
I adapted this from the approach here: http://robots.thoughtbot.com/custom-ember-computed-properties
As for Question #2, I can't think of a simpler solution.
I had to make a minor adjustment to Evan's solution, but this works perfectly for anyone else that needs it:
App.computed = {
allPresent: function () {
var propertyNames = Array.prototype.slice.call(arguments, 0);
var computedArgs = propertyNames.slice(0); // copy the array
computedArgs.push(function () {
return propertyNames.map(function (propertyName) {
// get the value for each property name
return this.get(propertyName);
}, this).every(Ember.isPresent);
});
return Ember.computed.apply(Ember.computed, computedArgs);
}
};
This can now be used as such:
_isSection2Complete: App.computed.allPresent('job', 'numberOfSiblings')

Watch multiple $scope attributes

Is there a way to subscribe to events on multiple objects using $watch
E.g.
$scope.$watch('item1, item2', function () { });
Starting from AngularJS 1.3 there's a new method called $watchGroup for observing a set of expressions.
$scope.foo = 'foo';
$scope.bar = 'bar';
$scope.$watchGroup(['foo', 'bar'], function(newValues, oldValues, scope) {
// newValues array contains the current values of the watch expressions
// with the indexes matching those of the watchExpression array
// i.e.
// newValues[0] -> $scope.foo
// and
// newValues[1] -> $scope.bar
});
Beginning with AngularJS 1.1.4 you can use $watchCollection:
$scope.$watchCollection('[item1, item2]', function(newValues, oldValues){
// do stuff here
// newValues and oldValues contain the new and respectively old value
// of the observed collection array
});
Plunker example here
Documentation here
$watch first parameter can also be a function.
$scope.$watch(function watchBothItems() {
return itemsCombinedValue();
}, function whenItemsChange() {
//stuff
});
If your two combined values are simple, the first parameter is just an angular expression normally. For example, firstName and lastName:
$scope.$watch('firstName + lastName', function() {
//stuff
});
Here's a solution very similar to your original pseudo-code that actually works:
$scope.$watch('[item1, item2] | json', function () { });
EDIT: Okay, I think this is even better:
$scope.$watch('[item1, item2]', function () { }, true);
Basically we're skipping the json step, which seemed dumb to begin with, but it wasn't working without it. They key is the often omitted 3rd parameter which turns on object equality as opposed to reference equality. Then the comparisons between our created array objects actually work right.
You can use functions in $watchGroup to select fields of an object in scope.
$scope.$watchGroup(
[function () { return _this.$scope.ViewModel.Monitor1Scale; },
function () { return _this.$scope.ViewModel.Monitor2Scale; }],
function (newVal, oldVal, scope)
{
if (newVal != oldVal) {
_this.updateMonitorScales();
}
});
Why not simply wrap it in a forEach?
angular.forEach(['a', 'b', 'c'], function (key) {
scope.$watch(key, function (v) {
changed();
});
});
It's about the same overhead as providing a function for the combined value, without actually having to worry about the value composition.
A slightly safer solution to combine values might be to use the following as your $watch function:
function() { return angular.toJson([item1, item2]) }
or
$scope.$watch(
function() {
return angular.toJson([item1, item2]);
},
function() {
// Stuff to do after either value changes
});
$watch first parameter can be angular expression or function. See documentation on $scope.$watch. It contains a lot of useful info about how $watch method works: when watchExpression is called, how angular compares results, etc.
how about:
scope.$watch(function() {
return {
a: thing-one,
b: thing-two,
c: red-fish,
d: blue-fish
};
}, listener...);
$scope.$watch('age + name', function () {
//called when name or age changed
});
Here function will get called when both age and name value get changed.
Angular introduced $watchGroup in version 1.3 using which we can watch multiple variables, with a single $watchGroup block
$watchGroup takes array as first parameter in which we can include all of our variables to watch.
$scope.$watchGroup(['var1','var2'],function(newVals,oldVals){
console.log("new value of var1 = " newVals[0]);
console.log("new value of var2 = " newVals[1]);
console.log("old value of var1 = " oldVals[0]);
console.log("old value of var2 = " oldVals[1]);
});

Javascript Object Composing

I'm wondering what the difference, or if any, the following JS is for building up an object. Is it a style thing/standards thing? If so, which is most common?
Example 1:
var MyNamspace = {
options: { test: 10 },
someFunction: function() { alert("Hello"); }
}
Example 2:
var MyNamespace = {};
MyNamespace.options = { test: 10 };
MyNamespace.someFunction = function() { alert("Hello"); }
Thanks.
There is no significant difference between the two (although the second is probably marginally slower).
The first example is more usual.
The first MyNamspace is missing an "e", else there is no functional difference.
The second pattern is used when there is already an object with properties (like someFunc.prototype) which you want to extend, but you'd need to leave the overwriting part (MyNamespace = {};) away.
The second option modifies an existing type, so while equal, the second is modifying the signature of an existing type, just like you could do: MyClass.prototype.someFunction = function() { }
This is where JavaScript can be conveniently flexible.

Is there a way to get a callback when an array item value has changed?

This may seem silly, but in this day and age, one should be able to expect JS to raise an event if contents of an array have changed.
A few questions been asked regarding getting notified when a variable changes (define getter or setter). And there seems to be a way to do that (at least for most browsers including IE6+)
My issue is that I'm trying to get notified if an item inside an array changes:
var ar = ["one", "two", "three"];
// setting the whole array will call the custom setter method
// (assuming you defined it)
ar = ["one", "three", "five"];
// however, this will only call the getter method and won't call the setter
// without defining custom setters for every item in the array.
ar[1] = "two";
Obviously, I'm trying to avoid forcing the coder to use old-school Java style .getVale() and .setValue() functions to access/modify data.
In short: no, you can't. You'll notice that Arrays don't provide any event dispatching mechanism, and their API doesn't include any callback type functionality.
In longer: as others have noted, it is possible to wrap the array… And it's also possible to poll the arrays contents:
function watchArray(arr, callback) {
var oldVal = "" + arr;
setInterval(function() {
var curVal = "" + arr;
if (curVal != oldVal) {
callback();
oldVal = curVal;
}
}, 100);
}
But this method has some obvious problems: it polls, it'll get slow to watch a bunch of arrays, etc.
I think timeout-based solutions are not the best.
If you can only use push and pop to modify your array, you can override push and pop methods of Array prototype (or only some object that you want to monitor):
var myWatchableArr = [];
myWatchableArr.setChangeCallback = function(callback){
this.changeCallback = callback;
}
myWatchableArr.push = function(e){
Array.prototype.push.call(this,e);
if(typeof this.changeCallback == "function")
this.changeCallback(this);
}
myWatchableArr.push(3);
myWatchableArr.setChangeCallback(function(arr){
console.log("the array has been changed", arr);
});
// now watching for changes
myWatchableArr.push(4);
If push and pop are not sufficient, you can add some setAt method to use like myWatchableArr.setAt(3, value) instead of myWatchableArr[3]=value.
Ok, based on #David Wolever's code and other comments, there actually is a solution:
Use the notes from John Dyer to implement the addProperty method. Place a setTimeout in the getter method to compare with original value a short time after the read takes place:
addProperty(myObject, 'vals',
function () {
var _oldVal = "" + this._val;
var _parent = this;
console.log('getter!');
setTimeout(function () {
var curVal = "" + _parent._val;
if (curVal != _oldVal)
console.log('array changed!');
}, 200);
return this._val;
},
function (value) {
console.log('setter!');
this._val = value;
});
myObject.vals = ["one", "two", "three"];
myObject.vals[1] = "five";

Categories

Resources