angular ng-bind-html and directive within it - angularjs

Plunker Link
I have a element which I would like to bind html to it.
<div ng-bind-html="details" upper></div>
That works. Now, along with it I also have a directive which is bound to the bound html:
$scope.details = 'Success! <a href="#/details/12" upper>details</a>'
But the directive upper with the div and anchor do not evaluate. How do I make it work?

I was also facing this problem and after hours searching the internet I read #Chandermani's comment, which proved to be the solution.
You need to call a 'compile' directive with this pattern:
HTML:
<div compile="details"></div>
JS:
.directive('compile', ['$compile', function ($compile) {
return function(scope, element, attrs) {
scope.$watch(
function(scope) {
// watch the 'compile' expression for changes
return scope.$eval(attrs.compile);
},
function(value) {
// when the 'compile' expression changes
// assign it into the current DOM
element.html(value);
// compile the new DOM and link it to the current
// scope.
// NOTE: we only compile .childNodes so that
// we don't get into infinite loop compiling ourselves
$compile(element.contents())(scope);
}
);
};
}])
You can see a working fiddle of it here

Thanks for the great answer vkammerer. One optimization I would recommend is un-watching after the compilation runs once. The $eval within the watch expression could have performance implications.
angular.module('vkApp')
.directive('compile', ['$compile', function ($compile) {
return function(scope, element, attrs) {
var ensureCompileRunsOnce = scope.$watch(
function(scope) {
// watch the 'compile' expression for changes
return scope.$eval(attrs.compile);
},
function(value) {
// when the 'compile' expression changes
// assign it into the current DOM
element.html(value);
// compile the new DOM and link it to the current
// scope.
// NOTE: we only compile .childNodes so that
// we don't get into infinite loop compiling ourselves
$compile(element.contents())(scope);
// Use un-watch feature to ensure compilation happens only once.
ensureCompileRunsOnce();
}
);
};
}]);
Here's a forked and updated fiddle.

Add this directive angular-bind-html-compile
.directive('bindHtmlCompile', ['$compile', function ($compile) {
return {
restrict: 'A',
link: function (scope, element, attrs) {
scope.$watch(function () {
return scope.$eval(attrs.bindHtmlCompile);
}, function (value) {
// Incase value is a TrustedValueHolderType, sometimes it
// needs to be explicitly called into a string in order to
// get the HTML string.
element.html(value && value.toString());
// If scope is provided use it, otherwise use parent scope
var compileScope = scope;
if (attrs.bindHtmlScope) {
compileScope = scope.$eval(attrs.bindHtmlScope);
}
$compile(element.contents())(compileScope);
});
}
};
}]);
Use it like this :
<div bind-html-compile="data.content"></div>
Really easy :)

Unfortunately I don't have enough reputation to comment.
I could not get this to work for ages. I modified my ng-bind-html code to use this custom directive, but I failed to remove the $scope.html = $sce.trustAsHtml($scope.html) that was required for ng-bind-html to work. As soon as I removed this, the compile function started to work.

For anyone dealing with content that has already been run through $sce.trustAsHtml here is what I had to do differently
function(scope, element, attrs) {
var ensureCompileRunsOnce = scope.$watch(function(scope) {
return $sce.parseAsHtml(attrs.compile)(scope);
},
function(value) {
// when the parsed expression changes assign it into the current DOM
element.html(value);
// compile the new DOM and link it to the current scope.
$compile(element.contents())(scope);
// Use un-watch feature to ensure compilation happens only once.
ensureCompileRunsOnce();
});
}
This is only the link portion of the directive as I'm using a different layout. You will need to inject the $sce service as well as $compile.

Best solution what I've found! I copied it and it work's exactly as I needed. Thanks, thanks, thanks ...
in directive link function I have
app.directive('element',function($compile){
.
.
var addXml = function(){
var el = $compile('<xml-definitions definitions="definitions" />')($scope);
$scope.renderingElement = el.html();
}
.
.
and in directive template:
<span compile="renderingElement"></span>

Related

Accessing ng-click when rendered via $sce.trustAsHtml

How do i get access to the ng-click function (updateRating) in the below?
https://jsfiddle.net/by2jax5v/171/
I'm using $sce.trustAsHtml to render $scope.content
$scope.bindHTML = $sce.trustAsHtml($scope.content);
Your above code does get compiled but it is sanitized by angular js considering an anchor tag as insecure, therefore ng-click does not work.
what you want to achieve can be achieved by using ng-html-compile by francis bouvier instead of ng-bind-html. It the thinnest library i have seen just 1kb. https://github.com/francisbouvier/ng_html_compile
also refer to https://stackoverflow.com/a/41790235
As it's not $compiled. so it doesn't tell Angular to search through that HTML and compile directives within it. You will have to use custom directive for this.
Updated fiddle.
var myApp = angular.module('myApp', []);
myApp.controller('MyCtrl', function($scope) {
$scope.content = "This text is <em>html capable</em> meaning you can have <a ng-click='updateRating(1)' href=\"#\">all</a> sorts <b>of</b> html in here.";
$scope.updateRating = function(message) {
alert(message);
}
});
myApp.directive('compile', ['$compile', function ($compile) {
return function(scope, element, attrs) {
scope.$watch(
function(scope) {
// watch the 'compile' expression for changes
return scope.$eval(attrs.compile);
},
function(value) {
// when the 'compile' expression changes
// assign it into the current DOM
element.html(value);
// compile the new DOM and link it to the current
// scope.
// NOTE: we only compile .childNodes so that
// we don't get into infinite loop compiling ourselves
$compile(element.contents())(scope);
}
);
};
}]);

Removing element from DOM via ng-if bound to attribute directive scope property

I assumed this would be straightforward, but it's seemingly not!
I'm trying to create a generic attribute directive that will call a method in one of my services and conditionally cause the element in which it is placed to not be added to the DOM if the service method returns false. Basically, ng-if, but an ng-if that internally calls a service method and acts on that
Link to Plunker
I have an element containing an attribute directive: e.g
<p ng-if="visible" my-directive>Hi</p>
I set visible to true in the myDirective directive. I was expecting the <p> element to be removed from the DOM when visible was falsy and added to the DOM when it's truthy. Instead, the ng-if never seems to spot that visible has been set to true in the directive's link function and, hence, the <p> element never displays.
I wasn't 100% sure it would work since the directive is removing the element on which it exists, bit of a catch 22 there.
I've spent far too long on this and have so far tried (unsucessfully):
Adding an ng-if attribute in the link function via these two methods
attr.ngIf = true;
element.attr('ng-if', true);
Changing the ng-if in the <p> to ng-show, thereby not removing the element (which I really want to do)
I'm wondering if it's something as simple as scope? Since the ng-if is bound to a property of the <p> element, is setting visible in the directive scope setting it on the same scope?
On the other hand, I may be drastically over-simplifying, I have a nasty feeling I may have to consider directive compilation and transclusion to get a solution for this.
Does anyone have any feel for where I might be going wrong?
tldr: apparently you want your directive to be self-contained and it should be able to remove and add itself to the DOM. This is possible and makes the most sense via isolated scope or manual manipulation of the DOM (see below).
General
When you do <p ng-if="visible" my-directive>Hi</p> angular looks for the visible on the current scope, which is the parent scope of the directive. When visible is defined, the directive is inserted in the DOM, e.g. taken from your plunker
<body ng-controller="MainCtrl">
<p my-directive="showMe" ng-if="visible">I should be shown</p>
</body>`<br>
app.controller('MainCtrl', function($scope) {
$scope.visible = 3;
});
would make the directive being shown. As you defined an isolated scope on your directive
app.directive('myDirective', function() {
return {
restrict: 'A',
scope: {
myDirective: '='
},
link: function(scope, element, attr, ctrl) {
scope.visible = (scope.myDirective == 'showMe') ? true : false;
}
}
});
scope.visible in the directive does not affect the visible taken into account for ngIf.
Child Scope
You could define a child scope to get access to the parent scope. If you do that, you can actually affect the right visible property, but you have to put it on an object so that the directive can follow the scope prototype chain.
<body ng-controller="MainCtrl">
<p my-directive ng-if="visibleDirectives.directive1">I should be shown</p>
</body>
The $timeouts are there for demonstration purposes. Initially the ngIf has to evaluate to true else the directive is not being created at all.
app.controller('MainCtrl', function($scope) {
$scope.visibleDirectives = { directive1 : true };
});
app.directive('myDirective', function($timeout) {
return {
restrict: 'A',
scope : true,
link: function(scope, element, attr, ctrl) {
console.log(scope);
$timeout(function() {
scope.visibleDirectives.directive1 = !scope.visibleDirectives.directive1;
$timeout(function() {
scope.visibleDirectives.directive1 = !scope.visibleDirectives.directive1;
}, 2000);
}, 2000);
}
}
});
Like this the directive has to know about the property that defines it's visibility beforehand (in this case scope.visibleDirectives.visible1), which is not very practical and prohibits several directives.
Isolated Scope
In your example you used an isolated scope. This allows reusing the directive. In order for the directive to be able to modify the appropriate property for ngIf you have to again give it the right reference.
<body ng-controller="MainCtrl">
<p my-directive="directive1" ng-if="directive1.visible">I should be shown</p>
</body>
Again you have to provide the property on an object so that the directive can follow the object reference to modify the right visible.
app.controller('MainCtrl', function($scope) {
$scope.directive1 = {
visible : true
};
});
app.directive('myDirective', function($timeout) {
return {
restrict: 'A',
scope : {
myDirective : '='
},
link: function(scope, element, attr, ctrl) {
$timeout(function() {
scope.myDirective.visible = !scope.myDirective.visible;
$timeout(function() {
scope.myDirective.visible = !scope.myDirective.visible;
}, 2000);
}, 2000);
}
}
});
In these cases the directive gets recreated everytime ngIf evaluates to true.
Manual manipulation of the DOM
You can also just manually remove and append the node of the directive without consulting angular.
<body ng-controller="MainCtrl">
<p my-directive>I should be shown</p>
</body>
In this case you don't need the angular version of setTimeout and can even use a setInterval as the Interval is created only once, but you have to clear it.
app.controller('MainCtrl', function($scope) { });
app.directive('myDirective', function() {
return {
restrict: 'A',
scope : { },
link: function(scope, element, attr, ctrl) {
var el = element[0];
var parent = el.parentNode;
var shouldBeShown = false;
var interval = setInterval(function() {
var children = parent.children;
var found = false;
for(var i = 0; i < children.length; i++) {
if(children[i] === el) {
found = true;
break;
}
}
if(shouldBeShown) {
if(!found)
parent.appendChild(el);
}
else {
if(found)
parent.removeChild(el);
}
shouldBeShown = !shouldBeShown;
}, 2000);
scope.$on('$destroy', function() {
clearInterval(interval);
});
}
};
});
If you want an element to be removed, use ng-show="visible" this will evaluate as a Boolean and show the element if it evaluates to true. Use "!visible" if you need to flip it.
Also, but adding the scope attribute to your directive you are adding an additional scope, think alternate timeline, that your controller scope that is tied to the page cannot see. That would explain why ng-show may not have worked for you before.

pass data from controller to directive's link?

In my controller :
myApp.controller('homeCtrl', function($scope, $rootScope, $state, 'red';
$rootScope.$on('new_story', function(event, data) {
$scope.cardObj = {key:'value'};
});
});
In my HTML :
<div clickmeee ></div>
<div id="feedContainer" card='{{cardObj}}'> </div>
In my directive :
myApp.directive('clickmeee', function($compile, $rootScope) {
return {
restrict: 'A',
scope: {
card: '#'
},
link: function(scope, element, attrs) {
element.bind('click', function() {   
scope.$watch('card', function(newVal, oldVal) {
alert(scope.card);
});       
});
}
};
});
How do I pass data from controller to this directive. I compile some html and prepend it to the div. All of that is sorted out but I need some data from object I am trying to pass.
Any help??
There are several problems in your code:
you define a scope attribute named 'card', but you use cardObj instead
you use a watch that is completely unnecessary. And worse: you create a new watch every time the element is clicked
you don't define any card attribute on your clickmeee element. Instead, you're placing it on another element, on which the directive is not applied
you're passing the attribute with '#'. That works, but the directive will receive a string, containing the JSONified object, rather than the object itself
you're not showming us where you emit an event that will initialize cardObj in the controller scope
Here is a plunkr showing a working version of your code.
Also, note that using bind('click') is a bad idea. You'd better have a template in your directive and use ng-click in the template, or simply not use a directive at all and just use ng-click directly on the div element.
Bad news. You are doing it wrong all the ways.
Firstly
card='{{cardObj}}' >
this one should be put in the
<div clickmeee ></div>
So you can take it as binded scope variable in your directive registration
Secondly
If you managed to use '#' syntax
card: '#'
it will turn your input to string, not a binded scope. Use '=' instead.
In the end
You dont need to use watch here:
scope.$watch('card', function(newVal, oldVal) {
alert(newVal);
});
since scope.card is binded via '=' connector. Just simple use alert(scope.card). (Need to warn you that alert an object is not a good idea)
I have tried your code here: plunker. Changed a litte bit by using cardObj as string for easier presentation. Does it match your work?
You should watch the card object:
myApp.directive('clickmeee', function() {
return {
restrict: 'A',
scope: {
card: '#'
},
link: function(scope, element, attrs) {
scope.$watch('card', function(value) {
console.log(value);
});
}
};
});
And:
<div clickmeee id="feedContainer" card='{{cardObj}}'> </div>
Whenever the controller changes the cardObj, the directive's watch on card is triggered:
$scope.$apply(function() {
$scope.cardObj = "test";
}

Angular composite (super) directive not allowing 2 way binding on component (child) directives

I have a need to create a composite directive that incorporates separate fully functional directives. One of my component directives adds an element to the dom and that element binds to a value in the component directive's controller. When the composite directive adds the component directive in the compile function, it seems to work but the piece that has the 2 way binding in the component directive does not appear to get compiled and just renders the {{ctrl.value}} string on the screen. I realize this is a bit convoluted so I have included a plunk to help clarify the issue.
app.directive('compositeDirective', function($compile){
return {
compile: compileFunction
}
function compileFunction(element, attrs){
attrs.$set("component-directive", "");
element.removeAttr("composite-directive");
element.after("<div>Component value when added in composite directive: {{compCtrl.myValue}}</div>");
return { post: function(scope, element){
$compile(element)(scope);
}};
}
});
app.directive('componentDirective', function(){
return {
controller: "componentController as compCtrl",
link: link
};
function link(scope, element){
element.after("<div>Component value: {{compCtrl.myValue}}</div>");
}
});
app.controller('componentController', function(){
var vm = this;
vm.myValue = "Hello";
});
http://plnkr.co/edit/alO83j9Efz62VTKDOVgc
I don't think any compilation will happen as a result of changes in the link function, unless you call $compile manually, i.e.,
app.directive('componentDirective', function($compile){
return {
controller: "componentController as compCtrl",
link: link
};
function link(scope, element){
var elm = $compile("<div>Component value: {{compCtrl.myValue}}</div>")(scope);
element.append(elm);
}
});
Updated plunk: http://plnkr.co/edit/pIixQujs1y6mPMKT4zxK
You can also use a compile function instead of link: http://plnkr.co/edit/fjZMd4FIQ97oHSvetOgU
Also, make sure to use .append() instead of .after().

Correct way to insert elements with angular directive using angular.element()

What is the correct way to insert element into the DOM using angular.element() ?
app.directive('validationTest', function(){
return {
restrict: 'A',
replace: false,
link: function(scope, element, attrs){
scope.call = function(){
console.log('Someone clicked it');
};
var newElement = angular.element('<span ng-click="call()">').text('Yo Yo');
element.append(newElement);
}
};
});
In this code, I am trying to add a span element within the element on which directive has been applied. I am able to add this span element as the child of the parent div on which append mehtod is called.
However, as you can see in the code, a ng-click also has been associated with this span. I know it is not useful from any point of view, it is just for demo purpose. So, normally, clicking on this span, a line should be printed in the console. However, it doesn't happen.
What am I missing here ? Have I used wrong approach for this append or there is some error in my code ?
If the HTML that you dynamically append to the DOM has directives, then you'll want to $compile and link it before appending it to the DOM:
var newElement = angular.element('<span ng-click="call()">').text('Yo Yo');
$compile(newElement)(scope);
element.append(newElement);
An alternative approach that is less error prone is to move your DOM manipulation to the compile function. By inserting the new element during the compile phase, the new HTML will be automatically linked during the linking phase (no manual compile /link required):
app.directive('myDirective', function() {
return {
compile: function(element, attr) {
var newElement = angular.element('<span ng-click="call()">').text('Yo Yo');
element.append(newElement);
return function(scope, element, attr) {
}
}
}
});
You're missing angular compilation. The compilation traverses your DOM looking for directives and initializes them. Without calling compile, nothing will initialize your ngClick. Try this to use $compile:
app.directive('validationTest', ['$compile', function($compile){
return {
restrict: 'A',
replace: false,
link: function(scope, element, attrs){
scope.call = function(){
console.log('Someone clicked it');
};
var newElement = angular.element('<span ng-click="call()">').text('Yo Yo');
element.append(newElement);
$compile(element)(scope);
}
};
});
For further information: https://docs.angularjs.org/api/ng/service/$compile
It's better to use templateUrl or template.
View $compile docs.

Resources