scope.$on() invokes several times for one broadcast - angularjs

In Angular.js, scope.$on("UPDATE", function(event, account, vipLabel, nodeName) is firing about 20 times when it should only invokes once for each $rootScope.$broadcast("UPDATE", this.account,this.vipLabel, this.nodeName). I know that scope.$on() contains an event object, but I haven't been able to find a use for it to help my issue.
I am stuck on this and I need the scope.$on() to fire only once for each broadcast.
angular.module('main.vips')
.factory("StatusTrackerService", function($timeout, $rootScope, Restangular) {
var CHECK_ITERATIONS = 1;
var TIME_ITERATION = 1000;
var eventCollection = {};
eventCollection.vipLabel;
function urlBuilder(account, eventId) {
var account = Restangular.one("account", account);
var eventId = account.one("event", eventId);
return eventId;
}
function NewStatusEvent(account, eventId, url, vipLabel, nodeName) {
this.iteration = 0;
this.account = account;
this.eventId = eventId;
this.url = url;
this.vipLabel = vipLabel;
this.nodeName = nodeName;
}
NewStatusEvent.prototype.runCheckLoop = function() {
this.url.get()
.then(function(data) {
if (data.Automation.Status != "SUCCESS") {
console.log("yes");
console.log(this.iteration);
console.log(CHECK_ITERATIONS);
if (this.iteration < CHECK_ITERATIONS) {
this.iteration++;
$rootScope.$broadcast("UPDATE", this.account,
this.vipLabel, this.nodeName);
console.log('-------------------');
console.log('checking ' + JSON.stringify(this.url));
console.log('iteration ' + this.iteration);
console.log('-------------------');
$timeout(function (){
this.runCheckLoop();
}.bind(this), TIME_ITERATION);
}
}
}.bind(this));
}
function runEventCheck(account, eventId, nodeName) {
url = urlBuilder(account, eventId);
eventCollection[eventCollection.vipLabel] = new NewStatusEvent(account, eventId,
url, eventCollection.vipLabel, nodeName);
eventCollection[eventCollection.vipLabel].runCheckLoop();
}
return { runEventCheck: runEventCheck,
eventCollection: eventCollection
};
});
Directive:
return {
templateUrl: "vips/directives/action_button.html",
restrict: "AE",
replace: true,
transclude: false,
scope: {
label: "#?",
icon: "#?",
type: "#?",
actions: "=?"
},
link: function(scope, element, attrs) {
element.on("click", function(event) {
var vipLabel = event.currentTarget.id;
StatusTrackerService.eventCollection.vipLabel = vipLabel;
});
scope.$on("UPDATE", function(event, account,
vipLabel, nodeName) {
console.log("Scope.on has fired this many times");
var nodeSelector = "#node-"+vipLabel+"-"+nodeName;
var nodeElement = angular.element(document.querySelector(nodeSelector));
if (!nodeElement.length) {
var vipElement = angular.element(document.querySelector('#vipLabel-'+vipLabel));
var elementGlobe = '<div id="'+vipLabel+nodeName+'">Element modified</div>';
vipElement.append(elementGlobe);
}
});
return scope.actions = services[attrs.type];
}
}
});
Logged output:
21 -Scope.on has fired this many times actionButton.js:52
------------------- status-checker.js:35
checking {"id":"0fd6afd9-2367-4a0-a5c9-ff0b3e60cdcf","route":"event","reqParams":null,"$fromServer":false,"parentResource":{"route":"account","parentResource":null,"id":"99006"},"restangularCollection":false} status-checker.js:36
iteration 1 status-checker.js:37
-------------------
Html:
<div action-button type="loadbalancer" label="Actions" icon="glyphicon glyphicon-cog"
actions="actions.loadbalancer"></div>
</div>
<div id="{{vip.label}}" action-button type="vip" icon="glyphicon glyphicon-pencil" actions="actions.vips"
class="pull-right"></div>
<div action-button type="node" icon="glyphicon glyphicon-pencil" actions="actions.nodes"
class="pull-right"></div>
UPDATE:
I added if (scope.type !== "vip") and it worked...now the event fires only once. It doesn't make sense to me because it is the <div id="{{vip.label}}" action-button type="vip" that I am using. It seems like it should be if (scope.type == "vip") instead...but it's the opposite.
scope.$on("UPDATE", function(event, account,
vipLabel, nodeName) {
if (scope.type !== "vip") {
console.log(scope);
var nodeSelector = "#node-"+vipLabel+"-"+nodeName;
var nodeElement = angular.element(document.querySelector(nodeSelector));
console.log(nodeElement.length);
if (nodeElement.length == 0) {
console.log("create node");
var vipElement = angular.element(document.querySelector('#vipLabel-'+vipLabel));
var elementGlobe = '<div id="'+vipLabel+nodeName+'">Element modified</div>';
vipElement.append(elementGlobe);
}
}
});

Related

Select Always Returning First Item of List (AngularJS)

No matter what I select, this function only returns the first item of the list. As requested, I have included the complete JS code. I have looked through this code four hours and everything seems right to me. This is my first contact with Angular and any help will be greatly appreciated!
Thank you!
HTML
<div class="form-horizontal input-append">
<select name="selectedSharedTenantId" data-ng-model="selectedSharedTenantId">
<option data-ng-repeat="item in getFilteredTenants()" value="{{item.id}}">{{item.displayName}}</option>
</select>
<button type="button" class="btn" ng-click="addSharedTenant();">
<i class="fas fa-plus"></i>
</button>
</div>
JS
$scope.addSharedTenant = function () {
alert($scope.selectedSharedTenantId)
}
COMPLETE JS CODE
angular.module('directives.datasetEditor', [
'services.dataset',
'ui.bootstrap'
])
.directive('datasetEditor', ['$modal', 'DatasetServices', function ($modal, DatasetServices) {
return {
restrict: 'A',
scope: {
tenantId: '=',
tenants: '=',
originalModel: '=model',
callback: '&callback'
},
link: function(scope, element, attrs) {
scope.model = scope.originalModel ? angular.copy(scope.originalModel) : {
entryType: 'Activity',
displayName: ''
};
var ModalInstanceCtrl = ['$scope', '$modalInstance',
function($scope, $modalInstance) {
var setSelectedSharedTenantId = function () {
var selectedTenant = $scope.getFilteredTenants()[0];
$scope.selectedSharedTenantId = selectedTenant ? selectedTenant.id : null;
};
$scope.getFilteredTenants = function () {
return _.filter($scope.tenants, function (o) {
alert(o.id)
return _.indexOf($scope.model.sharedTenantIds, o.id) == -1 && o.id != $scope.tenantId;
});
};
$scope.getTenantById = function (id) {
return _.findWhere($scope.tenants, {
id: id
});
};
$scope.removedSharedTenant = function (id) {
$scope.model.sharedTenantIds = _.without($scope.model.sharedTenantIds, id);
var selectedTenant = $scope.getFilteredTenants()[0];
setSelectedSharedTenantId();
};
$scope.addSharedTenant = function () {
//alert($scope.selectedSharedTenantId)
//alert($scope.model.sharedTenantIds)
if ($scope.selectedSharedTenantId) {
if ($scope.model.sharedTenantIds == null) {
$scope.model.sharedTenantIds = [];
}
$scope.model.sharedTenantIds.push($scope.selectedSharedTenantId);
setSelectedSharedTenantId();
}
};
$scope.submit = function(isValid) {
if (isValid) {
DatasetServices.save($scope.model).success(function(data, status, headers, config) {
$modalInstance.close(data);
});
} else {
$scope.submitted = true;
}
};
$scope.cancel = function() {
$modalInstance.dismiss();
};
$scope.submitted = false;
setSelectedSharedTenantId();
}];
function open() {
var modalInstance = $modal.open({
templateUrl: '../pkg/wisdom/common/angular/directives/dataset-editor/index.html',
controller: ModalInstanceCtrl,
scope: scope
});
modalInstance.result.then(
function(model) {
scope.callback({
model: model
});
},
function() {
}
);
};
element.on('click', open);
}
};
}]);

How to dismiss an angularjs alert when user changes route/state

I am using this angular js service/directive github pageto display alerts. An issue has been opened relating to the question I am asking but it has not been addressed by the developer.
i want first alert shown when user logs in to disappear when the user clicks logout but the alerts are stacking up on top of each other.
Here is a fiddle although I could not replicate the issue but it shows the structure of my code. fiddle
html:
<body ng-app="app">
<mc-messages></mc-messages>
<button ng-click="login()">Login</button>
<button ng-click="logout()">Logout</button>
</body>
js:
/*jshint strict:false */
'use strict';
var app = angular.module('app', ['MessageCenterModule']);
app.controller(function ($scope, messageCenterService, $location) {
$scope.login = function () {
$location.path('/');
messageCenterService.add('success',
'You are now loggedin!', {
status: messageCenterService.status.next
});
};
$scope.logout = function () {
$location.path('login');
messageCenterService.add('success',
'You are now loggedout!', {
status: messageCenterService.status.next
}
};
});
// Create a new angular module.
var MessageCenterModule = angular.module('MessageCenterModule', []);
// Define a service to inject.
MessageCenterModule.
service('messageCenterService', ['$rootScope', '$sce', '$timeout',
function ($rootScope, $sce, $timeout) {
return {
mcMessages: this.mcMessages || [],
status: {
unseen: 'unseen',
shown: 'shown',
/** #var Odds are that you will show a message and right after that
* change your route/state. If that happens your message will only be
* seen for a fraction of a second. To avoid that use the "next"
* status, that will make the message available to the next page */
next: 'next',
/** #var Do not delete this message automatically. */
permanent: 'permanent'
},
add: function (type, message, options) {
var availableTypes = ['info', 'warning', 'danger', 'success'],
service = this;
options = options || {};
if (availableTypes.indexOf(type) === -1) {
throw "Invalid message type";
}
var messageObject = {
type: type,
status: options.status || this.status.unseen,
processed: false,
close: function () {
return service.remove(this);
}
};
messageObject.message = options.html ? $sce.trustAsHtml(message) : message;
messageObject.html = !! options.html;
if (angular.isDefined(options.timeout)) {
messageObject.timer = $timeout(function () {
messageObject.close();
}, options.timeout);
}
this.mcMessages.push(messageObject);
return messageObject;
},
remove: function (message) {
var index = this.mcMessages.indexOf(message);
this.mcMessages.splice(index, 1);
},
reset: function () {
this.mcMessages = [];
},
removeShown: function () {
for (var index = this.mcMessages.length - 1; index >= 0; index--) {
if (this.mcMessages[index].status == this.status.shown) {
this.remove(this.mcMessages[index]);
}
}
},
markShown: function () {
for (var index = this.mcMessages.length - 1; index >= 0; index--) {
if (!this.mcMessages[index].processed) {
if (this.mcMessages[index].status == this.status.unseen) {
this.mcMessages[index].status = this.status.shown;
} else if (this.mcMessages[index].status == this.status.next) {
this.mcMessages[index].status = this.status.unseen;
}
this.mcMessages[index].processed = true;
}
}
},
flush: function () {
$rootScope.mcMessages = this.mcMessages;
}
};
}]);
MessageCenterModule.
directive('mcMessages', ['$rootScope', 'messageCenterService', function ($rootScope, messageCenterService) {
/*jshint multistr: true */
var templateString = '\
<div id="mc-messages-wrapper">\
<div class="alert alert-{{ message.type }} {{ animation }}" ng-repeat="message in mcMessages">\
<a class="close" ng-click="message.close();" data-dismiss="alert" aria-hidden="true">×</a>\
<span ng-switch on="message.html">\
<span ng-switch-when="true">\
<span ng-bind-html="message.message"></span>\
</span>\
<span ng-switch-default>\
{{ message.message }}\
</span>\
</div>\
</div>\
';
return {
restrict: 'EA',
template: templateString,
link: function (scope, element, attrs) {
// Bind the messages from the service to the root scope.
messageCenterService.flush();
var changeReaction = function (event, to, from) {
// Update 'unseen' messages to be marked as 'shown'.
messageCenterService.markShown();
// Remove the messages that have been shown.
messageCenterService.removeShown();
$rootScope.mcMessages = messageCenterService.mcMessages;
messageCenterService.flush();
};
$rootScope.$on('$locationChangeStart', changeReaction);
scope.animation = attrs.animation || 'fade in';
}
};
}]);
Hope this is clear enough for someone to help me. If not let me know and I can try to clarify.

AngularJS $watch newValue is undefined

I am trying to make a alert service with a directive. It is in the directive part I have some trouble. My have a directive that looks like this:
angular.module('alertModule').directive('ffAlert', function() {
return {
templateUrl: 'components/alert/ff-alert-directive.html',
controller: ['$scope','alertService',function($scope,alertService) {
$scope.alerts = alertService;
}],
link: function (scope, elem, attrs, ctrl) {
scope.$watch(scope.alerts, function (newValue, oldValue) {
console.log("alerts is now:",scope.alerts,oldValue, newValue);
for(var i = oldValue.list.length; i < newValue.list.length; i++) {
scope.alerts.list[i].isVisible = true;
if (scope.alerts.list[i].timeout > 0) {
$timeout(function (){
scope.alerts.list[i].isVisible = false;
}, scope.alerts.list[i].timeout);
}
}
}, true);
}
}
});
The reason for the for-loop is to attach a timeout for the alerts that has this specified.
I will also include the directive-template:
<div class="row">
<div class="col-sm-1"></div>
<div class="col-sm-10">
<div alert ng-repeat="alert in alerts.list" type="{{alert.type}}" ng-show="alert.isVisible" close="alerts.close(alert.id)">{{alert.msg}}</div>
</div>
<div class="col-sm-1"></div>
</div>
When I run this, I get this error in the console:
TypeError: Cannot read property 'list' of undefined
at Object.fn (http://localhost:9000/components/alert/ff-alert-directive.js:10:29)
10:29 is the dot in "oldValue.list" in the for-loop. Any idea what I am doing wrong?
Edit: I am adding the alertService-code (it is a service I use to keep track of all the alerts in my app):
angular.module('alertModule').factory('alertService', function() {
var alerts = {};
var id = 1;
alerts.list = [];
alerts.add = function(alert) {
alert.id = id;
alerts.list.push(alert);
alert.id += 1;
console.log("alertService.add: ",alert);
return alert.id;
};
alerts.add({type: "info", msg:"Dette er til info...", timeout: 1000});
alerts.addServerError = function(error) {
var id = alerts.add({type: "warning", msg: "Errormessage from server: " + error.description});
// console.log("alertService: Server Error: ", error);
return id;
};
alerts.close = function(id) {
for(var index = 0; index<alerts.list.length; index += 1) {
console.log("alert:",index,alerts.list[index].id);
if (alerts.list[index].id == id) {
console.log("Heey");
alerts.list.splice(index, 1);
}
}
};
alerts.closeAll = function() {
alerts.list = [];
};
return alerts;
});
try like this , angular fires your watcher at the first time when your directive initialized
angular.module('alertModule').directive('ffAlert', function() {
return {
templateUrl: 'components/alert/ff-alert-directive.html',
controller: ['$scope','alertService',function($scope,alertService) {
$scope.alerts = alertService;
}],
link: function (scope, elem, attrs, ctrl) {
scope.$watch(scope.alerts, function (newValue, oldValue) {
if(newValue === oldValue) return;
console.log("alerts is now:",scope.alerts,oldValue, newValue);
for(var i = oldValue.list.length; i < newValue.list.length; i++) {
scope.alerts.list[i].isVisible = true;
if (scope.alerts.list[i].timeout > 0) {
$timeout(function (){
scope.alerts.list[i].isVisible = false;
}, scope.alerts.list[i].timeout);
}
}
}, true);
}
}
});
if you have jQuery available, try this
if(jQuery.isEmptyObject(newValue)){
return;
}
inside scope.$watch

angularjs directive link function is never invoked

i had created this directive, its mean reason is customize ngGrid module, but it doesn't call the link method if i used inside an element with a ng-controller
<div ng-controller="TrainerEditionController">
<div>Header<br/>
<div id="trainer-grid"
class="grid-style"
data-keyfield="Id"
data-msgrid="gridOptions"
data-geturl="#Url.Action("List", "Trainer")"
data-deleteurl="#Url.Action("Delete","Trainer")">
<div data-item-template>
<div data-field="Name" data-editable>
</div>
<div data-field="$action">
<div class="btn" ng-click="edit()">Edit</div> <div class="btn" ng-click="delete()">Delete</div>
</div>
</div>
</div>
</div> </div>
here is my directive definition:
(function(window, $) {
angular.module('msGrid', ['ngGrid'])
.directive('msgrid', function() {
return {
priority: 3000,
restrict: 'A',
link: function($scope, $http, $element) {
$scope.__onEditEventHandler = null;
$scope.getPageData = function(pageUrl) {
$http.post(pageUrl)
.success(function(data, status) {
if (data.Success === true) {
$scope.myData = data.Result.Data;
$scope.gridOptions.pagingOptions.pages = data.Result.Pages;
$scope.gridOptions.pagingOptions.currentPage = data.Result.CurrentPage;
if (!$scope.$$phase) {
$scope.$apply();
}
} else {
//TODO: customize error notification
alert('Error on request, server application said: ' + data.Error + ' status: ' + status);
}
})
.error(function(data, status) {
//TODO: customize error notification
alert('Error on request, ignoring result status:' + status);
});
};
//set options
$scope.gridOptions = {
data: 'myData',
columnDefs: $element.data('ColumnDefs'),
rowHeight: 50,
headerRowHeight: 39,
enableRowSelection: false,
showFooter: true,
enablePaging: true,
pagingOptions: {
pages: [],
currentPage: 0
}
};
//load data from server
$scope.getPageData($element.data('geturl'));
$scope.onEditEventHandler = function(eventHandler) {
if (eventHandler === undefined) return;
$scope.__onEditEventHandler = eventHandler;
};
$scope.refresh = function(pageIndex) {
var pagingOptions = $scope.gridOptions.pagingOptions;
if (pageIndex === undefined)
pageIndex = 0;
if (pageIndex > pagingOptions.pages.length)
pageIndex = pagingOptions.pages.length;
$scope.getPageData(pagingOptions.pages[pageIndex]);
};
$scope.edit = function(row) {
var msColumnKeyField = $element.data('keyfield');
if (msColumnKeyField == null) return;
//get the key value of row
var keyValue = row.getProperty(msColumnKeyField);
if ($scope.__onEditEventHandler != null)
$scope.__onEditEventHandler(keyValue);
else {
var editUrl = $element.data('editurl');
if (editUrl == null) return;
window.location.replace(editUrl + '/' + keyValue);
}
};
$scope.delete = function(row) {
var msColumnKeyField = $element.data('keyfield');
if (msColumnKeyField == null) return;
//get the key value of row
var keyValue = row.getProperty(msColumnKeyField);
//send post to delete
var postData = {};
postData[msColumnKeyField] = keyValue;
$http.post($element.data('deleteurl'), postData)
.success(function(data, status) {
if (data.Success === true) {
//refresh grid page for new data
var pagingOptions = $scope.gridOptions.pagingOptions;
$scope.getPageData(pagingOptions.pages[pagingOptions.currentPage]);
} else {
//TODO: customize error notification
alert(data.Error);
}
})
.error(function(data, status) {
//TODO: customize error notification
alert('Error on delete, request ignored');
});
};
},
compile: function(tElement, tAttrs) {
//load row definitions
var requiredTemplate = tElement.find('*[data-item-template]');
var myColumnDefs = [];
//load columns templates
requiredTemplate.children('*[data-field]').each(function() {
var self = $(this);
var displayName = self.attr('data-displayName');
var fieldName = self.attr('data-field');
fieldName = (fieldName.indexOf('$') == 0) ? '' : fieldName;
var item = {
aggLabelFilter: '',
cellClass: self.attr('class'),
cellFilter: '',
enableCellEdit: self.attr('data-editable') === undefined,
field: fieldName,
displayName: displayName === undefined ? fieldName : displayName,
sortable: self.attr('data-sortable') != undefined
};
var cellTemplate = self.html().trim();
if (cellTemplate != '') {
cellTemplate = cellTemplate.replace('$value$', '{{row.getProperty(col.field)}}');
cellTemplate = cellTemplate.replace('ng-click="delete()"', 'ng-click="delete(row)"');
cellTemplate = cellTemplate.replace('ng-click="edit()"', 'ng-click="edit(row)"');
item['cellTemplate'] = '<div class="ngCellText" ng-class="col.colIndex()"><span ng-cell-text>' + cellTemplate + '</span></div>';
}
myColumnDefs.push(item);
});
tElement.html('<div ng-grid="gridOptions" class="' + tAttrs.class + '"></div>');
tElement.data('ColumnDefs', myColumnDefs);
},
deleteUrl: ''
};
})
.run([
'$templateCache', function($templateCache) {
$templateCache.put('footerTemplate.html',
'<div ng-show="showFooter" class="ngFooterPanel" ng-class="{\'ui-widget-content\': jqueryuitheme, \'ui-corner-bottom\':jqueryuitheme}" ng-style="footerStyle()">' +
'<div style="text-align: right"><ul class="pagination">' +
'<li ng-repeat="page in pagingOptions.pages"><div class="btn" ng-class="pagingOptions.currentPage==$index ? \'disabled\' :\'\'" ng-click="getPageData(page)">{{$index+1}}</div></li>' +
'</ul></div>' +
'</div>');
}
]);
}(window, jQuery));
if the directive is used in an element without a ng-controller it works fine
Your directive function is not valid.
When you have a compile function your linking function is actually postLink.
Also check the arguments of link function.
From the AngularJS $compile API
var myModule = angular.module(...);
myModule.directive('directiveName', function factory(injectables) {
var directiveDefinitionObject = {
priority: 0,
template: '<div></div>', // or // function(tElement, tAttrs) { ... },
// or
// templateUrl: 'directive.html', // or // function(tElement, tAttrs) { ... },
transclude: false,
restrict: 'A',
scope: false,
controller: function($scope, $element, $attrs, $transclude, otherInjectables) { ... },
controllerAs: 'stringAlias',
require: 'siblingDirectiveName', // or // ['^parentDirectiveName', '?optionalDirectiveName', '?^optionalParent'],
compile: function compile(tElement, tAttrs, transclude) {
return {
pre: function preLink(scope, iElement, iAttrs, controller) { ... },
post: function postLink(scope, iElement, iAttrs, controller) { ... }
}
// or
// return function postLink( ... ) { ... }
},
// or
// link: {
// pre: function preLink(scope, iElement, iAttrs, controller) { ... },
// post: function postLink(scope, iElement, iAttrs, controller) { ... }
// }
// or
// link: function postLink( ... ) { ... }
};
return directiveDefinitionObject;
});

restangular animation on data load

How would I use fadein, fadeout effect on data load with restangular. How would I get know about when to fade out and when to fadein
my controller method looks like
$scope.fetchResult = function() {
spinner_in();
return api.items.search($scope.filterCriteria).then(function(data) {
$scope.products = data;
$scope.totalPages = data.total;
$scope.productsCount = data.TotalItems;
}, function() {
$scope.products = [];
$scope.totalPages = 0;
$scope.productsCount = 0;
});
};
Here is my solution. Add a response interceptor and a request interceptor that sends out a rootscope broadcast. Then create a directive to listen for that response and request.:
angular.module('mean.system')
.factory('myRestangular',['Restangular','$rootScope', function(Restangular,$rootScope) {
return Restangular.withConfig(function(RestangularConfigurer) {
RestangularConfigurer.setBaseUrl('http://localhost:3000/api');
RestangularConfigurer.addResponseInterceptor(function(data, operation, what, url, response, deferred) {
var extractedData;
// .. to look for getList operations
if (operation === 'getList') {
// .. and handle the data and meta data
extractedData = data.data;
extractedData.meta = data.meta;
} else {
extractedData = data.data;
}
$rootScope.$broadcast('apiResponse');
return extractedData;
});
RestangularConfigurer.setRequestInterceptor(function (elem, operation) {
if (operation === 'remove') {
return null;
}
return (elem && angular.isObject(elem.data)) ? elem : {data: elem};
});
RestangularConfigurer.setRestangularFields({
id: '_id'
});
RestangularConfigurer.addRequestInterceptor(function(element, operation, what, url) {
$rootScope.$broadcast('apiRequest');
return element;
});
});
}]);
Here is the directive:
angular.module('mean.system')
.directive('smartLoadingIndicator', function($rootScope) {
return {
restrict: 'AE',
template: '<div ng-show="isAPICalling"><p><i class="fa fa-gear fa-4x fa-spin"></i> Loading</p></div>',
replace: true,
link: function(scope, elem, attrs) {
scope.isAPICalling = false;
$rootScope.$on('apiRequest', function() {
scope.isAPICalling = true;
});
$rootScope.$on('apiResponse', function() {
scope.isAPICalling = false;
});
}
};
})
;

Resources