I have a slideshow directive (self-made) which is adjustable - some variables change, for instance, I have a scope variable containing these settings:
{
animationSpeed : 30,
transitionTime : 1.5
}
As animation library I use greensock (GSAP) and the animations are defined as follows :
ModuleSlideshow.animation('.slide-left-animation', ['$window',function ($window) {
"use strict";
var getScope = function(e){
var scope = angular.element(e).scope();
if(scope){ scope = scope.$parent; }
if(scope){ scope = scope.$parent; }
return scope;
};
return {
enter: function (element, done) {
TweenMax.fromTo(element, getScope(element).animationSpeed, {left: -$window.innerWidth, ease : Power4.easeInOut}, {left: 0, ease: Power4.easeInOut, onComplete: done});
},
leave: function (element, done) {
TweenMax.to(element,getScope(element).animationSpeed,{left : $window.innerWidth,ease : Power4.easeInOut, onComplete: done});
}
};
}]);
As you can see there I try to grab the scope with the element I get (the element is a .slide div, and I go up to the parent twice (to grab correct variables).
This works sometimes, but sometimes not, and I get some JavaScript error saying that "cannot read property $parent of null". I tried to inject $scope directly, but this doesn't seem to work. How would you access scope in my case? Should I change the way I setup the animation?
I ran into a problem almost exactly like yours where I needed to access the element's scope. It seemed using element.scope() will be fine but will break especially if you intend to use $compileProvider.debugInfoEnabled(false);, which is recommended in Angular apps for performance. Although you lose access to the scope of the element when you turn off Debug Info, you still do have access to the element's controller via data().
Perhaps if you refactor your code to utilize a controller for your directive, you'll be able to access the scope variable you need.
angular.module('myApp')
.config(['$compileProvider', function ($compileProvider) {
$compileProvider.debugInfoEnabled(false);
}])
.controller('myDirectiveCtrl', function ($scope, $el) {
this.animationSpeed = $scope.animationSpeed;
})
.directive('myDirective', function () {
return {
restrict: 'E',
scope: {
animationSpeed: '='
},
controller: 'myDirectiveCtrl',
controllerAs: 'vm'
};
})
.animation('.my-directive', function () {
return {
addClass: function (element, className, done) {
var vm = element.data().$myDirectiveController;
console.log(vm.animationSpeed, className);
},
removeClass: function (element, className, done) {
var vm = element.data().$myDirectiveController;
console.log(vm.animationSpeed, className);
}
}
})
I tried the one above; I could not inject scope into the directive controller. I created a service instead to communicate like so and it works well and I can keep the them ..
angular
.module('App')
.service('SliderService', function (){
return {
direction: null
};
});
angular
.module("App")
.directive('slider',slider);
slider.$inject = ['SliderService'];
link: function (scope, elem, attrs) { var unbindWatch = scope.$watch('direction', function () { SliderService.direction = scope.direction });
Remember to kill watches and timers with scope.
scope.$on('destroy',function () { unbindWatch() }) }
Then for the animation use the following
angular
.module("App")
.animation('.slide-animation', slideAnimation);
slideAnimation.$inject =['SliderService'];
function slideAnimation ( SliderService) {
return {
beforeAddClass: function (element, className, done) {
if (className == 'ng-hide' && SliderService.direction) { ... }
}
}
Related
I'm having trouble with Angular's one time binding.
Let's say I want to use ngIf with one time binding, something like this:
<div ng-if="::showImage">
<img src="somesource" img-preloader/>
</div>
In this case angular creates a watch for the expression inside the if.
Once it has been resolved to a none-undefined value the watch is removed.
If it was resolved to a truthly value only then the descendant html tree is added to the DOM and subsequently rendered.
Now this is all great but I'd really like to avoid the initial watch, just parse the expression, and if its undefined - only then set up a watch. The reason being is fairly complex in my scenario but basically I have some mechanism that temporarily disables unneeded watches...
So I was looking for alternatives to the built-in angular's one time binding and came across angular-once.
Angular-once implements one-time-binding in a different way, it sets up a temp watch only if the expression is parsed to undefined, so if it resolves in the initial attempt no watch is created. Sounds great.
So I could do something like this:
<div once-if="showImage">
<img src="somesource" img-preloader/>
</div>
But, here's the problem - apparently the descendant HTML tree is first rendered by default and then if once-if resolves to false the descendant nodes are removed from the DOM.
Here's the snippet that does it:
{
name: 'onceIf',
priority: 600,
binding: function (element, value) {
if (!value) {
element.remove();
}
}
},
This is bad behavior for me, as creating the descendant tree is a no-go and results in other problems, for instance - in the above example the img will be downloaded.
So I'm looking for a way to do one-time-binding in directives like ngIf without setting up a watch if the expression parses successfully and without pre-rendering the descendant tree.
I was trying to avoid this, but for now I ended up implementing custom directives based on Angular's standard ones but with the necessary added functionality.
ngIf derived directive:
app.directive('watchlessIf', ['$animate', '$compile', '$parse', function($animate, $compile, $parse) {
return {
multiElement: true,
transclude: 'element',
priority: 600,
terminal: true,
restrict: 'A',
$$tlb: true,
link: function($scope, $element, $attr, ctrl, $transclude) {
function valueChangedAction(value) {
if (value) {
if (!childScope) {
$transclude(function(clone, newScope) {
childScope = newScope;
clone[clone.length++] = $compile.$$createComment('end watchlessIf', $attr.watchlessIf);
block = {
clone: clone
};
$animate.enter(clone, $element.parent(), $element);
});
}
} else {
if (previousElements) {
previousElements.remove();
previousElements = null;
}
if (childScope) {
childScope.$destroy();
childScope = null;
}
if (block) {
previousElements = getBlockNodes(block.clone);
$animate.leave(previousElements).then(function() {
previousElements = null;
});
block = null;
}
}
}
var block, childScope, previousElements;
if ($attr.watchlessIf.startsWith("::")) {
var parsedExpression = $parse($attr.watchlessIf)($scope);
if (parsedExpression != null) {
valueChangedAction(parsedExpression);
return;
}
}
$scope.$watch($attr.watchlessIf, valueChangedAction);
}
};
}]);
ngBind derived directive:
app.directive('watchlessBind', ['$compile', '$parse', function($compile, $parse) {
return {
restrict: 'AC',
compile: function watchlessBindCompile(templateElement) {
$compile.$$addBindingClass(templateElement);
return function watchlessBindLink(scope, element, attr) {
function valueChangedAction(value) {
element.textContent = (typeof value == "undefined") ? '' : value;
}
$compile.$$addBindingInfo(element, attr.watchlessBind);
element = element[0];
if (attr.watchlessBind.startsWith("::")) {
var parsedExpression = $parse(attr.watchlessBind)(scope);
if (parsedExpression != null) {
valueChangedAction(parsedExpression);
return;
}
}
scope.$watch(attr.watchlessBind, valueChangedAction);
};
}
};
}]);
Notes:
Unfortunately with such approach I'll have to implement similar directives for other Angular directives as well where I'd like to support potentially watch-less one time binding.
I'm using private angular stuff inside the directives, like the $$tlb option, although i really shouldn't...
I'm trying to inject $q into my directive, but though $q is defined as a resolver() at first, when calling the function it is undefined. Maybe something related to binding? I don't know.
(function () {
'use strict';
myForm.$inject = ["$q"];
angular
.module('myModule')
.directive('myForm', myForm);
function myForm($q) {
return {
restrict: 'EA',
scope: {
ngSubmitFunction: '&',
},
templateUrl: 'myTemplate',
controllerAs: 'ctrl',
controller: ["$scope", "$window", "$q", function ($scope, $window, $q) {
var vm = this;
vm.name = 'myForm';
$scope.submitPromise = function(){};
vm.ngSubmit = ngSubmit;
function ngSubmit($form) {
vm.submitDisabled = true;
$form.$setSubmitted();
if ($form.$valid) {
$scope.submitPromise().then(function() {
vm.submitDisabled = false;
});
}
}
}],
link: function (scope, element, attrs) {
console.log($q);
scope.submitPromise = function($q) {
console.log($q);
var deferred = $q.defer();
scope.ngSubmitFunction();
return deferred.promise;
}
}
};
}
}());
The objective is to call ngSubmit when user clicks on a button. ngSubmit disables the button, waits for the async calls to be over and then enables the button.
In the example code, the 1st console.log($q) (executed when loading the page) outputs this:
Q(resolver) {
if (!isFunction(resolver)) {
throw $qMinErr('norslvr', "Expected resolverFn, got '{0}'", resolver);
}...
Which to me looks like correct.
But when calling submitPromise() after pressing the button, this is the output:
undefined
TypeError: Cannot read property 'defer' of undefined
When is $q lost?
Note: this is not the only version I tried, originally all code was on controller, nothing on link. I've also been told this pattern is deprecated and to use this one, which is better:
function submitPromise($q) {
return $q(function (resolve) {
$scope.ngSubmitFunction();
})
}
Nothing worked. Everything produces the same error, $q gets undefined at some point and can't find out why.
Using $q as an argument parameter is causing $q to become undefined.
myForm.$inject = ["$q"];
angular
.module('myModule')
.directive('myForm', myForm);
function myForm($q) {
return {
link: function (scope, element, attrs) {
console.log($q);
//scope.submitPromise = function($q) {
//Remove $q as parameter
scope.submitPromise = function() {
console.log($q);
var deferred = $q.defer();
//scope.ngSubmitFunction();
deffered.resolve(scope.ngSubmitFunction());
return deferred.promise;
}
}
The myForm function is a directive construction function to which the AngularJS framework will inject service providers. But the submitPromise function is not injectable; it is a child function of myForm. All injections should be done in the parent function.
Also the code can be simplified by using $q.when to create a promise.
link: function (scope, element, attrs) {
console.log($q);
//scope.submitPromise = function($q) {
//Remove $q as parameter
scope.submitPromise = function() {
console.log($q);
return $q.when(scope.ngSubmitFunction());
}
}
You should solve with this different injection
(function () {
'use strict';
angular
.module('myModule')
.directive('myForm', ['$q', function($q){
return {
. . .
}
}]);
Hope I've been helpful.
The right code is ( I removed parameter from function declaration ):
$scope.submitPromise=function() {
return $q(function (resolve) {//$q is available in function declared in the same scope
$scope.ngSubmitFunction();
});
}
Above code use $q variable from scope ( javascript scope not angular $scope ), $q is visible for all functions declared inside myForm function.
Your previous code used function parameter not $q from scope, parameter was not passed, so was undefined.
Javascript scope means everything between open tag { and close tag }. Check this example:
function(y){//scope start
var x; //scope local variable
var someFunc=function(){
//here is available y and x variables
};
//scope end
}
//outside of scope - here variables x and y not exists
i'm using Angular directives like this:
'use strict';
var eventDirective = {
/**
* Initialize event directive and return the link
* calling all nested methods.
*
*/
init: function($scope, $element) {
var that = this;
return {
link: function(scope) {
scope.$watch('events', function() {
if (scope.events === undefined) {
return;
}
/**
* Every time the user access the event page, this methods
* will be called.
*
*/
__TableSorter__.init($element);
});
},
restrict: 'E'
};
},
__TableSorter__: {
init: function(element) {
console.log(element) // PRINTS ELEMENT
}
}
};
angular
.module('adminApp')
.directive('eventDirective', eventDirective.init.bind(eventDirective));
To illustrate I created this simple example. The TableSorter will run normally.
The problem is when I have several scripts, the code is too large. Is there any way to solve this? Maybe put scripts elsewhere as factories or services ?
My question is how to do this. I tried to inject a service within the directive but was resulting in undefined.
Thanks.
A good way to do should be, when you define your directive, you can set bindToController to true and right your logic inside a controller class. You may inject your services to that controller.
For example.
myModule.directive('directiveName', function factory(injectables) {
var directiveDefinitionObject = {
template: '<div></div>',
scope: {},
controllerAs: 'yourControllerClass',
bindToController: true
};
return directiveDefinitionObject;
});
yourControllerClass is angular controller here.
I know there is a lot of question on this, but i couldn't find my answer in them.
I have a directive for my popups that have many templates,
HTML
<popup template="popupTemplate"></popup>
Directive
app.directive('popup', function () {
return {
restrict: 'E',
scope: {
template: '='
},
link: function ($scope, $element, $attrs) {
// do something on $scope.template
}
}
});
now on another element I define the name of template for the target popup
<button popup-template="upload-avatar"></button>
directive
app.directive('popupTemplate', function () {
return {
link: function ($scope, $element, $attrs) {
$element.bind('click', function () {
$scope.$parent.popupTemplate = $attrs.popupTemplate;
$scope.$apply();
});
}
}
});
the problem:
when I'm clicking on an element inside a nested directive. cause I need to deal with:
$scope.$parent.popupTemplate
$scope.$parent.$parent.popupTemplate
It's not a good idea. I need to know how to access to first parent scope with a unique syntax instead of multiple $parent.
Just wrap popupTemplate:
<popup template="whatever.popupTemplate"></popup>
Then u can:
$scope.whatever.popupTemplate = $attrs.popupTemplate;
Without $parent at all.
Lets say u have parent scope A and child B. By default B copies all values from A. Copies here means coping pointer.
Compare in java:
void bad(String s) {
s = "new";
}
void good(String[] s) {
s[0] = "new";
}
I have a directive in which I pass in an attrs and then it is watched in the directive. Once the attrs is changed, then an animation takes place. My attrs always is undefined when the $watch gets triggered.
App.directive('resize', function($animate) {
return function(scope, element, attrs) {
scope.$watch(attrs.resize, function(newVal) {
if(newVal) {
$animate.addClass(element, 'span8');
}
});
};
});
And here is my test:
describe('resize', function() {
var element, scope;
beforeEach(inject(function($compile, $rootScope) {
var directive = angular.element('<div class="span12" resize="isHidden"></div>');
element = $compile(directive)($rootScope);
$rootScope.$digest();
scope = $rootScope;
}));
it('should change to a span8 after resize', function() {
expect($(element).hasClass('span12')).toBeTruthy();
expect($(element).hasClass('span8')).toBeFalsy();
element.attr('resize', 'true');
element.scope().$apply();
expect($(element).hasClass('span8')).toBeTruthy();
});
});
When the attrs changes, my watchers newValue is undefined and so nothing happens. What do I need to do to make this work? Here is a plunker
You are not watching the value of attrs.resize; you are watching the value pointed by attrs.resize instead, in the test case a scope member called isHidden. This does not exist, thus the undefined.
For what you aare trying to do, the following would work:
App.directive('resize', function($animate) {
return function(scope, element, attrs) {
scope.$watch(
// NOTE THE DIFFERENCE HERE
function() {
return element.attr("resize");
// EDIT: Changed in favor of line above...
// return attrs.resize;
},
function(newVal) {
if(newVal) {
$animate.addClass(element, 'span8');
}
}
);
};
});
EDIT: It seems that the attrs object does NOT get updated from DOM updates for non-interpolated values. So you will have to watch element.attr("resize"). I fear this is not effective though... See forked plunk: http://plnkr.co/edit/iBNpha33e2Xw8CHgWmVx?p=preview
Here is how I was able to make this test work. I am passing in a variable as an attr to the directive. The variable name is isHidden. Here is my test with the updated code that is working.
describe('resize', function() {
var element, scope;
beforeEach(inject(function($compile, $rootScope) {
var directive = angular.element('<div class="span12" resize="isHidden"></div>');
element = $compile(directive)($rootScope);
$rootScope.$digest();
scope = $rootScope;
}));
it('should change to a span8 after resize', function() {
expect($(element).hasClass('span12')).toBeTruthy();
expect($(element).hasClass('span8')).toBeFalsy();
element.scope().isHidden = true;
scope.$apply();
expect($(element).hasClass('span8')).toBeTruthy();
});
});
I am able to access the variable isHidden through the scope that is attached to the element. After I change the variable, the I have to run $digest to update and then all is golden.
I feel that I should probably be using $observe here as was noted by package. I will look at that and add a comment when I get it working.
As Nikos has pointed out the problem is that you're not watching the value of attrs.resize so what you can try doing is this:
Create a variable to hold your data and create these $watch functions:
var dataGetter;
scope.$watch(function () {
return attrs.resize;
}, function (newVal) {
dataGetter = $parse(newVal);
});
scope.$watch(function () {
return dataGetter && dataGetter(scope);
}, function (newVal) {
// Do stuff here
});
What should happen here is that Angular's $parse function should evaluate attrs.resize and return a function like this. Then you pass it the scope and do something. As long as attrs.resize is just a boolean then newVal in the 2nd watch expression should be a boolean, I hope.