Using scope in directive and service - angularjs

I am trying to create a developer console in my Angular app and I have the following problem.
On every page of my application I want my developer console to be available (like a floating thing in a corner of the screen). I have HTML template and I am switching content with ui-view. My controllers can include consoleService and then call something like myConsole.log(...). This function should add message to some array and then display it on web. For my console I have directive with HTML template, where I want to use ng-repeat to display all messages.
And now my problem. How should I design this? I was thinking like this:
First solution
Well, I can create global JS variable, store all messages there and then use <script> and write em out in my directive template.
WRONG! Not respecting Angular-way of doing things
Second solution
Ok, I`ll use $scope and put my messages there, and then I can use that magical Angular data bind to just let it work on its own. Ok, but here I have a problem, how can I inject $scope into my service?
WRONG! Injecting $scope into service is nonsense, because it would not make any sense, and again, it kinda oppose the Angular-way.
Third solution
Fine, lets store my messages on console service, since all services in Angular are singletons it should work pretty fine, but... there is no $scope inside a service, so I need to return this array of messages to a controller and then assign it to $scope in controller.
??? What do you think? My problem with this solution is that in every controller I need to assign message array to $scope and I just dont want to do that, since I need this console everywhere, on every page.
I hope that my explanation of what I want to do is clear, so I am just hoping for a hint, or advice on how to design it in Angular.

Use the third solution. Your service will have an array with all the messages and each controller can use one of it's functions (service.log(...)) which will basically just add one message to the service list.
Then on your directive, you just assign the getMessages to the directive scope:
var app = angular.module('plunker', []);
app.service('log', function(){
var messages = [];
return {
getMessages: function() { return messages; },
logMessage: function(msg) { messages.push(msg); }
}
})
app.controller('MainCtrl', function($scope, log) {
$scope.msg = '';
$scope.addMessage = function() {
log.logMessage($scope.msg);
};
});
app.directive('console', function(log) {
return {
restrict: "E",
scope: {},
replace: true,
template: "<div><div ng-repeat='msg in messages'>{{msg}}</div></div>",
link: function(scope, el, attr) {
scope.messages = log.getMessages();
}
};
});
plunker: http://plnkr.co/edit/Q2g3jBfrlgGQROsTz4Nn?p=preview

Related

When does Angulars link function run?

From what I understand it only runs once before the page is rendered. But is there ever a case where it runs after the page has been rendered?
I tried testing a few things with this plnkr:
angular
.module('app', [])
.directive('test', function($http) {
return {
restrict: 'E',
template: '<input />',
link: function(scope, el, attrs) {
var input = angular.element(el[0].children[0]);
input.on('change', function() {
console.log('change event');
scope.$apply(function() {
console.log('digest cycle');
});
});
input.on('keyup', function() {
console.log('keyup event');
var root = 'http://jsonplaceholder.typicode.com';
$http.get(root+'/users')
.success(function() {
console.log('http request successful');
})
.error(function() {
console.log('http request error');
});
});
console.log('link function run');
}
};
});
Does typing in the input field cause the link function to run?
Do event listeners cause the link function to run?
Do http requests (made with $http?) cause the link function to run?
Do digest cycles cause the link function to run?
The answer to all of these questions seem to be "no".
The link function runs when an instance of the directive is compiled, in this case, when a <test></test> element is created. That can be when angular's bootstrapping compiles the page, when it comes into being from a ng-if, when a ng-repeat makes it, when it's made with $compile, etc.
link will never fire twice for the same instance of the directive. Notably, it fires right after the template has been compiled in the directive's lifecycle.
1 - No, it causes to change the only ng-model if you have it binded.
2 - No, it will only launch the code inside the event binds.
3 - Again no, the event bind will launch the $http.get(). And please don't put an $http directly on your directive. Use a factory or something like that.
4 - Dunno
As Dylan Watt said, the directive link runs only when the directive is compiled (only once) per element/attr.... You can compile it in different ways. Plain http, $compile, ng-repeat....
You can create a $watch inside your directive to "relaunch" some code on a binded element change.
This maybe can help you: How to call a method defined in an AngularJS directive?

Sharing data between a directive and a pagination controller in Angular during an API request

I'm sure similar questions have been asked many times on Stack Overflow, but I am still confused.
I have an Angular app that shows a list of articles. The list is long, so it needs to be paginated. For pagination, I'm using UI Bootstrap.
So the app is currently organized like this:
there is a directive ("home directive") that draws the list of articles
there is a UI Bootrstrap directive for pagination with its own controller
or, schematically:
Now, when I change the page in the paginator, I need to send the request to the the server, get a new batch of articles and show them in the home directive:
So the paginator's controller is sending the request, but the home directive needs to be aware of the new response and to use it for drawing a new list of articles.
How is this done in Angular? I've read about services/factories, but am not entirely clear how the home directive becomes aware of the new response from the server. Will I need watchers in the home directory, or is this done differently? Could you please explain?
UPDATE:
OK, here is my (failing) attempt to write a service:
home-directive.js
angular.module('myApp')
.directive("homeDirective", [ 'ArticleService', function(ArticleService){
return {
restrict: 'E',
templateUrl: "home.html",
scope: { },
link: function(scope, element, attrs){
ArticleService.getArticles();
scope.$watch('ArticleService.articles', function () {
console.log(ArticleService.articles);
scope.articles = ArticleService.articles;
});
}
}]);
article-service.js
angular.module('myApp')
.service('ArticleService', ['$http', function($http){
var that = this;
this.getArticles = function(){
$http.get('/path/to/api').success(function(data) {
console.log('working');
that.articles = data;
console.log(that.articles);
});
};
}]);
The call to ArticleService.getArticles() from the directive starts the getArticles() function, as I can see from the logger in the console. The server sends its response. that.articles changes. However, the directive fails to register the change of ArticleService.articles and doesn't get the data.
UPDATE 2
OK, if in the directive, before setting a watcher, I add this line in the directive:
scope.ArticleService = ArticleService;
then it will work.

Dependencies between angular directives

I've created a small app that has 2 directives: One that adds a google map div and initialize it, and the second, which shows layers that contain markers. The directives are independent, but I want them to communicate: The layers directive is independent, yet it needs to use the google maps directive to add markers to the map. I used $broadcast through $rootScope to communicate between the directives.
The directives are defined as follows:
angular.module('googleMapModule', [])
.directive('myGoogleMap', function(){
template: '<div id="map" />',
controller: function($scope){
// some initializations
// Listen for event fired when markers are added
$scope.$on('addMarkersEvent', function(e, data){
// do something
}
}
}
angular.module('layersDirective', [])
.directive('myLayers', function() {
templateUrl: 'someLayersHtml.html',
controller: function($http, $scope, $rootScope){
// Get layers of markers, etc.
// On specific layer click, get markers and:
$rootScope.broadcast('addMarkersEvent', {
data: myMarkers
});
}
});
After this long prologue here are my questions:
How should the connection between the two directives be implemented? Is it correct to use $rootScope and $broadcast or should there be a dependency between the myLayers directive and the myGoogleMap directive?
Furthermore, I've read about when to use controller, link and compile, yet I don't know the right way to use them here. My guess is that myGoogleMap should define its API in a controller and that myLayers should be dependent on myGoogleMap.
The example I wrote here works fine in my application. I'm looking for guidance on how to do it right and to understand what I did wrong here.
Thanks, Rotem
There are some ways for directives to cooperate/communicate
If one is a sibling or child of the other, you can use require. E.g. for this template:
<dir1>
<dir2></dir2>
</dir1>
Use this code:
app.directive('dir1', function() {
return {
...
controller: function() {
// define api here
}
};
});
app.directive('dir2', function() {
return {
...
require: '^dir1',
link: function(scope, elem, attrs, dir1Controller) {
// use dir1 api here
}
};
});
A service, used by both directives to communicate. This is easy and works well if the directives can only be instantiated once per view.
Using $broadcast/$emit on the $rootScope (there is a slight difference: $broadcast will "flood" the scope hierarchy, possibly affecting performance; $emit will only call listeners on the $rootScope, but this means you have to do $rootScope.$on() and then remember to deregister the listener when the current scope is destroyed - which means more code). This approach is good for decoupling components. It may become tricky in debugging, i.e. to find where that event came from (as with all event-based systems).
Other
controller, link and compile
Very short:
Use the controller to define the API of a directive, and preferably to define most its logic. Remember the element, the attributes and any transclude function are available to the controller as $element, $attrs and $transclude respectively. So the controller can, in most cases, replace the link function. Also remember that, unlike the link function, the controller is elligible for dependency injection. (However you can still do dependency injection at the directive level, so, after all, the link function can also access dependencies.)
Use the link function to access required controllers (see case 1 above). Or, if you are feeling lazy, to define the directive's logic. I think the controller is cleaner though.
Use the compile function... rarely :) When you need very special transformations to the template (repetition is the first thing that comes to mind - see ng-repeat) or other mystical stuff. I use directives all the time, about 1% of them needs a compile function.
My guess is that myGoogleMap should define its API in the controller and that myLayers should be dependent on myGoogleMap
The question is how will you communicate this API using events? You probably need only to create an API in a custom event object. The listeners of your event will be using that custom event. If so, the controller does not really need to define an API.
As a bottom line, I am perfectly OK with using events in your case.
Generally communication between directives should be handled via controllers and using the require property on the directive definition object.
If we re-work your first directive we can simply add that method to the controller:
directive('myGoogleMap', function () {
return {
template: '<div id="map" />',
controller: function ($scope) {
var _this = this;
//Part of this directives API
_this.addMarkers = function(arg){
//Do stuff
}
}
};
});
Now we can require this controller in another directive, but one little known features is that you can actually require an array of directives. One of those directives can even be yourself.
All of them will be passed, in order, as an array to your link function:
directive('myLayers', function () {
return {
templateUrl: 'someLayersHtml.html',
controller: function ($http, $scope, $rootScore) {
// Some get layers of markers functionality
},
// Require ourselves
// The '?' makes it optional
require: ['myLayers', '?myGoogleMap'],
link: function(scope, elem, attrs, ctrls){
var myLayersCtrl = ctrls[0];
var myGoogleMapCtrl = ctrls[1];
//something happens
if(myGoogleMapCtrl) {
myGoogleMapCtrl.addMarkers(markers);
}
}
};
});
Now you can communicate explicitly opt-in by using the ? which makes the controller optional.
In order for that to work, you have to define both directives in the same module, i.e.:
var module = angular.module('myModule');
module.directive('myGoogleMap', function(){
template: '<div id="map" />',
controller: function($scope){
// some initializations
// Listen to event for adding markers
$scope.$on('addMarkersEvent', function(e, data){
// do something
}
}
}
module.directive('myLayers', function() {
templateUrl: 'someLayersHtml.html',
controller: function($http, $scope, $rootScore){
// Some get layers of markers functionality
// On specific layer click, get markers and:
$rootScope.broadcast('addMarkersEvent', {
data: myMarkers
});
}
});
Read more here.
EDIT:
Sorry i didn't understand your question, but according to your comment, quoting from the AngularJs Best Practices:
Only use .$broadcast(), .$emit() and .$on() for atomic events
that are relevant globally across the entire app (such as a user
authenticating or the app closing). If you want events specific to
modules, services or widgets you should consider Services, Directive
Controllers, or 3rd Party Libs
$scope.$watch() should replace the need for events
Injecting services and calling methods directly is also
useful for direct communication
Directives are able to directly communicate with each other through directive-controllers
You have already highlight one may for the directives to communicate using rootscope.
Another way directive can communicate if they are defined on the same html hierarchy is by use directive controller function. You have highlighted that too in your question. The way it would be done is (assuming myGoogleMap is defined on parent html), the two directive definitions become:
angular.module('googleMapModule', [])
.directive('myGoogleMap', function () {
template: '<div id="map" />',
controller: function ($scope) {
this.addMarkersEvent = function (data) {}
// some initializations
}
angular.module('layersDirective', [])
.directive('myLayers', function ($http, $rootScope) {
templateUrl: 'someLayersHtml.html',
require: '^myGoogleMap',
link: function (scope, element, attrs, myGoogleMapController) {
$scope.doWork = function () {
myGoogleMapController.addMarkersEvent(data);
}
}
});
Here you use the require property on the child directive. Also instead of using child controller all the functionality in the child directive is now added to link function. This is because the child link function has access to parent directive controller.
Just a side note, add both directives to a single module (Update: Actually you can have the directives in different modules, as long as there are being referenced in the main app module.)

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>");

Angularjs menu login logout loading

I am using Angularjs in a project.
For login logout I am setting a scope variable like below:
$scope.showButton = MyAuthService.isAuthenticated();
In markup its like
<li ng-show="showLogout">Logout</li>
When I logout it redirect to the login page but logout menu doesn't disappear.
Also tried like this:
$scope.showButton = MyAuthService.isAuthenticated();
In markup:
<li ng-class=" showLogout ? 'showLogout' : 'hideLogOut' ">Logout</li>
Seems scope change is not reflecting in my view, but when I reload page "logout menu" disappears as expected.
I also tried with directives like below:
MyApp.directive('logoutbutton', function(MyAuthService) {
return {
restrict: 'A',
link: function(scope, element, attrs, controller) {
attrs.$observe('logoutbutton', function() {
updateCSS();
});
function updateCSS() {
if (MyAuthService.isAuthorized()) {
element.css('display', 'inline');
} else {
element.css('display', 'none');
}
}
}
}
});
No luck with that too.
How can I hide it when the logout is successful and also after successful login how can I show "logout button"?
Setup a watch on MyAuthService.isAuthenticated() and when that fires, set your scope variable to the result of that service call. In your first example, the scope variable is only getting set once when the controller is initialized (I am assuming that's where it is being run). You can set the watch up in the controller or, if you want to use a directive, in the directive link function.
Something like this:
$scope.$watch(MyAuthService.isAuthenticated, function(newVal, oldVal){
$scope.showButton = newVal;
});
Edit: After read the MarkRajcok comment I realized that this solution is coupling view from business logic layer, also it exposes the service variable to be changed outside the service logic, this is undesirable and error prone, so the $scope.$watch solution proposed by BoxerBucks it's probably better, sorry.
You can use $scope.$watch as in the BoxerBucks answer, but I think that using watchers isn't proper for services, because usually you want to access services variables in differents controllers expecting that when you change that services variables, all the controllers that inject that service will be automatically updated, so I believe that this is a good way to solve your problem:
In your MyAuthServices do this:
app.service('MyAuthService', function(...){
var MyAuthServiceObj = this;
this.authenticated=false; // this is a boolean that will be modified by the following methods:
// I supose that you have methods similar to these ones
this.authenticateUser(...){
...
// At some point you set the authenticated var to true
MyAuthServiceObj.authenticated = true;
}
this.logout(){
....
// At some point you set the authenticated var to false
MyAuthServiceObj.authenticated = false;
}
});
Then in your controller do this:
$scope.myAuthService = MyAuthService;
finally in your html:
ng-show="myAuthService.authenticated"
And this should work without using a watcher like in BoxerBucks answer.
Check this excellent video about AngularJS providers, to understand how to use services properly.

Resources