angular array item not updating on scope change - angularjs

Spent a few hours on this already, sifted through numerous stack posts and blogs but can't seem to get this to make my model update. More specifically, I am trying to update an array item (ng-repeat). In the simple case below, I iterate over venues list, and upon toggling a "like" button, I update the server appropriately, and reflect the change on the venues item on the $scope.
in my search.html I have a directive:
<ion-content>
<venues-list venues="venues"></venues-list>
</ion-content>
and search controller I have:
app.controller('bleh', function(Service) {
...
$scope.venues = [{ id: 1, name: 'Venue1', like: false },{ id: 2, name: 'Venue2', like: false }];
...
});
Nothing unusual there. Then in my venues-list directive:
app.directive('venues-list', function() {
function venueListController($scope, Service) {
$scope.likeToggle = function(venue, $index) {
Service.likeVenue(venue.id, !venue.like).then(function() {
$scope.venues[$index].like= !venue.like;
});
}
}
return {
strict: 'E',
templateUrl: 'venue.html',
controller: venueListController,
scope: {
venues: '='
}
}
});
then in my venue.html I have:
<div ng-repeat="venue in venues">
<p>{{venue.name}}</p>
<button ng-click="likeToggle(venue, $index)">Like</button>
</div>
I have tried many different options:
$scope.$apply() // after updating the $scope or updating scope within apply's callback function;
$scope.$digest()
$timeout(function() { // $scope.venues[$index] .... }, 0);
safe(s,f){(s.$$phase||s.$root.$$phase)?f():s.$apply(f);}
so
safe($scope, function() { $scope.venues[$index].like = !venue.like });
I haven't yet used the link within the directive, but my venues.html template is obviously a little more elaborate than presented here.
EDIT:
Just to keep the discussion relevant, perhaps I should have mentioned - the data is coming back from the server with no issues, I am handling errors and I am definitely hitting the portion of the code where the $scope is to be updated. NOTE: the above code is a small representation of the full app, but all the fundamental pieces are articulated above.
Search Controller
Venues Service
venue-list directive and venue.html template to accompany the directive
directive controller
EDIT #2
$scope.foo = function() {
$scope.venues[0].like = !$scope.venues[0].like;
}
Just to keep it even simpler, the above doesn't work - so really, the bottom line is my items within an array are not reflecting the updates ...
EDIT #3
$scope.foo = function() {
$scope.venues[0].like = !$scope.venues[0].like;
}
My apologies - just to re-iterate what I was trying to say above - the above is changing the scope, however, the changes are not being reflected on the view.

Perhaps the issue is with your service and promise resolution.. Can you put a console.log there and see if the promise resolution is working fine? or Can you share that code bit. Also where are you checking for scope update? within directive or outside

OK after some refactoring I finally got it working.
The "fix" (if you want to call it that) to my specific problem was:
instead of passing an array of venues, I was iterating over the array on the parent controller, passing in a venue as an element attribute that would bind (two-way) on the isolated scope of the directive.
so, instead of:
<ion-content>
<venues-list venues="venues"></venues-list>
</ion-content>
I now have:
<ion-content>
<venues-list ng-repeat="venue in venues" venue="venue"></venues-list>
</ion-content>
and my directive now looks like:
app.directive('venues-list', function() {
function venueController($scope, Service) {
$scope.likeToggle = function(venue) {
Service.likeVenue(venue.id, !venue.like).then(function() {
$scope.venue.like = !venue.like;
});
}
}
return {
strict: 'E',
templateUrl: 'venue.html',
controller: venueController,
scope: {
venue: '='
}
}
});
This did the trick!

Related

Passing keys of object to directive

I have created a directive as a wrapper for md-autocomplete so that it's easier to re-use. In the parent controller, I have an object. I want to pass the keys of the object to my custom directive, but I'm having trouble. Simplified code, without md-autocomplete:
Here's the script
var app = angular.module('myApp',[])
.controller('parentController', function(){
var parent = this;
parent.things = {item1: {color: "blue"}, item2: {color: "red"}};
})
.directive('childDirective',function(){
return {
scope: {},
bindToController: {
items:'&'
},
controller: childController,
controllerAs: 'child',
template: '<pre>{{child.items | JSON}}<pre>' //should be [item1,item1]
}
function childController(){
//Just a dummy controller for now
}
})
HTML
<div ng-app="myApp" ng-controller="parentController as parent">
<my-directive items="Object.keys(parent.things)">
</my-directive>
</div>
TL;DR: How do I pass the keys of an object defined in the parent controller to a child directive? I need to pass just the keys, not the object itself, because my directive is designed to deal with an array of strings.
Try using a directive with local scope from user attribute (=)
app.directive('childDirective', function() {
return {
replace: true,
restrict: 'E',
scope: {
items: '='
},
template: '<pre>{{items | JSON}}<pre>'
};
});
Using the directive, object in attribute "items" is passed "as is" , as a scope variable "items"
<div ng-app="myApp" ng-controller="parentController as parent">
<my-directive items="getKeys(parent.things)">
</my-directive>
</div>
Using Object.keys(obj) as source will cause an infinite loop digest (the function is always returning a new different object). You need a function to save the result to a local updatable object, like in this example:
https://jsfiddle.net/FranIg/3ut4h5qm/3/
$scope.getKeys=function(obj){
//initialize result
this.result?this.result.length=0:this.result=[];
//fill result
var result=this.result;
Object.keys(obj).forEach(function(item){
result.push(item);
})
return result;
}
I'm marking #Igor's answer as correct, because ultimately it led me to the right place. However, I wanted to provide my final solution, which is too big for a comment.
The search for the answer to this question led me to create a directive that is more flexible, and can take several different types of input.
The real key (and my actual answer to the original question) was to bind the items parameter to a proxy getter/setter object in the directive. The basic setup is:
app.directive('myDirective',function(){
return {
...
controller: localControl,
bindToController: {
items: '<' //note one-way binding
}
...
}
function localControl(){
var child = this;
child._items = [],
Object.defineProperties(child,{
items: {
get: function(){return child._items},
set: function(x){child._items = Object.keys(x)}
}
});
}
});
HTML
<my-directive items="parent.items">
<!-- where parent.items is {item1:{}, item2:{}...} -->
</my-directive>
Ultimately, I decided I wanted my directive to be able to accept a variety of formats, and came up with this plunk as a demonstration.
Please feel free to offer comments/suggestions on improving my code. Thanks!

Confused refreshing Angular custom directive / communicating between adjacent directives

I am two weeks into Angular. I have watched several Pluralsite videos and read several post and this has resulted in great progress but also some confusion. I want to notify one directive of some change in another directive. Then refresh the directive. In other words it needs to go back to the server with the selection from the first and pull the appropriate data.
I have read about eventing and things like $watch() but then I have seen others say to avoid watch and to use $emit and $on. I have even seen one article say to use transclusion.
I have access to Pluralsight and other resources. I will self educate if someone could just point my nose in the right direction.
My directive markup html:
<div class="col-md-3">
<dashboard-main-nav></dashboard-main-nav>
</div>
<div class="col-md-3">
<dash-select ng-show="vm.isDashSelectionVisible">Selections</dash-select>
</div>
My app declaration: NOTE I know I need to get the parm from scope but not sure how...
(function ()
{
"use-strict";
...snip controller setup etc..
.directive("dashboardMainNav", function () {
return {
restrict: "E",
templateUrl: "/Navigation/GetDashItems",
scope: true
}
})
.directive("dashSelect", function () {
return {
restrict: "E",
templateUrl: "/Navigation/GetDashSelections/:" + $scope.??,
scope: true
}
});
})();
routingController:
(function () {
...snip...
function routingController($http, $scope) {
var vm = this;
var isDashSelectionVisible = false;
var dashSelectionId = 0;
$scope.LetterSearch= function (dashSelId) {
vm.isDashSelectionVisible = true;
vm.dashSelectionId = dashSelId;
alert("Letters Clicked: " + dashSelId);
}
}
})();
Rendered HTML:
<dashboard-main-nav>
....snip....
Letters
</dashboard-main-nav>
..... snip.....
<dash-select>
Numbers
</dash-select>
I am not showing the $routeProvider config that wires up the routingController as that works fine. I just need to get that custom directive to grab the parm from scope..refresh then update the dom.
Thank You for your patience and knowledge sharing.

ng-repeat overrides property in function but not in view model

I am new to angular and have a problem using my custom directive with ng-repeat. I want to display some posts I get from a rest interface and then use their _id property inside the directive for other purposes. However, it turns out that the property is always the one from the last displayed post when used from inside a function (test in the sample below). When trying to display the id directly over the viewmodel it shows the right one. Hope this makes sense. Any help would be appreciated.
//directive.js
angular.module('app').directive('gnPost', myGnPost);
function myGnPost() {
return {
restrict: 'E',
controller: 'postCtrl',
controllerAs: 'postCtrl',
bindToController: {
post: '='
},
scope: {},
templateUrl: 'template.html'
};
};
//controller.js
angular.module('app').controller('postCtrl', myPostCtrl);
function myPostCtrl(postRestService) {
vm = this;
vm.test = function () {
return vm.post._id;
};
};
// template.html
<p>{{postCtrl.post._id}}</p>
//displays the right id
<p>{{postCtrl.test()}}</p>
//displays the id of the last element of ng-repeat
//parent page.html
<gn-post ng-repeat="singlePost in posts.postList" post="singlePost"></gn-post>
In your controller, you have the following line:
vm = this;
It should be:
var vm = this;
By omitting the var, there is a vm variable created on the global scope instead of a local one per controller instance. As a result each iteration when vm.test is called, it's always pointing at the function defined on the last controller.
Fiddle - try including/omitting the var in postCtrl
It's good practice to use strict mode in Javascript to prevent that issue and others. In strict mode, it impossible to accidentally create global variables, as doing so will throw an error and you'll see the problem straight away. You just need to add this line at the start of your file or function:
"use strict";

Access config constants inside view in angularjs

I've configured a constant on a module like below(simplified version of my actual scenario):
var app = angular.module('app', []);
angular.config('AnalyticsConstant', function(){
property: {
click: 'clicked',
swipe: 'swiped',
externalLink: 'opened external link'
},
page: {
message: {
list: 'Message listing',
show: 'Message show'
}
}
}
Now I based on user action taken (say swipe), I want to trigger an analytics event. Since swipe/click or recognizing if an element has an external link, is something done on view level, I want to pass a hash to my controller method.
for example:
<ion-list>
<ion-item ng-repeat="message in messages track by $index" ui-sref="message_path({id: message.id})" class="item-text-wrap">
<my-track-directive source='message', property='click'>
</ion-item>
</ion-list>
Here certainly in myTrackDirective, I can read these two values and check if source/property key is available in AnalyticsConstant. Once found out, I'll also have to check if the value are key another key in AnalyticsConstant.source/property hash. Another pain will be, I'll need to stringify the keys source/property so that I can check in hash, however that's not a problem.
I was wondering if I can access the AnalyticsConstant in view, so that the directive line becomes something like:
<my-track-directive source='AnalyticsConstant[page][message][list]', userAction='AnalyticsConstant[property][click]'>
I could think of three solutions:
Add inject it root scope. ie.
app.run(function($rootScope, AnalyticsConstant){
$rootScope.analyticsConstant = AnalyticsConstant
}
But this is not a good solution, as if by mistake anyone changes $rootScope.analyticsConstant to something else, whole functionality may get screwed.
Inject in each controller and set it at $scope level.
$scope.analyticsConstant = AnalyticsConstant
This will be a lot of duplication. Also, it'll also not ensure if $scope.analyticsConstant won't get corrupted by mistake.(Though disaster will also be scoped and limited :) )
Write a function which returns AnalyticsConstant inside 'module.run' or may be inside each controller.
function getAnalyticsConstant = function(){
return AnalyticsConstant
}
I particularly liked the third approach. But question remains where to place this (rootScope or controller-scope)?
My questions are:
Is there any better way to access configured constant inside view in angular?
What might be other problems with each of these approach I listed above?
Is the directive approach is better or should I collect this data in some model and then pass it to analytics event function ?
Thanks
I would use value to define constants:
app.value('AnalyticsConstant', function(){
property: {
click: 'clicked',
swipe: 'swiped',
externalLink: 'opened external link'
},
page: {
message: {
list: 'Message listing',
show: 'Message show'
}
}
}
So in each controller/directive you just nee to create instance, for example:
app.directive('myTrackDirective', ['AnalyticsConstant', function(AnalyticsConstant) {
return {
restrict: 'E',
replace: true,
scope: {/* ... */},
templateUrl: 'some.html',
link: function(scope) {
scope.AnalyticsConstant = AnalyticsConstant;
}
};
}]);
After you can call AnalyticsConstant from HTML
As a side note (doesn't refer to question):
Try to use MixPanel. Works great for Cordova-Ionic-Angular

angularjs directive: $rootScope:infdig error

I'm trying to build a pagination directive with angularjs 1.2.15:
This is my view:
<input type="text" ng-model="filter.user">
<input type="number" ng-model="filter.limit" ng-init="filter.limit=5">
<ul>
<li ng-repeat="user in (filteredUsers = (users | orderBy:order:reverse | filter:filter.user ) | limitTo: filter.limit)" ng-click="showDetails(user)">
{{user.id}} / {{user.firstname}} {{user.lastname}}
</li>
</ul>
<div pagination data='filteredUsers' limit='filter.limit'></div>
and here is my pagination directive:
app.directive('pagination', function(){
return {
restrict: 'A',
templateUrl: 'partials/pagination.html',
scope: {
data: '=data',
limit: '=limit'
}
}
})
Everything works perfectly fine without the pagination directive. However with my new directive as soon as I load the page I get a $rootScope:infdig error which I don't understand since the directive is not doing anything to manipulate data that could end up in an infinite loop.
What is the problem here and how can I solve it? Thanks!
Update:
Here are the controller and the resource.
Controller:
usersModule.controller('usersController',
function ($scope, Users) {
function init(){
$scope.users = Users.get();
}
init();
})
Resource (gets users as an array from a REST API):
app.factory('Users', function($resource) {
return $resource('http://myrestapi.tld/users', null,{
'get': { method:'GET', isArray: true, cache: true }
});
});
Update 2
Here is a demo: http://plnkr.co/edit/9GCE3Kzf21a7l10GFPmy?p=preview
Just type in a letter (e.g. "f") into the left input.
The problem is not within the directive, it's within the $watch the directive creates.
When you send filteredUsers to the directive, the directive creates the following line:
$scope.$watch("filteredUsers", function() {
// Directive Code...
});
Notice in the following example how we reproduce it without a directive:
http://plnkr.co/edit/uRj19PyXkvnLNyh5iY0j
The reason it happens is because you are changing filteredUsers every time a digest runs (since you put the assignment in the ng-repeat statement).
To fix this you might consider watching and filtering the array in the controller with the extra parameter 'true' for the $watch:
$scope.$watch("users | orderBy:order:reverse | filter:filter.user", function(newVal) {
$scope.filteredUsers = newVal;
}, true);
You can check the solution here:
http://plnkr.co/edit/stxqBtzLsGEXmsrv3Gp6
the $watch without the extra parameter (true) will do a simple comparison to the object, and since you create a new array in every digest loop, the object will always be different.
When you're passing the true parameter to the $watch function, it means it will actually do deep comparison to the object that returns before running the $watch again, so even if you have different instances of arrays that has the same data it will consider them equal.
A quick fix is to add a "manual" $watchCollection in the directive, instead of a 2-way binding.
app.directive('pagination', function($parse){
return {
restrict: 'A',
template: '',
scope: {
limit: '=limit'
},
link: function(scope, elem, attrs) {
var dataExpr = $parse(attrs.data);
var deregister = scope.$parent.$watchCollection(dataExpr, function(val) {
scope.data = val;
});
scope.$on('$destroy', deregister);
}
}
})
$watchCollection monitors the contents of the array, not the reference to it.
See it running here.
Generally speaking, i don't like expressions like that:
filteredUsers = (users | orderBy:order:reverse | filter:filter.user )
inside views. Views should only render $scope properties, not create new ones.
This error may remove to clear the browser history from setting. I got same issue and apply many solution to resolve this, But cannot resolve.
But when I remove browser history and cache this issue is resolve. May this help for you.

Resources