AngularJS : ng-repeat list not updating - directives - angularjs

I am new to angular and I have the following code:
Here is my html:
<drawer title="'Add a new Item'"
visible="showAddItemDrawer"
on-close="showAddApplicationItem = false">
<!-- items list refresh does not work from inside the drawer. -->
<add-item></add-item>
</drawer>
<!--list refresh works if i place the code out here.
<add-item></add-item> -->
<div ng-repeat="item in itemList">
<item-list></item-list>
</div>
Here is my directive.js:
directive("additem", function () {
return {
restrict:"E",
transclude:true,
controller:"ItemsCtrl",
templateUrl:"partials/add-item.html" //contains a form to add item
};
})
.directive("itemlist", function () {
return {
restrict:"E",
transclude:true,
templateUrl:"partials/item-list.html" //contains code to display the items in a list
};
})
I have a form in add-item.html to add an item. The form shows up when you click on add button(like an accordion). I call the push() to add a new item to the scope.
The list update works if i place the directive call outside the drawer..
If i place it inside the drawer,the scope is not getting updated until i hit refresh.
Can anyone point me what I am doing wrong with the directives? Thanks a lot!
Edit: Added additional code :
In the form to add an item:
<button type="submit"
ng-click="addItemService()">
Add Item
</button>
addItemService() code:
$scope.addItemService = function () {
var data = {
"name": $scope.itemName,
};
ItemService.addItem(data, $scope.listgroupid)
.success(function (result) {
$scope.itemName = "";
viewList(); //The function that sets the scope of the list
})
.error(function () {
});
};
viewlist() code:
var viewList = function () {
ListService.getList($scope.listgroupid)
.success(function (result) {
$scope.itemList = result;
//In the angular inspector, I am able to see the new item in the variable result
})
.error(function () {
});
};

EDIT: function scope is different from variable scope, use $scope.$parent.itemList when referencing new variable value

i had an issue where the directive scope (while debugging) and the batarang inspected scope (when selecting an element in the dev tools and typing $scope.varname in the console) were not matching after a successfully resolved promise.
the promise updates a list for a ng-repeat which contains a directive.
the debugged scope held the values from the last data before the promise was loading new data.
so i added a timeout around the data-loaded event broadcast which pushes the broadcast into the next digest cycle and angular gets informed about the update.
$scope.$apply most often throws the "apply already in progress" error.
the timeout solves this a little more softly...
$timeout( function () {
// broadcast down to all directive children
$scope.$broadcast('dataLoaded');
}, 1);

need to see the code wherein you are calling push for item.
In the absence of that I suspect this is scope issue. Remember that ngRepeat and drawer reside in difference scopes by default and until you have explicitly made them share certain variables , changes inside drawer won't be reflected in ngRepeat. For more on scopes inside directives read http://docs.angularjs.org/api/ng.$compile.
The 'isolate' scope takes an object hash which defines a set of local
scope properties derived from the parent scope. These local properties
are useful for aliasing values for templates. Locals definition is a
hash of local scope property to its source:
# or #attr - bind a local scope property to the value of DOM
attribute. The result is always a string since DOM attributes are
strings. If no attr name is specified then the attribute name is
assumed to be the same as the local name. Given and widget definition of scope: { localName:'#myAttr' },
then widget scope property localName will reflect the interpolated
value of hello {{name}}. As the name attribute changes so will the
localName property on the widget scope. The name is read from the
parent scope (not component scope).
= or =attr - set up bi-directional binding between a local scope property and the parent scope property of name defined via the value
of the attr attribute. If no attr name is specified then the attribute
name is assumed to be the same as the local name. Given and widget definition of scope: {
localModel:'=myAttr' }, then widget scope property localModel will
reflect the value of parentModel on the parent scope. Any changes to
parentModel will be reflected in localModel and any changes in
localModel will reflect in parentModel. If the parent scope property
doesn't exist, it will throw a NON_ASSIGNABLE_MODEL_EXPRESSION
exception. You can avoid this behavior using =? or =?attr in order to
flag the property as optional.
& or &attr - provides a way to execute an expression in the context of
the parent scope. If no attr name is specified then the attribute name
is assumed to be the same as the local name. Given and widget definition of scope: {
localFn:'&myAttr' }, then isolate scope property localFn will point to
a function wrapper for the count = count + value expression. Often
it's desirable to pass data from the isolated scope via an expression
and to the parent scope, this can be done by passing a map of local
variable names and values into the expression wrapper fn. For example,
if the expression is increment(amount) then we can specify the amount
value by calling the localFn as localFn({amount: 22}).

You can either use a $rootScope.$broadcast() to fire an event that your directive would capture, and then let the directive update the list with ng-repeat or you could call a $scope.$apply() with the updation wrapped in it within the controller to indicate angular that he list is updated, but when using $scope.$apply() be careful, and make sure it that it is delayed/deferred until the next digest cycle.
A library called Underscore.js lets you delay $apply until the next digest cycle.

Related

ionic, pass scope variable to popover scope

I would like to pass a variable from the view scope ($scope.sort) into the popover scope.
Once set in the popover, the variable 'goes out' of it with no issue though, with code below in$scope.closeSortPopover`.
I thought the popover scope was the same as the scope of the controller it comes from.
But it seems it's not the case.
Where is the popover scope ?
If I understand correctly from here the popover scope is a child of the controller scope. So how can I access it... ?
FYI my popover is a list of <ion-radio>s offering different sorting opions for the big list in my main view.
html:
<button ng-click="openSortPopover($event, sort)">
controllers.js
$ionicPopover.fromTemplateUrl('templates/computeSortPopover.html', {
scope: $scope
}).then(function(popover) {
$scope.sortPopover = popover;
});
$scope.openSortPopover = function($event, sort) {
$scope.sortPopover.show($event);
};
$scope.closeSortPopover = function(sort) {
$scope.sortPopover.hide();
$scope.sort = sort;
};
In the controller js, you are assigning the scope of the controller to the scope of the popover. Thus all the methods in the controller scope are accessible via the popover view. In other words, both the controller view and popover view share the same scope.
In fact you might think this a violation of mvc as one controller cannot own two views, but with angular, you actually can't otherwise.
To create isolated scope, your only option is directives.
I discovered that due to javascript inheritence, the scope variable must be contained into an object to be passed to inherited scopes.
So instead of this:
$scope.sort = ....
I declared this in my controller:
$scope.data={};
$scope.data.sort = ....
and used in my html (both in the initial view and in the popover templates):
data.sort
Documented here apparently: https://github.com/angular/angular.js/wiki/Understanding-Scopes#javascript-prototypal-inheritance

Two-way binding within nested directives and ui-select

Problem I have been working on:
The original problem (which only addressed one-way binding) can be found here:
Unable to pass/update ngModel from controller to directive
I have managed to get the one-way binding from inside the directive out to the view working. But if I want push in a selection when the page loads, for example, I cannot get the two-way binding to function. I can see that the ng-model inside the first directive is getting the data, but I tried various scope settings with the child directive and either it breaks it or it does the same thing as before - nothing.
I have a basic $watch function set up, so that when I push a simple object into the binding that is attached to ng-model in the view, the watcher assigns the $viewValue to the directive's scope object. It does this, and the directive responds only by having any existing selection wiped out, even though I can clearly see the objects inside ng-model binding assigned to ui-select.
Here is the watch function:
scope.$watch(function() {
return ngModel.$viewValue;
}, function(newVal) {
console.log(newVal, scope.options);
scope.options.selected = newVal;
});
I use this function to update the view whenever we interact with the ui-select (which works fine):
scope.changer = function() {
ngModel.$setViewValue(scope.options.selected);
console.log(scope.options.selected);
};
A Plunker for tinkering
So the expected behavior is:
selections from the ui-select should be displayed in the select, and also get passed to the view
by clicking the 'Change' button, data should be displayed in the view, and get passed to the ui-select
The situation was that I had a directive with ui-select inside it. The way I was able to get data from the main controller scope through the directive scope and down to ui-select's scope was by putting a watch function on ngModel.$viewValue in the link function of the directive.
I then assigned that new value, whenever there was a change, to an object on that directive's scope, to hold it for me. Then I set a watch function in the link function of ui-select to watch scope.$parent.myVal so that if anything was ever pushed to that variable from the main controller, ui-select would see it. When that happened, I would assign that new value to $select.selected which is the container for selected items in the ui-select directive.
It should be noted that ui-select uses scope: true in it's directive, and thus becomes a child of whatever scope it is instantiated in, which allows you to access the scope of it's parent with $parent.
Watch function for the directive:
scope.$watch(function() {
return scope.$parent.myVar;
}, function(newVal) {
$select.selected = newVal;
})
Watch function to be added to ui-select:
scope.$watch(function() {
return ngModel.$viewValue;
}, function(newVal) {
scope.myVar = newVal;
})
Plunker Demo

Predefined function to use in ng-repeat nested in a directive

I have a list directive that basically get all the items from a service and show them in a table. When I click on an item in the table, it opens a form in relation with the type of item.
Everything works great, but now, I have one instance of that list where I need to override the event when I click on an item. So I added an attribute in the directive:
<list factory-name="Workers"
on-item-click="$state.go('worker.workerDetails', {workerId: item._id})">
</list>
So when I get to the function that gets called when I click an item, I can do something like that:
<tr ng-repeat="item in items" ng-click="edit(item)></tr>
$scope.edit = function(item) {
if ($attrs.onItemClick) {
setTimeout(function(){ $scope.$apply($attrs.onItemClick); });
} else {
edit(item);
}
};
The problem is that I cannot isolate the scope since some nested directive need to access it and I would prefer not to modify my list directive with a bunch of exception, only an override function (onItemClick);
Right now it's not working, the stateparams don't get assigned.
Thank you very much
In case you can't isolate scope and use "&" bingind, you should use $parser service.
First parse attribute:
var onItemClick = $parse($attrs.onItemClick);
then call it like this:
onItemClick($scope, { item: item });
You pass your context scope as the first argument, and an object containing local (per call) variables the second argument.
And not to polute outer scope, you can use "child scope", by specifying scope: true in directive definition.

Template in child directive has different scope than the child directive's link function

Here is a non-functioning Plunkr with the relevant code:
http://plnkr.co/edit/cM67vJaL0gjma0SoPbK2?p=info
Why does the "pullThirdPartyCollection.html" template have a different scope than the pullThirdPartyCollection directive's link function? I have to use ng-model="$parent.$parent.bggUsername" to make sure two way binding happens correctly. Otherwise, scope.bggUsername is null in the link function's scope.pullCollection function.
If I set an initial value in the link function: scope.bggUsername = 'test', that initial value will display on the screen. Even when I change the value in the input box, and I have an output value {{bggUsername} right next to it, that value changes too. However, when I click the search button and the pullCollection function, inside the link function is called, the scope inside the link function does not have the value I expect for bggUsername.
Also, the pullThirdPartyCollection link function's scope's $id is 20. When I print the value of $id in the template pullThirdPartyCollection.html.js, the value of $id is 33. That is how I figured out I could change the link functions bggUsername by calling $parent.$parent.bggUsername in the template. Still, it does not make sense why the template and the link function have two different scopes. Any ideas what I am I doing wrong?
Here is some of the basic setup
<div paginated-search query="query" total-items="totalItems" current-page="currentPage" items-per-page="itemsPerPage" search-on-load="searchOnLoad" urls="urls" select-item="selectItem(item)" get-search-types="getSearchTypes()" show-search-types="showSearchTypes" selected-search-type="selectedSearchType" edit-item="editItem(e,id)" search-button-id="search-items-button">
<div pull-third-party-collection ></div>
</div>
The pullThirdPartyCollection directive:
angular.module('directives.pullThirdPartyCollection', [])
.directive('pullThirdPartyCollection', function($q, urls, KeyCloakService) {
return {
require: '^paginatedSearch',
restrict: 'EAC',
templateUrl: 'pullThirdPartyCollection.html',
scope:{
},
link: function postLink(scope, element, attrs, paginatedSearchCtrl) {
scope.pullCollection = function(e) {
var deferred = $q.defer();
$(e.currentTarget).button('loading');
KeyCloakService.makeRequest(urls.collectionserviceurl + '/pulldata/bgg?username=' + scope.bggUsername + '&collectiontype=trade&urltype=all&subject=' + KeyCloakService.auth.subject,
null,
function(data) {
$(e.currentTarget).button('reset');
scope.pulled = true;
paginatedSearchCtrl.searchItems();
deferred.resolve();
},
null,
'application/json',
'application/json',
'GET');
return deferred.promise;
};
scope.toggleAllow = function(){
scope.allow = !scope.allow;
};
}
};
});
An existing entry in stackoverflow led me in the right direction:Two way binding not working in directive with transcluded scope. I knew AngularJS had a tendency to create child scopes for transcluded directives. I just didn't realize it can create different scopes between the template and the link function. The child scope's bggUsername initially receives itsvalue from the parent. After that a shadow copy is created and is no longer kept in synch with the parent. Apparently the solution is to only bind objects. From the documentation: every ng-model should have a "." in it. I changed the bound property from a primitive to an object and the object stays in synch inside the link function. So the solution was to make bggUsername an object and change ng-model="bggUsername" to ng-model="bggUsername.value".

What's the difference between using function and using inline expression to set scope variable

I found some differences between executing function and using expression to set variable, specifically, it seems that ng-if fails to detect the changes made by expression. I don't understand why.
Pseudo-code:
if($scope.editingProfile)
display edit section
click(backToList button)
hide edit section
The backToList button has a ng-click attribute, when I write ng-click="backToList()" to execute $scope.backToList() in which the $scope.editingProfile is set to false it works good. But when I write ng-click="editingProfile=false" to set the variable directly, the ng-if used to hide the section won't work.
fiddle: http://jsfiddle.net/brfbtbd7/1/
It is because ng-if directive creates a child scope. So ng-if="editingProfile" is the editing profile object on the parent which gets inherited to the child scope created by the ng-if. That is what gets displayed # Editing {{editingProfile.name}}<br>. When you do ng-click="editingProfile=false" you are actually updating the child scope's inherited version of editingProfile which does not gets reflected in the one at the parent. But when you do ng-click="backToList()" The function is in the controller and so the $scope.editingProfile refers to the one on the controller (parent) hence that change is reflected and ng-if becomes falsy and it hides the content.
You could solve this my adding one more level in the scope property hierarchy.
angular.module("testApp", []).controller("editCtrl",
function ($scope){
$scope.model = {};
$scope.model.editingProfile = null;
$scope.edit = function() {
$scope.model.editingProfile = {
name: "test"
};
}
$scope.backToList = function() {
$scope.model.editingProfile = null;
}
}
)
and
<div ng-if="model.editingProfile">Editing {{model.editingProfile.name}}
<br> <a ng-click="backToList()">click to execute function to go back</a>
<br/> <a ng-click="model.editingProfile=null">click to set variable to go back(NOT working)</a>
</div>
Fiddle

Resources