Accessing parent directive scope - angularjs

So, I am trying to have 2 directives (techincally 3) on one page, which looks like this:
<div kd-alert newsletter></div>
<div kd-alert cookie></div>
this is on the index page, so there are no controllers.
I have been playing around with isolating scopes with directives and I have found that even though within the link function, scopes are isolated, if your directives use controllers the templates can see both controllers and if both controllers have a property with the same name they can be overwritten by the other controller, which is a nightmare so I decided to create a parent directive with one controller that serves the other 2 directives.
The parent directive in this case is called kd-alert and looks like this:
.directive('kdAlert', function () {
return {
restrict: 'A',
controller: 'AlertController',
link: function (scope, element, attr, controller) {
// Have to use a watch because of issues with other directives
scope.$watch(function () {
// Watch the dismiss
return controller.dismiss;
// If the value changes
}, function (dismiss) {
// If our value is false
if (dismiss === false || dismiss === 'false') {
// Remove the class from the element
element.removeClass('ng-hide');
// Else, if the value is true (or anything else)
} else {
// Add the class to the element
element.addClass('ng-hide');
}
});
// Get our buttons
var buttons = element.find('button');
// Binds our close button
scope.bindCloseButton = function (cookieName) {
// If we have a button
for (var i = 0; i < buttons.length; i++) {
// Get our current button
var button = angular.element(buttons[i]);
// If our button is the close button
if (button.hasClass('close')) {
// If the button is clicked
button.on('click', function (e) {
console.log('clicked');
// Prevent any default actions
e.preventDefault();
// dismiss the alert
controller.dismissAlert(cookieName);
// Remove our element
element.remove();
});
}
}
};
}
};
})
The controller handles methods for both child directives but is still pretty thin. It looks like this:
.controller('AlertController', ['$cookies', 'SubscriberService', 'toastr', function ($cookies, subscriverService, toastr) {
var self = this;
// Set our dismiss to false
self.dismiss = false;
// Set the flag
self.getDismissValue = function (cookieName) {
// Set our cookie
self.dismiss = $cookies[cookieName] || false;
};
// Set the flag
self.dismissAlert = function (cookieName) {
// Set our cookie
self.dismiss = $cookies[cookieName] = true;
};
// Saves our email address
self.subscribe = function (email, cookieName) {
// Subscribe
subscriverService.subscribe(email).success(function () {
// If we succeed, display a message
toastr.success('You will now recieve occasional newsletters.');
// Dismiss the alert
self.dismissAlert(cookieName);
});
};
}])
Now I have a cookie directive which works fine...
.directive('cookie', function () {
return {
restrict: 'A',
require: '^kdAlert',
templateUrl: '/assets/tpl/directives/cookie.html',
link: function (scope, element, attr, controller) {
console.log(scope);
// Get our cookie name
var cookieName = 'cookieAlert';
// Get our dismiss value
controller.getDismissValue(cookieName);
// Bind our close button
scope.bindCloseButton(cookieName);
}
};
})
When I refresh my page I can clearly see the scope with the bindCloseButton method within that scope. So far so good.
The problem is with the newsletter directive, it looks like this:
.directive('newsletter', function () {
return {
restrict: 'A',
require: '^kdAlert',
templateUrl: '/assets/tpl/directives/newsletter.html',
link: function (scope, element, attr, controller) {
console.log(scope);
// Get our cookie name
var cookieName = 'newsletterAlert';
// Get our dismiss value
controller.getDismissValue(cookieName);
// Bind our close button
scope.bindCloseButton(cookieName);
// Saves our email address
scope.subscribe = function (valid) {
// If we are not valid
if (!valid) {
// Return from the function
return;
}
// Subscribe
controller.subscribe(scope.email, cookieName);
};
}
};
})
Again, if I refresh the page I can clearly see the bindCloseButton method within that scope, but for some reason I get this error:
scope.bindCloseButton is not a function
And that appears on the line within the newsletter directive.
If I remove the cookie directive off the page, I still get the error.
Can anyone explain why?

use controller.bindCloseButton instead of scope.bindCloseButton .
This is happening because of the isolation of scope. you are doing in this approach and that's why you are losing scopes here.

Related

can anybody explain this directive?

Am new to angularjs. I have seen this piece of code from this link. It is working fine for me. But i am not getting how it is working ? From where it was called?
Can anybody explain this ?
var app = angular.module('plunker', []);
var ValidSubmit = ['$parse', function ($parse) {
return {
compile: function compile(tElement, tAttrs, transclude) {
return {
post: function postLink(scope, element, iAttrs, controller) {
var form = element.controller('form');
form.$submitted = false;
var fn = $parse(iAttrs.validSubmit);
element.on('submit', function(event) {
scope.$apply(function() {
element.addClass('ng-submitted');
form.$submitted = true;
if(form.$valid) {
fn(scope, {$event:event});
}
});
});
scope.$watch(function() { return form.$valid}, function(isValid) {
if(form.$submitted == false) return;
if(isValid) {
element.removeClass('has-error').addClass('has-success');
} else {
element.removeClass('has-success');
element.addClass('has-error');
}
});
}
}
}
}
}]
app.directive('validSubmit', ValidSubmit);
Here is the gist of what the directive is doing:
A function is assigned to handle the "onsubmit" event for that element
It looks like the directive is intended to be an attribute, which accepts a function as its value. That function is retrieved:
var fn = $parse(iAttrs.validSubmit);
An event handler for onsubmit is set up:
element.on('submit', function(event) {
And then that function is called if the form is valid:
if(form.$valid) {
fn(scope, {$event:event});
The directive monitors the state of the form that contains the element (valid or invalid)
The $watch function is used to fire an event whenever the form changes between valid and invalid:
scope.$watch(function() { return form.$valid}
Whenever the form is submitted, CSS classes are applied to the element
When the validity of the form changes, the has-error or has-success CSS class is applied to the element (based on whether the form is valid or not).
Note that the CSS classes will only be added / removed if the form is being submitted, due to the guard clause at the beginning of that function:
if(form.$submitted == false) return;

How to dynamically load controller to a directive

So I have a directive that Will be acting as a side panel in my app. When a user clicks a button the side panel will open. In said side panel I need the controller and view for this area to be dynamic based on which button the users clicks. I have found a way to load up the template dynamically but I am running into issues with loading the controller dynamically.
Enough talking here is the code.
Directive Code
app.directive('itemForm', function($compile) {
var item1Template = '<div ng-include="view"></div>';
var item2Template = '<h1> Hello item2 Form </h1>';
var getTemplate = function(contentType) {
if(contentType === 'item1') {
return item1Template;
} else if(contentType === 'item2') {
return item2Template;
}
};
return {
restrict: 'E',
replace: 'true',
scope: {
formType: '#formType'
},
//templateUrl: scope.template,
link: function(scope, element) {
if(scope.formType === 'item1') {
scope.view = '/views/item1.html';
}
element.html(getTemplate(scope.formType)).show();
$compile(element.contents())(scope);
}
};
});
Html
<item-form form-type='{{form.type}}'> </item-form>
Controller for view that directive lives in
$scope.form = {};
$scope.openItemOneDlg = function() {
$scope.isFormOpen = !$scope.isFormOpen; // this opens the side panel
$scope.form.type = 'item1';
};
$scope.openItemTwoDlg = function() {
$scope.isFormOpen = !$scope.isFormOpen; // this opens the side panel
$scope.form.type = 'item2';
};
You can broadcast (using $broadcast) an event on click of the button. And have a listener (using $on) in the directive. This way, whenever the event is fired, directive logic will get executed.
You can refer the answer on this link for the usage of $broadcast and $on:
On-and-broadcast-in-AngularJS

Set focus to first invalid form element in AngularJS

Basically, what I'm trying to accomplish, is to set focus to the first invalid element after a form submit has been attempted. At this point, I have the element being flagged as invalid, and I can get the $name of the element so I know which one it is.
It's "working" but a "$apply already in progress" error is being thrown...
So I must be doing something wrong here :)
Here's my code so far:
$scope.submit = function () {
if ($scope.formName.$valid) {
// Good job.
}
else
{
var field = null,
firstError = null;
for (field in $scope.formName) {
if (field[0] != '$')
{
if (firstError === null && !$scope.formName[field].$valid) {
firstError = $scope.formName[field].$name;
}
if ($scope.formName[field].$pristine) {
$scope.formName[field].$dirty = true;
}
}
}
formName[firstError].focus();
}
}
My field looping is based on this solution, and I've read over this question a few times. It seems like the preferred solution is to create a directive, but adding a directive to every single form element just seems like overkill.
Is there a better way to approach this with a directive?
Directive code:
app.directive('ngFocus', function ($timeout, $log) {
return {
restrict: 'A',
link: function (scope, elem, attr) {
scope.$on('focusOn', function (e, name) {
// The timeout lets the digest / DOM cycle run before attempting to set focus
$timeout(function () {
if (name === attr.ngFocusId) {
if (attr.ngFocusMethod === "click")
angular.element(elem[0]).click();
else
angular.element(elem[0]).focus();
}
});
})
}
}
});
Factory to use in the controller:
app.factory('focus', function ($rootScope, $timeout) {
return function (name) {
$timeout(function () {
$rootScope.$broadcast('focusOn', name);
}, 0, false);
};
});
Sample controller:
angular.module('test', []).controller('myCtrl', ['focus', function(focus) {
focus('myElement');
}
Building a directive is definitely the way to go. There is otherwise no clean way to select in element in angularjs. It's just not designed like this. I would recommend you to check out this question on this matter.
You wouldn't have to create a single directive for every form-element. On for each form should suffice. Inside the directive you can use element.find('input');. For the focus itself I suppose that you need to include jQuery and use its focus-function.
You can howerever - and I would not recommend this - use jQuery directly inside your controller. Usually angular form-validation adds classes like ng-invalid-required and the like, which you can use as selector. e.g:
$('input.ng-valid').focus();
Based on the feedback from hugo I managed to pull together a directive:
.directive( 'mySubmitDirty', function () {
return {
scope: true,
link: function (scope, element, attrs) {
var form = scope[attrs.name];
element.bind('submit', function(event) {
var field = null;
for (field in form) {
if (form[field].hasOwnProperty('$pristine') && form[field].$pristine) {
form[field].$dirty = true;
}
}
var invalid_elements = element.find('.ng-invalid');
if (invalid_elements.length > 0)
{
invalid_elements[0].focus();
}
event.stopPropagation();
event.preventDefault();
});
}
};
})
This approach requires jquery as the element.find() uses a class to find the first invalid element in the dom.

AngularJS: Preventing 'mouseenter' event triggering on child elements

I'm playing right now with the AngularJS framework and I stumbled upon a problem. I made a directive which is called 'enter'. It triggers functions on mouseenter and mouseleave. I applied it as an attribute to the table row elements. It is now triggered for every child element (all the columns and etc), but it should be only triggered, when you go with your mouse over the table row.
This is how my directive looks like:
myapp.directive('enter', function(){
return {
restrict: 'A', // link to attribute... default is A
link: function (scope, element){
element.bind('mouseenter',function() {
console.log('MOUSE ENTER: ' + scope.movie.title);
});
element.bind('mouseleave',function() {
console.log('LEAVE');
});
}
}
});
Here is an example: http://jsfiddle.net/dJGfd/1/
You have to open the Javascript console to see the log messages.
What is the best way to achieve the functionality that I want in AngularJS? I prefer to not use jQuery if there is a reasonable AngularJS solution.
You can try this:
myapp.directive('enter', function () {
return {
restrict: 'A',
controller: function ($scope, $timeout) {
// do we have started timeout
var timeoutStarted = false;
// pending value of mouse state
var pendingMouseState = false;
$scope.changeMouseState = function (newMouseState) {
// if pending value equals to new value then do nothing
if (pendingMouseState == newMouseState) {
return;
}
// otherwise store new value
pendingMouseState = newMouseState;
// and start timeout
startTimer();
};
function startTimer() {
// if timeout started then do nothing
if (timeoutStarted) {
return;
}
// start timeout 10 ms
$timeout(function () {
// reset value of timeoutStarted flag
timeoutStarted = false;
// apply new value
$scope.mouseOver = pendingMouseState;
}, 10, true);
}
},
link: function (scope, element) {
//**********************************************
// bind to "mouseenter" and "mouseleave" events
//**********************************************
element.bind('mouseover', function (event) {
scope.changeMouseState(true);
});
element.bind('mouseleave', function (event) {
scope.changeMouseState(false);
});
//**********************************************
// watch value of "mouseOver" variable
// or you create bindings in markup
//**********************************************
scope.$watch("mouseOver", function (value) {
console.log(value);
});
}
}
});
Same thing at http://jsfiddle.net/22WgG/
Also instead
element.bind("mouseenter", ...);
and
element.bind("mouseleave", ...);
you can specify
<tr enter ng-mouseenter="changeMouseState(true)" ng-mouseleave="changeMouseState(false)">...</tr>
See http://jsfiddle.net/hwnW3/

AngularJS dropdown directive hide when clicking outside

I'm trying to create a multiselect dropdown list with checkbox and filter option. I'm trying to get the list hidden with I click outside but could not figure it out how. Appreciate your help.
http://plnkr.co/edit/tw0hLz68O8ueWj7uZ78c
Watch out, your solution (the Plunker provided in the question) doesn't close the popups of other boxes when opening a second popup (on a page with multiple selects).
By clicking on a box to open a new popup the click event will always be stopped. The event will never reach any other opened popup (to close them).
I solved this by removing the event.stopPropagation(); line and matching all child elements of the popup.
The popup will only be closed, if the events element doesn't match any child elements of the popup.
I changed the directive code to the following:
select.html (directive code)
link: function(scope, element, attr){
scope.isPopupVisible = false;
scope.toggleSelect = function(){
scope.isPopupVisible = !scope.isPopupVisible;
}
$(document).bind('click', function(event){
var isClickedElementChildOfPopup = element
.find(event.target)
.length > 0;
if (isClickedElementChildOfPopup)
return;
scope.$apply(function(){
scope.isPopupVisible = false;
});
});
}
I forked your plunker and applied the changes:
Plunker: Hide popup div on click outside
Screenshot:
This is an old post but in case this helps anyone here is a working example of click outside that doesn't rely on anything but angular.
module('clickOutside', []).directive('clickOutside', function ($document) {
return {
restrict: 'A',
scope: {
clickOutside: '&'
},
link: function (scope, el, attr) {
$document.on('click', function (e) {
if (el !== e.target && !el[0].contains(e.target)) {
scope.$apply(function () {
scope.$eval(scope.clickOutside);
});
}
});
}
}
});
OK I had to call $apply() as the event is happening outside angular world (as per doc).
element.bind('click', function(event) {
event.stopPropagation();
});
$document.bind('click', function(){
scope.isVisible = false;
scope.$apply();
});
I realized it by listening for a global click event like so:
.directive('globalEvents', ['News', function(News) {
// Used for global events
return function(scope, element) {
// Listens for a mouse click
// Need to close drop down menus
element.bind('click', function(e) {
News.setClick(e.target);
});
}
}])
The event itself is then broadcasted via a News service
angular.factory('News', ['$rootScope', function($rootScope) {
var news = {};
news.setClick = function( target ) {
this.clickTarget = target;
$rootScope.$broadcast('click');
};
}]);
You can then listen for the broadcast anywhere you need to. Here is an example directive:
.directive('dropdown', ['News', function(News) {
// Drop down menu für the logo button
return {
restrict: 'E',
scope: {},
link: function(scope, element) {
var opened = true;
// Toggles the visibility of the drop down menu
scope.toggle = function() {
element.removeClass(opened ? 'closed' : 'opened');
element.addClass(opened ? 'opened' : 'closed');
};
// Listens for the global click event broad-casted by the News service
scope.$on('click', function() {
if (element.find(News.clickTarget.tagName)[0] !== News.clickTarget) {
scope.toggle(false);
}
});
// Init
scope.toggle();
}
}
}])
I hope it helps!
I was not totally satisfied with the answers provided so I made my own. Improvements:
More defensive updating of the scope. Will check to see if a apply/digest is already in progress
div will also close when the user presses the escape key
window events are unbound when the div is closed (prevents leaks)
window events are unbound when the scope is destroyed (prevents leaks)
function link(scope, $element, attributes, $window) {
var el = $element[0],
$$window = angular.element($window);
function onClick(event) {
console.log('window clicked');
// might need to polyfill node.contains
if (el.contains(event.target)) {
console.log('click inside element');
return;
}
scope.isActive = !scope.isActive;
if (!scope.$$phase) {
scope.$apply();
}
}
function onKeyUp(event) {
if (event.keyCode !== 27) {
return;
}
console.log('escape pressed');
scope.isActive = false;
if (!scope.$$phase) {
scope.$apply();
}
}
function bindCloseHandler() {
console.log('binding window click event');
$$window.on('click', onClick);
$$window.on('keyup', onKeyUp);
}
function unbindCloseHandler() {
console.log('unbinding window click event');
$$window.off('click', onClick);
$$window.off('keyup', onKeyUp);
}
scope.$watch('isActive', function(newValue, oldValue) {
if (newValue) {
bindCloseHandler();
} else {
unbindCloseHandler();
}
});
// prevent leaks - destroy handlers when scope is destroyed
scope.$on('$destroy', function() {
unbindCloseHandler();
});
}
I get $window directly into the link function. However, you do not need to do this exactly to get $window.
function directive($window) {
return {
restrict: 'AE',
link: function(scope, $element, attributes) {
link.call(null, scope, $element, attributes, $window);
}
};
}
There is a cool directive called angular-click-outside. You can use it in your project. It is super simple to use:
https://github.com/IamAdamJowett/angular-click-outside
The answer Danny F posted is awesome and nearly complete, but Thịnh's comment is correct, so here is my modified directive to remove the listeners on the $destroy event of the directive:
const ClickModule = angular
.module('clickOutside', [])
.directive('clickOutside', ['$document', function ($document) {
return {
restrict: 'A',
scope: {
clickOutside: '&'
},
link: function (scope, el, attr) {
const handler = function (e) {
if (el !== e.target && !el[0].contains(e.target)) {
scope.$apply(function () {
console.log("hiiii");
// whatever expression you assign to the click-outside attribute gets executed here
// good for closing dropdowns etc
scope.$eval(scope.clickOutside);
});
}
}
$document.on('click', handler);
scope.$on('$destroy', function() {
$document.off('click', handler);
});
}
}
}]);
If you put a log in the handler method, you will still see it fire when an element has been removed from the DOM. Adding my small change is enough to remove it. Not trying to steal anyone's thunder, but this is a fix to an elegant solution.
Use angular-click-outside
Installation:
bower install angular-click-outside --save
npm install #iamadamjowett/angular-click-outside
yarn add #iamadamjowett/angular-click-outside
Usage:
angular.module('myApp', ['angular-click-outside'])
//in your html
<div class="menu" click-outside="closeThis">
...
</div>
//And then in your controller
$scope.closeThis = function () {
console.log('closing');
}
I found some issues with the implementation in https://github.com/IamAdamJowett/angular-click-outside
If for example the element clicked on is removed from the DOM, the directive above will trigger the logic.
That didn't work for me, since I had some logic in a modal that, after click, removed the element with a ng-if.
I rewrote his implementation. Not battle tested, but seems to be working better (at least in my scenario)
angular
.module('sbs.directives')
.directive('clickOutside', ['$document', '$parse', '$timeout', clickOutside]);
const MAX_RECURSIONS = 400;
function clickOutside($document, $parse, $timeout) {
return {
restrict: 'A',
link: function ($scope, elem, attr) {
// postpone linking to next digest to allow for unique id generation
$timeout(() => {
function runLogicIfClickedElementIsOutside(e) {
// check if our element already hidden and abort if so
if (angular.element(elem).hasClass('ng-hide')) {
return;
}
// if there is no click target, no point going on
if (!e || !e.target) {
return;
}
let clickedElementIsOutsideDirectiveRoot = false;
let hasParent = true;
let recursions = 0;
let compareNode = elem[0].parentNode;
while (
!clickedElementIsOutsideDirectiveRoot &&
hasParent &&
recursions < MAX_RECURSIONS
) {
if (e.target === compareNode) {
clickedElementIsOutsideDirectiveRoot = true;
}
compareNode = compareNode.parentNode;
hasParent = Boolean(compareNode);
recursions++; // just in case to avoid eternal loop
}
if (clickedElementIsOutsideDirectiveRoot) {
$timeout(function () {
const fn = $parse(attr['clickOutside']);
fn($scope, { event: e });
});
}
}
// if the devices has a touchscreen, listen for this event
if (_hasTouch()) {
$document.on('touchstart', function () {
setTimeout(runLogicIfClickedElementIsOutside);
});
}
// still listen for the click event even if there is touch to cater for touchscreen laptops
$document.on('click', runLogicIfClickedElementIsOutside);
// when the scope is destroyed, clean up the documents event handlers as we don't want it hanging around
$scope.$on('$destroy', function () {
if (_hasTouch()) {
$document.off('touchstart', runLogicIfClickedElementIsOutside);
}
$document.off('click', runLogicIfClickedElementIsOutside);
});
});
},
};
}
function _hasTouch() {
// works on most browsers, IE10/11 and Surface
return 'ontouchstart' in window || navigator.maxTouchPoints;
}

Resources