How to redefine a module in angularjs? - angularjs

I was hoping I can append a module to the main module after bootstrap.
I found this issue: https://github.com/angular/angular.js/issues/3881, which is just what I want.
They say:
I no longer feel this is necessary, although it would be nice to see warnings if we redefine a module (but even this may be beneficial during testing)
I'm not sure what redefine a module mean, so I tried it as my guessing:
html
<div ng-controller="Ctrl">
{{hi }}
<input ng-model="hi" />
<button ng-click="say()">Say</button>
<ul>
<li phone="{{p}}" ng-repeat='p in ps'></li>
</ul>
</div>
You can see there is a phone directive.
angular code
angular.module('ctrl',[])
.controller('Ctrl', ['$scope', function($scope){
$scope.hi = 'Hi';
$scope.say = function() {
alert("Say something");
};
$scope.ps = ["1234567890123","001122334455667"];
}]);
angular.module('app', ['ctrl']);
angular.element(document).ready(function () {
angular.bootstrap(document, ['app']);
});
You can see there is no phone definition yet.
Then I try to redefine the app module and append a new phone module:
angular.module('phone', [])
.directive('phone', function() {
return {
restrict: "A",
scope: {
p : "#phone"
},
link: function(scope,el){
el.text(scope.p.substr(0,5)+"...");
el.css("cursor", "pointer");
el.on("click", function(){
el.text(scope.p);
el.css("cursor", "text");
});
}
};
});
setTimeout(function() {
console.log('redefine module');
angular.module('app', ['ctrl', 'phone']);
}, 3000);
It will run in 3 seconds.
But that's still not working. I'm not sure if my approach is correct, how to fix it?
You can see a live demo here: http://jsbin.com/xukafo/1/edit
Updated:
Let me make it clear. The module I want to append to the main module is a 3rd party module, which has already defined some modules and directives. Since it's big and slow, I want to load it asynchronously, and append it to the main(bootstrapped) module. I can't modify the source code of that module.
There is already a tricky solution: https://stackoverflow.com/a/18365367/4022015
I had tried that solution, it's working but can never pass protractor e2e testing. So I created a feature request for "appending a module" but told there is already one exist(the one in the question). And people say we don't need this feature because we can "redefine" a module, but I don't know how to "redefine" it. So there is this question.

Redefining a module means, first defining a module:
angular.module('module1', []);
and then defining it again somewhere else:
angular.module('module1', [])
.config(...);
whereas the intention is to add a .config block (notice that .module() is called with a single parameter:)
angular.module('module1')
.config(...);

Related

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 script won't run when first loaded

On page load the console log prints but the toggleClass/click won't work I even use angular.element but it has the same result.I need to change state in order for the toggleClass to work.I dunno what's wrong in my code.
.run(['$rootScope', function ($rootScope) {
console.log('test');//this prints test and it's ok
//this part won't load at the first loading of page.
$('.toggle-mobile').click(function(){
$('.menu-mobile').toggle();
$(this).toggleClass('toggle-click');
});
//....
}])
even doing it this way doesn't work.
$rootScope.$on('$viewContentLoaded', function () {
angular.element('.toggle-mobile').on('click', function (event) {
angular.element(this).toggleClass('toggle-click');
angular.element('.menu-mobile').toggle();
event.preventDefault();
});
});
The Angular way to render items is different from "On DOM Ready" that is why we need to treat these as 2 separate things.
Angular could render items later on even after DOM is ready, this could happen for example if there is an AJAX call($http.get) and that is why a directive may be the recommended approach.
Try something like this:
<body ng-controller="MainCtrl">
<div toggle-Me="" class="toggle-mobile"> Sample <div class="menu-mobile">Sample 2</div>
</div>
<script>
var myApp = angular.module('myApp', []);
myApp.controller('MainCtrl', ['$scope', function ($scope) {}]);
myApp.directive("toggleMe", function() {
return {
restrict: "A", //A - means attribute
link: function(scope, element, attrs, ngModelCtrl) {
$(element).click(function(){
$('.menu-mobile').toggle();
$(this).toggleClass('toggle-click');
});
}
};
});
...
By declaring the directive myApp.directive("toggleMe",... as an attribute toggle-Me="" every time angular generates the input element it will execute the link function in the directive.
Disclaimer: Since the post lacks from a sample html I made up something to give an idea how to implement the solution but of course the suggested html is not part of the solution.

Tabs directive not exposing an api in my scope

So I'm trying the Tabs directive and having some problems.
the structure is something like:
//routes
$routeProvider..when('/course/:id', {
controller: 'CourseCtrl',
templateUrl: '/app/views/course.html'
});
//course.html
<div ng-controller="CourseTabsCtrl">
<tabset>
<tab>
<tab-heading>Title</tab-heading>
<div ng-include="'/view.html'"></div>
</tab>
....
</tabset>
</div>
Problem is i can't access the api to enable or disable tabs, select a tab, in none of the controllers CourseTabsCtrl or CourseCtrl.
Is this because the directive is working on an isolated scope? and if so, is there a way to get around that? How can i fix it?
Thanks
Looking at the source and the documentation, you should be able to pass in an expression to the <tab> directive, that dictates whether it is enabled or disabled.
app.controller('CourseTabsCtrl', function ($scope) {
$scope.expr = true;
});
<div ng-controller="CourseTabsCtrl">
<tabset>
<tab disabled="expr">
<tab-heading>Title</tab-heading>
<div ng-include="'/view.html'"></div>
</tab>
....
</tabset>
</div>
If however, this is not enough for you. You could 'hack' the tabset directive and use another controller than the one currently specified. Now, you would have to replicate the old behaviour of the default TabSetController, but you could add functionality on top of it to cater to your needs.
The best way (I've found) to do this is to decorate the directive itself.
Like so:
app.config(function ($provide) {
$provide.decorator('tabSetDirective', function ($delegate) {
// $delegate in a directive decorator returns an array. The first index is the directive itself.
var dir = $delegate[0];
dir.controller = 'CourseTabsController';
return $delegate;
});
});
You could build further on this and pass in the controller to the directive itself.
<tabset ctrl="someCustomCtrl"></tabset>
The config block would then look like this:
app.config(function ($provide) {
$provide.decorator('tabSetDirective', function ($delegate) {
// $delegate in a directive decorator returns an array. The first index is the directive itself.
var dir = $delegate[0];
dir.controller = function ($scope, $element, $attrs, $controller) {
return $controller($attrs.ctrl, {
$scope: $scope
});
};
return $delegate;
});
});
Note: If you go with the decorator way, you may have to do it in the config block for the angular-ui module. In that case, you will probably want to have a look here, to be able to configure third party modules without touching their core code.
A second gotcha to the passed-in controller way, is that you need to make use of $injector in order to get dependencies (apart from $scope, $element and $attrs) into the controller. This can either be done in the config block, or in the controller itself by adding $injector as a dependency, like so:
app.controller('CourseTabsController', function ($scope, $injector) {
var $timeout = $injector.get('$timeout');
// etc...
});
What the f* am I on about?
Given that I don't personally work with the AngularUI Bootstrap components, and the fact that there is no plunker/jsBin available I'm throwing out some tips n tricks on how to add custom behaviour to third party components, without polluting their core code.
To address the questions at the end of your post:
Is this because the directive is working on an isolated scope?
It very well might be, the idea of isolated scopes is to not pollute the outside world with their inner properties. As such, it's highly likely that the only live 'endpoint' connected to the <tab> directive API is the default AngularUI TabSetController.
... and if so, is there a way to get around that? How can i fix it?
You can either do what I've suggested and roll your own controller (bare in mind that you should duplicate the code from the TabSetController first), that way you should have full access to the endpoint of the <tab> directive. Or, work with the options that are available to the directive as of this writing and wait for some more functionality to be introduced.
I'll try to fire up a jsBin soon enough to further illustrate what I mean.
Edit: We can do the whole decorator dance and duplication of the old controller behaviour, without the need to pass in a new controller. This is how we would achieve that:
app.config(function ($provide) {
$provide.decorator('tabSetDirective', function ($delegate, $controller) {
// $delegate in a directive decorator returns an array. The first index is the directive itself.
var dir = $delegate[0];
var origController = dir.controller;
dir.controller = function ($scope) {
var ctrl = $controller(origController, { $scope: $scope });
$scope.someNewCustomFunction = function () {
console.log('I\'m a new function on the ' + origController);
};
$scope.someNewCustomFunction();
return ctrl;
};
return $delegate;
});
});
Here's a jsBin illustrating the last example: http://jsbin.com/hayulore/1/edit

How to show html in angularjs template instead of string?

I have a variable in my scope:
$scope.myvar = "Hello<br><br>World"
In my template I use:
<div>{{myvar}}</div>
The issue is myvar shows the literal text, whereas I want it to show the line breaks. How to do this? Note that I want to make it such that if I in the future, myvar gets updated with other HTML, then what is shown on the page should be the "compiled" html as opposed to the literal string with the html tags in it.
You can use ngBindHtml for that.
Keep in mind that due to Angular's Strict Conceptual Escaping the content needs to be either sanitized (using the additonal module ngSanitize) or explicitely "trustedAsHtml" (using $sce.trustAsHtml()). The latter is supposed to be used only for content you know is safe (e.g. nothing user defined) and is not recommended anyway.
Note: ngSanitize is an non-core module and should be included separately:
<script src=".../angular.min.js"></script>
<script src=".../angular-sanitize.min.js"></script>
Examples:
/* Using ngSanitize */
angular.module('app', ['ngSanitize'])
.controller('myCtrl', function ($scope) {
$scope.myHtml = 'Hello<br /><br />world !';
});
/* Using $sce.trustAsHtml() */
angular.module('app', [])
.controller('myCtrl', function ($sce, $scope) {
$scope.myHtml = $sce.trustAsHtml('Hello<br /><br />world !');
});
Note that ngSanitize will filter "non-appropriate" content, while $sce.trustAsHtml will allow anything.
See, also, this short demo.
Use ng-bind-html within <div>. Here is the example:
In your html file :
<div ng-controller="ngBindHtmlCtrl">
<div ng-bind-html="myvar"></div>
</div>
In your js:
angular.module('ngBindHtmlExample', ['ngSanitize'])
.controller('ngBindHtmlCtrl', ['$scope', function ngBindHtmlCtrl($scope) {
$scope.myvar = 'Hello<br><br>World';
}]);
Example taken from AngularJs doc.
You can use ng-bind-html to bind to HTML directly. Here's the official documentation
#ExpertSystem is correct or if you're lazy like me you could do:
lrApp.directive('bindHtml', function () {
return {
restrict: 'A',
link: function (scope, elem, attrs) {
scope.$watch(attrs.bindHtml,function(nv,ov){
elem.html(nv);
});
}
};
});
1) Add the angular-sanitize.js library. This functionality used to be a part of the main library, but the Angular team has been splitting off sections to make it more modular.
2) Use the ng-bind-html tag:
<p ng-bind-html="myvar">

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