Angularjs watch array and get changed object - angularjs

In the context of inserting or deleting from an array in angular, is it possible to watch the array and then get the object that was added or deleted from the array? I don't care about the objects properties in the array, only the objects themselves being added or deleted. So I believe $watchCollection is a good fit here so it's not a deep watch.
For example, I have this array as a model for a dual list box:
$scope.employees = [
{
name: "Bob",
id: "0"
},
{
name: "Carl",
id: "1"
},
{
name: "Bill",
id: "2"
}
];
The listbox will automatically update $scope.employees when i move one off of it or onto it (insert/delete). If I do:
$scope.$watchCollection('employees', function(){
//somehow get changed object
var changedObject = ...;
$scope.changedItems.push(changedObject);
});
I want to be able to get the added/deleted item so I can use it or save it somewhere.

The $watchCollection handler function receives both new and old value:
$scope.$watchCollection('employees', function(newValue, oldValue){
console.log(newValue);
console.log(oldValue);
var addedArray = newValue.filter(x => !oldValue.find(x));
var removedArray = oldValue.filter(x => !newValue.find(x));
var changedObject = {added: addedArray, removed: removedArray};
$scope.changedItems.push(changedObject);
});
For more information, see AngularJS $watchCollection API Reference

Related

AngularJS scope issue with $http

Continuing my adventures with AngularJS, I have what I believe is a scope issue, but not entirely sure.
In my controller I'm building two test array's for demo purposes, search.filterDemo and search.filter (real data).
Controller
app.controller('searchBtnCtrl', ['$scope', '$http', function ($scope, $http) {
var search = this;
search.filter = {};
search.results = [];
search.resultsDemo = [];
search.keywordsToFilter = [
{ name: 'itemA' },
{ name: 'itemB' },
{ name: 'itemC' },
{ name: 'itemD' }
];
$scope.performSearch = function () {
search.resultsDemo = [
{ name: 'itemA', content: 'This is demo content' },
{ name: 'itemB', content: 'This is demo content' },
{ name: 'itemC', content: 'This is demo content' },
{ name: 'itemD', content: 'This is demo content' }
];
$http.get('http://localhost/content').then(function (response) {
search.resultCount = response.data.response.docs.length;
for (var i = 0; i < search.resultCount; i++) {
search.results.push({ name: search.results[i]._name[0], content: search.resultsTemp[i]._content[0]});
}
console.log(search.results);
});
console.log(search.resultsDemo);
console.log(search.results);
}
}]);
Output from the console log for search.resultsDemo is as I expect it to be:
Array [ Object, Object, Object, Object, Object, Object, Object, Object ]
Then if I click on the Array link I see that the array has 8 items
Array[8]
This is all correct to me, my first array is keeping it's scope.
However my second array is not quite.
Output from the second array is as follows:
Inside the $http call it displays properly -
Array [ Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, 3 moreā€¦ ]
However the second console.log of the array just 3 lines later displays as:
Array[ ]
And if I click on the array link it shows:
Array[0]
The interesting thing is that the data is in there, it's just not showing up as objects like the first array (I can click the array link and I do see the objects listed under the Array[0] in the console, and I can twirl them open to see the data.
So is this a scope issue, or is it something else related to the async nature of Angular that's not formatting my second (real) array correctly?
It's driving me batty, as I need to have my second array formatted properly like the first array for processing later in the function.
thanks!
That is because your second console.log is outside the scope of your then statement. When your second console.log runs, the data likely has not been returned yet.
In fact, I would wager to say that your second console.log(search.results) likely ends up in your console before your first console.log(search.results) does.

AngularJS : watching a particular property in an array of objects for changes in the property's value

In my custom directive, I'm adding elements to the DOM based on the number of objects in my datasource array. I need to watch a specific property in each object. As I add these elements to the DOM, I want to set up a $watch on the checked property of each object in the toppings array, but it's not working, and I don't know why. I set up a breakpoint inside the function that should be invoked when the property changes from true to false or false to true, but that function is never invoked. Is the reason obvious? I'm just learning Angular, so I could easily be making a stupid error.
$scope.bits = 66; (i.e. onions and olives)
$scope.toppings = [
{ topping: 1, bits: 2, name: 'onions' },
{ topping: 2, bits: 4, name: 'mushrooms' },
{ topping: 3, bits: 8, name: 'peppers' },
{ topping: 4, bits: 16, name: 'anchovies' },
{ topping: 5, bits: 32, name: 'artichokes' },
{ topping: 6, bits: 64, name: 'olives' },
{ topping: 7, bits: 128, name: 'sausage' },
{ topping: 8, bits: 256, name: 'pepperoni' }
]
Each object in the model gets a new checked property which will be true or false.
NOTE: the object array will at most contain a dozen or so items. Performance is not a concern.
link: function link(scope, iElement, iAttrs, controller, transcludeFn) {
<snip>
// At this point scope.model refers to $scope.toppings. Confirmed.
angular.forEach(scope.model, function (value, key) {
// bitwise: set checked to true|false based on scope.bits and topping.bits
scope.model[key].checked = ((value.bits & scope.bits) > 0);
scope.$watch(scope.model[key].checked, function () {
var totlBits = 0;
for (var i = 0; i < scope.model.length; i++) {
if (scope.model[i].checked) totlBits += scope.model[i].bits;
}
scope.bits = totlBits;
});
});
<snip>
Array of Objects:
$scope.toppings = [
{ topping: 1, bits: 2, name: 'onions' },
{ topping: 2, bits: 4, name: 'mushrooms' },
{ topping: 3, bits: 8, name: 'peppers', checked:undefined /*may be*/ }
];
Watch using AngularJs $WatchCollection:
Instead of monitoring objects array above, that can change for any property in the object, we will create an array of properties of the elements for which we are watching the collection (.checked).
We filter the array's elements to only monitor those elements which have .checked defined and map that to an array for angular watchCollection.
When a change fires, I will compare the old and new arrays of (.checked) to get exact changed element using lodash difference method.
$scope.$watchCollection(
// Watch Function
() => (
$scope
.toppings
.filter(tp => tp.checked !== undefined)
.map(tp => tp.checked)
),
// Listener
(nv, ov) => {
// nothing changed
if(nv == ov || nv == "undefined") return;
// Use lodash library to get the changed obj
let changedTop = _.difference(nv,ov)[0];
// Here you go..
console.log("changed Topping", changedTop);
})
You use MAP to collect all of the property values you need + convert them into a small string representation (in this case 1 and 0) and then join them together into a string that can be observed.
A typescript example:
$scope.$watch(
() => this.someArray.map(x => x.selected ? "1" : "0").join(""),
(newValue, oldValue, scope) => this.onSelectionChanged(this.getSelectedItems()));
The watchExpression parameter to $scope.$watch should either be a string or a function. I've not experimented extensively with this (I try and avoid explicit watches where possible) but I think it does also work when you watch 'simple' scope properties as object references, but not so well with more complex references.
I think if you supply the reference as a string, e.g. 'model[' + key + '].checked' then you may have some success (I only say this because I've done something similar with $watchCollection previously).
Alternatively you should be able to supply a function, e.g.
$scope.$watch(function() { return scope.model[key].checked; }, function() { ... });
Hope this helps!
Use $watchCollection instead.
From docs:
$watchCollection(obj, listener);
Shallow watches the properties of an object and fires whenever any of the properties change (for arrays, this implies watching the array items; for object maps, this implies watching the properties). If a change is detected, the listener callback is fired.
The obj collection is observed via standard $watch operation and is examined on every call to $digest() to see if any items have been added, removed, or moved.
The listener is called whenever anything within the obj has changed. Examples include adding, removing, and moving items belonging to an object or array.

Backbone pass specific models in the collection to view?

var theseMessages = window.App.data.messages.where({ chat_id: 2 })
this.messagesView = new MessagesView({ collection: theseMessages });
I am not sure the proper way to pass in to the view constructor the collection that has a specific property.
Example my collection may look like
[
{
"chat_id": "1",
"facebook_name": "Tim Parker",
"facebook_email": "tim#aol.com",
"facebook_id": "1663565265",
"gravatar": null,
"message": "Whats up",
"_id": "533b6a4c7cc6647012f441ad",
"__v": 0,
"sent": "2014-04-02T01:39:24.747Z"
},
{
"chat_id": "2",
"facebook_name": "nick matthews",
"facebook_email": "john#aol.com",
"facebook_id": "1663565265",
"gravatar": null,
"message": "Hey admin",
"_id": "434636",
"__v": 0,
"sent": "2014-04-02T01:48:45.585Z"
}
]
So when I want to render this collection in my app and only pass the models that have chat_id : 2 I would think something like this is sufficient?
var theseMessages = window.App.data.messages.where({ chat_id: 2 })
this.messagesView = new MessagesView({ collection: theseMessages });
But it gives me an error any ideas?
Edit: Should have been more specific from the get go, I am using a marionette collection view. So I need to pass a collection, but there has to be a way to limit the collection to items with specific attributes, maybe I need to change my logic?
var Marionette = require('backbone.marionette');
var MessageView = Marionette.ItemView.extend({
className: 'message row',
template: require('../../templates/message.hbs')
});
module.exports = CollectionView = Marionette.CollectionView.extend({
className: 'collection',
initialize: function() {
this.listenTo(this.collection, 'change', this.render);
},
itemView: MessageView
});
I think you meant to do this:
this.messagesView = new MessagesView({ model: theseMessages });
In your line where you filter the Collection:
var theseMessages = window.App.data.messages.where({ chat_id: 2 })
the variable theseMessages just holds an array of items from the Collection, not another Collection itself -- that's probably why you're getting an error in MessagesView.
I would do the filtering dynamically in either the Collection or in MessagesView itself when rendering, rather than trying to pass an already-filtered Collection to MessagesView's constructor. Most of the time, I'd probably do it in MessageView, since I tend to think of filtering as a view-specific thing. But filtering in the Collection would be fine if that Collection isn't going to be used in another view at the same time.
Either way, here's some example code. To filter in the MessageView:
// within MessageView
serializeData: function() {
var data = Marionette.CollectionView.prototype.serializeData.call(this);
return _.where(data, { chat_id: 2 });
}
Or to filter in your Collection instead:
// within your Collection
toJSON: function() {
return this.chain()
.where({ chat_id: 2 })
.map(function(model){ return model.toJSON(); }
.value();
}
I haven't tested that code, but something like that should work.
Incidentally, to clarify the collection: vs model: subject that was brought up in other comments here: you are correct in using the collection: option in the view's constructor. Backbone & Marionette want a collection: for a Collection, and a model: for a single Model.

Backbone Collection Set method remove existing elements and then add all elements

I have a backbone collection.I want to add or remove some models dynamically in the collection. But if i am using collection.set() method then it is going to remove first all elements and then it will add all elements again.
What i want to do is trigger add event of collection for those whose are really new added and trigger remove events for those whose are removed from previous collection.
Here is a example [http://jsfiddle.net/PkJCx/2/]
From the docs
The set method performs a "smart" update of the collection with the
passed list of models. If a model in the list isn't yet in the
collection it will be added; if the model is already in the collection
its attributes will be merged; and if the collection contains any
models that aren't present in the list, they'll be removed.
It is also a good idea to provide `idAttribute' to the model so that the collection identifies that based on the id. Otherwise the collection would not know if the model is a new one or not.
So after setting the id and using set, you can see that is performs a smart update
$(function () {
var MyModel = Backbone.Model.extend({
// This attribute should be set as a default
defaults: {
Name: ''
},
// Set the id attribute so that the collection
// know that it is the old model
idAttribute: 'id'
});
var Coll = Backbone.Collection.extend({
model: MyModel
});
var models = [{
Name: 'A',
id: 1
}, {
Name: 'B',
id: 2
}];
var collection = new Coll(models);
collection.bind('add', function (model) {
alert('addb')
});
collection.bind('remove', function () {
alert('add')
});
models = [{
Name: 'A',
id :1
}, {
Name: 'B',
id: 2
}, {
Name: 'C',
id: 3
}];
collection.add(models);
});
Check Fiddle
It will not try to remove the other 2, but Backbone is smart enough to identify that 2 of them are old models and then just merges the newer one into the collection.

Handling Subsidiary Views in Backbone.js

I have a basic Backbone application which obtain an array of JSON objects from a remote service and displays them: all good so far. However, each JSON object has an array of tags and I want to display the tags in a separate area of the webpage.
My question is: what is the most Backbone-friendly way of doing this? I could parse the existing data again in a second view, which is cleaner but takes up more computation (processing the entire array twice).
An alternative is gathering up the tag information in the primary view as it is working through the array and then passing it along to the subsidiary view, but then I'm linking the views together.
Finally, I'd like to filter based on those tags (so the tags will become toggle buttons and turning those buttons on/off will filter the information in the primary view); does this make any difference to how this should be laid out?
Bonus points for code snippets.
Hm. I'm not sure if this is the Backbone-friendly way, but I'll put the logic to retrieve a list of tags (I think that's what you meant by "parse") in the collection.
Both the main view and the subview will "listen" to the same collection, and the subview will just call collection.getTags() to get a list of tags it needs.
// Model that represents the list data
var ListDataModel = Backbone.Model.extend({
defaults: function() {
return {
name: null,
tags: []
};
}
});
// Collection of list data
var ListDataCollection = Backbone.Collection.extend({
model: ListDataModel,
initialize: function() {
var me = this;
// Expires tag collection on reset/change
this.on('reset', this.expireTagCache, this);
this.on('change', this.expireTagCache, this);
},
/**
* Expires tag cache
* #private
*/
expireTagCache: function() {
this._cachedTags = null;
},
/**
* Retrieves an array of tags in collection
*
* #return {Array}
*/
getTags: function() {
if (this._cachedTags === null) {
this._cachedTags = _.union.apply(this, this.pluck('tags'));
}
return this._cachedTags;
},
sync: function(method, model, options) {
if (method === 'read') {
var me = this;
// Make an XHR request to get data for this demo
Backbone.ajax({
url: '/echo/json/',
method: 'POST',
data: {
// Feed mock data into JSFiddle's mock XHR response
json: JSON.stringify([
{ id: 1, name: 'one', tags: [ 'number', 'first', 'odd' ] },
{ id: 2, name: 'two', tags: [ 'number', 'even' ] },
{ id: 3, name: 'a', tags: [ 'alphabet', 'first' ] }
]),
},
success: function(resp) {
options.success(me, resp, options);
},
error: function() {
if (options.error) {
options.error();
}
}
});
}
else {
// Call the default sync method for other sync method
Backbone.Collection.prototype.sync.apply(this, arguments);
}
}
});
var listColl = new ListDataCollection();
listColl.fetch({
success: function() {
console.log(listColl.getTags());
}
});
I guess two reasons for handling this in the collection:
It keeps the View code cleaner (This is given that we are not doing very complex logic in the tag extraction - It's just a simple _.pluck() and _.union().
It has 0 business logic involved - It can arguably belong to the data layer.
To address the performance issue:
It does go through the collection twice - However, if the amont of data you are consuming is too much for the client to process even in this case, you may want to consider asking the Backend to provide an API endpoint for this. (Even 500 pieces of data with a total of 1000 tags shouldn't bee too much for a somewhat modern browser to handle nowadays.)
Hmm. Does this help?
JSFiddle to go with this with the collection and the model: http://jsfiddle.net/dashk/G8LaB/ (And, a log statement to demonstrate the result of .getTags()).

Resources