Knockout 3.5.0 arrayChange - javascript

I have been facing some issues regarding "foreach" binding to an ko.observableArray for knockout, specifically knockout 3.5.0. The "foreach" binding is creating 2 elements on the U.I side while in the current data context, i am only having one single object within an observableArray. i am binding it as follows:
<!-- ko foreach: { data: $root.(...my own javascript function)') } -->
...
<!-- /ko -->
What i am doing here is that i am manually adding a new entry into an empty observableArray by calling .push() on the observableArray. The entry is added successfully to the observableArray, however it will show 2 UI elements instead of one even though I am only pushing in one entry into the observableArray.
However, if i set the attribute "includeDestroyed: false", it seems that the problem will disappear.
From what i have gathered, there were a few changes in Knockout 3.5.0 that relates to the implementation of foreach binding being changed in knockout. There has been a change to the implementation of "arrayChange" which detects any changes in the knockout observable array.
What I would like to know is the exact flow of what goes on during an "arrayChange" event, and is there any way i can find out how an empty observableArray handles a "push" function

From the official Knockout github page, when looking into release changelogs you can spot the following:
To improve performance when array changes are from a known, single
operation, such as push, the foreach binding no longer filters out
destroyed items by default. To turn this off and filter out destroyed
items, you can set includeDestroyed: false in the foreach binding or
set ko.options.foreachHidesDestroyed = true to use the previous
behavior by default.
So you can either set the "ko.options" globbaly, or set the flag like you do.
For more info you can look here.
As for the flow of the event, i suggest looking into the debug version of ko 3.5, along with a debugger and coffe!

Related

Ember binding on custom objects

See twiddle here: https://ember-twiddle.com/2150099882893760cef237ff2bd22e85
Basically, in crit-service I create Ember Objects "Crits" and "Crit", and fill them with some data.
The crit-service is used by two different components, which basically do the same thing: display the Crits.
The problem is that the "change" buttons do not work. By debugging, I see that the values are changed, but the view is not updated. Why is this? Since Ember.Object is Observable, shouldn't setting a value notify the template? And most importantly, how can I get this to work?
P.S. I've seen a workaround by using Ember.A() instead of Objects. However, this would add boilerplate, as my data model is really objects and not arrays of key-value pairs.
This seems to be an issue with the {{#each-in}} helper which does not reload on changes. A quick fix is to use the {{get}} helper.
So instead of this:
{{#each-in obj as |key val|}}
{{key}}={{val}}
{{/each-in}}
Do this:
{{#each-in obj as |key|}}
{{key}}={{get obj key}}
{{/each-in}}
However, this will never work if you add additional properties.
here is a working twiddle with that solution.
Another solution that will always work is to call .rerender() on the component. This is save thanks to glimmer, which does only update the parts of the DOM that have changed. However, you would have to call it on your common root component of the two components, or on both components.

Asyncronous knockout observableArray select options loading

I hava a selectOptions ajax-based asincronous loader; it accepts remote address and returns an observable array, correctly populated with descriptions and keyvalues to be accepted by the following binding
<select data-bind="value: selectedVal, options: opts, optionsText: 'desc', optionsValue:'key', optionsCaption: ''"/></div>
The fact is that, being asincronous, when I trigger a select options change, based on some user actions, I assign it to my model observable array, I do not get the select popuated, but remains empty.
mymodel.opts = loadOptions("<remoteaddress>");
I know when the second line is called the anwer is not arrived yet, but the returned value is an observableArray, so it should respond correctly whenever is populated, having been assigned to an observable array binded with the ui.
If I hardcode the returned object from the ajax call (when it returns) taking it from console.log in Firefox, or if I pass the observable array opts into the loadOptions, and change it to build up the opts inside it, then it works, but I really need to use loadOptions as is, asincronous. I also tried to append mymodel.opts.valueHasMutated(), but yet ko cannot use the newlly arrived observableArray.
If possible leaving intact the options loader, and if possible without using a custom binding, can I use the incoming observable array for binding when it will be ready?
The problem you've got is that when this line runs:
mymodel.opts = loadOptions("<remoteaddress>");
it's replacing the entire observable array with a different observableArray, rather than updating the current one. You need to update the existing one - can you change loadOptions to return a normal array, rather than an observable one? You can then do:
//clear any existing entries
mymodel.opts.removeAll();
//push the new entries in
mymodel.opts.push.apply(mymodel.opts, loadOptions("<remoteaddress>"));

Angularjs: buggy two way data binding

I am trying to create recursive grid layout using directives.
There is a parent container(appliedgrids) which contains array of grids inside it.
A grid contains array of columns inside it.
Each column has two properties: span(width of column) and data (data inside column)
Each column data contains either grid again or a widget. If it contains grid then it make recursive call to grid directive.
My problem is when I delete a grid using remove button inside it- it gets removed from appliedgrid container but two way data binding doesn't work as it should. In place of current grid, last grid gets removed from the UI.
Link- http://plnkr.co/edit/DzKIHKvJdLoZiYY3jgDx?p=preview
Steps to reproduce:
1) Click remove button on first grid, you will see that in place of first, second grid gets removed. While json data of appliedgrid contains second grid inside it. So two way binding of angular doesn't work as it supposed to.
I did a little thinking in my previous answer and it turns out it was not correct.
Firstly, do not use track by $index. It makes no sense in your case. track by is an optimisation of ng-repeat to correlate (potentially new) objects in the array that are "business-wise" equal with old objects in the array, so that it re-uses scopes and DOM elements in an effort to minimize DOM manipulation. That is: if you give ng-repeat a hint which new object in the new array is "equal" to an old object in the old array, it will reuse its scope and hopping that the new object is not dramatically different compared to the old one, less $watch callbacks will fire and less DOM updates will occur.
Your actual problem is that you are "statically" or "once-off" binding data with statements like:
$scope.gridIndex = $parse($attrs.gridIndex)($scope);
$scope.gridValues=$parse($attrs.appliedgrid)($scope);
$scope.gridParent=$parse($attrs.appliedgrids)($scope);
The first grid item is indeed removed from the array but ng-repeat does not remove its scope and DOM element because track by $index is used. But still, the new 0-index object (2nd, previously) is used to update the scope (the one created for the 1st object).
You do not see this reflecting to the UI because $scope.gridValues was evaluated in the beginning and is not evaluated again.
So, even though $scope.appliedgrid now points to [{span:12,data:[object]}], $scope.gridValues still points to [{span:6,data:[object]},{span:6,data:[grid2]}].
Removing track by $index solves the problem because ng-repeat tracks objects by reference so each object is associated with the same scope until it is removed from the array.
You can verify it with AngScope, a small Firebug-based scope inspector. You have to open it in a separate tab with "Launch the preview in a separate window" in order for it to work in plunker.
I tried to find a quick fix for it but there was no luck. I guess, you have to re-write it using isolated scopes and real 2-way binding.
Short answer: remove track by $index from ng-repeat.
Long answer: When you are write track by $index you're actually saying to ng-repeat that:
The 1st DOM element will be associated to an object that is tagged as the "0" object
The 2nd DOM element will be associated to an object that is tagged as the "1" object
When you remove the 1st object from the array, angular digests and finds out the following:
The 1st DOM element is still associated to an object tagged as the "0" object
The 2nd DOM element is not associated to any object, so it has to be removed
This is because when ng-repeat runs again, your previously 2nd object which was tagged as "1", is now your 1st and only object which is tagged as "0", since the $index is evaluated again starting from 0.
Angular believes that the 1st DOM element still points to the same object cause it finds it tagged as "0", regardless that it's a completly different object. Under the hood, $scope has the correct model values but ng-repeat skips re-rendering of the DOM element.
It very difficult to write down what really happens. Hope I got it right and helped you.

Strange knockout js select option binding issue

Does anybody have a minute to take a look at a puzzling problem I'm having with Knockout JS and binding select lists?
The person's favourite color should be selected in the list by calling value: favColorId in the select list, rather than with the function-based call of value: favColorId(), something very strange is going on here, I've never used () in the past, it's also causing some other weird issues where it won't recall the value into the span (so changing the selected item does nothing).. I have tried recreating a simple sample as best I can demonstrating the issue.
http://jsfiddle.net/goneale/ph8Jw/
I have included my mapDictToArray() function but it simply converts a
javascript object into a key-value JS array. I wouldn't think that is
contributing to the problem.
Actually, that was part of the problem. The function returns a JavaScript array, not an observable array and therefor can't be used properly by Knockout. I've made the following changes to your code:
// The "mapDictToArray" makes a normal JS array, not a ko.observableArray();
// You can't simply "merge" a JS array with an observableArray(); you'll need
// some extra functionality for that: ko.utils.arrayPushAll()
// viewModel.colors(mapDictToArray(dict));
ko.utils.arrayPushAll(viewModel.colors(), mapDictToArray(dict));
// Apply the bindings *after* you've added the contents to the "colors" observable, in order to
// get the correct selected value
ko.applyBindings(viewModel);
That should do the trick (with the correct HTML without the () )!
JSFiddle.
UPDATE
I thought about my solution, but something wasn't correct. The only thing that was correct, was the part that you need to apply the bindings after you've added the contents of the colors observable. This is your fiddle, with that part moved down.
This works in your case, but you'll need to use the arrayPushAll method when there is already data inside the observableArray. This method merges, while you overwrite it when not using it (example with data inside the observable).

Backbone models, JSON and HTML Forms

Backbone models .get() and .set() routines are fine if your values are simple attributes. I have inherited an app however where the values are also JSON objects.
i.e. model.get("key") returns
{start:
{top:5, bottom:6},
end:{...}
}
etc. Setting any of these values using .set() is a PITA because you have to pull out the old value, make the change you want, and punt it back in again. i.e. if I want bottom = 7, you can't just do .set("start.bottom",7). I need to do something like:
var tempValue = model.get("start");
tempValue.bottom = 7;
model.set("start",tempValue)
Models have an attributes property, where you can do model.attributes.start.bottom = 7. Now this presumably doesn't fire any event handlers, because looking at the Backbone code, that is done in set.
So two questions:
If I don't need the change handlers to fire, is there anything wrong
with setting attributes directly.
Is there anyway of firing the
change handlers manually after setting attributes?
The Backbone changelog says - "The Model#change method has been removed, as delayed attribute changes are no longer available" -but I'm not entirely sure why this would be. It sounds useful.
Related to this: I'm also trying to parse a HTML form into the model. I've been trying to use ModelBinder, but it can't handle nested JSON. e.g. <input type="text" name="start.top">
Edit:
I've just realised you can do model.set({}) with an object. e.g. model.set({start :{top:7}}); but it's a bit clunky
If I don't need the change handlers to fire, is there anything wrong with setting attributes directly.
I think that this may answer your question:
Backbone.js get and set nested object attribute
Is there anyway of firing the change handlers manually after setting attributes?
The set method (without silent option) triggers 2 events: "change" and "change:attributeName".
If you need it, you can trigger them manually by invoking:
model.trigger("change change:attributeName")

Categories