How to easily determine what's added/removed in a $scope.$watch? - javascript

I am tasked with wrapping a JQuery-style UI component within an Angular directive. I have completed the task but it looks rather ugly. The underlying component's API only has add and remove methods for controlling a list of items. The only thing I can think to do is watch the two-way bound variable passed into my directive with scope.$watchCollection and then iterate over the old and new arguments to determine what has been added or removed, then invoke the add/remove API calls on the underlying component. This works, but I'm curious if there is a better way to accomplish this. The code is hideous and confusing.
Here is a demo minimal recreation of the component in it's natural habitat:
http://codepen.io/Chevex/pen/ofGeB?editors=101
You can see the component is instantiated on an element that has the items in the list within the markup. The component then returns an API to add/remove individual items. Please note that I have no control over the underlying component in our actual application and that the real component does far more than my demo component (which could easily be re-written with an ng-repeat).
Here is a demo minimal recreation of my directive wrapper:
http://codepen.io/Chevex/pen/LDgCF?editors=101
This works great and simulates a two-way bound variable with a jquery-style component that only has add/remove methods for individual items. I want to know if there is a better way to determine new/removed items within $watchCollection.
// Watch items for changes.
scope.$watchCollection('items', function (newItemList, oldItemList) {
// Iterate over the newItemList and determine what items are new.
if (newItemList) {
newItemList.forEach(function (newItem) {
var addItem = true;
if (oldItemList) {
oldItemList.forEach(function (oldItem) {
if (newItem === oldItem) {
addItem = false;
}
});
}
if (addItem) {
listApi.addItem(newItem);
}
});
}
// Iterate over the oldItemList and determine what was removed.
if (oldItemList) {
oldItemList.forEach(function (oldItem, oldItemIndex) {
var removeItem = true;
if (newItemList) {
newItemList.forEach(function (newItem) {
if (oldItem === newItem) {
removeItem = false;
}
});
}
if (removeItem) {
listApi.removeItem(oldItemIndex);
}
});
}
});
All of that just to determine what should be added or removed using the component API whenever the collection changes. Is this the best way? I ask because I've been finding myself writing similar logic repeatedly and wondered if there may be a more proper and/or simpler way to do it.

You could use something like this:
Array.prototype.diff = function(arr) {
return this.filter(function(el) { return arr.indexOf(el) < 0; });
};
var addedItems = newItemList.diff(oldItemList);
var removedItems = oldItemList.diff(newItemList);

Related

Making Vue.js detect changes to array using $set()

I'm having trouble updating an array that is displayed as a list. I'm trying to make Vue detect the changes using $set() (as explained in the documentation), but I can't make it work.
Here's my code:
this.choices = this.currentScene.choices;
for (i = 0; i < this.choices.length; i++) {
choice = this.currentScene.choices[i];
choice.parsedText = this.parseText(choice.text);
this.choices.$set(i, choice);
}
Vue still doesn't update the view. What am I doing wrong? Thanks in advance!
Edit: Yes, "this" refers to the Vue instance.
It would definitely be useful to have a JSfiddle of your code, but I'm going to take a crack anyways.
I'm not sure you need to use that function to update the array, since as the documentation points out, its only when you need to change the index of the item.
JavaScript has a built in function called .map that takes a callback function and returns a new array with the callback applied to each item.
For example, you could translate your function to this, assuming that .parseText is a method on the Vue class.
var self = this; // so that we can access the Vue class inside map
this.choices = this.currentScene.choices.map(function(choice) {
choice.parsedText = self.parseText(choice.text);
return choice;
});
And Vue should pick up those changes.
You could use a computed property for this, so you never have to manually update the array. Anytime choices changes you would see the change reflected in this.parsedChoices:
computed: {
parsedChoices: function(){
return this.currentScene.choices.map(function(choice) {
choice.parsedText = this.parseText(choice.text);
return choice;
}.bind(this)); // bind Vue class as value of `this` inside func
}
}

How to temporary stop calculating computed when executing multiple array push/remove operations

I have computed which based on observableArray property headers. Also i have methods for adding and removing multiple headers:
function ViewModel() {
var self = this;
self.headers = ko.observableArray();
self.newHeaders = ko.computed(function() {
var countOfNew = 0;
ko.arrayForEach(self.headers(), function(header) {
if (!header.id) {
countOfNew++;
}
});
return countOfNew;
});
self.addHeaders = function(headers) {
ko.arrayForEach(headers, function(header) {
self.headers.push(header);
}
};
self.removeHeaders = function(headers) {
ko.arrayForEach(headers, function(header) {
self.headers.remove(header);
}
};
}
When i call addHeaders or removeHeaders, newHeaders called for each item in headers array. There is any solution how temporary stop calculating computed field? (someting like as ko.valueWillMutate, ko.valueHasMutated which used for subscribers).
From Knockout.js Performance Gotcha #2 - Manipulating observableArrays
A better pattern is to get a reference to our underlying array, push
to it, then call .valueHasMutated(). Now, our subscribers will only
receive one notification indicating that the array has changed.
I forked Ryan Niemeyer jsfiddle example here .. Open the console and check how many times the message 'calling computed object' is logged!.
Keep in mind that an observable array is still just an observable. You can get the array's value var arr = oarr(), do whatever you want with it, and put it back in oarr(arr).
Knockout 3.1.0 includes the rateLimit extender that can improve performance in this scenario quickly and easily. Simply apply the extender to your observable array:
self.headers = ko.observableArray().extend({rateLimit: 0});
(Version 3.1.0 is currently [3/3/2014] available as a beta release.)
If you're only interested in delaying the computed observable (and not everything that's dependent on the array), you can apply the rateLimit extender to the computed observable instead. If you're using Knockout 3.0 or earlier, you can use the throttle extender on the computed to achieve a similar result:
self.newHeaders = ko.computed(function() {
...
}).extend({throttle: 1});

How to force knockoutjs to update UI (reevaluate bindings)

(I know there are other questions here asking the same thing; I've tried them and they don't apply here)
I have a collection being displayed by a Knockout JS foreach. For each item, the visible binding is set by call a method, based on something external to the item itself. When the externality changes, I need the UI to be redrawn.
A striped down version can be seen in this Fiddle: http://jsfiddle.net/JamesCurran/2us8m/2/
It starts with a list of four folder names, and displays the ones starting with 'S'.
<ul data-bind="foreach: folders">
<li data-bind="text: $data,
visible:$root.ShowFolder($data)"></li>
</ul>
<button data-bind="click:ToA">A Folders</button>
Clicking the button should display the ones starting with 'A' instead.
self.folders = ko.observableArray(['Active', 'Archive', 'Sent', 'Spam']);
self.letter = 'S';
// Behaviours
self.ShowFolder = function (folder)
{
return folder[0] === self.letter;
}
self.ToA = function ()
{
self.letter = 'A';
}
UPDATE:
After Loic showed me how easily this example could be fixed, I reviewed the differences between this example and my actual code. I'm using an empty object as a dictionary to toggle if an item is selected self.Selected()[item.Id] = !self.Selected()[item.Id];
The object being changed is already an observable. I assumed that Knockout didn't realize that the list is dependent on the external observable, but it does. What Knockout was missing was that the observable was in fact changing. So, the solution was simply:
self.Selected()[item.Id] = !self.Selected()[item.Id];
self.Selected.notifySubscribers();
Here's what I came up with:
What you have to understand is that Knockout is only "answering" to data changes in observables. If an observable changes, it will trigger every object that uses it. By making your self.letter an observable. You can simply change it's value and uses it somewhere like self.letter() and it will automagically redraw when needed.
http://jsfiddle.net/2us8m/3/
function WebmailViewModel() {
// Data
var self = this;
self.folders = ko.observableArray(['Active', 'Archive', 'Sent', 'Spam']);
self.letter = ko.observable('S');
// Behaviours
self.ShowFolder = function (folder)
{
return folder[0] === self.letter();
}
self.ToA = function ()
{
self.letter('A');
}
};
ko.applyBindings(new WebmailViewModel());
In case you have complex bindings, like storing an object inside an observable. If you want to modify that object you have multiple possible choices.
self.Selected()[item.Id] = !self.Selected()[item.Id];
You could change it to this by making everything "observables" but if my memory is right, it can become complicated.
self.Selected()[item.Id](!self.Selected()[item.Id]());
I remember I had one similar issue where I had dependency problem where I had to update a country, region, city. I ended up storing it as list inside an observable to prevent update on individual element change. I had something like this.
var path = PathToCity();
path[0] = 'all';
path[1] = 'all';
PathtoCity(path);
By doing this, the change would be atomic and there will be only one update. I haven't played a lot with knockout for a while. I'm not sure but I do believe that the last time I worked with knockout, it was able to "optimize" and prevent to redraw the whole thing. But be careful because if it is not able to guess that you didn't change many thing, it could redraw the whole observable tree (which could end up pretty bad in term of performance)
In your example, we could use the same behaviour with my modified example:
http://jsfiddle.net/2us8m/4/

Why does my ko computed observable not update bound UI elements when its value changes?

I'm trying to wrap a cookie in a computed observable (which I'll later turn into a protectedObservable) and I'm having some problems with the computed observable. I was under the opinion that changes to the computed observable would be broadcast to any UI elements that have been bound to it.
I've created the following fiddle
JavaScript:
var viewModel = {};
// simulating a cookie store, this part isnt as important
var cookie = function () {
// simulating a value stored in cookies
var privateZipcode = "12345";
return {
'write' : function (val) { privateZipcode = val; },
'read': function () { return privateZipcode; }
}
}();
viewModel.zipcode = ko.computed({
read: function () {
return cookie.read();
},
write: function (value) {
cookie.write(value);
},
owner: viewModel
});
ko.applyBindings(viewModel);?
HTML:
zipcode:
<input type='text' data-bind="value: zipcode"> <br />
zipcode:
<span data-bind="text: zipcode"></span>?
I'm not using an observable to store privateZipcode since that's really just going to be in a cookie. I'm hoping that the ko.computed will provide the notifications and binding functionality that I need, though most of the examples I've seen with ko.computed end up using a ko.observable underneath the covers.
Shouldn't the act of writing the value to my computed observable signal the UI elements that are bound to its value? Shouldn't these just update?
Workaround
I've got a simple workaround where I just use a ko.observable along side of my cookie store and using that will trigger the required updates to my DOM elements but this seems completely unnecessary, unless ko.computed lacks the signaling / dependency type functionality that ko.observable has.
My workaround fiddle, you'll notice that the only thing that changes is that I added a seperateObservable that isn't used as a store, its only purpose is to signal to the UI that the underlying data has changed.
// simulating a cookie store, this part isnt as important
var cookie = function () {
// simulating a value stored in cookies
var privateZipcode = "12345";
// extra observable that isnt really used as a store, just to trigger updates to the UI
var seperateObservable = ko.observable(privateZipcode);
return {
'write' : function (val) {
privateZipcode = val;
seperateObservable(val);
},
'read': function () {
seperateObservable();
return privateZipcode;
}
}
}();
This makes sense and works as I'd expect because viewModel.zipcode depends on seperateObservable and updates to that should (and does) signal the UI to update. What I don't understand, is why doesn't a call to the write function on my ko.computed signal the UI to update, since that element is bound to that ko.computed?
I suspected that I might have to use something in knockout to manually signal that my ko.computed has been updated, and I'm fine with that, that makes sense. I just haven't been able to find a way to accomplish that.
sigh, I found someone with my exact same problem
If dependentObservables don't notifySubscribers on write, why do they
even bother to do it on read? They get added to the observables list
and subscribed to, but then they never trigger on updates. So what is
the point of subscribing to them at all?
Ryan Niemeyer answers:
I think that for your scenario, dependentObservables may not be the
right tool for the job. dependentObservables are set up to detect
dependencies in the read function and re-evaluate/notify whenever any
of those dependencies change. In a writeable dependentObservable, the
write function is really just a place to intercept the write and allow
you to set any observables necessary, such that your read function
would return the proper value (write is typically the reverse of read
in most cases, unless you are transforming the value).
For your case, I would personally use an observable to represent the
value and then a manual subscription to that observable to update the
original value (the one that you may not have control over).
It would be like: http://jsfiddle.net/rniemeyer/Nn5TH/
So it looks like this fiddle would be a solution
var viewModel = {};
// simulating a cookie store, this part isnt as important
var cookie = function () {
// simulating a value stored in cookies
var privateZipcode = "12345";
return {
'write' : function (val) {
console.log("updated cookie value with: " + val);
privateZipcode = val;
},
'read': function () {
return privateZipcode;
}
}
}();
viewModel.zipcode = ko.observable(cookie.read());
// manually update the cookie when the observable changes
viewModel.zipcode.subscribe(function(newValue) {
cookie.write(newValue);
});
ko.applyBindings(viewModel);​
That makes sense and its somewhat simpler to use. Overall I'm not sure how great of an idea it is to treat a cookie as an observable since the server could edit it in an ajax request, etc.
Try making your internal privatezipcode an observable. See here: http://jsfiddle.net/KodeKreachor/fAGes/9/

Updating Divs when observable collection changes

Is there any plugin or jquery utility that will update my list of divs bound to an observable collection when items get added/removed to that collection? We have 1000s of items and so are looking for the most optimal way to add/remove divs when the bound list changes.
We would be interested in hearing about this in KO or jquery.tmpl.
This is maybe not the answer that your looking for, but it could be one way to do it.
I would wrap the array within an object that has an Add and Remove method(or other function that change the array)
var Collection = (function () {
var collectionArray = new Array();
//"private" methods
function updatePageDivs()
{
//Logic to update your Divs
}
//"public" methods
return{
Add: function(element){
collectionArray[collectionArray.length] = element;
updatePageDivs();
},
Remove: function(element){
//other logic to remove elements and to trigger the updatePage
}
}
});
You can now call
Collection.Add()
and
Collection.Remove()
to change your javascript collection. Both will update your pagedivs.

Categories