how to insert an angular 1.5 component with ng-bind-html - angularjs

I have a component, and i would like to inject it dynamically into my html.
I have a component like this:
angular.module('test1', []);
angular.module('test1').component('test1', {
templateUrl: 'components/test1/test1.template.html',
controller: function test1Controller($scope) {
}
});
the test1.template.html file looks like this:
<p>TEST 1</p>
on my controller i have this:
angular.module('myApp')
.controller('ctrlCtrl', function ($scope, $sce) {
$scope.tag = "<test1/>";
});
on my index.html, i have this:
<ng-bind-html ng-bind-html="tag"></ng-bind-html>
but the tag will not show up. I have tried writing literaly "'<p>hi!</p>'" on the ng-bind-html field, and the text "hi!" shows up on a paragraph, so i don't think this error is because of a typo.
I also tried to use $sce.trustAsHtml, but it didn't work neither :(
$scope.tag = $sce.trustAsHtml("<test1/>");
when i insert an input field, the trustAsHtml method does work, but when i try to inject my components dynamically, it just won't let me, please help D:

Why ng-include won't work?
Components need to be compiled before you can use them on the markup. Try editing the html of the app with the developer tools from your browser, by artificially injecting your component on the markup: it won't work.
How to dynamically include components?
you'll need to use directives, this tutorial (thanks to #Artem K.) is friendly to follow, but you can also read the angular's official documentation, it is a little hard to understand though.
Following the logic of the final example of the angular's official documentation, you can create a directive that compiles everything that is passed to it, like this:
// source: https://docs.angularjs.org/api/ng/service/$compile
angular.module('myApp')
.directive('my-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);
}
);
};
});
and then, on your index.html, you'll have to invoke the directive, sending the the string containing the component's tag as an argument.
<div compile="tag"></div>
As #charlietfl and #Artem K. said, you have to understand the angular's $compile so, thanks guys for pointing me in the right direction :)

Related

Run 'ng-click' inside a directive's isolated scope

Thanks in advance for taking the time to look into this question
I have serverside generated code that renders a directive wrapped around pre-rendered content.
<serverside-demo color="blue">
<p><strong>Content from Server, wrapped in a directive.</strong></p>
<p>I want this color to show: <span ng-style="{color: color}">{{color}}</span></p>
<button ng-click="onClickButtonInDirective()">Click Me</button>
</serverside-demo>
This means that 1.) the directive tag, 2.) the content inside the directive tag, 3.)the ng-click and 4.) The curly braces syntax are all generated by the server.
I want AngularJs to pick up the generated code, recompile the template and deal with the scope.
The problem is that I am having trouble getting it working. I understand that because the ng-click is inside the controller block, it is picked up not by the directive isolated scope, but the parent controllers. Instead I want the opposite... to pick up the onClickButtonInDirective scope function inside the serversideDemo link
I have created a jsfiddle best explaining my problem, which aims to clearly demonstrate the working "traditional" way of loading the template separately (which works) comparing it to the server-side way.
https://jsfiddle.net/stevewbrown/beLccjd2/3/
What is the best way to do this?
Thank you!
There are two major problem in your code
1- directive name and dom element not matched, - missing in dom element
app.directive('serverSideDemo', function() {
use <server-side-demo color="blue"> instead of <serverside-demo color="blue">
2- you need to compile the html code of server-side-demo dom with directive scope in link function
$compile(element.contents())(scope);
Working jsfiddle
Use templateUrl instead of template to fetch the content of directive from server:
app.directive('serverSideDemo', function() {
return {
restrict: 'AE',
scope: {
color: '='
},
templateUrl: 'link/that/returns/html/content',
link: function(scope, element, attrs) {
scope.onClickButtonInDirective = function() {
console.log('You clicked the button in the serverside demo')
scope.color = scope.color === 'blue' ? 'red' : 'blue';
}
}
};
});
Have a look at angular docs for more details

How to not refer to the controllerAs name in directive

Here is the minimal code to describe the issue. On the page, I have:
<div ng-controller='AController as a'>
<div a-directive></div>
</div>
In js, I have:
app.directive("aDirective", function($compile){
return {
scope: true,
link: function(scope, element, attrs) {
var template = "<h1>{{a.label}}</h1>";
element.append($compile(template)(scope));
}
}
});
app.controller("AController", function($scope){
self = this;
self.label = "some text";
});
That works, but the issue is that {{a.label}}, which made the view and controller/model tightly coupled. Is there any way to get rid of that a., and not to mention the controllerAs-name in the directive code at all? (just like what I did in the controller code)
To improve this you can pass the text to display as a parameter to the directive. Something like this is the initial idea:
<div a-directive="a.label"></div>
However, I DO recommend using an alias for the controller, so I made a Plunker where you can see all of this working together with some improvements.
Check it out here: http://plnkr.co/edit/1hBSBxwSEPXoj9TULzRQ?p=preview
I would also recommend to use template instead of link and restricting the directive to an element instead of using it as attribute, since it is modifying the DOM. But yeah, you could keep improving it till the end of the times :)

Dynamic id inside AngularJS template

I'm wrapping a jQuery plugin inside a AngularJS directive. The way I would like to call the directive is for example:
<my-dialog data-trigger-id="myTriggerId">My dialog content...</my-dialog>
Inside my directive template it looks like this:
<button id="{{triggerId}}">Button text...</button>
I attach the event for the jQuery plugin (where you specify the trigger selector) inside the link function of my directive. My problem is that it works if I hardcode the id of the button inside the directive template like this:
<button id="myTriggerId">Button text...</button>
The generated html looks fine in the browser, which means that rendering an element with a dynamic id works. It's just that the jQuery plugin cannot find this element if I use the dynamic id but it works with the hardcoded version.
I also looked up AngularJS compile because it looks like at the point where the jQuery plugin wants to initialize the element doesn't exist yet.
Is there a gotcha I'm missing? Thanks!
Edit: I finally managed to simplify it down and create a jsfiddle example. If you run the example, you will see in the console that the element doesn't exist at the time I'm logging it but if you inspect the DOM, you will see that it's there and has the correct id.
However if you hardcode the id in the template (id=test instead of id={{elemId}}), the console log will show that one element could be found. I hope this helps to find a solution.
http://jsfiddle.net/a1nxyv8u/7/
The digest has not yet rendered in the DOM by the time you are calling you $("#test").length.
You need to add in a $timeout so that the digest will complete, then call your method
var app = angular.module('app', []);
app.directive('myDialog', ['$timeout', function ($timeout) {
return {
restrict: 'E',
template: '<button id="{{elemId}}" class="{{elemClass}}">Open dialog</button>',
link: function (scope, element, attrs) {
var selector = scope.elemSelector,
elemClass = (selector.indexOf('.') > -1) ? selector.substr(1) : '',
elemId = (selector.indexOf('#') > -1) ? selector.substr(1) : '';
scope.elemClass = elemClass;
scope.elemId = elemId;
$timeout(function() {
console.log('elem: ', $('#test').length);
});
// jQuery plugin init here but element doesn't seem to exist yet.
},
scope: {
elemSelector: '#'
}
}
}]);
Although it should be noted that you should try and alleviate any Id's at all and just use $(element) instead unless your jQuery absolutely needs the Id.

Testing ui-grid wrapped in another directive

I'm trying to create a directive that will allow me to set certain configurations to ui-grid as well as to add some functionality to it. I got something basic working, but the problem is that the jasmine test is giving me a hard time.
The JS code looks like this:
angular.module('myGridDirective', ['ui.grid'])
.directive('myGrid', function() {
return {
restrict: 'E',
replace: true,
templateUrl: 'templates/my-grid.html'
};
});
The template looks like this:
<div><div id="test-id" class="gridStyle" ui-grid="gridOptions"></div></div>
And the test looks like this:
describe('my grid directive', function() {
var $compile,
$rootScope;
beforeEach(module('myGridDirective'));
beforeEach(module('templates/my-grid.html'));
beforeEach(function() {
inject(function(_$compile_, _$rootScope_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
});
});
it('Replaces the element with an ui-grid directive element', function() {
var element = $compile("<my-grid></my-grid>")($rootScope);
$rootScope.$digest();
expect(element.html()).toEqual('<div id="test-id" class="gridStyle" ui-grid="gridOptions"></div>');
});
});
The problem is that, while the directive is working (i.e. using <my-grid></my-grid> anywhere in my html file works), the test is failing.
I get the error message:
TypeError: $scope.uiGrid is undefined in .../angular-ui-grid/ui-grid.js (line 2879)
The relevant line in ui-grid.js is (the first line is 2879):
if (angular.isString($scope.uiGrid.data)) {
dataWatchCollectionDereg = $scope.$parent.$watchCollection($scope.uiGrid.data, dataWatchFunction);
}
else {
dataWatchCollectionDereg = $scope.$parent.$watchCollection(function() { return $scope.uiGrid.data; }, dataWatchFunction);
}
The thing is, if I replace the ['ui.grid'] array in the directive module creation with an empty array, the test passes. The only problem, is that if I do that, I'll have to include 'ui.grid' anywhere the directive is used otherwise the directive stops working, which is something I cannot do.
I already tried transcluding, but that didn't seem to help, not to mention that the directive itself works, so it doesn't seem logical to have to do that just for the test.
Any thoughts ?
Ok, I figured out the solution.
At first found one way to solve this, which is:
Initialize the gridOptions variable with some data so that the ui-grid will get constructed
However, once I got that to work, I tried to add 'expect' statements, when it hit me that now I have a lot of 3rd party html to test, which is not what I want.
The final solution was to decide to mock the inner directive (which should be tested elsewhere), and to use the mock's html instead.
Since ngMock does not support directives, I found this great article explaining how to mock directives using $compileProvider, which solved my problem altogether.

Angular: ng-bind-html filters out ng-click?

I have some html data that I'm loading in from a json file.
I am displaying this html data by using ngSanitize in my app and using ng-bind-html.
Now I would like to convert any links in the json blob from the standard
link
to:
<a ng-click="GotoLink('some_link','_system')">link</a>.
So I'm doing some regExp on the json file to convert the links, but for some reason however ng-bind-html is filtering out the ng-click in it's output, and I can't figure out why. Is it supposed to do this, and if so is it possible to disable this behavior?
Check out this jsFiddle for a demonstration:
http://jsfiddle.net/7k8xJ/1/
Any ideas?
Ok, so the issue is that it isn't compiling the html you include (angular isn't parsing it to find directives and whatnot). Can't think of a way to make it to compile from within the controller, but you could create a directive that includes the content, and compiles it.
So you would change
<p ng-bind-html="name"></p>
to
<p compile="name"></p>
And then for the js:
var myApp = angular.module('myApp', ['ngSanitize']);
angular.module('myApp')
.directive('compile', ['$compile', function ($compile) {
return function(scope, element, attrs) {
scope.$watch(
function(scope) {
return scope.$eval(attrs.compile);
},
function(value) {
element.html(value);
$compile(element.contents())(scope);
}
)};
}]).controller('MyCtrl', function($scope) {
var str = 'hello http://www.cnn.com';
var urlRegEx = /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[\-;:&=\+\$,\w]+#)?[A-Za-z0-9\.\-]+|(?:www\.|[\-;:&=\+\$,\w]+#)[A-Za-z0-9\.\-]+)((?:\/[\+~%\/\.\w\-]*)?\??(?:[\-\+=&;%#\.\w]*)#?(?:[\.\!\/\\\w]*))?)/g;
result = str.replace(urlRegEx, "<a ng-click=\"GotoLink('$1',\'_system\')\">$1</a>");
$scope.GotoLink = function() { alert(); }
$scope.name = result;
});
Angular 1.2.12: http://jsfiddle.net/7k8xJ/4/
Angular 1.4.3: http://jsfiddle.net/5g6z58yy/ (same code as before, but some people were saying it doesn't work on 1.4.*)
I still faced some issue with the compile, as that was not fulfilling my requirement. So, there is this, a really nice and easy hack to work around this problem.
We replace the ng-click with onClick as onClick works. Then we write a javascript function and call that on onClick event.
In the onClick function, we find the scope of the anchor tag and call that required function explicitly.
Below is how its done :)
Earlier,
<a id="myAnchor" ng-click="myControllerFunction()" href="something">
Now,
<a id="myAnchor" onClick="tempFunction()" href="something">
at the bottom or somewhere,
<script>
function tempFunction() {
var scope = angular.element(document.getElementById('myAnchor')).scope();
scope.$apply(function() {
scope.myControllerFunction();
});
}
</script>
This should work now. Hope that helps someone :)
For more info, see here.
Explicitly Trusting HTML With $sce
When you want Angular to render model data as HTML with no questions asked, the $sce service is what you’ll need. $sce is the Strict Contextual Escaping service – a fancy name for a service that can wrap an HTML string with an object that tells the rest of Angular the HTML is trusted to render anywhere.
In the following version of the controller, the code asks for the $sce service and uses the service to transform the array of links into an array of trusted HTML objects using $sce.trustAsHtml.
app.controller('XYZController', function ($scope, $sce) {
$sce.trustAsHtml("<table><tr><td><a onclick='DeleteTaskType();' href='#workplan'>Delete</a></td></tr></table>");

Resources