Angular scope not updating after orderBy - javascript

I'm building an angular component that renders a table, and I'm running into some issues with the sorting function. The scope in this case looks like this:
$scope.listState = {
sortBy: '<string>',
sortReverse: <bool>,
headings: {...},
list: [
{
rowCols: {
user: 'timfranks#gmail.com',
name: 'Tim Franks',
since: '11/6/12'
}
rowState: {...}
},
{
{
user: 'albertjohns#sbcglobal.net',
name: 'Alber Johns',
since: '11/12/13'
},
rowState: {...}
},
{
{
user: 'johnsmith#sine.com',
name: 'John Smith',
since: '7/28/14'
},
rowState: {...}
}
]
};
I originally tried to sort the list via:
ng-repeat="row in $ctrl.list | orderBy:$ctrl.listState.sortBy:$ctrl.listState.sortReverse"
This didn't work, although in tracing with the debugger I found that orderBy was in fact getting the right arguments and returning a properly sorted list. Just to be sure, I changed the code to use orderBy in my controller, like this:
this.listState.list = _this.orderBy(listState.list, 'rowCols.' + listState.sortBy, listState.sortReverse);
This works for the first time (called within the constructor), but then stops working. I'm pretty sure this is some aspect of Angular's scope that I don't fully understand. Any help would be appreciated.

Using a filter in an ng-repeat expression does not update the original model, but rather creates a copy of the array.
You are on the right track with the this.listState.list = _this.orderBy(...) ... and it does make sense that it gets called only once.
If the listState.list model is getting new values after the controller loads and you want to resort with those new values, you would probably want to use something like:
$scope.$watchCollection('listState.list', function listChanged(newList, oldList){
$scope.listState.list = _this.orderBy(...);
});
I can't recall if $watchCollection is going to register a change if the order changes, but if you end up in an infinite loop with that code, you could put a blocker like:
var listSorting = false;
$scope.$watchCollection('listState.list', function listChanged(newList, oldList){
if(!listSorting){
listSorting = true;
$scope.listState.list = _this.orderBy(...);
$timeout(function resetSortBlock(){ // try it without the $timeout to see if it will work
listSorting = false;
});
}
});

Figured it out. The issue was that I used an attribute directive to render the list rows, and the attribute directive and ng-repeat were on the same element. This created a scope conflict between the two directives. I moved my directive to a div within the ng-repeat and it works fine.

Related

Angular $scope variable breaking on update

In my Menu controller I have a function to save a Menu. The $scope.menu variable holds the object that represents my menu data. The saveMenu function sends an Ajax call off to a REST API endpoint and then receives the updated menu back as a response. The problem is that when I assign $scope.menu to the response all my data bindings in the HTML template break. Suddenly all the menu data disappears.
The controller code.
$scope.saveMenu = function() {
var menu = $scope.createJsonMenuRequest();
var method = "PUT";
var url = FoodUrls.foodAPI + "menus/" + menu.id;
var req = {
method: method,
url: url,
headers: {
"Content-Type": "application/json"
},
data: angular.toJson(menu)
};
$http(req).success(function(data) {
$scope.menu = $.extend(true, {}, data);
});
};
The createJsonMenuRequest function simply goes through the menu and removes some properties from the copy that the API doesn't like.
Why does the binding to the HTML template break?
Updated
Before the assignment statement in the success function, the $scope.menu looks something like this.
{
name: "My Menu",
sections: [
{ $$hashKey: "object:17", id: 1, name: "blarg"}
]
}
Afterwards it looks like this...
{
name: "My Menu",
sections: [
{ id: 1, name: "blarg-edited"}
]
}
It loses the $$hashKeys that Angular is putting in there when the menu is originally created. Not sure what the significance of that is.
I would not recommend using jQuery's extend functions against any property on an angular $scope. $scope is a complicated object with lots of pointers to specific properties and such. I would recommend using angular.merge, as it should do a better job of merging the objects correctly without breaking the scope.
https://docs.angularjs.org/api/ng/function/angular.merge
I can't comment yet, so I'll just put this here.
How far did you debug?
I'd look at $scope.menu before the update, and then again after in your success method.
What's your data / template look like?
And just for my own curiosity, why the deep $.extend ? Could be totally valid, I've just never used it in this way.
Saw your update, the $hashkey shouldn't be an issue, if you don't want it, you angular.copy(data) or simply put a track by in your ng-repeat :
data-ng-repeat="item in menuItems track by $index"

angularjs: Bound data in custom direct no longer updating after using "track by" in ng-repeat

This is an addendum to this question I asked previously:
Why does my custom directive not update when I derive the output from the bounded data?
My current dilemma is that I have duplicates in data that I generate inside a custom directive used in an ng-repeat. This means I have to use "track by". This somehow breaks the binding and when I update the model, it no longer updates. If I don't use update and remove the duplicates (which for the example can be done easily but for my real problem I cannot), it works. Here is the jsfiddle of how the issue:
http://jsfiddle.net/Lwsq09d0/2/
My custom directive has this:
scope: {
data: "="
},
link: function (scope, element, attrs) {
scope.$watch(function () {
return scope.data
}, function () {
var getPages = function(extra) {
var pages = [];
pages.push('...');
for (var i = scope.data.list[0]; i <= scope.data.list[1] + extra; i++) {
pages.push(i);
}
pages.push('...');
return pages;
}
scope.pages = getPages(1);
}, true);
},
// Remove "track by $index" to see this working and make sure to remove the duplicates
// "..." pushed in to the generated data.
template: '<ul><li ng-repeat="d in pages track by $index" my-button="d"></li></ul>'
In the fiddle, I have an ng-click call a controller function to modify data.
I've seen other questions about track by breaking binding, but I haven't seen one where the ng-repeat variable is generated in the custom directive via the bound data.
Thanks for any help.
Track by is optimized not to rebuild the DOM for already created items. In your example, you are using $index as the identifier. As such, ng-repeatsees the identifier 1 (for the second element in the pages array) and decides that it does not have to rebuild the DOM. This is causing the problem that you are experiencing.
One possible solution might be to generate page Objects that have a unique id, and to track by that:
var lastID = 0;
function createPage(name){
return { name: name, id: lastID++ };
}
// ... Directive code
pages.push(createPage('...')); // Do this for everything that you push to pages array
// ... More directive code
template: '<ul><li ng-repeat="d in pages track by d.id" my-button="d.name"></li></ul>'
Your JSFiddle, updated to work: http://jsfiddle.net/uv11fe93/

Meteor.js and non shuffling each

I have a problem implementing a non shuffling each over a reactive collection.
With this I mean that I have a collection ordered by a score value, and I don't want to change the order even if a score change.
I'd like it to be reactive but without the list items changing position every time.
The current code is this
ListController = FastRender.RouteController.extend({
find: function (tag){
if(!!tag){
return Lib.Items.Tags.SupportedType[tag].find;
}
return {};
},
findOptions: function(order, pageNum) {
var findOption = { limit: Lib.Items.Settings.ItemsPerPage * (parseInt(pageNum) || this.increment)};
if(!!order){
_.extend(findOption, Lib.Items.ListOrders.SupportedType[order].findOption);
}
return findOption;
},
waitOn: function() {
return [
Meteor.subscribe('items.all', this.params.tag, this.params.order, this.pageNum),
];
},
data: function () {
return {
"items": Items.find(this.find(), this.findOptions(this.params.order, this.params.pageNum))
};
},
action: function(){
this.render();
}
});
[...]
this.route('list', {
path: '/list/:pageNum?',
controller: ListController
});
this.route('list_order_limit', {
path: '/list/order/:order/:pageNum?',
controller: ListController
});
this.route('list_tag_limit', {
path: '/list/tag/:tag/:pageNum?',
controller: ListController
});
this.route('list_tag_order_limit', {
path: '/list/tag/:tag/order/:order/:pageNum?',
controller: ListController
});
and
{{#each items}}
{{> item}}
{{/each}}
With this everything works but the list change orders (which is correct but not the behaviour I'd like).
I tried some different solutions but nothing worked.
I started by modifying the data.
I put
data: function () {
return {
"items": Items.find(this.find(), this.findOptions(this.params.pageNum))
};
},
In this way the data knows nothing about the ordering (only the subscription knows about which order is used to take the first number of items items).
The problem is that in this way even if the route change from one ordering to another the list is not updated. And while this happens I see the subscription getting called with the new order parameter (so the data sent via ddp should actually be correct).
I even tried stopping and resubscribing but nothing changed.
What I'm doing wrong? Is there a better way to have a list reactive in its item but not in its order?
Thanks in advance.
I don't see anything in your code that explicitly references the order by the score value; however I believe this is an unresolved issue in Meteor: https://github.com/meteor/meteor/issues/1276.
One of the workarounds suggested in that thread was to add an explicit sort by _id on the client, which will ensure that the cursor is stable even when items change.
I can imagine a more unconventional way to work around this, which is to compute the desired order in the publish function and use the publish API to send custom fields over to the client, and then sorting by these fields. This is more complicated than a cursor but will result in the items showing up in the exact order determined at publish time.

ui-select2 inside directive isn't updating controller model

I have a directive that takes in a collection and builds out a dropdown.
.directive("lookupdropdown", function () {
return {
restrict: 'E',
scope: {
collectionset: '=',
collectionchoice: '='
},
replace: true,
template: '<select class="input-large" ui-select2 ng-model="collectionchoice" data-placeholder="">' +
' <option ng-repeat="collection in repeatedCollection" value="{{collection.id}}">{{collection.description}}</option>' +
'</select>',
controller: ["$scope", function ($scope) {
$scope.repeatedCollection = new Array(); //declare our ng-repeat for the template
$scope.$watch('collectionset', function () {
if ($scope.collectionset.length > 0) {
angular.forEach($scope.collectionset, function (value, key) { //need to 'copy' these objects to our repeated collection array so we can template it out
$scope.repeatedCollection.push({ id: value[Object.keys(value)[0]], description: value[Object.keys(value)[1]] });
});
}
});
$scope.$watch('collectionchoice', function (newValue, oldValue) {
debugger;
$scope.collectionchoice;
});
} ]
}
});
This works fine. It builds out the drop down no problem. When I change the dropdown value, the second watch function gets called and I can see that it sets the value of collection choice to what I want. However, the collectionchoice that I have put into the directive doesn't bind to the new choice.
<lookupDropdown collectionset="SecurityLevels" collectionchoice="AddedSecurityLevel"></lookupDropdown>
That is the HTML markup.
This is the javascript:
$scope.SecurityLevels = new Array();
$scope.GetSecurityLevelData = function () {
genericResource.setupResource('/SecurityLevel/:action/:id', { action: "#action", id: "#id" });
genericResource.getResourecsList({ action: "GetAllSecurityLevels" }).then(function (data) {
$scope.AddedSecurityLevel = data[0].SCRTY_LVL_CD;
$scope.SecurityLevels = data;
//have to get security levels first, then we can manipulate the rest of the page
genericResource.setupResource('/UserRole/:action/:id', { action: "#action", id: "#id" });
$scope.GetUserRoles(1, "");
});
}
$scope.GetSecurityLevelData();
Then when I go to post my new user role, I set the user role field like this:
NewUserRole.SCRTY_LVL_CD = $scope.AddedSecurityLevel;
but this remains to be the first item EVEN though I have updated the dropdown, which according the watch function, it has changed to the correct value. What am I missing here?
You faced this issue because of the prototypical nature inheritance in Javascript. Let me try and explain. Everything is an object in Javascript and once you create an object, it inherits all the Object.Prototype(s), which eventually leads to the ultimate object i.e. Object. That is why we are able to .toString() every object in javascript (even functions) because they are all inherited from Object.
This particular issue on directives arises due to the misunderstanding of the $scope in Angular JS. $scope is not the model but it is a container of the models. See below for the correct and incorrect way of defining models on the $scope:
...
$scope.Username = "khan#gmail.com"; //Incorrect approach
$scope.Password = "thisisapassword";//Incorrect approach
...
$scope.Credentials = {
Username: "khan#gmail.com", //Correct approach
Password: "thisisapassword" //Correct approach
}
...
The two declarations make a lot of difference. When your directive updated its scope (isolated scope of directive), it actually over-rid the reference completely with the new value rather then updating the actual reference to the parent scope hence it disconnected the scope of the directive and the controller.
Your approach is as follows:
<lookupDropdown collectionset="SecurityLevels" collectionchoice="$parent.AddedSecurityLevel"></lookupDropdown>
The problem with this approach is that although it works, but it not the recommended solution and here is why. What if your directive is placed inside another directive with another isolated scope between scope of your directive and the actual controller, in that case you would have to do $parent.$parent.AddedSecurityLevel and this could go on forever. Hence NOT a recommended solution.
Conclusion:
Always make sure there is a object which defines the model on the scope and whenever you make use of isolate scopes or use ng directives which make use of isolate scopes i.e. ng-model just see if there is a dot(.) somewhere, if it is missing, you are probably doing things wrong.
The issue here was that my directive was being transcluded into another directive. Making the scope im passing in a child of the directive it was in. So something like $parent -> $child -> $child. This of course was making changes to the third layer and second layer. But the first layer had no idea what was going on. This fixed it:
<lookupDropdown collectionset="SecurityLevels" collectionchoice="$parent.AddedSecurityLevel"></lookupDropdown>

knockout javascript foreach binding

I'm trying to allow a user to create a casting and add an array of categories to this casting object. I was trying to use knockout's foreach binding to the array of categories and let users add new categories to the casting. I have created a jsfiddle to illustrate what I'm trying to explain here.
http://jsfiddle.net/msell/ueNg7/16/
The JSON object gets built up correctly as a user modifies a casting, but I cant quite get the list of castings to display.
You have several problems:
You are using Knockout 1.2.1
The foreach binding was not added until Knockout 2.0.
You are not using an observableArray
You need to modify your categories property to be a ko.observableArray(), instead of just an empty array. Otherwise Knockout will not be able to observe when you push to it, and the remove method will not exist.
Your this binding is wrong.
When called from event handlers, this will be set incorrectly. You can fix this in various ways, discussed in length in the Knockout documentation, but one easy fix is to change the references to viewModel instead of to this.
To fix all these, you should upgrade to Knockout 2.0, and change your view model declaration to be
var viewModel = {
name: ko.observable(''),
description: ko.observable(''),
categories: ko.observableArray(),
categoryToAdd: ko.observable(''),
removeCategory: function(category) {
viewModel.categories.remove(category);
},
addCategory: function() {
viewModel.categories.push(new Category(viewModel.categoryToAdd()));
viewModel.categoryToAdd('');
}
};
Here is a corrected JSFiddle: http://jsfiddle.net/ueNg7/19/
You need to use ko.observableArray for you array otherwise Knockout wont know when you change your array and wont update, also you should use a template instead, read here http://knockoutjs.com/documentation/template-binding.html#note_2_using_the_foreach_option_with_a_named_template
var viewModel = {
name: ko.observable(''),
description: ko.observable(''),
categories: ko.observableArray([]),
categoryToAdd: ko.observable(''),
removeCategory: function(category) {
this.categories.remove(category);
},
addCategory: function() {
this.categories.push(new Category(this.categoryToAdd()));
this.categoryToAdd('');
}
};

Categories

Resources