Get width after data binding in directive - angularjs

In directive, I write an element with ngRepeat, and I need to get width after data binding.
I try to use post in link, but it also can't work.
How can I get the width in directive?

The width isn't available in link as the element isn't part of the DOM yet. You can however watch for any change of the width and handle the change once it occurs.
scope.watch(function() {return element.style.width}, function() {
// handle width change here
});

compile: function () {
return {
pre: function ($scope, iElement, iAttrs) {
function getWidth() {
return iElement.width();
}
}
$scope.$watch(getWidth, function (newWidth) {
...
});
}
}

Related

AngularJS directive doesn't update scope value even with apply

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;
}
})
}
};
}

addClass() doesn't work with getElementById in AngularJS

I'm trying to manipulate an element class within a directive. The directive is of a toolbar and it's supposed to add a class to 2 elements after some scroll.
The element directive itseld;
The view, to add/remove margin;
This is my html structure:
<ag-toolbar class="ag-toolbar--sec"></ag-toolbar>
<div ui-view="app" autoscroll="false" id="appView"></div>
And this is my directive:
function agToolbar($window) {
return {
restrict: 'E',
link: function(scope, element, attrs) {
var elView;
setTimeout(function(){
elView = document.getElementById("appView");
}, 400);
angular.element($window).bind("scroll", function() {
if (this.pageYOffset >= 128) {
element.addClass('scroll');
elView.addClass('agMargin');
} else {
element.removeClass('scroll');
elView.removeClass('agMargin');
};
});
}
};
}
In the console I keep getting the error:
elView.addClass is not a function
elView.removeClass is not a function
But the element.addClass is working fine. Any ideas why?
addClass belongs to jqLite (or jQuery if available), see https://docs.angularjs.org/api/ng/function/angular.element.
That is, you need to wrap the DOM element in a jqLite/jQuery element:
elView = angular.element(document.getElementById("appView"));

modifying ng-class from directive

I'm trying to write a directive that watches an element's width, and conditionally sets an ng-class variable. Not quite sure if I'm going about this the right way and would appreciate some help! My html
<div ng-class="{'compact' : compact}" calc-width>stuff here</div>
My directive (so far)
app.directive('calcWidth', function() {
return {
restrict: 'A',
link: function(scope, elem, attrs) {
var width = elem[0].clientWidth;
scope.compact = false;
if (width < 600) {
scope.compact = true;
}
},
};
});
Firstly, this isn't modifying the ng-class variable as expected. Secondly, how do I go about 'watching' for changes in the element's width? Normally I could just use the window resize function, but the element's size also changes in other cases such as side panels opening. Should I even be doing this? Or would it be best to somehow trigger this directive from another directive/controller/service which controls the panels?
You need to wrap the contents of your link function inside a $watch function.
link: function(scope, elem, attrs) {
scope.$watch(function () { // this function will be executed for each digest cycle.
var width = elem[0].clientWidth;
scope.compact = false;
if (width < 600) {
scope.compact = true;
}
})
}

When do we know the actual width of an element in Angular?

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');
});

Stuck creating a custom css style directive

For an only visual editor I'm trying to create a new directive that writes a CSS style. I'm stuck at trying to get the directive to update when a checkbox is clicked to make the background-color property transparent.
Here's my (non-working) directive:
myApp.directive('customstyle', function () {
return {
restrict: 'E',
link: function (scope, element, attrs) {
var bgColor;
scope.$watch(attrs.myTransparent, function (value) {
if (value) {
bgColor = 'transparent';
} else {
bgColor = attrs.myBgcolor;
}
updateStyle();
}, true);
function updateStyle() {
var htmlText = '<style>.' + attrs.myClass + '{';
htmlText += 'background-color: ' + bgColor + ';';
htmlText += "}</style>";
element.replaceWith(htmlText);
}
updateStyle();
}
}
});
and html element:
<customstyle my-class="examplediv" my-transparent="settings.Window.Transparent" my-bgcolor="settings.Window.BackgroundColor"></customstyle>
Here's a jsfiddle of the situation: http://jsfiddle.net/psinke/jYQc6/
Any help would be greatly appreciated.
Try using the directive directly on the element you want to change, it's easier to do and to maintain.
HTML:
<div class="examplediv customstyle"
my-transparent="settings.Window.Transparent"
my-bgcolor="{{settings.Window.BackgroundColor}}">
</div>
Note: Use {{settings.Window.BackgroundColor}} to pass the property's value and not a String.
Directive:
myApp.directive('customstyle', function () {
return {
restrict: 'AC',
link: function (scope, element, attrs) {
scope.$watch(attrs.myTransparent, function (value) {
element.css('background-color', (value ? 'transparent' : attrs.myBgcolor));
});
}
}
});
Note: Use element.css() to change CSS properties directly on the element.
jsFiddler: http://jsfiddle.net/jYQc6/8/
I was having the same problem and using bmleite's solution solved it. I had a custom element with a custom attribute very similar to the one above, and changing the directive to be applied on a regular DIV instead of the custom attribute fixed it for me.
In my solution I also have the following line of code right after the element has been modified:
$compile(element.contents())(scope);
Remember to inject the $compile service in the directive function declaration:
myApp.directive('directiveName', function ($compile) { ...
Thanks for a great post!

Resources