Access Element Style from Angular directive - angularjs

I'm sure this is going to be a "dont do that!" but I am trying to display the style on an angular element.
<div ng-repeat="x in ['blue', 'green']" class="{{x}}">
<h3 insert-style>{{theStyle['background-color']}}</h3>
</div>
Result would be
<div class='blue'><h3>blue(psudeo code hex code)</h3></div>
<div class='green'><h3>green(psudeo code hex code)</h3></div>
I basically need to get the style attributes and display them.
Directive Code...
directives.insertStyle = [ function(){
return {
link: function(scope, element, attrs) {
scope.theStyle = window.getComputedStyle(element[0], null);
}
}
}];
Fiddle example: http://jsfiddle.net/ncapito/G33PE/

My final solution (using a single prop didn't work, but when I use the whole obj it works fine)...
Markup
<div insert-style class="box blue">
<h4 > {{ theStyle['color'] | toHex}} </h4>
</div>
Directive
directives.insertStyle = [ "$window", function($window){
return {
link: function(scope, element, attrs) {
var elementStyleMap = $window.getComputedStyle(element[0], null);
scope.theStyle = elementStyleMap
}
}
}];

Eureka!
http://jsfiddle.net/G33PE/5/
var leanwxApp = angular.module('LeanwxApp', [], function () {});
var controllers = {};
var directives = {};
directives.insertStyle = [ function(){
return {
link: function(scope, element, attrs) {
scope.theStyle = window.getComputedStyle(element[0].parentElement, null)
}
}
}];
leanwxApp.controller(controllers);
leanwxApp.directive(directives);
So that just took lots of persistence and guessing. Perhaps the timeout is unnecessary but while debugging it seemed I only got the style value from the parent after the timeout occurred.
Also I'm not sure why but I had to go up to the parentElement to get the style (even though it would realistically be inherited (shrug)?)
Updated fiddle again
Did one without the timeout but just looking at the parentElement for the style and it seems to still work, so scratch the suspicions about the style not being available at all, it's just not available where I would expect it.
Also holy cow there are a lot of ways to debug in Chrome:
https://developers.google.com/chrome-developer-tools/docs/javascript-debugging
I used
debugger;
statements in the code to drop in breakpoints without having to search all the fiddle files.
One more quick update
The code below comes out of Boostrap-UI from the AngularUI team and claims to provide a means to watch the appropriate events (haven't tried this but it looks like it should help).
http://angular-ui.github.io/bootstrap/
/**
* $transition service provides a consistent interface to trigger CSS 3 transitions and to be informed when they complete.
* #param {DOMElement} element The DOMElement that will be animated.
* #param {string|object|function} trigger The thing that will cause the transition to start:
* - As a string, it represents the css class to be added to the element.
* - As an object, it represents a hash of style attributes to be applied to the element.
* - As a function, it represents a function to be called that will cause the transition to occur.
* #return {Promise} A promise that is resolved when the transition finishes.
*/
.factory('$transition', ['$q', '$timeout', '$rootScope', function($q, $timeout, $rootScope) {
var $transition = function(element, trigger, options) {
options = options || {};
var deferred = $q.defer();
var endEventName = $transition[options.animation ? "animationEndEventName" : "transitionEndEventName"];
var transitionEndHandler = function(event) {
$rootScope.$apply(function() {
element.unbind(endEventName, transitionEndHandler);
deferred.resolve(element);
});
};
if (endEventName) {
element.bind(endEventName, transitionEndHandler);
}
// Wrap in a timeout to allow the browser time to update the DOM before the transition is to occur
$timeout(function() {
if ( angular.isString(trigger) ) {
element.addClass(trigger);
} else if ( angular.isFunction(trigger) ) {
trigger(element);
} else if ( angular.isObject(trigger) ) {
element.css(trigger);
}
//If browser does not support transitions, instantly resolve
if ( !endEventName ) {
deferred.resolve(element);
}
});
// Add our custom cancel function to the promise that is returned
// We can call this if we are about to run a new transition, which we know will prevent this transition from ending,
// i.e. it will therefore never raise a transitionEnd event for that transition
deferred.promise.cancel = function() {
if ( endEventName ) {
element.unbind(endEventName, transitionEndHandler);
}
deferred.reject('Transition cancelled');
};
return deferred.promise;
};
// Work out the name of the transitionEnd event
var transElement = document.createElement('trans');
var transitionEndEventNames = {
'WebkitTransition': 'webkitTransitionEnd',
'MozTransition': 'transitionend',
'OTransition': 'oTransitionEnd',
'transition': 'transitionend'
};
var animationEndEventNames = {
'WebkitTransition': 'webkitAnimationEnd',
'MozTransition': 'animationend',
'OTransition': 'oAnimationEnd',
'transition': 'animationend'
};
function findEndEventName(endEventNames) {
for (var name in endEventNames){
if (transElement.style[name] !== undefined) {
return endEventNames[name];
}
}
}
$transition.transitionEndEventName = findEndEventName(transitionEndEventNames);
$transition.animationEndEventName = findEndEventName(animationEndEventNames);
return $transition;
}]);

The issue you'll face is that getComputedStyle is considered a very slow running method, so you will run into performance issues if using that, especially if you want angularjs to update the view whenever getComputedStyle changes.
Also, getComputedStyle will resolve every single style declaration possible, which i think will not be very useful. So i think a method to reduce the number of possible style is needed.
Definitely consider this an anti-pattern, but if you still insist in this foolishness:
module.directive('getStyleProperty', function($window){
return {
//Child scope so properties are not leaked to parent
scope : true,
link : function(scope, element, attr){
//A map of styles you are interested in
var styleProperties = ['text', 'border'];
scope.$watch(function(){
//A watch function to get the styles
//Since this runs every single time there is an angularjs loop, this would not be a very performant way to do this
var obj = {};
var computedStyle = $window.getComputedStyle(element[0]);
angular.forEach(styleProperties, function(value){
obj[value] = computedStyle.getPropertyValue(value);
});
return obj;
}, function(newValue){
scope.theStyle = newValue;
});
}
}
});

This solution works if you don't HAVE to have the directive on the child element. If you just place the declaration on the ng-repeat element itself, your solution works:
<div insert-style ng-repeat="x in ['blue', 'green']" class="{{x}}">
Fiddle

Related

How can you detect when HTML rendering is completed in AngularJS

I've done extensive research on this subject, but no matter what I do, I find it extremely difficult to achieve this objective.
I want to execute code when all elements have been fully rendered in AngularJS web application. I think I found solution suggesting to use routers and views, but I could not make that work on my case, as it seems it requires certain configuration.
When you have ng-repeat and a lot of nested directives that will generate HTML/Content based on various conditions using ng-if, I noticed that HTML rendering continues even after document ready event is fired or view content have been loaded ie $viewContentLoaded event is triggered.
The closest idea I have is to use $watch over the length of the children of the element of a given directive. Every time the $watch is executed, increment counter renderCount. Then, in another timer event, check if the counter renderCount didn't change over the past say 3-5 seconds, then we can make an assumption that rendering is done.
The code to watch for the children, and check if no more rendering is taking place, could be as follows:
app.directive('whenRenderingDone', function($interval, $parse){
return {
link: function (scope, el, attrs) {
var renderingCount = 0;
function watchForChildren() {
scope.$watch(function(){
return $(':input', el).length;
}, function(newVal, oldVal){
if (newVal) {
renderingCount++;
}
})
}
watchForChildren();
//Check counter every 3 seconds, if no change since last time, this means rendering is done.
var checkRenderingDone = $interval(function(){
var lastCount = lastCount || -1;
if (lastCount === renderingCount) {
var func = $parse(attrs.whenRenderingDone);
$interval.cancel(checkRenderingDone);
func(scope);
}
lastCount = renderingCount || -1;
}, 3000);
}
}
});
I will try to implement the above approach, and if you have feedback please let me know.
Tarek
I developed the following directive which is working well under Chrome and IE11:
app.directive('whenRenderingDone', function($timeout, $parse){
return {
link: function (scope, el, attrs) {
var lastCount;
var lastTimer = 5000; // Initial timeout
//Check counter every few seconds, if no change since last time, this means rendering is done.
var checkRenderingDone = function (){
var mainPromiseResolved = scope.mainPromiseResolved;
lastCount = lastCount || -1;
if (lastCount === el.find('*').length && mainPromiseResolved) {
console.log('Rendering done, lastCount = %i', lastCount);
var func = $parse(attrs.whenRenderingDone);
func(scope);
} else {
lastCount = el.find('*').length;
console.log('mainPromiseResolved = %s, lastCount %i', mainPromiseResolved, lastCount)
console.log('Rendering not yet done. Check again after %i seconds.', lastTimer/1000.00);
stopCheckRendering = $timeout(checkRenderingDone, lastTimer);
lastTimer = lastTimer - 1000;
if (lastTimer <= 0) {
lastTimer = 1000;
}
return stopCheckRendering;
}
}
var stopCheckRendering;
stopCheckRendering = checkRenderingDone();
el.on('$destroy', function() {
if (stopCheckRendering) {
$timeout.cancel(stopCheckRendering);
}
});
}
}
});
I hope this will be of help to you, and if you have any comment to improve, please let me know. See this to give you an idea about how it is working.
Tarek
You can use $$postDigest to run code after the digest cycle completes. You can read more about the scope lifecycle here
// Some $apply action here or simply entering the digest cycle
scope.$apply(function () { ... });
...
scope.$$postDigest(function () {
// Run any code in here that will run after all the watches complete
// in the digest cycle. Which means it runs once after all the
// watches manipulate the DOM and before the browser renders
});

In Angular 1.5, how to compile the html of a parent component from a child component?

I have two angular components: app-menuitem and app-menu. app-menu has a list of app-menuitem as children but there is no transclude.
App-menuitem
angular.module('app')
.component('appMenuitem', {
transclude: false,
controller: menuitemController,
require: {
parent: '^?app-menu'
},
bindings: {
...
groupradio: '#',
isactive: '<', // bind to active (just init)
...
},
templateUrl: 'angular/components/simple/menuitem/menuitem.html'
});
function menuitemController($rootScope, $scope, $element, $attrs) {
var ctrl = this;
//Default values
ctrl.$onInit = function () {
if(ctrl.isactive){
ctrl.active = true;
}else{
ctrl.active = false;
}
ctrl.selectRadioItem = function(){
if(!ctrl.active){
var currentMenu = this.parent.items.menu;
var levelMenu = this.parent.items.level;
for(var i = 0; i < currentMenu.length; i++){
var currentMenuItem = currentMenu[i];
if(currentMenuItem.groupradio === ctrl.groupradio){
if(currentMenuItem.index === ctrl.index){
currentMenuItem.isactive = true;
}else{
currentMenuItem.isactive = false;
}
currentMenu[i] = currentMenuItem;
}
}
this.parent.items.menu = currentMenu;
console.dir(this.parent); //<-- updates are visible but the html did not change.
}
...
As you can see at the end of this code, I managed to modify the parent component app-menu from this child component app-menuitem, but the HTML is never compiled again in this case. Someone has an idea ?
I suggest not to change values of parent directly from children. Instead, expose a method on the parent's controller that is invoked from the child with the needed update and let the parent handle the updates.
This allows you both to avoid more costly bindings as well as keep the control of a controller's properties in the controller itself (hence allowing you to more easily find the error source in your code). It is also more testable, if you are testing your code.
Small Tip: For test purposes, if something doesn't update after you
update the model, you could always try to do $scope.$apply() after the
update and see if there's a $digest timing issue. Do not use in
production unless you have to - it is costly and can easily cause run
time exceptions

Angular Custom Directive - How to make it Behave Like Ng-if

I'm trying to build a custom directive to either show items on a page or completely remove them based on authorization data, and I'm clearly missing something, 'cuz its not working and I'm having a hard time figuring out why.
I've been following the guide here:
http://adamalbrecht.com/2014/09/22/authorization-with-angular-and-ui-router/
Which said to take a copy of the NgIf source code, and modify it (as I have below).
I'm really confused because not only is it not working as expected, but even when I put break-points within the function calls in the directive, it never hits those break points.
I'm not sure what I'm doing wrong. Is there something else not documented within the steps that I need to do in order to use a custom directive, or did I somehow miss a step? Regular ng-if's work on the same page just fine.
EDIT: I should note that AuthorizeService.isAuthorizedForOne returns a promise value which is either true or false. This works fine in other contexts.
'use strict';
/**
* #ngdoc directive
* #name ngIfPermission
* #restrict A
*
* #description
**/
var ngIfPermissionDirective = ['$animate', function($animate, AuthorizeService) {
return {
multiElement: true,
transclude: 'element',
priority: 600,
terminal: true,
restrict: 'A',
$$tlb: true,
link: function($scope, $element, $attr, ctrl, $transclude) {
console.log("I am here");
var block, childScope, previousElements;
$attr.$observe("ngIfPermission", function(value){
console.log("I am there");
var permissions = JSON.parse(value.replace(/'/g, '"'));
AuthorizeService.isAuthorizedForOne(permissions).then(function(auth){
if (!childScope) {
$transclude(function(clone, newScope) {
childScope = newScope;
clone[clone.length++] = document.createComment(' end ngIfPermission: ' + $attr.ngIfPermission + ' ');
// Note: We only need the first/last node of the cloned nodes.
// However, we need to keep the reference to the jqlite wrapper as it might be changed later
// by a directive with templateUrl when its template arrives.
block = {
clone: clone
};
$animate.enter(clone, $element.parent(), $element);
});
}
},
function(err) {
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;
}
});
});
}
};
}];
How I'm referencing it:
<div ng-if-permission="['OOGY']">You can see this.</div>
<div ng-if-permission='["BOOGY"]'>or this</div>
I think you might got the declaration for the directive wrong.
app.directive( 'ngIfPermissionDirective', function($animate){
//directive here
));
DEMO http://plnkr.co/edit/BhubrfMAiW3K4ANI3pTx

Dynamically added element's directive doesn't work

I'm trying to build a simple infinite scroll. It loads the data fine but after loading, new added elements' directives don't work.
This is relevant part of the scroll checking and data loading directive.
.directive("scrollCheck", function ($window, $http) {
return function(scope, element, attrs) {
angular.element($window).bind("scroll", function() {
// calculating windowBottom and docHeight here then
if (windowBottom >= (docHeight - 100)) {
// doing some work here then
$http.get('service page').then(function (result) {
if (result.data.trim() != "") {
var newDiv = angular.element(result.data);
element.append(newDiv);
}
// doing some other work
},function () {
// error handling here
});
}
scope.$apply();
});
};
})
Service page returns some repeats of this structure as result.data
<div ...>
<div ... ng-click="test($event)"></div>
<div ...>...</div>
</div>
As i said data loads just fine but those test() functions in ng-clickdirectives don't work. How to get em work?
I believe you are going to need to compile the html element returned. Something like this
$compile(newDiv)(scope); // Corrected. Thanks
You'll need to be sure and pass in $compile into your function

Why will my twitter widget not render if i change the view in angularjs?

Hi and thanks for reading.
I have a angular app im making and ive stumbled on a problem. set up as so
index.html-
<html ng-app="myApp">
...
<div ng-view></div>
<div ng-include="'footer.html'"></div>
...
</html>
I wont bother putting my routes its pretty simple /home is shows the /home/index.html and so on...
/home/index.html (default view when you come to the site)
<div class="responsive-block1">
<div class="tweet-me">
<h1> tweet me </h1>
</div>
<div class="twitter-box">
<twitter-timeline></twitter-timeline>
</div>
twitter timeline directive
directives.directive("twitterTimeline", function() {
return {
restrict: 'E',
template: '<a class="twitter-timeline" href="https://twitter.com/NAME" data-widget-id="XXXXXXXXXXXXXX">Tweets by #NAME</a>',
link: function(scope, element, attrs) {
function run(){
(!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+"://platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs"));
console.log('run script');
};
run();
}
};
});
So I have just created a basic twitter directive using the tag from twitter. But when I change the view example to /blog then go back to /home the twitter widget no longer renders at all.
Im also using an $anchorScroll and if i jump to anyway on the page with this the widget also disappears. Any info would be great thanks.
See this post: https://dev.twitter.com/discussions/890
I think that you may be able to get the widget to re-render by calling
twttr.widgets.load().
If you find that this does not work, you will need to wrap this code into $timeout in your controller:
controller('MyCtrl1', ['$scope', '$timeout', function ($scope, $timeout) {
$timeout = twttr.widgets.load();
}])
To build on Sir l33tname's answer:
In services declaration:
angular.module('app.services', []).
service('tweetWidgets', function() {
this.loadAllWidgets = function() {
/* widgets loader code you get when
* declaring you widget with Twitter
* this code is the same for all widgets
* so calling it once will reference whatever
* widgets are active in the current ng-view */
!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+"://platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");
};
this.destroyAllWidgets = function() {
var $ = function (id) { return document.getElementById(id); };
var twitter = $('twitter-wjs');
if (twitter != null)
twitter.remove();
};
});
Then in controller declarations:
angular.module('app.controllers', []).
controller('view_1_Controller', tweetWidgets) {
// load them all
tweetWidgets.loadAllWidgets();
}).
controller('view_2_Controller', tweetWidgets) {
// now destroy them :>
tweetWidgets.destroyAllWidgets();
});
Now whenever you leave view #1 to go to view #2, your controller for view #2 will remove the widgets associated with view #1 and when you return to view #1 the widgets will be re-instatiated.
The problem is because when Angular switches views the script tag that was originally inserted is not removed from the document. I fixed this on my own website by removing the Twitter script element whenever my Twitter timeline directive is not in the view. See the code below with comments.
function (scope, el, attrs) {
el.bind('$destroy', function() {
var twitterScriptEl = angular.element('#twitter-wjs');
twitterScriptEl.remove();
});
// function provided by Twitter that's been formatted for easier reading
function (d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0], p = /^http:/.test(d.location) ? 'http' : 'https';
// If the Twitter script element is already on the document this will not get called. On a regular webpage that gets reloaded this isn't a problem. Angular views are loaded dynamically.
if (!d.getElementById(id)) {
js = d.createElement(s);
js.id = id;
js.src = p + "://platform.twitter.com/widgets.js";
js.parentNode.insertBefore(js, fjs);
}
}(document, "script", "twitter-wjs");
}
Basically it's what Loc Nguyen say.
So every time you recreate it you must remove it first.
var $ = function (id) { return document.getElementById(id); };
function loadTwitter() {!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+"://platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");}
var twitter = $('twitter-wjs');
twitter.remove();
loadTwitter();
Answer by #b1r3k works without problems :
put this in your controller:
$timeout(function () { twttr.widgets.load(); }, 500);
For those trying to load twttr.widgets.load() inside their controller, you will most likely get an error that twttr is not defined AT SOME POINT in your UX, because the async call to load the twitter script may not be completed by the time you controller instantiates and references twttr.
So I created this TwitterService
.factory('TwitterService', ['$timeout', function ($timeout) {
return {
load: function () {
if (typeof twttr === 'undefined') {
(function() {
!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+'://platform.twitter.com/widgets.js';fjs.parentNode.insertBefore(js,fjs);}}(document, 'script', 'twitter-wjs');
})();
} else {
$timeout = twttr.widgets.load();
};
}
}
}])
and then call TwitterService.load() inside the controllers that require your widgets. This worked pretty well. It basically just checks if the twttw object exists and if it does, just reload the script... otherwise just reload the script.
Not sure if this is the best implementation, but it seems like all other solutions have edge cases where it will throw an error. I have yet to find one with this alternative.

Resources