I am newer to AngularJS and having an issue that I hope someone can point me in the right direction to figuring out. I have created a directive called sizeWatcher which is placed as an attribute into the HTML which essentially just gets the height of the element it's placed on and echos that height into a scope variable named style that I set onto another element through the use of ng-style="style".
I'm finding that whenever I open my accordion, the $watch fires on the directive but it's firing multiple times. I have a console.log in my $watch and am seeing 3 log entries, the first 2 are the same (guessing this happens on click before the accordion opens, and then the accordion opens and the 3rd log entry is the final height after the accordion is opened). The main issue is that the style variable is only getting set to the smaller heights before the accordion is expanded even though the log is registering the greater height as the last time the directive is hit -- How can I ignore the first $watch event firings and only act accordingly on the last and final run-through of the directive? Any insight into this would be greatly appreciated. Relevant code attached below:
TEMPLATE:
<div class="content-wrap" id="height-block" ng-style="style">
<!-- Other HTML etc... -->
<uib-accordion size-watcher close-others="oneAtATime">
<!-- Accordion Directive HTML.. -->
</uib-accordion>
</div>
JavaScript:
.directive("sizeWatcher", function () { //add size-watcher attribute to element on the page to have it echo its' height to the {{style}} scope
function link(scope, element, attrs) {
scope.$watch(function () { //watch element for changes
var height = element[0].offsetHeight;
console.log(height);
if (height > 150) {
scope.style = {
height: height + 'px'
};
}
});
}
return {
restrict: "AE", //attribute & element declarations
link: link
};
})
How can I ignore the first $watch event firings and only act
accordingly on the last and final run-through of the directive?
You can ignore watcher when new or old values are undefined and not equal to each other:
$scope.$watch(function () {
return element.height(); // or something else
},
function (newVal, oldVal) {
if (newVal !== undefined && oldVal !== undefined && newVal !== oldVal) {
// your stuff
if (newVal > 150) {
scope.style = {
height: newVal + 'px'
};
}
}
});
Anyways you can play with if statement regards to your needs
FYI, to improve performance $watch returns cancel callback so you can stop watcher whenever you want:
var cancelWatch = $scope.$watch(function () {
return element.height();
},
function (newVal, oldVal) {
if (<some condition>) {
cancelWatch();
}
});
Obviously an answer to this needs a link to the Angular-Documentation for $watch ;)
it states the following:
After a watcher is registered with the scope, the listener fn is called asynchronously (via $evalAsync) to initialize the watcher. In rare cases, this is undesirable because the listener is called when the result of watchExpression didn't change. To detect this scenario within the listener fn, you can compare the newVal and oldVal. If these two values are identical (===) then the listener was called due to initialization.
which probably explains your first call.
I'm guessing the second call happens because the accordion is rerendered after initialization (with a title/ or label or anything) which triggers the $digest and thus the $watch expression on the height.
Finally the third call happens when you open the accordion and the height actually changes.
To fix this you can compare the newValue and oldValue of the watched expression like Maxim Shoustin said in his answer. Here is an example (again taken from the Angular-docs)
scope.$watch(
// This function returns the value being watched. It is called for each turn of the $digest loop
function() { return food; },
// This is the change listener, called when the value returned from the above function changes
function(newValue, oldValue) {
if ( newValue !== oldValue ) {
// Only increment the counter if the value changed
scope.foodCounter = scope.foodCounter + 1;
}
}
);
However if you actually want to change the style of the element you might want to take a look into ng-class instead of manually registering any watchers!
This is happening because you are not using $watch correct way,
The first parameter to $watch is a variable which you want to watch(this can be a callback).
The second parameter to $watch is a callback which performs the desired action on change
So in your case it would be something like this
scope.$watch(
function () {
return element[0].offsetHeight;
},
function () { //watch element for changes
var height = element[0].offsetHeight;
console.log(height);
if (height > 150) {
scope.style = {
height: height + 'px'
};
}
}
)
Please notice the first function, so whenever the value it is returning changes, the second callback will execute
Hope this helps you
Related
I'm usin a directive to show a div on the screen only when the screen size is smaller than 600px. The problem is, the scope value isn't being updated, even using $apply() inside the directive.
This is the code:
function showBlock($window,$timeout) {
return {
restrict: 'A',
scope: true,
link: function(scope, element, attrs) {
scope.isBlock = false;
checkScreen();
function checkScreen() {
var wid = $window.innerWidth;
if (wid <= 600) {
if(!scope.isBlock) {
$timeout(function() {
scope.isBlock = true;
scope.$apply();
}, 100);
};
} else if (wid > 600) {
if(scope.isBlock) {
$timeout(function() {
scope.isBlock = false;
scope.$apply();
}, 100);
};
};
};
angular.element($window).bind('resize', function(){
checkScreen();
});
}
};
}
html:
<div ng-if="isBlock" show-block>
//..conent to show
</div>
<div ng-if="!isBlock" show-block>
//..other conent to show
</div>
Note: If I don't use $timeout I'll get the error
$digest already in progress
I used console logs inside to check if it's updating the value, and inside the directive everything works fine. But the changes doesn't go to the view. The block doesn't show.
You should use do rule in such cases to get the advantage of Prototypal Inheritance of AngularJS.
Basically you need to create a object, that will will have various property. Like in your case you could have $scope.model = {} and then place isBlock property inside it. So that when you are inside your directive, you will get access to parent scope. The reason behind it is, you are having scope: true, which says that the which has been created in directive is prototypically inherited from parent scope. That means all the reference type objects are available in your child scope.
Markup
<div ng-if="model.isBlock" show-block>
//..conent to show
</div>
<div ng-if="!model.isBlock" show-block>
//..other conent to show
</div>
Controller
app.controller('myCtrl', function($scope){
//your controller code here
//here you can have object defined here so that it can have properties in it
//and child scope will get access to it.
$scope.model = {}; //this is must to use dot rule,
//instead of toggle property here you could do it from directive too
$scope.isBlock = false; //just for demonstration purpose
});
and then inside your directive you should use scope.model.isBlock instead of scope.isBlock
Update
As you are using controllerAs pattern inside your code, you need to use scope.ag.model.isBlock. which will provide you an access to get that scope variable value inside your directive.
Basically you can get the parent controller value(used controllerAs pattern) make available controller value inside the child one. You can find object with your controller alias inside the $scope. Like here you have created ag as controller alias, so you need to do scope.ag.model to get the model value inside directive link function.
NOTE
You don't need to use $apply with $timeout, which may throw an error $apply in progress, so $timeout will run digest for you, you don't need to worry about to run digest.
Demo Here
I suspect it has something to do with the fact that the show-block directive wouldn't be fired if ng-if="isBlock" is never true, so it would never register the resize event.
In my experience linear code never works well with dynamic DOM properties such as window sizing. With code that is looking for screens size you need to put that in some sort of event / DOM observer e.g. in angular I'd use a $watch to observe the the dimensions. So to fix this you need to place you code in a $watch e.g below. I have not tested this code, just directional. You can watch $window.innerWidth or you can watch $element e.g. body depending on your objective. I say this as screens will be all over the place but if you control a DOM element, such as, body you have better control. also I've not use $timeout for brevity sake.
// watch window width
showBlock.$inject = ['$window'];
function bodyOverflow($window) {
var isBlock = false;
return {
restrict: 'EA',
link: function ($scope, element, attrs) {
$scope.$watch($window.innerWidth, function (newWidth, oldWidth) {
if (newWidth !== oldWidth) {
return isBlock = newWidth <= 600;
}
})
}
};
}
// OR watch element width
showBlock.$inject = [];
function bodyOverflow() {
var isBlock = false;
return {
restrict: 'EA',
link: function ($scope, element, attrs) {
$scope.$watch($element, function (new, old) {
if (newWidth) {
return isBlock = newWidth[0].offsetWidth <= 600;
}
})
}
};
}
I'm trying to implement the "infinite-scroll" feature in a list using a directive, which should load progressively a new set of "orders" when the scroll of the html element reaches or exceeds 75% of the scrollable height and append it to the existing list.
Unfortunately, the watcher doesn't trigger when i scroll the list.
The directive is located in the right tag and the watcher triggers the listener function only the first time, when the element is rendered by the browser.
The strange thing is that if i change path and then i return to the path where the list is, the watcher start behaving correctly and trigger the listener function everytime i perform a scroll.
<ol orders-loader class="orders-list">...</ol>
angular:
(function () {
angular.
module('myApp')
.directive('ordersLoader', ['$window', '$timeout', 'ordersResource', ordersloaderDirective])
function ordersloaderDirective($window, $timeout, loading, ordersResource) {
return {
restrict: 'A',
link: function (scope, element, attrs) {
scope.orders = ordersResource; /*ordersResource use $resource to api calls
and then stocks the data in a array exposed in the scope*/
$timeout(function () {
scope.$watch(function () { return element[0].scrollTop }, function () {
if (*the scroll exceedes more or less 75% of the total scrollHeight*/) {
/*asking for more orders*/
}
});
}, 0);
}
}
}
I can't figure out where is the problem.
Solved
As yeouuu suggested, there was no digest cycle after the list scroll event, so i added:
element.bind('scroll', function () {
scope.$apply();
});
just before the $timeout function.
Whenever using plugins outside of angularJs that should trigger watcher you need to explicitly apply them. Otherwise Angular won't be aware of these changes/events.
In your case that means adding scope.$apply(); after the event.
Your edited solution:
element.bind('scroll', function () {
scope.$apply();
});
More information can be found here about the scope life cycle: https://docs.angularjs.org/guide/scope#scope-life-cycle
First, this is a demo on plunk: plunk
Code:
html:
<counter></counter>
<hr>
<counter2></counter2>
js:
directiveSlideShare.directive('counter', function() {
return {
restrict: "E",
template: 'assign 0 to counter<button>increse counter</button><div>{{counter}}</div>',
link: function(scope, ele, attrs) {
scope.assignCounter = function() {
scope.counter = 1;
}
ele.find('button').on('click', function() {
if (!scope.counter) {
alert("please click 'assign' button first");
return;
}
console.log(scope.counter);
// this will never being applied
scope.counter++;
});
}
};
});
directiveSlideShare.directive('counter2', function() {
return {
template: 'test: <input ng-model="test" type="text" ng-focus="noticeUser()"><span>{{noticeStr}}</span><div>value of test(will update when blur): <span id="domEvil"></span></div>',
link: function(scope, ele, attrs) {
var count = 0;
scope.noticeUser = function() {
scope.noticeStr = "count" + (!count ? "" : (" +" + count));
count++;
};
ele.find('input').on('change', function() {
document.querySelector('#domEvil').textContent = scope.test;
// This will being applied at first time???
scope.noticeStr = '';
console.warn(scope.noticeStr);
});
}
};
});
In counter, first I click assign ... button to assign 1 to scope.counter, and then I click increase.. button to increase scope.counter, as expected, the view will not being updated.
Then I focus the test input, this will trigger $digest to run, I guess so(appreciate if anyone can explain the real thing happen here ^_^), then the view of scope.counter is being updated. And at the same time, noticeUser will be called. noticeStr will be assigned and update its view.
After I enter something and then blur, the test input will trigger change event, and the function will be called which assign empty string to scope.noticeStr, this will update the view this time, and become empty. why?? Then when you repeat these(focus the input, enter something, blur the input), the view of scope.noticeStr will not be updated even though its value being changed which is expected.
When you are adding the 'onClick' or 'onChange' handler, you are using the DOM events and not telling Angular to 'watch' for these events and thus whatever happens inside these handlers is not part of the '$digest' cycle.
Instead, use ng-click and ng-change to do what you want, like in this modified plunker - http://plnkr.co/edit/1dLYWZJ97iYyGDwBhXSg?p=preview
The reason for the update in 'why' of your question is because of the ng-focus thats on your input. Thats triggering a digest cycle that then updates the view on the first to the currently incremented value.
My goal is to create a directive that can be applied to a given element, so that when the element is clicked, a modal is created. I would like to have the modal created and appended to the body node, which is outside of my ng-app element. Due to requirements of more than one app on a page, I can't put ng-app on the <html> or <body> tags. Yet for proper z positioning, I would to place the modal element as high up in the body as I can.
My directive looks like this:
var module = angular.module('Test', ['ngAnimate']);
module.directive('modal', function($compile, $animate) {
function link(scope, element, attr) {
element.on('click', function () {
var modal = $compile('<div class="modal"></div>')(scope);
scope.$apply(function () {
$animate.enter(modal, angular.element(document.body));
});
});
}
return {
link: link,
scope: {}
};
});
When I use $animate.enter to append the modal to the body, it is appended but the animation does not run. My HTML looks like this:
<body>
<div ng-app="Test">
<button modal>Open Modal</button>
</div>
</body>
If I move the ng-app from the div to the body, then the animation works. But I can't do this because I need to have the option of placing more than one ng-app on a given page.
Is it possible?
Working (or not-working) example:
http://plnkr.co/edit/vUi2PmLjea36nrJ9i3R2?p=preview
The short answer is: No you can't.
(At least Angular seems not to be designed to allow it).
The somewhat longer answer is: No, you can't. Here is why:
The reason is how $animate is currently implemented:
In your case, it adds the elements= to the DOM (to document.body in particular) and then checks to see if it should proceed with the animation specific stuff (or if animations are "disabled").
According to the source code, the function that checks if animations are "disabled" is:
function animationsDisabled(element, parentElement) {
if (rootAnimateState.disabled) return true;
if(isMatchingElement(element, $rootElement)) {
return rootAnimateState.disabled || rootAnimateState.running;
}
do {
//the element did not reach the root element which means that it
//is not apart of the DOM. Therefore there is no reason to do
//any animations on it
if(parentElement.length === 0) break;
var isRoot = isMatchingElement(parentElement, $rootElement);
var state = isRoot ? rootAnimateState : parentElement.data(NG_ANIMATE_STATE);
var result = state && (!!state.disabled || state.running || state.totalActive > 0);
if(isRoot || result) {
return result;
}
if(isRoot) return true;
}
while(parentElement = parentElement.parent());
return true;
}
As you can see, since your modal is not a child (but a sibling) of the $rootElement, isRoot will always be false and the do-while loop will run until there is no parent-element. At that point if(parentElement.length === 0) break; will break the loop and the function will return true, thus cancelling the animation.
The longest answer is: Depends (on how badly you need it).
The available options (I can think of) are:
Accept your fate and find some other way (besides $animate) to perform your animations.
Create your own fork of angular-animate and change one line of code, so that animations are not cancelled even if the target-element is not a child of the $rootElement.
If you like to live dangerously: What if we "temporarily swapped the $rootElement" ?
I tried various approaches (which outside of the scope of this answer) and the only one I could make work(?), was swapping the HTML element associated with the jqLite object (i.e. $rootElement[0]).
I wrapped the functionality in a service (along with some convenience features, e.g. restoring the original element after a set period of time):
module.factory('myRootElement', function ($rootElement, $timeout) {
/* Save the original element for reference */
var original = $rootElement[0];
/* Fake the $rootElement for the specified
* period of time (in milliseconds) */
function fakeForMillis(millis) {
$rootElement[0] = document.body;
$timeout(function () {
$rootElement[0] = original;
}, millis || 0);
}
/* Return the Service object */
return {
fakeForMillis: fakeForMillis
};
});
Finally, you only need to temporarily swap the $rootElement fo the animation to take place. (Unfortunately, you have to specify the time required for the animation, but I bet there are better ways to find it out programmatically - again outside the scope of this answer.)
myRootElement.fakeForMillis(1000);
$animate.enter(modal, angular.element(document.body));
See, also, this short demo.
I have no idea what I am talking about and I have by no means investigated the consequences of this approach, so use at your own risk and don't be surprised if strange things come your way.
I'm tring to create a directive that will center a div.
So far, I have this code:
app.directive("setcenter", function () {
return {
scope:{
setcenter: '='
},
link: function (scope, element, attrs) {
scope.$watch('setcenter', function (newValue) {
if (newValue == true) {
var width = element.width();
element.css('position', 'absolute');
element.css('top', '80px');
element.css('left', '50%');
element.css('z-index', '200');
element.css('margin-left', '-' + width / 2 + 'px');
}
});
}
}
});
The problem is the width of the element. The whole point for this directive is that the div that uses this directive, don't have a width set. I want this directive to figure out the width and center the div.
The problem I encounter is that when the directive is invoked, the actual width of the div is not yet known. When I use this in my situation, the div is 800px, but when the page is finished loading, the div is 221px.
So, what can I do to wait till the actual width is known of the div?
First, I only have used this logic when I defined a controller for a directive rather than a link function. So defining it in a link function instead may cause different behavior, but I suggest you try it there first and if you can't get it to work then switch to using a controller.
As far as I can tell, the only change you would need to make this work would be to change $scope to scope calls and $element to element since the dependency injected objects become standard link function parameters.
$scope.getElementDimensions = function () {
return { 'h': $element.height(), 'w': $element.width() };
};
$scope.$watch($scope.getElementDimensions, function (newValue, oldValue) {
//<<perform your logic here using newValue.w and set your variables on the scope>>
}, true);
$element.bind('resize', function () {
$scope.$apply();
});
The idea for this usage came to me after reading a very similar type of usage about watching the $window rather than the current element, the original work can be found here.
Angular.js: set element height on page load
James' answer led me to:
app.directive('measureInto', function () {
return {
restrict: 'A',
link: function (scope, element, attrs) {
scope.$watch(function() {
return element[0].clientWidth;
}, function(value){
scope[attrs.measureInto] = element[0].clientWidth + 10;
});
}
};
});
So, at runtime, I add this and assign into whatever scope variable I want the width of the element I'm looking for
I had a similar issue and found that the dimensions were reliably correct when all the ng-ifs (or anything else using ngAnimate) on the page had been resolved - it's possible something similar is happening here. If so, this would do the trick without adding any new listeners:
$scope.tryGetElementDimensions = function () {
if (!angular.element("your-element") || ((angular.element("your-element")[0].classList).contains("ng-animate")
$timeout(function() {
$scope.tryGetElementDimensions()
})
}
else {
$scope.getElementDimensions()
}
$scope.getElementDimensions = function (){
//whatever you actually wanted to do
}
link: function (scope, element, attrs) {
$scope.tryGetElementDimensions()
}
Angular adds ng-animate and ng-enter, ng-leave classes while it's animating and you can be confident it's finished when these classes have all been removed. $timeout without a second argument just waits for the next digest.
Can't comment yet, therefore this answer.
Found a similar solution like the one strom2357 is suggesting. $timeout works really well to let you know when the dom is ready, and it is super simple. I am using this solution to get the ui-view element width. Found it in a fiddle.
var app = angular.module('app', []);
app.controller('MyController', function($timeout, $scope){
$timeout(function(){
//This is where you would get width or height of an element
alert('DOM ready');
});
alert('DOM not ready');
});