angularjs directive 2 way binding not working - angularjs

Here is my directive, it's simple task is to Locale Date String time:
.directive('localeDateString',['$window', function ($window) {
return {
restrict: 'E',
replace: true,
scope: {
time: '='
},
template: '<span>{{timeLocal}}</span>',
link: function ($scope, $element) {
if ($scope.time != null) {
profileDate = new Date($scope.time);
var cultureCode = $window.ApiData.CultureCode;
$scope.timeLocal = profileDate.toLocaleDateString(cultureCode);
}
}
};
}])
Usage in HTML:
<li ng-repeat="note in profile.AccountProfile.Notes" class="noteItem">
<locale-date-string time="note.Created" ></locale-date-string>
<span>{{note.UserName}}</span>
<!-- other stuff .. -->
</li>
When I'm loading the object "profile" from JSON everything is OK
the problem is when i change "note.Created" from controller - the directive seem not to work(other members of Note are updating ok):
In the controller:
DataService.updateProfileRemark(objRemark)
.then(function (response) {
// all is ok;
var profileIndex = $scope.ProfileList.indexOf(profile);
var noteIndex = $scope.ProfileList[profileIndex].AccountProfile.Notes.indexOf(note);
// this is working:
$scope.ProfileList[profileIndex].AccountProfile.Notes[noteIndex].UserName = objRemark.UserName;
// this is not:
$scope.ProfileList[profileIndex].AccountProfile.Notes[noteIndex].Created = Date.now();
},
function (errResponse) {
// handle err
}
);
For example, here is the scope before "updateProfileRemark":
and after:
Why the 2 way binding not working?
Thanks.

link is only executed once. If you want to setup two-way binding between $scope.timeLocal and $scope.time, setup a $watch:
link: function ($scope, $element) {
$scope.$watch('time', function(newTime) {
if (newTime != null) {
var profileDate = new Date(newTime);
var cultureCode = $window.ApiData.CultureCode;
$scope.timeLocal = profileDate.toLocaleDateString(cultureCode);
}
});

Related

angularjs bind to controller with isolated scope

I have a pretty simple directive and I want to use the bindToController option. So, I created my directive like this:
(function () {
'use strict';
angular.module('sapphire.directives').directive('list', list);
function list() {
return {
restrict: 'A',
template: '<div class="row flex-column" ng-class="{ \'spinner-dark\': controller.loading }" ng-include="controller.templateUrl" ng-if="controller.loading || controller.models.length"></div>',
controller: 'ListDirectiveController',
controllerAs: 'controller',
scope: true,
bindToController: {
method: '&list',
templateName: '#'
}
};
};
})();
And then I created my controller like this:
(function () {
'use strict';
angular.module('sapphire.directives').controller('ListDirectiveController', listDirectiveController);
listDirectiveController.$inject = ['ListDirectiveService', 'Selections'];
function listDirectiveController(broadcast, selections) {
var self = this;
console.log(self);
// Bindings
self.limit = 0;
self.total = 0;
self.loading = true;
self.templateUrl = 'app/directives/lists/list/' + (self.templateName || 'list-default') + '.html';
self.isSelected = selections.isSelected;
self.select = selections.select;
// Method binding
self.list = list;
init();
//////////////////////////////////////////////////
function init() {
list();
};
// Our list method
function list() {
// Set our initial limit
self.limit += 10;
self.loading = true;
// Get our items
return self.method({ limit: self.limit }).then(function (response) {
self.loading = false;
self.models = response;
self.total = response.length;
});
};
///////// ------ Removed for brevity ------ /////////
};
})();
When I use this directive I get an error stating:
self.method is not a function
which is why I am console.logging the controller to see what is bound to it. Surely enough, the method and templateName are missing.
I have tried a few ways to get this to work:
scope: {
method: '&list',
templateName: '#'
},
bindToController: true
or
scope: {},
bindToController: {
method: '&list',
templateName: '#'
}
but nothing seems to work. I can't get my isolated scope to be bound to my controller....
Does anyone know what I am doing wrong?
PS: I am using angular 1.6.4
To use the directive I do this:
<div class="invisible-container" list="controller.listUsers(limit)" template-name="users"></div>
Ok, so I figured this out. The scope is bound, but it isn't available straight away. I had to create an init method and invoke it from the directive. Only then was everything bound.
I did it like this:
(function () {
'use strict';
angular.module('sapphire.directives').directive('list', list);
function list() {
return {
restrict: 'A',
template: '<div class="row flex-column" ng-class="{ \'spinner-dark\': controller.loading }" ng-include="controller.templateUrl" ng-if="controller.loading || controller.models.length"></div>',
controller: 'ListDirectiveController',
controllerAs: 'controller',
scope: {
method: '&list',
templateName: '#'
},
bindToController: true,
link: function (scope, element, attrs, controller) {
controller.init();
}
};
};
})();
and the controller now looks like this:
(function () {
'use strict';
angular.module('sapphire.directives').controller('ListDirectiveController', listDirectiveController);
listDirectiveController.$inject = ['ListDirectiveService', 'Selections'];
function listDirectiveController(broadcast, selections) {
var self = this;
// Bindings
self.limit = 0;
self.total = 0;
self.loading = true;
self.isSelected = selections.isSelected;
self.select = selections.select;
// Method binding
self.init = init;
////////////////////////////////////////////////////
function init() {
list();
getTemplate();
bindEvents();
};
function bindEvents() {
broadcast.onPrepend(onPrepend);
broadcast.onRefresh(onRefresh);
};
function getTemplate() {
self.templateUrl = 'app/directives/lists/list/' + (self.templateName || 'list-default') + '.html';
};
function list() {
// Set our initial limit
self.limit += 10;
self.loading = true;
// Get our items
return self.method({ limit: self.limit }).then(function (response) {
self.loading = false;
self.models = response;
self.total = response.length;
});
};
function onPrepend(event, args) {
if (args && args.target && args.target === self.templateName) {
self.models.unshift(args.model);
}
};
function onRefresh(event, args) {
if (args && args.target && args.target === self.templateName) {
self.limit -= 10;
self.models = [];
list();
}
};
};
})();

Calling parent directive method from child directive through attrs

This is the scenario :
<export-team>
<ul>
<li>
<button buy-ticket="{{data}}" buy-callback="onBuyTicket()">buy</button>
</li>
<li>
<button buy-ticket="{{data}}" buy-callback="onBuyTicket()">buy</button>
</li>
</ul>
</export-team>
The buyTicket directive
(function() {
'use strict';
angular
.module('myApp')
.directive('buyTicket', buyTicket);
/** #ngInject */
function buyTicket($parse, ngDialog, authService, APPCONFIG, $rootScope, shareToken, contestsFactory, shareCurrentTicket, shareIdSession, shareSessionAAMS, $location) {
var vm = this;
var directive = {
restrict: 'A',
link : function(scope, element, attributes) {
var buyCompatible = attributes['buyCompatible'];
function addZero(i) {
if (i < 10) {
i = "0" + i;
}
return i;
}
var buyTicket = function(contest) {
var d = new Date();
var y = d.getFullYear();
var m = addZero(d.getMonth()+1);
var day = addZero(d.getDate());
var h = addZero(d.getHours());
var min = addZero(d.getMinutes());
var s = addZero(d.getSeconds());
var date = ''+y+m+day+h+min+s+'';
var transactionId = $rootScope.TRANSACTIONID;
var currentTOKEN = shareToken.get();
var data = {
idSessione:currentTOKEN, // ->TOKEN
userAgent:navigator.userAgent,
sessioneAAMS:contest.aams_session_id,
gameId:APPCONFIG.GAME_ID,
transactionId:transactionId,
dateTime:date,
buyIn:contest.buy_in
}
var dialogLoading = ngDialog.open({
closeByDocument : false,
closeByEscape : false,
showClose : false,
id : 'ft-modal-loading',
controller: ['$scope', function($scope){
$scope.bodyUrl = 'app/components/modals/body/loading.html';
$scope.title = 'Acquisto Ticket';
$scope.error = 'Il sistema sta procedendo all\'acquisto del ticket';
}]
});
contestsFactory.buyTicket(data).success(function(response){
dialogLoading.close();
if (response.esito == "0") {
if (!buyCompatible) {
shareCurrentTicket.set(response.ticketSogei);
shareSessionAAMS.set(contest.aams_session_id);
shareIdSession.set(contest.id_session);
$location.path('my-contests/'+contest.id_contest+'/'+contest.contest_status);
}
} else {
var message = response.descrizione;
var ids = ngDialog.getOpenDialogs();
var dialogError = ngDialog.open({
id : "ft-modal-error-2",
controller: ['$scope', function($scope){
$scope.bodyUrl = 'app/components/modals/body/error.html';
$scope.title = 'Errore';
$scope.error = message;
}]
});
}
})
.error(function(){
var dialogErrorNotEndled = ngDialog.close('ft-modal-loading');
ngDialog.open({
id : 'ft-modal-error',
controller: ['$scope', function($scope){
$scope.bodyUrl = 'app/components/modals/body/error.html';
$scope.title = 'Errore';
$scope.error = 'Il servizio non รจ attualmente disponibile';
}]
});
})
}
var openConfirmBuyTicket = function(contest) {
contest = JSON.parse(contest);
if (ngDialog.isOpen('ft-modal-contest-detail')) {
ngDialog.close('ft-modal-contest-detail');
};
if (!authService.isLogged()) {
ngDialog.open({
controller: ['$scope', function($scope){
$scope.bodyUrl = 'app/components/modals/body/not_logged.html';
$scope.title = 'Spiacenti';
$scope.error = 'Devi essere loggato per poter partecipare ad un contest';
}]
});
} else {
ngDialog.openConfirm({
controller: ['$scope', function($scope){
$scope.title = 'CONFERMA';
$scope.bodyUrl = 'app/components/modals/body/confirm_buy.html';
$scope.contest_name = contest.name_contest;
$scope.buy_in = contest.buy_in;
$scope.currency = APPCONFIG.CURRENCY_SYMBOL;
}],
}).then(function (confirm) {
buyTicket(contest);
}, function(reject) {
});
}
}
element.on('click', function(e){
var contest = attributes['buyTicket'];
openConfirmBuyTicket(contest);
})
}
};
return directive;
}
})();
The export directive
(function() {
'use strict';
angular
.module('myApp')
.directive('exportTeam', exportTeam);
/** #ngInject */
function exportTeam(contestsFactory, ngDialog, APPCONFIG, formatDateFactory) {
var vm = this;
var directive = {
restrict: 'AE',
transclude: true,
controller : function($scope) {
$scope.test = function() {
alert('hey');
}
},
link : function(scope, element, attributes) {
element.on('click', function(e){
var ticket = attributes['exportTeam'];
var id_session = attributes['idsession'];
scope.openExportTeamDialog(ticket, id_session, false);
})
scope.openExportTeamDialog = function(ticket, aams_session_id, afterSave) {
ngDialog.open({
id : 'ft-modal-exportTeam-detail',
className : 'ngdialog ngdialog-theme-default ft-dialog-exportTeam',
controller: ['$scope', 'contestsFactory', 'APPCONFIG', function($scope, contestsFactory, APPCONFIG){
$scope.title = "Aggiungi contest compatibili";
$scope.bodyUrl = 'app/components/modals/body/exportTeam.html';
$scope.contentLoading = true;
$scope.currency = APPCONFIG.CURRENCY_SYMBOL;
$scope.afterSave = afterSave;
$scope.CompatibleContests = [];
contestsFactory.getCompatibleContests(ticket).then(function(response){
angular.forEach(response.data[0], function(item, i){
var multientryOptions = [];
if(item.multientry > 1) {
item.isMultientry = false;
var n = parseInt(item.multientry);
for (i = 1; i <= n; i++) {
multientryOptions.push({
text : i+" team",
value : i
})
}
item.multientryOptions = multientryOptions;
item.multientryOptionSelected = multientryOptions[0];
}else{
item.isMultientry = true;
};
})
$scope.CompatibleContests = response.data[0];
$scope.contentLoading = false;
})
}]
});
}
scope.openExportTeamDialog('N3E94100A725F9QG', 'M3E921013C6DCFCT', false);
}
};
return directive;
}
})();
The buy-ticket directive makes an http call, on the response i want to be able to call the onBuyTicket method of the <export> directive.
I'm trying to understand the best way to do that.
Thanks everyone
This sample show to you how can call an function from your directive
In this sample you can see we just insert data in our directive, and then we handle the data and other action in the directive.
var app = angular.module("app", []);
app.controller("ctrl", function ($scope) {
$scope.dataFromYourController = [
{ name: "Concert Jennifer", value: 200 },
{ name: "007", value: 100 }
];
})
.directive("export", function () {
var template = "<div>" +
"<ul>" +
"<li ng-repeat=\"array in arrays\">" +
"<button ng-click=\"onBuyTicket()\">buy Ticket {{array.name}}</button><hr>" +
"</li>" +
"</ul>" +
"</div>";
return {
restrict: "E",
template: template,
scope: {
data: "="
},
link: function (scope, elem, attrs, ngModel) {
scope.arrays = scope.data;
scope.onBuyTicket = function () {
alert("calling function from directive");
}
}
};
})
<!doctype html>
<html ng-app="app" ng-controller="ctrl">
<head>
</head>
<body>
<h1>call action from your directive</h1>
<export data="dataFromYourController"></export>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
</body>
</html>
#eesdil
var directive = {
restrict: 'AE',
transclude: true,
controller : function($scope) {
$scope.onBuyTicket = function() {
alert('hey');
}
}
}
Ho can I call that from the buy-ticket directive ?
Use the $parent
<button buy-ticket="{{data}}" buy-callback="$parent.onBuyTicket()">buy</button>
So the expor directive something like:
var directive = {
restrict: 'AE',
template: '<ng-tansclude></ng-transclude>',
transclude: true,
controller : function($scope) {
$scope.onBuyTicket = function() {
alert('hey');
}
}
}
UPDATED:
see the plunker:
https://plnkr.co/edit/fmyJ4oPLvTiI0TzO7h1b?p=preview
It really depends what you can call and what you cannot based on the scopes... here if you would remove the scope from the export directive would work without the $parent also as export would share the same scope as the parent (main view)
The best way to communicate events from a child directive to a parent directive (or controller) is to use the $emit method of the scope.
What you want to do is take an ng-click event, get additional information with an $http call, and $emit an event with the additional information to be used by your parent directive (or controller).
HTML
<button buy-ticket="data" ng-click="onBuyTicket()">buy</button>
The directive:
angular.module("myApp").directive("buyTicket", function($http) {
function linkFn(scope,elem,attrs) {
scope.onBuyTicket = function() {
var buyData = scope.$eval(attrs.buyTicket);
var url = someFunction(buyData);
$http.get(url).then (function (response) {
var httpData = response.data;
scope.$emit("buyTicket.click", buyData, httpData);
});
};
};
return {
restrict: "AE",
link: linkFn
};
});
In the parent controller:
$scope.$on("buyTicket.click", function (buyData, httpData) {
console.log(buyData);
console.log(httpData);
});
Notice that I used the $eval method to get the data from the variable named by the buy-ticket attribute.
When choosing a name for the event, I recommend including the name of the directive in the event's name. It makes it clear the source of the event and is unlikely to be duplicated elsewhere.

Adding a new data model to Malhar-Angular-Dashboard

Im' working on the Malhar Angular Dashboard, based on this github project https://github.com/DataTorrent/malhar-angular-dashboard.
As per the documentation in the link post just above, under the 'dataModelType' heading 1/2 way down:
`The best way to provide data to a widget is to specify a dataModelType in the Widget Definition Object (above). This function is used as a constructor whenever a new widget is instantiated on the page.`
And when setting up the Widget Definition Objects, there are various options to choose from :
templateUrl - URL of template to use for widget content
template - String template (ignored if templateUrl is present)
directive - HTML-injectable directive name (eg. "ng-show")
So when I add my own widget definition column chart, I attempt to use the 'template' option; however it does NOT inject the {{value}} scope variable I'm setting.
Using the original datamodel sample widget def, it works fine using the 'directive' option. If I mimic this method on my column chart definition then it works ! But it doesn't work using the template option.
Here's the 'widgetDefinitions' factory code :
(function () {
'use strict';
angular.module('rage')
.factory('widgetDefinitions', ['RandomDataModel','GadgetDataModel', widgetDefinitions])
function widgetDefinitions(RandomDataModel, GadgetDataModel) {
return [
{
name: 'datamodel',
directive: 'wt-scope-watch',
dataAttrName: 'value',
dataModelType: RandomDataModel // GOTTA FIGURE THIS OUT !! -BM:
},
{
name: 'column chart',
title: 'Column Chart',
template: '<div>Chart Gadget Here {{value}}</div>',
dataAttrName: 'value',
size: {width: '40%',height: '200px'},
dataModelType: ColumnChartDataModel
},
];
}
})();
and here are the factories:
'use strict';
angular.module('rage')
.factory('TreeGridDataModel', function (WidgetDataModel, gadgetInitService) {
function TreeGridDataModel() {
}
TreeGridDataModel.prototype = Object.create(WidgetDataModel.prototype);
TreeGridDataModel.prototype.constructor = WidgetDataModel;
angular.extend(TreeGridDataModel.prototype, {
init: function () {
var dataModelOptions = this.dataModelOptions;
this.limit = (dataModelOptions && dataModelOptions.limit) ? dataModelOptions.limit : 100;
this.treeGridActive = true;
//this.treeGridOptions = {};
this.updateScope('THIS IS A TreeGridDataModel...'); // see WidgetDataModel factory
},
updateLimit: function (limit) {
this.dataModelOptions = this.dataModelOptions ? this.dataModelOptions : {};
this.dataModelOptions.limit = limit;
this.limit = limit;
},
destroy: function () {
WidgetDataModel.prototype.destroy.call(this);
}
});
return TreeGridDataModel;
});
'use strict';
angular.module('rage')
.factory('ColumnChartDataModel', function (WidgetDataModel) {
function ColumnChartDataModel() {
}
ColumnChartDataModel.prototype = Object.create(WidgetDataModel.prototype);
ColumnChartDataModel.prototype.constructor = WidgetDataModel;
angular.extend(ColumnChartDataModel.prototype, {
init: function () {
var dataModelOptions = this.dataModelOptions;
this.limit = (dataModelOptions && dataModelOptions.limit) ? dataModelOptions.limit : 100;
this.treeGridActive = true;
var value = 'THIS IS A ColChartDataModel...';
//$scope.value = value;
this.updateScope(value); // see WidgetDataModel factory
},
updateLimit: function (limit) {
this.dataModelOptions = this.dataModelOptions ? this.dataModelOptions : {};
this.dataModelOptions.limit = limit;
this.limit = limit;
},
destroy: function () {
WidgetDataModel.prototype.destroy.call(this);
}
});
return ColumnChartDataModel;
});
and finally the directives:
'use strict';
angular.module('rage')
.directive('wtTime', function ($interval) {
return {
restrict: 'A',
scope: true,
replace: true,
template: '<div>Time<div class="alert alert-success">{{time}}</div></div>',
link: function (scope) {
function update() {
scope.time = new Date().toLocaleTimeString();
}
update();
var promise = $interval(update, 500);
scope.$on('$destroy', function () {
$interval.cancel(promise);
});
}
};
})
.directive('wtScopeWatch', function () {
return {
restrict: 'A',
replace: true,
template: '<div>Value<div class="alert alert-info">{{value}}</div></div>',
scope: {
value: '=value'
}
};
})
.directive('wtFluid', function () {
return {
restrict: 'A',
replace: true,
templateUrl: 'app/views/template2/fluid.html',
scope: true,
controller: function ($scope) {
$scope.$on('widgetResized', function (event, size) {
$scope.width = size.width || $scope.width;
$scope.height = size.height || $scope.height;
});
}
};
});
I'd like to know why ONLY the directive option will update the wigdet's data and not the template option.
thank you,
Bob
I believe I see the problem. The dataAttrName setting and updateScope method are actually doing something other than what you're expecting.
Look at the makeTemplateString function here. This is what ultimately builds your widget's template. You should notice that if you supply a template, the dataAttrName does not even get used.
Next, take a look at what updateScope does, and keep in mind that you can override this function in your own data model to do what you really want, a la:
angular.extend(TreeGridDataModel.prototype, {
init: function() {...},
destroy: function() {...},
updateScope: function(data) {
// I don't see this "main" object defined anywhere, I'm just going
// off your treegrid.html template, which has jqx-settings="main.treeGridOptions"
this.widgetScope.main = { treeGridOptions: data };
// Doing it without main, you could just do:
// this.widgetScope.treeGridOptions = data;
// And then update your treegrid.html file to be:
// <div id="treeGrid" jqx-tree-grid jqx-settings="treeGridOptions"></div>
}
});

AngularJS - bind to directive resize

How can i be notified when a directive is resized?
i have tried
element[0].onresize = function() {
console.log(element[0].offsetWidth + " " + element[0].offsetHeight);
}
but its not calling the function
(function() {
'use strict';
// Define the directive on the module.
// Inject the dependencies.
// Point to the directive definition function.
angular.module('app').directive('nvLayout', ['$window', '$compile', layoutDirective]);
function layoutDirective($window, $compile) {
// Usage:
//
// Creates:
//
var directive = {
link: link,
restrict: 'EA',
scope: {
layoutEntries: "=",
selected: "&onSelected"
},
template: "<div></div>",
controller: controller
};
return directive;
function link(scope, element, attrs) {
var elementCol = [];
var onSelectedHandler = scope.selected();
element.on("resize", function () {
console.log("resized.");
});
$(window).on("resize",scope.sizeNotifier);
scope.$on("$destroy", function () {
$(window).off("resize", $scope.sizeNotifier);
});
scope.sizeNotifier = function() {
alert("windows is being resized...");
};
scope.onselected = function(id) {
onSelectedHandler(id);
};
scope.$watch(function () {
return scope.layoutEntries.length;
},
function (value) {
//layout was changed
activateLayout(scope.layoutEntries);
});
function activateLayout(layoutEntries) {
for (var i = 0; i < layoutEntries.length; i++) {
if (elementCol[layoutEntries[i].id]) {
continue;
}
var div = "<nv-single-layout-entry id=slot" + layoutEntries[i].id + " on-selected='onselected' style=\"position:absolute;";
div = div + "top:" + layoutEntries[i].position.top + "%;";
div = div + "left:" + layoutEntries[i].position.left + "%;";
div = div + "height:" + layoutEntries[i].size.height + "%;";
div = div + "width:" + layoutEntries[i].size.width + "%;";
div = div + "\"></nv-single-layout-entry>";
var el = $compile(div)(scope);
element.append(el);
elementCol[layoutEntries[i].id] = 1;
}
};
}
function controller($scope, $element) {
}
}
})();
Use scope.$watch with a custom watch function:
scope.$watch(
function () {
return [element[0].offsetWidth, element[0].offsetHeight].join('x');
},
function (value) {
console.log('directive got resized:', value.split('x'));
}
)
You would typically want to watch the element's offsetWidth and offsetHeight properties. With more recent versions of AngularJS, you can use $scope.$watchGroup in your link function:
app.directive('myDirective', [function() {
function link($scope, element) {
var container = element[0];
$scope.$watchGroup([
function() { return container.offsetWidth; },
function() { return container.offsetHeight; }
], function(values) {
// Handle resize event ...
});
}
// Return directive definition ...
}]);
However, you may find that updates are quite slow when watching the element properties directly in this manner.
To make your directive more responsive, you could moderate the refresh rate by using $interval. Here's an example of a reusable service for watching element sizes at a configurable millisecond rate:
app.factory('sizeWatcher', ['$interval', function($interval) {
return function (element, rate) {
var self = this;
(self.update = function() { self.dimensions = [element.offsetWidth, element.offsetHeight]; })();
self.monitor = $interval(self.update, rate);
self.group = [function() { return self.dimensions[0]; }, function() { return self.dimensions[1]; }];
self.cancel = function() { $interval.cancel(self.monitor); };
};
}]);
A directive using such a service would look something like this:
app.directive('myDirective', ['sizeWatcher', function(sizeWatcher) {
function link($scope, element) {
var container = element[0],
watcher = new sizeWatcher(container, 200);
$scope.$watchGroup(watcher.group, function(values) {
// Handle resize event ...
});
$scope.$on('$destroy', watcher.cancel);
}
// Return directive definition ...
}]);
Note the call to watcher.cancel() in the $scope.$destroy event handler; this ensures that the $interval instance is destroyed when no longer required.
A JSFiddle example can be found here.
Here a sample code of what you need to do:
APP.directive('nvLayout', function ($window) {
return {
template: "<div></div>",
restrict: 'EA',
link: function postLink(scope, element, attrs) {
scope.onResizeFunction = function() {
scope.windowHeight = $window.innerHeight;
scope.windowWidth = $window.innerWidth;
console.log(scope.windowHeight+"-"+scope.windowWidth)
};
// Call to the function when the page is first loaded
scope.onResizeFunction();
angular.element($window).bind('resize', function() {
scope.onResizeFunction();
scope.$apply();
});
}
};
});
The only way you would be able to detect size/position changes on an element using $watch is if you constantly updated your scope using something like $interval or $timeout. While possible, it can become an expensive operation, and really slow your app down.
One way you could detect a change on an element is by calling
requestAnimationFrame.
var previousPosition = element[0].getBoundingClientRect();
onFrame();
function onFrame() {
var currentPosition = element[0].getBoundingClientRect();
if (!angular.equals(previousPosition, currentPosition)) {
resiszeNotifier();
}
previousPosition = currentPosition;
requestAnimationFrame(onFrame);
}
function resiszeNotifier() {
// Notify...
}
Here's a Plunk demonstrating this. As long as you're moving the box around, it will stay red.
http://plnkr.co/edit/qiMJaeipE9DgFsYd0sfr?p=preview
A slight variation on Eliel's answer worked for me. In the directive.js:
$scope.onResizeFunction = function() {
};
// Call to the function when the page is first loaded
$scope.onResizeFunction();
angular.element($(window)).bind('resize', function() {
$scope.onResizeFunction();
$scope.$apply();
});
I call
$(window).resize();
from within my app.js. The directive's d3 chart now resizes to fill the container.
Here is my take on this directive (using Webpack as bundler):
module.exports = (ngModule) ->
ngModule.directive 'onResize', ['Callback', (Callback) ->
restrict: 'A'
scope:
onResize: '#'
onResizeDebounce: '#'
link: (scope, element) ->
container = element[0]
eventName = scope.onResize || 'onResize'
delay = scope.onResizeDebounce || 1000
scope.$watchGroup [
-> container.offsetWidth ,
-> container.offsetHeight
], _.debounce (values) ->
Callback.event(eventName, values)
, delay
]

angular directive encapsulating a delay for ng-change

I have a search input field with a requery function bound to the ng-change.
<input ng-model="search" ng-change="updateSearch()">
However this fires too quickly on every character. So I end up doing something like this alot:
$scope.updateSearch = function(){
$timeout.cancel(searchDelay);
searchDelay = $timeout(function(){
$scope.requery($scope.search);
},300);
}
So that the request is only made 300ms after the user has stopped typing. Is there any solution to wrap this in a directive?
As of angular 1.3 this is way easier to accomplish, using ngModelOptions:
<input ng-model="search" ng-change="updateSearch()" ng-model-options="{debounce:3000}">
Syntax: {debounce: Miliseconds}
To solve this problem, I created a directive called ngDelay.
ngDelay augments the behavior of ngChange to support the desired delayed behavior, which provides updates whenever the user is inactive, rather than on every keystroke. The trick was to use a child scope, and replace the value of ngChange to a function call that includes the timeout logic and executes the original expression on the parent scope. The second trick was to move any ngModel bindings to the parent scope, if present. These changes are all performed in the compile phase of the ngDelay directive.
Here's a fiddle which contains an example using ngDelay:
http://jsfiddle.net/ZfrTX/7/ (Written and edited by me, with help from mainguy and Ryan Q)
You can find this code on GitHub thanks to brentvatne. Thanks Brent!
For quick reference, here's the JavaScript for the ngDelay directive:
app.directive('ngDelay', ['$timeout', function ($timeout) {
return {
restrict: 'A',
scope: true,
compile: function (element, attributes) {
var expression = attributes['ngChange'];
if (!expression)
return;
var ngModel = attributes['ngModel'];
if (ngModel) attributes['ngModel'] = '$parent.' + ngModel;
attributes['ngChange'] = '$$delay.execute()';
return {
post: function (scope, element, attributes) {
scope.$$delay = {
expression: expression,
delay: scope.$eval(attributes['ngDelay']),
execute: function () {
var state = scope.$$delay;
state.then = Date.now();
$timeout(function () {
if (Date.now() - state.then >= state.delay)
scope.$parent.$eval(expression);
}, state.delay);
}
};
}
}
}
};
}]);
And if there are any TypeScript wonks, here's the TypeScript using the angular definitions from DefinitelyTyped:
components.directive('ngDelay', ['$timeout', ($timeout: ng.ITimeoutService) => {
var directive: ng.IDirective = {
restrict: 'A',
scope: true,
compile: (element: ng.IAugmentedJQuery, attributes: ng.IAttributes) => {
var expression = attributes['ngChange'];
if (!expression)
return;
var ngModel = attributes['ngModel'];
if (ngModel) attributes['ngModel'] = '$parent.' + ngModel;
attributes['ngChange'] = '$$delay.execute()';
return {
post: (scope: IDelayScope, element: ng.IAugmentedJQuery, attributes: ng.IAttributes) => {
scope.$$delay = {
expression: <string>expression,
delay: <number>scope.$eval(attributes['ngDelay']),
execute: function () {
var state = scope.$$delay;
state.then = Date.now();
$timeout(function () {
if (Date.now() - state.then >= state.delay)
scope.$parent.$eval(expression);
}, state.delay);
}
};
}
}
}
};
return directive;
}]);
interface IDelayScope extends ng.IScope {
$$delay: IDelayState;
}
interface IDelayState {
delay: number;
expression: string;
execute(): void;
then?: number;
action?: ng.IPromise<any>;
}
This works perfectly for me: JSFiddle
var app = angular.module('app', []);
app.directive('delaySearch', function ($timeout) {
return {
restrict: 'EA',
template: ' <input ng-model="search" ng-change="modelChanged()">',
link: function ($scope, element, attrs) {
$scope.modelChanged = function () {
$timeout(function () {
if ($scope.lastSearch != $scope.search) {
if ($scope.delayedMethod) {
$scope.lastSearch = $scope.search;
$scope.delayedMethod({ search: $scope.search });
}
}
}, 300);
}
},
scope: {
delayedMethod:'&'
}
}
});
Using the directive
In your controller:
app.controller('ctrl', function ($scope,$timeout) {
$scope.requery = function (search) {
console.log(search);
}
});
In your view:
<div ng-app="app">
<div ng-controller="ctrl">
<delay-search delayed-method="requery(search)"></delay-search>
</div>
</div>
I know i'm late to the game but,hopefully this will help anyone still using 1.2.
Pre ng-model-options i found this worked for me, as ngchange will not fire when the value is invalid.
this is a slight variation on #doug's answer as it uses ngKeypress which doesn't care what state the model is in.
function delayChangeDirective($timeout) {
var directive = {
restrict: 'A',
priority: 10,
controller: delayChangeController,
controllerAs: "$ctrl",
scope: true,
compile: function compileHandler(element, attributes) {
var expression = attributes['ngKeypress'];
if (!expression)
return;
var ngModel = attributes['ngModel'];
if (ngModel) {
attributes['ngModel'] = '$parent.' + ngModel;
}
attributes['ngKeypress'] = '$$delay.execute()';
return {
post: postHandler,
};
function postHandler(scope, element, attributes) {
scope.$$delay = {
expression: expression,
delay: scope.$eval(attributes['ngKeypressDelay']),
execute: function () {
var state = scope.$$delay;
state.then = Date.now();
if (scope.promise) {
$timeout.cancel(scope.promise);
}
scope.promise = $timeout(function() {
delayedActionHandler(scope, state, expression);
scope.promise = null;
}, state.delay);
}
};
}
}
};
function delayedActionHandler(scope, state, expression) {
var now = Date.now();
if (now - state.then >= state.delay) {
scope.$parent.$eval(expression);
}
};
return directive;
};

Resources