All,
I have an attribute directive called scrolly. The directive looks like this:
.directive('scrolly', function () {
return {
restrict: 'A',
scope: {
scrollFunction: '&scrolly'
},
link: function (scope, element, attrs) {
var raw = element[0];
scope.$watch(element[0].children.length, function(){
if(raw.clientHeight <= raw.scrollHeight){
scope.scrollFunction();
}
});
element.bind('scroll', function () {
if (raw.scrollTop + raw.offsetHeight >= raw.scrollHeight) {
scope.scrollFunction();
}
});
}
};
})
The objective is to perform an infinite scroll. The scroll part works fine. The part that isn't working is the scope.$watch part. Basically, the objective for the watch portion is to execute the paging function until the element becomes scrollable at which point the scroll binding would take over. Although the purpose of my directive is paging, I do not want to get hung up on that. The root question is how to watch an attribute of an element.
scope.$watch(angular.bind(element, function(){
return element.children.length;
}), function(oldValue, newValue) {
if(raw.clientHeight <= raw.scrollHeight){
scope.scrollFunction();
}
});
In a $watch you can use a function as the watch parameter to watch specific properties on things.
scope.$watch(function() {
return element[0].children.length;
},
function() {
// do your thang on change
})
Related
Defining scope to add a callback function on an anuglarjs sortable directive breaks sort functionality
I found a simple directive that sits on top of jquery-ui for sorting a list visually. I wanted to add a function callback so my controller could be informed when the list was sorted so I can make a call to the server to sort if on the server as well as this sort needs to be persistent.
I added a scope object on my directive and defined a callback, but the act of adding this scope object messes up the sorting. It doesn't actually sort anymore visually. Ultimately I'm looking to get informed that a sort has happened. Any ideas?
This psi-sortable would go on the ul tag and it expects the ng-model to be the list in question. I simply wanted to add a callback so I can be informed of the sort happened.
angular.module('psi.sortable', [])
.value('psiSortableConfig', {
placeholder: "placeholder",
opacity: 0.8,
axis: "y",
helper: 'clone',
forcePlaceholderSize: true
})
.directive("psiSortable", ['psiSortableConfig', '$log', function (psiSortableConfig, $log) {
return {
require: '?ngModel',
/*scope: {
onSorted: '&'
},*/
link: function (scope, element, attrs, ngModel) {
if (!ngModel) {
$log.error('psiSortable needs a ng-model attribute!', element);
return;
}
var opts = {};
angular.extend(opts, psiSortableConfig);
opts.update = update;
// listen for changes on psiSortable attribute
scope.$watch(attrs.psiSortable, function (newVal) {
angular.forEach(newVal, function (value, key) {
element.sortable('option', key, value);
});
}, true);
// store the sortable index
scope.$watch(attrs.ngModel + '.length', function () {
element.children().each(function (i, elem) {
jQuery(elem).attr('sortable-index', i);
});
});
// jQuery sortable update callback
function update(event, ui) {
// get model
var model = ngModel.$modelValue;
// remember its length
var modelLength = model.length;
// rember html nodes
var items = [];
// loop through items in new order
element.children().each(function (index) {
var item = jQuery(this);
// get old item index
var oldIndex = parseInt(item.attr("sortable-index"), 10);
// add item to the end of model
model.push(model[oldIndex]);
if (item.attr("sortable-index")) {
// items in original order to restore dom
items[oldIndex] = item;
// and remove item from dom
item.detach();
}
});
model.splice(0, modelLength);
// restore original dom order, so angular does not get confused
element.append.apply(element, items);
// notify angular of the change
scope.$digest();
//scope.onSorted();
}
element.sortable(opts);
}
};
}]);
To avoid adding isolate scope, simply use scope.$eval:
app.directive("psiSortable", ['psiSortableConfig', '$log', function (psiSortableConfig, $log) {
return {
require: '?ngModel',
/*scope: {
onSorted: '&'
},*/
link: function (scope, element, attrs, ngModel) {
if (!ngModel) {
$log.error('psiSortable needs a ng-model attribute!', element);
return;
}
//
//...
//
// jQuery sortable update callback
function update(event, ui) {
//...
̶s̶c̶o̶p̶e̶.̶o̶n̶S̶o̶r̶t̶e̶d̶(̶)̶;̶
scope.$eval(attrs.onSorted, {$event: event});
scope.$apply();
}
element.sortable(opts);
}
};
}]);
For more information, see
AngularJS scope/rootScope API Reference - $eval
I am writing and AngularJS directive for DagreD3. I have some problems with $scope update in Angular. When I update the Model, the Directive does not re-render the graph.
A plunker can be found here.
My directive looks like this:
myApp.directive('acDagre', function() {
function link(scope, element, attrs) {
scope.$watch(scope.graph, function(value) {
alert('update'); //NOT EVEN THIS IS CALLED ON UPDATE
});
var renderer = new dagreD3.Renderer();
renderer.run(scope.graph, d3.select("svg g"));
}
return {
restrict: "A",
link: link
};
The variable $scope.graph is modified in the Controller during runtime like this:
$scope.addNode = function(){
$scope.graph.addNode("kbacon2", { label: "Kevin Bacon the second" });
}
Did I understand something wrong in Angular? Everytime the Variable $scope.graph is changed, i want the graph to update.
You can find more information in the Plunker.
Thank you for very much your help!
The watcher should look either like this:
scope.$watch('graph', function(value) {
console.log('update');
});
Or like this:
scope.$watch(function () { return scope.graph; }, function(value) {
console.log('update');
});
It will not fire when adding nodes however, cause it's comparing by reference.
You can add true as a third parameter to perform a deep watch instead (it will use angular.equals):
scope.$watch('graph', function(value) {
console.log('update');
}, true);
Note that this is more expensive.
Example:
.directive('acDagre', function() {
var renderer = new dagreD3.Renderer();
function link(scope, element, attrs) {
scope.$watch(function () { return scope.graph; }, function(value) {
render();
}, true);
var render = function() {
renderer.run(scope.graph, d3.select("svg g"));
};
}
return {
restrict: "A",
link: link
};
});
Demo: http://plnkr.co/edit/Dn1t3sMH58mDz9HhqYD5?p=preview
If you are just changing the nodes you can define the watchExpression like this instead:
scope.$watch(function () { return scope.graph._nodes; }
Deep watching large objects can have a negative effect on performance. This will of course depend on the size and complexity of the watched object and the application, but it's good to be aware of.
I have a custom directive:
.directive('myDirective', function() {
return {
scope: {ngModel:'='},
link: function(scope, element) {
element.bind("keyup", function(event) {
scope.ngModel=0;
scope.$apply();
});
}
}
});
This works as planned, setting the variables to 0 on keyup, but it doesn't reflect the changes on the input themselves. Also when initialized, the values of the model are not in the input. Here is an example:
http://jsfiddle.net/prXm3/
What am I missing?
You need to put a watcher to populate the data since the directive creates an isolated scope.
angular.module('test', []).directive('myDirective', function () {
return {
scope: {
ngModel: '='
},
link: function (scope, element, attrs) {
scope.$watch('ngModel', function (val) {
element.val(scope.ngModel);
});
element.bind("keyup", function (event) {
scope.ngModel = 0;
scope.$apply();
element.val(0); //set the value in the dom as well.
});
}
}
});
Or, you can change the template to
<input type="text" ng-model="$parent.testModel.inputA" my-directive>
the data will be populated thought it will break your logic to do the event binding.
So it is easier to use the watcher instead.
Working Demo
I'm building an application using AngularJS and UniformJS. I'd like to have a reset button on the view that would reset my select's to their default value. If I use uniform.js, it isn't working.
You can examine it here:
http://plnkr.co/edit/QYZRzlRf1qqAYgi8VbO6?p=preview
If you click the reset button continuously, nothing happens.
If you remove the attribute, therefore no longer using uniform.js, everything behaves correctly.
Thanks
UPDATE:
Required the use of timeout.
app.controller('MainCtrl', function($scope, $timeout) {
$scope.reset = function() {
$scope.test = "";
$timeout(jQuery.uniform.update, 0);
};
});
Found it. For the sake of completeness, I'm copying my comment here:
It looks like Uniform is really hacky. It covers up the actual select element, and displays span instead. Angular is working. The actual select element's value is changing, but the span that Uniform displays is not changing.
So you need to tell Uniform that your values have changed with jQuery.uniform.update. Uniform reads the value from the actual element to place in the span, and angular doesn't update the actual element until after the digest loop, so you need to wait a little bit before calling update:
app.controller('MainCtrl', function($scope, $timeout) {
$scope.reset = function() {
$scope.test = "";
$timeout(jQuery.uniform.update, 0);
};
});
Alternatively, you can put this in your directive:
app.directive('applyUniform',function($timeout){
return {
restrict:'A',
require: 'ngModel',
link: function(scope, element, attr, ngModel) {
element.uniform({useID: false});
scope.$watch(function() {return ngModel.$modelValue}, function() {
$timeout(jQuery.uniform.update, 0);
} );
}
};
});
Just a slightly different take on #john-tseng's answer. I didn't want to apply a new attribute to all my check-boxes as we had quite a few in the application already. This also gives you the option to opt out of applying uniform to certain check-boxes by applying the no-uniform attribute.
/*
* Used to make sure that uniform.js works with angular by calling it's update method when the angular model value updates.
*/
app.directive('input', function ($timeout) {
return {
restrict: 'E',
require: '?ngModel',
link: function (scope, element, attr, ngModel) {
if (attr.type === 'checkbox' && attr.ngModel && attr.noUniform === undefined) {
element.uniform({ useID: false });
scope.$watch(function () { return ngModel.$modelValue }, function () {
$timeout(jQuery.uniform.update, 0);
});
}
}
};
});
Please try blow code.
app.directive('applyUniform', function () {
return {
restrict: 'A',
link: function (scope, element, attrs) {
if (!element.parents(".checker").length) {
element.show().uniform();
// update selected item check mark
setTimeout(function () { $.uniform.update(); }, 300);
}
}
};
});
<input apply-uniform type="checkbox" ng-checked="vm.Message.Followers.indexOf(item.usrID) > -1" ng-click="vm.toggleSelection(item.usrID)" />
I have a directive that binds some functions to the local scope with $scope.$on.
Is it possible to bind the same function to multiple events in one call?
Ideally I'd be able to do something like this:
app.directive('multipleSadness', function() {
return {
restrict: 'C',
link: function(scope, elem, attrs) {
scope.$on('event:auth-loginRequired event:auth-loginSuccessful', function() {
console.log('The Ferrari is to a Mini what AngularJS is to ... other JavaScript frameworks');
});
}
};
});
But this doesn't work. The same example with the comma-separated event name string replaced with ['event:auth-loginRequired', 'event:auth-loginConfirmed'] doesn't wrk either.
What does work is this:
app.directive('multipleSadness', function() {
return {
restrict: 'C',
link: function(scope, elem, attrs) {
scope.$on('event:auth-loginRequired', function() {
console.log('The Ferrari is to a Mini what AngularJS is to ... other JavaScript frameworks');
});
scope.$on('event:auth-loginConfirmed', function() {
console.log('The Ferrari is to a Mini what AngularJS is to ... other JavaScript frameworks');
});
}
};
});
But this is not ideal.
Is it possible to bind multiple events to the same function in one go?
The other answers (Anders Ekdahl) are 100% correct... pick one of those... BUT...
Barring that, you could always roll your own:
// a hack to extend the $rootScope
app.run(function($rootScope) {
$rootScope.$onMany = function(events, fn) {
for(var i = 0; i < events.length; i++) {
this.$on(events[i], fn);
}
}
});
app.directive('multipleSadness', function() {
return {
restrict: 'C',
link: function(scope, elem, attrs) {
scope.$onMany(['event:auth-loginRequired', 'event:auth-loginSuccessful'], function() {
console.log('The Ferrari is to a Mini what AngularJS is to ... other JavaScript frameworks');
});
}
};
});
I suppose if you really wanted to do the .split(',') you could, but that's an implementation detail.
AngularJS does not support multiple event binding but you can do something like this:
var handler = function () { ... }
angular.forEach("event:auth-loginRequired event:auth-loginConfirmed".split(" "), function (event) {
scope.$on(event, handler);
});
Yes. Like this:
app.directive('multipleSadness', function() {
return {
restrict: 'C',
link: function(scope, elem, attrs) {
function sameFunction(eventId) {
console.log('Event: ' + eventId + '. The Ferrari is to a Mini what AngularJS is to ... other JavaScript frameworks.');
}
scope.$on('event:auth-loginRequired', function() {sameFunction('auth-loginRequired');});
scope.$on('event:auth-loginConfirmed', function () {sameFunction('auth-loginConfirmed');});
}
};
});
But just because you can, doesn't mean you should :). If the events are continue to propagate up to another listener and they are handled differently there, then maybe there is a case to do this. If this is going to be the only listener than you should just emit (or broadcast) the same event.
I don't think that's possible, since the event might send data to the callback, and if you listen to multiple events you wouldn't know which data came from which event.
I would have done something like this:
function listener() {
console.log('event fired');
}
scope.$on('event:auth-loginRequired', listener);
scope.$on('event:auth-loginConfirmed', listener);
Also like with $rootScope.$onMany (solution from #ben-lesh) it's possible to extend $on method:
var $onOrigin = $rootScope.$on;
$rootScope.$on = function(names, listener) {
var self = this;
if (!angular.isArray(names)) {
names = [names];
}
names.forEach(function(name) {
$onOrigin.call(self, name, listener);
});
};
took from here.