watch on clientWidth not firing when changing with css - javascript

TL;DR: If I change the size of a DOM element by css, its directive does not notice this, even if a watch on the size is present.
Goal
I have a couple of custom directives (simple-graph) that each contain an svg. I want the one over which the mouse hovers to increase in size. Attempt:
1) css
simple-graph {height: 100px;}
simple-graph:hover {height: 200px;}
(This part works.)
2) html
<simple-graph ng-repeat="data in datasets" sg-rst="data"></simple-graph>
3) directive
angular.module('myApp').directive('simpleGraph', function () {
return {
restrict: 'E',
scope: {rst: '=sgRst'},
link: function (scope, el, attrs) {
//Add an svg element to el[0], do initialisation, etc. (not shown)
//Also, $watch rst to update graph when data is changed. (not shown)
//Watch size of simple-graph
scope.$watch(function(){
return el[0].clientWidth + el[0].clientHeight;
}, updateSize);
function updateSize() {
//update size of svg element, using el[0].clientHeight and .clientWidth (not shown).
}
}
};
});
Problem / Question
The resize of the simple-graph element, caused by the changing height set in the css file, is not noticed by the scope.$watch in the directive. Question: can I somehow force it to fire??
Current workaround
To the attributes of the simple-graph element (in the html), I've added sg-hover="hover" ng-mouseenter="hover=true" ng-mouseleave="hover=false", which toggles the value of the attribute sg-hover, and
I funnel this change into the directive by changing its scope property to scope: {rst: '=sgRst', hover: '=sgHover'} to its scope, and
I make sure this toggling is noticed by adding a scope.$watch('hover', updateSize).
This works, and shows that the updateSize function does indeed work as wanted. But it's terribly elaborate, and I think there must be a faster way. So, again my question: can I somehow force the $watch function to fire when the element client size is changed through css?
Thanks!

As I commented on the question, I don't think you can achieve what you want with the css (at least not in a simple way)
you could change your directive to something like this, at least to avoid repetition:
return {
restrict: 'E',
scope: {rst: '=sgRst'},
replace: true,
template: "<div ng-mouseover='hover=true' ng-mouseout:'hover=false' class='simple-graph' ng-class='{hover: hover}'><svg></svg></div>"
link: function (scope, el, attrs) {
//create or find the svg element to el[0], do initialisation, etc. (not shown)
//Also, $watch rst to update graph when data is changed. (not shown)
//Watch size of simple-graph
scope.$watch('hover', updateSize);
function updateSize() {
//update size of svg element, using el[0].clientHeight and .clientWidth (not shown).
}
}
Then change your css to be:
.simple-graph .hover {
height: 200px
}

Related

Adding Angular directive using decorator

I'm using Angular 1.4.8 with Angular UI. What I'm trying to do is decorate the ui-sref directive so it will highlight a menu element (by setting the CSS class 'active') if the current $state.name matches the ui-sref state.
I test to see if the element is descendent from my nav header, and if it is, I want to add an ngClass attribute to the element. For right now, I just want to make them all highlight; I can add the test for matching the state later. The true will be replaced with the actual test. Right now I just want the active class set
.config(['$provide', ($provide: angular.auto.IProvideService) => {
return $provide.decorator('uiSrefDirective', [
'$delegate', ($delegate) => {
var originalUiSref = $delegate[0];
var originalUiSrefLink = originalUiSref.link;
originalUiSref.compile = () => (scope, element, attrs, uiSref) => {
var topBar = $('nav.top-bar');
if (topBar.length > 0 && $.contains(topBar[0], element[0])) {
element.parent().attr('ng-class', '{ active: true }');
}
originalUiSrefLink.call($delegate, scope, element, attrs, uiSref);
};
return $delegate;
}
]);
}])
The original DOM element:
<a ui-sref="requests">Requests</a>
After adding the decorator, this is what I see in my browser's DOM:
<a ui-sref="requests" ng-class="{ active: true }" href="/requests">Requests</a>
Great! I can see the added attribute in the DOM, but my browser is ignoring it. It's almost as though it's getting added after Angular scans the DOM for directives. If I change the code to:
element.parent().addClass('active');
... then it works fine. What am I doing wrong?
Amy you do not need to implement this directive as angular-ui in fact already have it, please check ui-sref-active

How to deal with css and custom directive in angulajs

Consider a custom directive in angularjs:
.directive('myDir', function() {
return {
restrict: 'E',
template: "..."
}
})
As far as I see the tag <my-dir></myDir> has no default style associated (or, at least, it is not a block tag). Now I want to style it and place in the right point of my page. I have 2 alternatives:
1) Use this template:
.directive('myDir', function() {
return {
restrict: 'E',
template: "<div class="layout">...</div>",
}
})
And css:
.layout {
/* bla bla bla */
}
But this introduce a new unnecessary level in the DOM three, since if I wrote something like <my-dir class="layout"></my-dir> with the proper css attached it would have worked anyway BUT I'll have to remember to add the same css class every time I use <my-dir> inside my code (and this is not DRY).
2) This led me to add style inside post-link function:
.directive('myDir', function() {
return {
restrict: 'E',
template: "...",
link: function(scope, element, attrs) {
element.addClass('layout');
}
}
})
Which strategy is better? Are there pros or cons I can't see?
UPDATE:
Using replace: true in directive definition is not an option, since it has been deprecated and when using bootstrap things like <my-dir class="visible-xs"></my-dir> may be useful.
You could solve this problem by having replace: true option in your directive, which will basically replace your directive DOM with the template which you have given from your directive.
Directive
.directive('myDir', function() {
return {
restrict: 'E',
template: "<div class="layout">...</div>",
replace: true //added replace true property
}
})
Resultant HTML
<div class="layout">..Content..</div>
Like above I shown, you could get rid off the unnecessary HTML tag by simply using replace option of directive.
Demo Plunkr
Note
As per Angular Docs replace option has been deprecated in Angular2,
but in your case you can avoid that.
But if you really wanted to go avoid the use of replace option of directive then I'd suggest you to use your directive as an attribute instead of going for an element but then you need to take <div class="layout"> outside and then your could put your directive over that like <div class="layout" my-dir>. But if you again think over it in Angular2 perspective, you are changing your component structure to old angular way using attribute/class

Changing text based on width of nested directive Angular

So I have an ng-repeat that loops over data objects and displays it in a list directive. One of the objects in the list should use the abbreviated version when the length of the text element is equal to or exceeds the length of the container. So what I have is this:
//this is the list directive
<shift-list selection="selection" shifts="shifts"></shift-list>
//this is the template of the list directive that adds site-base-name directive
<ion-item collection-repeat="shift in shifts">
<div class="item-text">
<site-base-name shift="shift"></site-base-name>
</div>
//and here is what my site-base-name directive looks like
return {
restrict: 'E',
link: function(scope, element) {
$timeout(function(){
scope.siteBaseWidth = element[0].offsetWidth;
scope.parentWidth = element.parent()[0].offsetWidth;
});
},
replace: true,
scope: {
shift: '='
},
template: ''+
'<div class="sitebase">{{(siteBaseWidth + 20 >= parentWidth) && shift.sites_abbreviation || shift.sites_name }} : {{shift.bases_name}}</div>'
}
}
So what I'm doing is nesting the directive site-base-name inside the list directive and then finding the size of the element and its parent. Then I'm using the abbreviated version of the name (shift.sites_abbreviation) if the size exceeds my condition (siteBaseWidth + 20 >= parentWidth). The problem with this is the behavior I'm getting is buggy and inconsistent. It also applies the changes after the DOM is loaded, so you can see the text change within the window.
My question is what is the best way to find the width of the text element and parent element then apply a condition that decides what data to populate the binding with? Preferably a clean Angular solution.
Relying on the element offsetWidth is not very clean, as if forces you to wait for the DOM to be updated (hence your $timeout and the flickering observed).
A cheap way to solve your issue would be to initially display your text with an opacity of 0.01 (so it is not visible, but still takes some space), and with your site-base-name directive, once you know its size and adjust the text, you can set its opacity back to 1.
Now a better solution would be to get rid of this $timeout and offsetWidth. If you use a monospace font, you could just calculate thestring.length times pixelsPerCharacter, before even it is displayed.

How can I use a directive wrapper for ng-grid with the grid options determined by a directive attribute?

I am attempting to create a re-usable directive wrapper for ng-grid where I can apply the location of the ng-grid options through the use of an attribute.
Here is the skeleton of the code which gets very close to what I want:
angular.module('myApp').directive('grid', function() {
return {
restrict: 'E',
template: '<div class="gridStyle" ng-grid="someObject.gridOptions"></div>',
link: function(scope, element, attributes) {
// no code here necessary, to use the hard-coded ng-grid options
}
}
}]);
What I would like to do is supply the variable for ng-grid by using an attribute, similar to this:
<grid dataLocation="someObject.gridOptions"></grid>
I've tried using the compile and the link options with multiple methods, including reading the attributes, and then using the element.html() method to update the html to set the ng-grid attribute to "someObject.gridOptions", as well as using {{someScopeVariable}}, and setting scope.someScopeVariable to "someObject.gridOptions" in the linking function. I have verified using the chrome's html inspector that the div's attribute looks correct, but I have not been able to get the item to show up on my page.
I suspect I'm running into issues since the ng-grid is already a compiled directive. Is there any way I can pull this off? I've tried a large number of compile and linking methods with no success yet.
To access the original template's attributes, the template option of directive should be defined as a function as follows:
angular.module('myApp').directive('grid', function() {
return {
restrict: 'E',
template: function(element, attrs){
return '<div ng-grid="' + attrs.dataLocation + '"></div>';
}
};
});
Your HTML stays the same:
<grid dataLocation="someObject.gridOptions"></grid>

Angular directive $observe jquery widget initialization issue

I made a directive that wraps freebases jquery search widget. I want the user to be able to change the search language on the fly so I'm using $observe to watch the 'lang' attribute.
The issue is the plugin reinitialized every time the attribute changes but does not end the execution of the previous initialization. When the user selects terms, I'm adding it to an array. If the lang attribute has been modified, the selected term is added for every change to the attribute.
I could just filter the array and only allow items with the same ID to appear once, but I'm wondering if there is a better solution.
See plunker here.
The directive:
directive('suggest', function() {
return {
restrict: 'E',
template: "<input type='text'>",
replace:true,
scope:{onSelect:'&'},
link: function(scope, element, attrs) {
attrs.$observe('lang', function(value) {
$(element).suggest({
lang: value
})
.bind("fb-select", function(e, info) {
console.log(info);
scope.onSelect({data:info});
scope.$apply();
});
});
}
};
});

Categories

Resources