Watch svg element after replaceWith in Angular Directive - angularjs

Recently have been trying to build kind of a bulletproof directive for inserting inline svgs. It works pretty fine, but recently I wanted to add some animation triggered when class "animate" is add to the inserted element. Problem is that $watch applies to the old element (the one before replaceWith).
I have been trying anything, but I can not make it work. How to get an access to the element after the replacement?
Here is my code:
angular.module('test')
.directive('svgPng', ['$compile', function ($compile) {
return {
link: function(scope,elem,attrs) {
elem.on('load', function(){
var ext = function(s){return s.substr(s.length-3);},
src = attrs.src;
if (ext(src) === 'svg'){
if(window.Modernizr && window.Modernizr.svg){
Snap.load(src, function (svg){
elem = elem.replaceWith($compile(svg.node)(scope));
if(attrs.animate === 'true'){
scope.$watch(function() {return elem.attr('class'); }, function(newValue){
//some animation
}
}
console.log(elem); //gives old elem instead of svg.node
});
} else {
if(attrs.fallback){
elem.attr('src', attrs.fallback);
} else {
elem.attr('src', attrs.src.substr(3) + 'png');
}
}
}
});
}
};
}]);

elem isn't getting updated with the newly compiled element because .replaceWith doesn't return the new element. http://api.jquery.com/replacewith/
The .replaceWith() method, like most jQuery methods, returns the jQuery object so that other methods can be chained onto it. However, it must be noted that the original jQuery object is returned. This object refers to the element that has been removed from the DOM, not the new element that has replaced it
You need to store the compiled element and replace with that.
var compiled = $compile(svg.node)(scope);
elem.replaceWith(compiled);
elem = compiled;

Related

Angular Directive not updating element using interval

i have this directive
angular.module('mydirectives').directive('slideShow', function ($interval) {
return{
scope:{slideShow:'=slideShow'},
link:function(scope, element, attrs){
element.css("background-size","cover");
element.css("background-repeat","none");
element.css("background-position","center center");
element.css("background-blend-mode","color");
element.css("background-color","rgba(0, 0, 0, 0.5)");
scope.index=0;
function nextSlide()
{
if(!scope.slideShow) return;
if(scope.slideShow.sources.length===0) return;
var url=scope.slideShow.sources[scope.index++];
if(scope.index>=scope.slideShow.sources.length) scope.index=0;
element.css({'background-image': 'url(' + url +')'});
}
nextSlide();
var interval= $interval(nextSlide,3000)
scope.$on("$destroy",function(){
$interval.cancel(interval);
})
}
}
});
this is how i apply it
<section class="primary" slide-show="slideShow">
now the controller which provides property "slideShow" gets the value via http request. when it comes back with response it sets the value of slideShow like this
$scope.slideShow={sources:["http:\\sources\someimage.jgp"]}
webApi.getHomePageModel().then(function(model){
$scope.model=model;
$scope.slideShow=model.slideShow;
},function(error){
console.dir(error);
});
The Problem: when this runs only the default value of slideshow works and element's background-image is set but after the response to http the new value is set to slideShow but the when interval function "nextSlide" executes then background-image is not updated. in debugger i can see the url values is being picked up correctly but element is not updated.
EDIT:I was making a stupid mistake, the updated model was not as expected the elements in sources were not strings as expected (they were being generated as complex objects rather than string value.) all working now. also no need for scope.$applyAsync because the $interval service handles that for you
If you are using setInterval then you need to manually rerun angular's digetst cycle:
function nextSlide()
{
if(!scope.slideShow) return;
if(scope.slideShow.sources.length===0) return;
var url=scope.slideShow.sources[scope.index++];
if(scope.index>=scope.slideShow.sources.length) scope.index=0;
element.css({'background-image': 'url(' + url +')'});
scope.applyAsync(); //this line!
//May not work in older angular versions, if such you should use scope.apply()
}
I got it working with this implementation
angular.module('app').directive('slideShow', function ($interval) {
return{
scope:{slideShow:'=slideShow'},
link:function(scope, element, attrs){
var index=0;
function nextSlide()
{
if(!scope.slideShow) return;
if(scope.slideShow.images.length===0) return;
var url=scope.slideShow.images[index++];
if(index>=scope.slideShow.images.length) index=0;
element.css({'background-image': 'url(' + url +')'});
}
var interval=false;
var watchSlideShow=scope.$watch("slideShow",function(){
if(!scope.slideShow) return;
if(scope.slideShow.images.length===0) return;
if(interval) return;
nextSlide();
var interval= $interval(nextSlide,5000);
});
scope.$on("$destroy",function(){
$interval.cancel(interval);
watchSlideShow();
});
}
}
});

Adding a UI-Bootstrap Element to DOM inside Directive

I am new to angular and I am trying to figure out the following problem. When the user highlights some text, I would like a ui-bootstrap popover to surround the highlighted text. Since this would manipulate the DOM I think I need to use a directive for this. I was able to successfully implement a simpler version of this problem here
app.directive('selectOnClick', function ($window) {
return {
link: function (scope, element) {
element.on('click', function () {
var span = document.createElement("span");
span.style.fontWeight = "bold";
span.style.color = "green";
if (window.getSelection) {
var sel = window.getSelection();
if (sel.rangeCount) {
var range = sel.getRangeAt(0).cloneRange();
range.surroundContents(span);
sel.removeAllRanges();
sel.addRange(range);
}
}
});
}
}
});
In the above code I am able to surround the highlighted text with a span tag. However I would like to instead use a ui-bootstrap popover. I tried replacing the span part with var popover=angular.element("<a href=#' uib-popover='hello' popover-title='hello'></a>"); but this did not work. Am I on the right track or would this approach not work with a ui-bootstrap element?
UPDATED
Here is my attempt at adding the the popover element
app.directive('selectOnClick', function ($window, $compile) {
return {
link: function (scope, element) {
element.on('click', function () {
var popover=angular.element("<a href=#' uib-popover='hello' popover-title='hello'></a>");
if (window.getSelection) {
var sel = window.getSelection();
if (sel.rangeCount) {
var range = sel.getRangeAt(0).cloneRange();
range.surroundContents($compile(popover));
sel.removeAllRanges();
sel.addRange(range);
}
}
});
}
}
});
Unfortunately I am getting the error TypeError: Failed to execute 'surroundContents' on 'Range': parameter 1 is not of type 'Node'. on the line range.surroundContents($compile(popover)); So I suppose $compile(popover) is not the correct type. Can it be converted to Node type somehow?

Appending ngBindHtml function using .attr in link function not working

A am attempting to attach the ngBindHtml directive in an application within a link function of a directive. The module in which the directive is located injects ngSanitize like such:
angular.module('ui.bootstrap.contextMenu', ['ngSanitize'])
.directive('contextMenu', cm);
where cm is the directive function. The link function looks like:
var link = function ($scope, element, attrs) {
element.on('contextmenu', function (event) {
event.stopPropagation();
$scope.$apply(function () {
event.preventDefault();
var options = $scope.$eval(attrs.contextMenu);
var model = $scope.$eval(attrs.model);
if (options instanceof Array) {
if (options.length === 0) {
return;
}
renderContextMenu($scope, event, options, model);
} else {
throw '"' + attrs.contextMenu + '" not an array';
}
});
});
};
where renderContextMenu sketches out the html that will be attached to the body of the page. Within this function I have the following lines of code:
$div.attr('ng-bind-html', text);
$a.append($div);
which should produce something that looks like:
<a><div ng-bind-html="the text"></div></a>
and it does. The problem is that the text is not actually displayed. Does anyone have any thoughts on this?
I think the "angular-y" way of doing this is to put the html code for the context menu that you're hoping to bind directly into the template, and just hide/show it as appropriate: only show it if a valid contextmenu event occurs and has options.length > 0.

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

Access Element Style from Angular directive

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

Resources