Invoke a Method Directly on a Sibling Directive - angularjs

Is it possible to call a method directly on a specific directive if the ID of that specific directive is known? I know how to do it through listener events (broadcast or emit). I suppose I could do my manipulation using jQuery but I'd like to be able to do it only through Angular. Also, I'd like to avoid the listener event because it seems "wasteful" for every instance of that directive to have to determine if that particular event "belongs" to them.
HTML
<custom-element ce-Id="5"></custom-element>
<custom-element ce-Id="6"></custom-element>
<custom-element ce-Id="7"></custom-element>
<custom-element ce-Id="8"></custom-element>
<custom-element ce-Id="9"></custom-element>
<custom-element ce-Id="10"></custom-element>
So using the example above, is it possible for an event on directive ce-Id="6" (say a click event) to trigger something to happen specifically on ce-Id="7" without using a listener?

You can define a custom API in the factory function of the directive and keep track of subscribers. This code will only run once. Can move it to a service as well.
Example:
app.directive('customElement', function() {
var subscribers = {};
var subscribe = function(id, callback) {
subscribers[id] = callback;
};
var unsubscribe = function(id) {
subscribers[id] = null;
};
var notify = function(id) {
var target = parseInt(id) + 1;
var action = subscribers[target];
if (action) action();
};
var api = {
subscribe: subscribe,
unsubscribe: unsubscribe,
notify: notify
};
return {
restrict: 'E',
template: '<div>I am custom element: {{ ceId }}</div>',
scope: {
ceId: '#',
},
link: function(scope, element, attrs) {
var id = scope.ceId;
if (!id) return;
var onReceive = function() {
console.log('customElement ' + id + ' has received notification.');
};
api.subscribe(id, onReceive);
var onClick = function() {
scope.$apply(function () {
api.notify(id);
});
};
element.on('click', onClick);
scope.$on('$destroy', function() {
element.off('click', onClick);
api.unsubscribe(id);
});
}
};
});
Demo: http://plnkr.co/edit/2s1bkToSuHPURQUcvZcd?p=preview

Related

re-use google-places autocomplete input after page navigation

I need in a angularjs single page application a google-places autocomplete input, that shall run as a service and shall be initialized once at runtime. In case of navigation, the with goolge-places initialized element and the appropriate scope are destroyed.
I will re-use the places input field after navigate to the page containing the places autocomplete input field. With the method element.replaceWith() it works well.
After replacing the element, I can not reset the input by the "reset" button. How can I bind the new generated scope to the "reset" button and the old scope variables. Because the old scope and elements are destroyed by the navigation event?
.factory('myService', function() {
var gPlace;
var s, e;
var options = {
types: [],
componentRestrictions: {country: 'in'}
};
function init() {
}
function set(element, scope) {
console.log('set');
if (!gPlace) {
e = element;
gPlace = new google.maps.places.Autocomplete(element[0], options);
google.maps.event.addListener(gPlace, 'place_changed', function() {
scope.$apply(function() {
scope.place.chosenPlace = element.val();
});
});
} else {
element.replaceWith(e);
}
}
init();
return {
'init':init,
'set':set
};
});
the navigation (element and scope destroying) will be simulated in this plunk by the ng-if directive that will be triggered by the "remove" button.
see here plunk
If you want you can create a service that holds the last selected place and shares it among controllers and directives:
.service('myPlaceService', function(){
var _place;
this.setPlace = function(place){
_place = place;
}
this.getPlace = function(){
return _place;
}
return this;
});
Then create a directive that uses this service:
.directive('googlePlaces', function(myPlaceService) {
return {
restrict: 'E',
scope: {
types: '=',
options: '=',
place: '=',
reset: '='
},
template: '<div>' +
'<input id="gPlaces" type="text"> <button ng-click="resetPlace()">Reset</button>' +
'</div>',
link: function(scope, el, attr){
var input = document.querySelector('#gPlaces');
var jqEl = angular.element(input);
var gPlace = new google.maps.places.Autocomplete(input, scope.options || {});
var listener = google.maps.event.addListener(gPlace, 'place_changed', function() {
var place = autocomplete.getPlace();
scope.$apply(function() {
scope.place.chosenPlace = jqEl.val();
//Whenever place changes, update the service.
//For a more robust solution you could emit an event using scope.$broadcast
//then catch the event where updates are needed.
//Alternatively you can $scope.$watch(myPlaceService.getPlace, function() {...})
myPlaceService.setPlace(jqEl.val());
});
scope.reset = function(){
scope.place.chosenPlace = null;
jqEl.val("");
}
scope.$on('$destroy', function(){
if(listener)
google.maps.event.removeListener(listener);
});
});
}
}
});
Now you can use it like so:
<google-places place="vm.place" options="vm.gPlacesOpts"/>
Where:
vm.gPlacesOpts = {types: [], componentRestrictions: {country: 'in'}}

Angular: Directive not re-compiling inside $interval

I have the following scenario:
Inside my controller I have an $interval running which gets some data from my server. Base on the value returned I want to apply a css class and do some other logic, so I've created a directive to handle this. The problem is the directive doesnt re-evaluate inside every interval so the css class which is applied at the start, stays applied throughout every $interval no matter if other conditions are met. I have created a simple reproduction of my problem here: http://jsfiddle.net/Jw54n/
Here is my code for my app:
var app = angular.module("app", []);
app.controller("home", function ($interval) {
var vm = this;
vm.data = 10;
$interval(function () {
vm.data = Math.floor((Math.random() * 10) + 1);
}, 1000);
});
app.directive("valueAlerter", function () {
return {
restrict: "E",
replace: true,
template: "<div>{{model}}</div>",
require: "^ngModel",
scope: {
model: "=ngModel"
},
link: function ($scope, elem, attrs) {
var value = $scope.model;
var amberValue = 5;
var redValue = 9;
elem.addClass("success");
if (value >= redValue) {
elem.addClass("danger");
elem.removeClass("success");
} else if (value >= amberValue) {
elem.addClass("warning");
elem.removeClass("success");
}
}
};
});
You need to watch model changes and then apply css:
$scope.$watch(function(){
return $scope.model;
},function(value){
//your conditions related to css
});

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
]

Why is this ng-show directive not working in a template?

I am trying to write a directive that will do a simple in-place edit for an element. This is my code so far:
directive('clickEdit', function() {
return {
restrict: 'A',
template: '<span ng-show="inEdit"><input ng-model="editModel"/></span>' +
'<span ng-show="!inEdit" ng-click="edit()">{{ editModel }}</span>',
scope: {
editModel: "=",
inEdit: "#"
},
link: function(scope, element, attr) {
scope.inEdit = false;
var savedValue = scope.editModel;
var input = element.find('input');
input.bind('keyup', function(e) {
if ( e.keyCode === 13 ) {
scope.save();
} else if ( e.keyCode === 27 ) {
scope.cancel();
}
});
scope.edit = function() {
scope.inEdit = true;
setTimeout(function(){
input[0].focus();
input[0].select();
}, 0);
};
scope.save = function() {
scope.inEdit = false;
};
scope.cancel = function() {
scope.inEdit = false;
scope.editModel = savedValue;
};
}
}
})
The scope.edit function sets inEdit to true, and that works well - it hides the text and shows the input tag. However, the scope.save function, which sets scope.inEdit to false does not work at all. It does not hide the input tag and show the text.
Why?
You are calling scope.save() from a event handler reacting to the keyup event. However this event handler is not called by/through the AngularJS framework. AngularJS will only scan for changes of the model if it believes that changes might have occured in order to lessen the workload (AngularJS as of now does dirty-checking with is computational intensive).
Therefore you must make use of the scope.$apply feature to make AngularJS aware that you are doing changes to the scope. Change the scope.save function to this and it shall work:
scope.save = function(){
scope.$apply(function(){
scope.inEdit = false;
});
});
Also it appears that there is actually no need to bind this save function to a scope variable. So you might want to instead define a "normal" function or just integrate the code into your event handler.

Accessing hammer event info from Angular directive inside controller

I'm new to Angular, and I'm trying to get the XY coordinates of a tap using angular-hammer.js directives. Here's how the directives are set up:
var hmTouchevents = angular.module('hmTouchevents', []),
hmGestures = ['hmHold:hold',
'hmTap:tap',
'hmDoubletap:doubletap',
'hmDrag:drag',
'hmDragup:dragup',
'hmDragdown:dragdown',
'hmDragleft:dragleft',
'hmDragright:dragright',
'hmSwipe:swipe',
'hmSwipeup:swipeup',
'hmSwipedown:swipedown',
'hmSwipeleft:swipeleft',
'hmSwiperight:swiperight',
'hmTransform:transform',
'hmRotate:rotate',
'hmPinch:pinch',
'hmPinchin:pinchin',
'hmPinchout:pinchout',
'hmTouch:touch',
'hmRelease:release'];
angular.forEach(hmGestures, function(name){
var directive = name.split(':'),
directiveName = directive[0],
eventName = directive[1];
hmTouchevents.directive(directiveName, ["$parse", function($parse) {
return {
scope: true,
link: function(scope, element, attr) {
var fn, opts;
fn = $parse(attr[directiveName]);
opts = $parse(attr["hmOptions"])(scope, {});
scope.hammer = scope.hammer || Hammer(element[0], opts);
return scope.hammer.on(eventName, function(event) {
return scope.$apply(function() {
return fn(scope, {
$event: event
});
});
});
}
};
}
]);
});
My html looks like this:
<div ng-controller="IndexCtrl" >
<div class='tap-area' hm-tap="tap();">
</div>
</div>
My controller looks like this:
App.controller('IndexCtrl', function ($scope, Myapp) {
$scope.tap = function(ev){
//How do I get the event.gesture.center.pageX in here?
};
});
I figured out how to make this work. After return scope.hammer.on(eventName, function(event) { I added scope.event = event; and then in my controller I can get XY coords of a tap by using this.event.center.pageX or this.event.center.pageY.
It was posted long time ago but here is another solution.
Just add $event to your html

Resources