I am trying to understand how best to achieve the following in angularjs.
1) I have an array of objects:
$scope.myArray = [{special:false, name:'alice'}, {special:false, name:'bob'}];
if myArray[0].special changes to true, using $watch how can I detect that this was because of a change in element 0 of the array. Everything I have read on $watch deals with notifying that the array as a whole as changed but there is no way to retrieve the element/index of the array that has changed. I want to have this cope with varying lengths of arrays, so it would not suffice to simply have:
$scope.watch('myArray[0].special')
2) I have a dictionary:
$scope.myValues = {id1:{special:false, name:'alice'}, id2:{special:false, name:bob'}};
a similar question - if I was to change myValues.id1.special to true, how could I get the corresponding name field (or know it was id1)?
I would really appreciate any help and guidance here on best practice and what is actually possible with angularjs and $watch (or $watchcollection) with respect to arrays and maps. I think this is similar to question AngularJS : Watch array of objects and animate the changed property but I'm also interested to see how to watch across all properties in a dictionary.
Many many thanks in advance.
A watch listener provides you with both the newValue and the oldValue - so it's your job to compare the two to see what has changed. Since you wish to perform value rather than reference comparison, use $watch with the third parameter set to true:
$scope.$watch( 'myObject', function( newVal, oldVal ){
// Compare newVal and oldVal here to see what has changed.
}, true);
Related
I have a list of items in a component:
list: Array<MyType>;
The user can select and deselect elements on click:
toggleItem(item: MyType) {
if (this.selection.has(item)) {
this.selection.delete(item);
return;
}
this.selection.add(item);
}
The selected items are stored in a Set:
selected: Set<MyType> = new Set();
Now I need to toggle a CSS class and a title-attribute depending whether an element is selected or not:
<button class="toggle"
type="button"
[ngClass]="{'selected': selection.has(item)}"
[title]="selection.has(item) ? 'Select' : 'Deselect'"
(click)="toggleItem(item)">
{{ item.title }}
</button>
Now I've read somewhere that it is a bad idea, to evaluate function calls as Angular periodically will call them for change detection, like here:
[ngClass]="{'selected': selection.has(item)}"
They say that it's better to check a variable or a member of the object instead, like:
[ngClass]="{'selected': item.selected}"
Is is true and does it decrease performance the way I use it currently? Should I add a property to each item that is set when it's added to or removed from the Set?
Whenever Angular performs change detection, it checks whether anything that's variable and is in the template has changed.
Now, the check is pretty straight forward in case of model variables as Angular can simply read values of them in order to check for a change.
But that's not really the case with a function. With functions, Angular can only determine if the variable has changed by calling the function itself.
Now it doesn't make much of a difference if the function is a one-liner return of a value.
But in case the function has complex logic, it will essentially kill all the performance. As every time the change detection cycle runs, Angular will call that function in order to check for changes and detect them.
Hence, it's not really recommended to write functions in data-binding syntax in templates.
NOTE: I've also written a Medium Article about this. I'd suggest you check it out. It's mainly about Angular Reactive Forms performance. But it will help you better understand what I said above.
A little background on the problem
I've built a custom system to automatically watch some "store" properties that comes from a JSON of a nosql database. Nothing too complicated except the nesting (required for several reasons not discussed here) of objects.
The data structure looks like this:
{
store: {
objA: {
objB: {
prop1: 'some value'
}
}
}
}
However, since it's a nosql database that provide that store property, objB can just be NOT present after the load from the database.
Example of the template used
I have custom components that have props bound directly to that data store
<my-selector :value.sync="store.objA.objB.prop1">
</my-selector>
However, it crashes when "objB" is not present with the usual javascript error saying that it cannot get "objB" of undefined, but that is normal.
I'm trying to find a vuejs way to prepare the data for me.
Ideas
In order to counter that crash :
I can NOT use v-if in that case to mask the selector. Because that selector can be used even if the data is not yet set (example: for optional data).
I could fix the "load" function that gets the data from the database so it initialize the required properties (like objB) before assigning data to the data store. However, that would imply that my initial VueJS data object would contain these required properties as well. It's probably that I will use if I can't find any alternative solution, but I don't think that's the easiest way around because I would have to fix any incoming data before assignment.
My preferred choice would be to create a directive (or any other thing built in the template) that would add them for me if they are missing.
VueJS always evaluates the bindings value
I thought of that solution:
<my-selector v-autocreate="'store.objA.objB.prop1'" :value.sync="store.objA.objB.prop1">
</my-selector>
However the directive binding "v-autocreate" is not picked up first (checked with the debugger).
I did not find documentation relative of the order of load of directives or attributes.
I was also hoping to get all bindings of a node with the directive "bind"
function in order to NOT repeat the string, but it seems we can't get that information (I'm used to knockoutjs where we can pick all bindings assigned to a node in order to behave differently).
I would like to reach that goal but I'm not sure that it's possible (I would need something like a pre-bind / beforeBind event on directive haha) :
<my-selector v-autocreate :value.sync="store.objA.objB.prop1">
</my-selector>
Where v-autocreate would assure to do the vm.$set of the missing properties.
You could create a method that checks each property in an object path, creates it if it doesn't exist, then returns the value of the last property.
Example (not tested):
get(object, path) {
path = path.split('.')
let index = 0
const length = path.length
let val
while (object != null && index < length) {
let key = path[index++]
if(object[key] == null) {
this.$set(object, key, {})
}
object = object[key]
}
return val
}
Usage:
<my-selector :value="get(store,'objA.objB.prop1')">
</my-selector>
You may be interested in lodash's get function, which is what the code example is based on.
https://github.com/lodash/lodash/blob/master/get.js
So I understand that Ember can compute on an array and elements within it. There are two options here.
someArray.[] and someArray.#each
If say I changed one of the element in array and there is a computed property that depends on it. Which one should I use? Thanks.
someArray.[] will only be used when the array items are added/removed.
When the particular property in the array object is changed then someArray.#each will be called.
isNameChanged: function() {
console.log('is Name Changed')
}.property('someArray.#each.name')
Can check this Ember.js: Observing array property using #each doesn't work
I have made a simple proof-of-concept Polymer 1.0 app that demonstrates my problem: JSBin.
In my problem, I am using array mutation methods to alter the array, which contains the list of shopping items.
However, this doesn't seem to work as intended. I do get a change in dom-repeat and when printing the length of the array. But I do not get the change event when I am printing the array itself nor when I wrap it in a function.
In short, why does this work?
<p>Number of items: [[list.length]]</p>
And why does this not work?
<p>Items inline: [[list]]</p>
<p>Observe function : [[_observe(list)]]</p>
Also, when I uncomment the following line (in the JSBin), things seem to work as indened. But I don't like it since it's a bit hackish.
app.notifyPath('list', app.list.slice());
I have stumbled upon the slice() fix by reading this issue: https://github.com/Polymer/polymer/issues/2068
EDIT
So, after reviewing the comments, the answer to the question "Is this by design" is YES. The array itself doesn't change (since it's only a reference), but it's property do change. That's why the slice() forces the reload since it creates a shallow copy.
However, one could argue whether this is OK. Yes, the variable list does not change per se. But putting [[list]] in the HTML code actually triggers toString(). And result of that function has changed.
I guess I'm stuck with piggybacking the length property for now...
As alluded to in the comments the notifyPath and slice calls are creating a shallow copy of the array and assigning a different reference back to the list variable - triggering an update of the binding. Without maintaining a separate (watchable) variable or messing around with object references, the only other workaround I can think of would be to piggy back on the list.length property instead of the list itself and pass that through some kind of "formatting" function. e.g.
<p>Items inline: [[format(list.length)]]</p>
app.format = function(){
return app.list.toString();
};
» Fiddle
As pointed out by #zb you could expand on this and make the function reusable with any array by passing the relevant variable as an argument too:
<p>Items inline: [[format(list, list.length)]]</p>
app.format = function(list){
return list.toString();
};
» Fiddle
I can detect changes in a javascript array by using Array.observe.
Like this:
Array.observe(myArray, function (changes) {
// handle changes... in this case, we'll just log them
changes.forEach(function (change) {
console.log(change.object);
});
However I am not able to find an easy way of getting just the changed element (considering elements were added).
Is there a way to detect what was added without comparing this array to a copy of the original array ?
Accourding to documentation you can get this info from the data passed into the callback.
Also please pay attention that Array.observe is obsolete and consider using Proxy instead