This question already has answers here:
Controller Required By Directive Can't Be Found
(2 answers)
Closed 7 years ago.
I have 2 directives that I want to share functions between, so I figured I would create a parent directive.
I did it like this:
.directive('kdAlert', function () {
return {
restrict: 'A',
scope: {
},
link: function (scope, element) {
// Set our dismiss to false
scope.dismiss = false;
// Have to use a watch because of issues with other directives
scope.$watch(function () {
// Watch the dismiss
return scope.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
self.bindCloseButton = function (callback) {
// 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();
// Callback
callback()
// Remove our element
element.remove();
});
}
}
};
}
};
})
.directive('cookie', function () {
return {
restrict: 'A',
require: 'kdAlert',
templateUrl: '/assets/tpl/directives/cookie.html',
link: function (scope, element, attr, controller) {
// Dismiss the alert
scope.dismissAlert = function () {
// Set our cookie
scope.dismiss = $cookies.dismissCookieAlert = true;
};
// Bind our close button
scope.bindCloseButton(scope.dismissAlert);
}
};
})
.directive('newsletter', function () {
return {
restrict: 'A',
require: 'kdAlert',
controller: 'NewsletterController',
templateUrl: '/assets/tpl/directives/newsletter.html',
link: function (scope, element, attr, controller) {
// 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);
};
// Dismiss the alert
self.dismissAlert = function () {
// Set our cookie
self.dismiss = $cookies.dismissNewsletterAlert = true;
};
// Bind our close button
scope.bindCloseButton(scope.dismissAlert);
}
};
})
The newsletter has a controller also, which just looks like this:
.controller('NewsletterController', ['$q', '$cookies', 'SubscriberService', 'toastr', function ($q, $cookies, service, toastr) {
var self = this;
// Saves our email address
self.subscribe = function (email) {
// Create our deferred promise
var deferred = $q.defer();
// Subscribe
service.subscribe(email).success(function () {
// If we succeed, display a message
toastr.success('You will now recieve occasional newsletters.');
// Resolve our promise
deferred.resolve();
});
// Return our promise
return deferred.promise;
};
}])
but when I run my application I get an error:
Controller 'kdAlert', required by directive 'newsletter', can't be found!
does anyone know how I can get it to work?
You have to modify require: 'kdAlert', to require: '^kdAlert', and add a controller to your kdAlert in order to make the require work, require will try to find the controller of the kdAlert directive and pass it as a parameter to the directive link function.
Related
I have this directive as also known from other threads.
Now I was running into an AngularJS apply exception:
Error: $rootScope:inprog
Action Already In Progress
$digest already in progress
Does anyone know how I can prevent this exception?
I tried it with a trigger but with no success.
(function() {
'use strict';
angular
.module('myProject.common')
.directive('asScrollTop', asScrollTop);
asScrollTop.$inject = ['chatService'];
function asScrollTop(chatService) {
var directive = {
restrict: 'A',
scope: {
chatMessagesOfUser: '=',
currentUser: '=',
messagesPage: '='
},
link: link
};
return directive;
////////////
function link(scope, element, attr) {
console.log(element);
var trigger = false;
element.on('scroll', function() {
if(element[0].scrollTop <= 0 && trigger == false) {
trigger = true;
var page = scope.messagesPage + 1;
chatService.getChatMessagesOfUser(scope.currentUser.id, page).success(function(response) {
scope.chatMessagesOfUser = response;
scope.$apply();
trigger = false;
}).error(function(data) {
console.log('error in asScroll.directive.js#link');
});
}
});
}
}
})();
That just means that you don't need to call scope.$apply(); because a digest is already occurring, so whatever you were trying to achieve with it is already happening. Just remove the line and things should work fine.
I have a InAppBrowser working fine in my app
$scope.openInAppBrowser = function (url) {
var ref = window.open(encodeURI(url), '_blank', 'location=yes');
ref.addEventListener('loadstop', function (event) {
if (event.url.match("close")) {
$scope.refreshGamePage = 1; // this variable is under watch in a directive
ref.close();
}
});
}
here is the directive written to refresh the page
module.directive('reloadPage', ['$http', function ($http) {
return {
restrict: 'A',
link: function ($scope, element, attrs) {
$scope.refreshPage = function () {
if($scope.refreshGamePage ==1)
return true;
else
return false;
};
$scope.$watch($scope.refreshPage, function (v) {
if (v) {
$scope.setGamePage(); // this function will contains code to refresh game page
}
});
}
};
}]);
but it seems like loadstop event listener is unable to update scope variable.
Can anyone help me with this?
Basically idea is I want to refresh current page (one from which InAppBrowser opened) in my app as soon as InAppBrowser closes itself.
Any better way to achieve this will be appreciated.
Just use angular $apply() method to change the value of angular
variables because it is changed out of angularjs turn.
$scope.openInAppBrowser = function (url) {
var ref = window.open(encodeURI(url), '_blank', 'location=yes');
ref.addEventListener('loadstop', function (event) {
if (event.url.match("close")) {
$scope.$apply(function () {
$scope.refreshGamePage = 1;
});
ref.close();
}
});
}
Hi I am trying to create an angular asyncValidator but I think I am doing something wrong becausse it does not get executed.Here is my code:
(function () {
'use strict';
angular
.module('project')
.directive('imageValidator', imageValidator);
imageValidator.$inject = ['utilitiesService', '$q'];
function imageValidator(utilitiesService, $q) {
var directive = {
restrict: 'A',
require: 'ngModel',
link: link,
};
return directive;
function link(scope, element, attrs, ngModel) {
ngModel.$asyncValidators.imageValidator = function (url) {
var deferred = $q.defer();
if (url) {
var image = new Image();
if (utilitiesService.isNotExternalFile(url)) {
image.src = utilitiesService.getUrl(url)
} else {
image.src = url;
}
image.onload = function () {
deferred.resolve(true);
scope.$apply();
}
image.onerror = function () {
deferred.reject(false);
scope.$apply();
}
} else {
deferred.resolve(true);
}
return deferred.promise;
}
}
}
})();
The code gets executed oance but it does not get executed when I change things in the input.
When it first gets executed it sets the class ng-valid-image-validator which is correct I want it to be valid if there is nothing in the input.
But when I write something in the input ng-valid-image-validator gets removed but it does not add the ng-invalid-image-validator and I asume this happens because the custom validator code does not get executed.
Does anyone have any ideeas what I am doing wrong?
EDIT : I just noticed that the validator gets executed again if emptying the input.
I have a gallery of images which each pull in related data. I've made a directive to lazy load the images once they are in view. It works well, but each of the directives continues watching for the scroll event, with around 200 of them, it's a lot of events being fired unnecessarily. Is there a way to remove the directive, or disable it?
app.directive('lazyLoadGallery', function(resourceService, $rootScope){
return{
link: function (scope, element, attrs) {
var isLoaded = false;
$('.issue-gallery').on('scroll', function(){
console.log($rootScope.number);
if((attrs.lazyLoadGallery/10) % 1 === 0 && !isLoaded) {
if($(element).visible()){
isLoaded = true;
resourceService.issueListPages.list({number:$rootScope.number}).$promise.then(function(data){
$rootScope.issueList = $rootScope.issueList.concat(data.results);
$rootScope.number ++;
$(element).removeAttr('lazy-load-gallery');
});
};
}else{
$(element).removeAttr('lazy-load-gallery');
}
})
}
}
});
my attempt was to remove the attribute from the DOM. Even though it is removed the directive still is watching for scroll events and working as if it wasn't removed.
I was unable to $destroy listeners on the parent object without eliminating it's event for all directives. I came up with a name spaced event which cleans up listeners.
app.directive('lazyLoadGallery', function(resourceService, $rootScope, $compile){
return{
controller: function($scope, $element, $attrs) {
var isLoaded = false;
angular.element('.issue-gallery').on('scroll.'+ $scope.$id, function () {
if (($attrs.lazyLoadGallery / 10) % 1 === 0 && !isLoaded) {
if ($($element).visible()) {
isLoaded = true;
resourceService.issueListPages.list({number: $rootScope.number}).$promise.then(function (data) {
$rootScope.issueList = $rootScope.issueList.concat(data.results);
$rootScope.number++;
angular.element('.issue-gallery').off('scroll.'+ $scope.$id);
});
}
;
} else {
angular.element('.issue-gallery').off('scroll.'+ $scope.$id);
}
})
}
}
});
The angular documentation states that the return of $on function is
Returns function() Returns a deregistration function for this
listener.
So in your case just assign it to a variable and call it when you don't need it anymore.
var deregister = $scope.$on('something', function() {
if (success) {
deregister();
}
});
Reference: Unit Testing AngularJS Directives: scopes not updating?
Case
I have a directive called editable that take an ng-model and creates a toggleable/editable field. The directive works and the parent scope is updated correctly, there are no problems in the actual function of the directive. I cannot seem to write a test that supports this though. It took me a long time to get the directive working properly with all the caveats so I really want to get some tests in place to make sure it continues to work in all of the different cases.
The Directive (which works)
I can't be sure which pieces will be relevant so i included the whole thing.
app.directive('editable',
['$templateCache', '$compile',
function ($templateCache, $compile) {
return {
restrict: 'A',
transclude: true,
templateUrl: 'template/directives/editable.html',
replace: true,
require: 'ngModel',
scope: true,
compile: function(element, attrs, transcludeFn) {
return function (scope, iElement, iAttrs, ctrl) {
var validityId = 'editable-' + scope.$id;
scope.lastSavedValue = iElement.find('input').val();
scope.storeValue = function() {
scope.lastSavedValue = iElement.find('input').val();
};
scope.edit = function() {
scope.storeValue();
scope.editing = true;
$('input', iElement).focus().select();
ctrl.$setValidity(validityId, true);
};
scope.ok = function() {
var inputCtrl = iElement.find('input').controller('ngModel');
if(inputCtrl.$valid === true) {
scope.editing = false;
scope.value = inputCtrl.$viewValue;
ctrl.$setValidity(validityId, false);
ctrl.$setViewValue(inputCtrl.$viewValue); // Not sure (why) this is needed
}
};
scope['delete'] = function() {
scope.deleted = true;
scope.editing = false;
};
scope.undo = function() {
var inputCtrl = iElement.find('input').controller('ngModel');
if(scope.lastSavedValue) {
inputCtrl.$setViewValue(scope.lastSavedValue);
scope.value = scope.lastSavedValue;
ctrl.$setViewValue(scope.lastSavedValue);
}
iElement.find('input').val(scope.value);
scope.editing = false;
scope.deleted = false;
ctrl.$setValidity(validityId, false);
};
transcludeFn(scope, function(clone) {
var $editingReplacement = $(clone).filter('[editing]');
var $viewingReplacement = $(clone).filter('[viewing]');
var $deletedReplacement = $(clone).filter('[deleted]');
var $viewingControls = $('.editable-view-container .controls', iElement);
var $editingControls = $('.editable-input-container .controls', iElement);
var $deletedControls = $('.editable-delete-container .controls', iElement);
if($editingReplacement.length) {
$('.editable-input-container', iElement).html($editingReplacement.html());
$('.editable-input-container', iElement).append($editingControls);
$compile($('.editable-input-container', iElement))(scope);
} else {
$('.editable-input-container', iElement).find('input').attr('ng-model', iAttrs['ngModel']);
$compile($('.editable-input-container', iElement))(scope);
}
if($viewingReplacement.length) {
$('.editable-view-container', iElement).html($viewingReplacement.html());
$('.editable-view-container', iElement).append($viewingControls);
$compile($('.editable-view-container', iElement))(scope);
}
if($deletedReplacement.length) {
$('.editable-delete-container', iElement).html($deletedReplacement.html());
$('.editable-delete-container', iElement).append($deletedControls);
}
});
/**
* Deleted (Isolated Scope)
*
* Tracks if the user has clicked the delete button
*
* #type {Boolean}
*/
scope.deleted = false;
/**
* Editing (Isolated Scope)
*
* Tracks the state of the view
*
* #type {Boolean}
*/
scope.editing = false;
/**
* Initial Loader
*
* Run once after ctrl is loaded
*
* #return {[type]} [description]
*/
var unbindWatcher = scope.$watch(function() { return ctrl.$modelValue; }, function(newVal, oldVal) {
if(typeof ctrl.$modelValue !== 'undefined') {
scope.value = ctrl.$modelValue;
scope.editing = ctrl.$modelValue ? false : true;
unbindWatcher();
}
});
};
}
};
}
]);
Spec
Fails at the end
describe('Editable Directive', function() {
// Keep references to element and scope so that they are available to all tests
var element, scope, ctrl;
beforeEach(module('ltAccountApp'));
beforeEach(module('templates'));
beforeEach(inject(function ($rootScope, $compile) {
var linkFn, el;
// scope = $rootScope;
scope = $rootScope.$new();
scope.testValue = 'xxx';
el = angular.element('\
<div editable="" ng-model="testValue"></div>\
');
// The $compile method returns the directive's link function
linkFn = $compile(el);
// The link function returns the resulting DOM object
element = linkFn(scope);
element.scope().$apply();
ctrl = element.controller('ngModel');
}));
it('should assign input value to scope value', function() {
expect(element.find('input').val()).toEqual(scope.testValue);
});
it('should have access to parent scope variable passed into directive', function() {
expect(ctrl.$viewValue).toEqual(scope.testValue);
expect(ctrl.$modelValue).toEqual(scope.testValue);
});
it('should manage state editing correctly', function() {
expect(element.scope().editing).toBe(false);
element.scope().edit();
expect(element.scope().editing).toBe(true);
});
it('should manage state deleted correctly', function() {
expect(element.scope().deleted).toBe(false);
element.scope()['delete']();
expect(element.scope().deleted).toBe(true);
});
it('should be able to modify parent scope variable passed into directive', function() {
// Not sure what this does, added from referenced SO question
// spyOn(scope, '$apply').andCallThrough();
var newValue = 'yyy';
element.scope().edit();
element.find('input').val(newValue);
element.find('input').trigger('input');
element.scope().ok();
expect(ctrl.$viewValue).toEqual(newValue);
expect(ctrl.$modelValue).toEqual(newValue);
expect(element.scope().value).toEqual(newValue);
expect(scope.$apply).toHaveBeenCalled();
expect(scope.testValue).toEqual(newValue); // <-fails
});
});
So...
Everything seems to be working until I actually expect the parent scope to have the changed value.
I know there is a lot here, I appreciate any guidance you can provide.
Plunk of Angular + Jasmine with directive (work in progress)
Lets say you have to update the value of input field to 'kasrak'. Trying doing it like this:
var elm = element.find("input");
elm.val('kasrak');
elm.trigger($sniffer.hasEvent('input') ? 'input' : 'change');
$scope.$digest()
Key is the third line with $sniffer service. I found this in angular-ui's bootstrap tests.
Here is the link to the function in test spec: https://github.com/angular-ui/bootstrap/blob/master/src/typeahead/test/typeahead.spec.js#L31