Is it a good practice to call services directly from a Angular JS Directive?
Here is an example: directive to take name as input and calls a service(connects to db) to get the HTML content and render it.
a service call is made inside the linkFn to get the HTML content
or
return {
restrict:'EA',
scope:{
name:"=",
getPartial : "&"
},
link:linkFn
}
here getHTMLContent is implemented in the controller and calls the same service.
No. Imagine if you put that directive in an "ng-repeat". You'd have way too many calls to the server. How you retrieve your model should be separate from the way you present the model.
Related
I have a page that has multiple components and I've created each component as a directive. When the page is first loaded, that's when I grab all the data that should be available on the page. So all of the data exists on the controller for that route, which we'll just call pageCtrl. And then what I've been doing is binding any required data to each directive through the attributes, which of course ends up creating an isolate scope for each of them.
I know there are a few ways to share data, so given this situation, is there a recommended way of doing it? Or has anyone had better success doing it one particular way? While it's working perfectly fine the way I'm doing it, I've run into a few caveats. If I need just even one bit of information from the pageCtrl, I need to add another attribute to the directive. So it ends up creating more code on the directive element itself.
I was thinking about just creating a service that would store all the data, which the pageCtrl could initialize, instead of setting it on itself. Any feedback would be appreciated.
good question :)
First solution is to create in parent controller object and pass this object (via ng-model) to all directives. This object will be passed by reference (not by value) so controller and all directives will have access to the same object.
```
// in controller
$scope.shared_data = {someItems: []};
// in html
<my-directive ng-model=shared_data></my-directive>
Second solution is to create some simple service to store all of those data.
// in this solution you have to inject additional service to directive controller
(extended idea of point 2) creating service/factory that will be responsible by collecting and returning data. This service could be injected into directive and use the same methods to collect data. To avoid making multiple calls to API (REST) it could have some cache for each sensitive method.
Communication via events.... (probably the worsts solution for your example)
The first two ideas are probably the best, I do not know full specification of your product so final solution picking belongs to You:).
My advice is to try/play with all of those methods to really understand what is going on and how and when to use each of them :)
You can directly call your parent controller from child directive controller by using $parent.
App.controller('aCtrl', ['$scope', function ($scope) {
$scope.refresh=function(){
.......... //Updated Data get from DB
};
...........
}]);
App.directive('bDirective',function(){
restrict: 'EC',
replace: true,
scope: {},
controller: function($scope) {
$scope.$parent.refresh();
}
...
});
HTML:
<div ng-controller="aCtrl">
<div class="bDirective"></div> //directive
</div>
I have a one-page site that I am building out and this is my first time using Angular on a site. Building it on top of Laravel too for the backend but that is beyond the scope of this question.
I need to be able to open a modal on a main page view which will add a new resource (e.g. a new client) or edit a resource. I want to somehow get the form's html inside the modal body when the $uibModal.open()'s controller is called and set the $scope.modalBody equal to the injected items.modalBody (the only way this works is if I use:
$scope.modalBody = $sce.trustAsHtml(items.modalBody);
The only problem now is that anything inside the HTML body, Angular will not use it's magic and do any data-binding. It is still in the raw form of
{{ object.property }} or since I'm using Laravel and avoiding conflict with the Blade template engine:
<% object.property %>
See screenshot:
screenshot
I have been banging my head against the wall on this one...I have tried putting $scope.$apply() in my directive and my controller, neither of which worked. I have a feeling that is the source of my problem though. I have also tried making the html just a <new-client></new-client> directive and using templateUrl: 'views/clients/add.php' which would be ideal, but the template is not being included inside the <new-client></new-client>.
I'm using ui-bootstrap 0.14.3 and Angular 1.4.8.
Could this be a bug? Or am I doing something wrong? Anyone have a better way of getting a form into my modal? Let me know what code you want to see so I don't clutter this post with unnecessary code blocks.
I have come across a similar issue with using jQuery's AJAX to receive template strings and append it to a server.
So when HTML is added via jQuery, bound html string, etc., angular doesn't know it needs to automagically compile this data.
What you need to do is use the $compile service, to $compile your html and then attach the correct $scope to it:
`$compile('jQuerySelectorReturningHtmlOrAnHTMLStringThatNeedsToBeCompiled')($scope);`
There are multiple examples in Angulars Documentation for $compile that can give you an idea of what is happening. I think by what you have described the same thing is happening here in your situation.
The key is to call this $compile service function after the html has been bound to the page.
EDIT:
There are a few other options based on some comments, that will serve as a viable solution to rendering this content on your view. For example a directive that takes a string attribute representing the HTML string of your desired view.
1. Modify your directive template in the compile step:
You have the ability to modify your template before the directive compiles and binds any attributes to it, to that directives scope:
app.directive('myAwesomeCompileStepDirective', [myAwesomeCompileStepDirectivef]);
function myAwesomeCompileStepDirectiveFn() {
return {
restrict: 'EA',
compile: function compileFn(tAttrs, tElement) {
//Here you can access the attrs that are passed into your directive (aka html string)
tElement.html(tAttrs['stringThatYouWantToReplaceElementWith']);
return function linkFn(scope, element, attrs, controller, transcludeFn) {
//if all you want to do is update the template you really don't have to do anything
//here but I leave it defined anyways.
}
}
}
}
You can view a file I wrote for a npm component which uses this method to modify my directive template before it is compiled on the page & you can also view the codepen for the complete component to see it in action.
2. Use $compile service to call $compile in link function using directive attrs.
In the same way as the aforementioned method, you can instead inject the $compile service, and call the function mentioned above. This provides a bit more work, for you but more flexibility to listen to events and perform scope based functions which is not available in the compile function in option 1.
I'm trying to come up with some code which allows me to run a function in the controller but only once the whole dom is setup and ready (including the directives link function run etc.).
I'm currently communicating between ctrl/service and the directive via $rootScope broadcasts. The first broadcast at the time of the controller loading is not being picked up by the directive. The reason is of course that the controller loads before the directive link function runs. I've read a few similar questions on SO where people recommended on using $timeout for these calls. This unfortunately doesn't always work and I don't want to clutter my ctrl/services with lots of $timeout calls. Therefore I'm looking for another solution to my problem.
Communication pattern is as follows:
1.) Controller tells Service to prepare some data (via function call in service)
2.) Service tells directive to display data (via broadcast)
3.) Directive displays data ...or not in my case :(
EDIT:
As timing is essential in my app, I'm basically looking for a way to initiate a function in the controller as soon as all angular components have finished loading. That function in the controller will display content by assigning a value to a scope variable. At the same time it will start taking the time. I can of course only start doing that once the directives are loaded, otherwise the tmining is wrong or the directive is not yet ready to display content etc.
I've read through a blog post by Ben Nadel, which basically shows how directives are loaded. I was hoping to setup an outer directive which loads last so I can trigger the finished loading from there. Unfortunately that doesn't work as soon as any of the inner directives use a templateUrl.
http://www.bennadel.com/blog/2603-directive-controller-and-link-timing-in-angularjs.htm
Using $timeout would be terrible. Don't do that. You can't define how long a server call is going to take.
I would recommend using this pattern:
Have the controller use a service to load some data, and have the
promise in the controller assign the return data to a scope variable.
Pass that scope variable into your directive.
Setup a watch in the directive link function, when it loads it will go from undefined to desired value. Done!
// in your controller
YourService.all().then(function(data) {
$scope.data = data;
});
// in your view
<some-directive your-data="data"></some-directive>
// in your directive
angular.module('blah.directives').directive('someDirective', function() {
return {
scope: {
yourData: '='
},
link: function(scope, element, attrs) {
var watcher = scope.$watch('yourData', function() {
if(scope.yourData === undefined) return;
// at this point your data will be loaded, do work
// optionally kill watcher at this point if it's not going to update again
watcher();
});
}
}
});
I know that if I want to create a reusable item, such as a date picker, then creating it as a Directive is recommended.
However, let's say that on my homepage, I have a Welcome section that displays the quote of the day with a background image that comes from a Rest service. Should this be a Directive that can encapsulate the markup and controller logic? Or should it be a simple AngularJs Controller that binds to markup in my index.html?
What constitutes whether or not something should be created as a Directive?
Directive is only a wrapper for a controller. It means if you have a controller you can use it. But you also may use the same controller as a controller of a directive for example instead of link function use controller.
This allow as to draw clear line where to use directive and where to use a controller.
We have to use Controller if we want to reproduce logic of piece of HTML markup. When we want to use the same $scope assignments, the same functions inside $scope, ... but HTML markup is always different for every other place where we use this controller.
We have to use directive when we have same logic in a controller of a directive and same HTML markup.
So in your case it is definitely a directive.
This is my own common sense of course, and may not be ideal.
There are three things you will require to implement this functionality:
AngularJS Template a.k.a. Markup to display quote with an image next to it.
AngularJS service to encompass the REST call in order to fetch above details from the server.
AngularJS controller to consume the AngularJS service to feed the data back the template (point 1) to update it accordingly after every rest call.
So the fact is you can achieve this without even writing an AngularJS Directive but what if you need to replicate the same feature in many places. In that sense, you will probably have to copy the same template somewhere else which will again need a separate controller to consume the same service (as using the same controller multiple times in the DOM is not recommended and a bad practice).
With the Directive API, you can put the markup in a directive template and consume the service in a directive controller to render the UI. So the next time if you want multiple instance of the widget, you just have to inject the directive, that's it - rest will work without any issue.
App = angular.module('App', []);
App.directive('welcomeQuote', function(QuoteService) {
return {
restrict: 'E',
template: '<div><img ng-src="{{quote.img}}" /><span ng-bind="quote.title"></span></div>',
controller: function(scope) {
// returns {img: 'angular.png', title: 'AngularJS';
QuoteService.fetch().then(function(data) {
scope.quote = data;
});
}
}
});
App.factory('QuoteService', function($http) {
return function() {
fetch: function() {
return $http.get('http://quote-server.com/new')
}
};
});
Finally you can use the widget as:
<welecome-quote></welcome-quote>
I have code in a directive relating to a "highlightable image." The goal is to be able to create highlights on an image and attach a comment to it. So when you click on the image, it creates an annotation.
I have code in the controller for a tooltip toolbar, which you can use to attach a comment to the annotation. Currently, when the 'comment' button on this toolbar is pressed, the information is stored into the database. If you click 'cancel' on the toolbar, the highlight should be deleted.
My dilemma is that I'm not sure how to structure things to conform to the MVC design pattern. Specifically, clicking cancel occurs in the toolbar, but must affect the highlights (which are managed by the directive). And clicking comment must make a call to a service to modify the database.
I've considered moving the toolbar code into the directive, but decided otherwise because directives shouldn't be dealing with backend. And ideally, the toolbar should be in its own directive. I'd like to know what you think the 'correct' way of doing it is.
Based on the functionality that you described,
Create a service:
highlightImageService (this will use the $http service to persist data to the database)
addHighlightToImage(image, highlight)
attachComment(highlight, comment)
deleteHighlight(highlight)
Create a controller:
highlightImageController
createHighlight()
attachComment()
deleteHighlight()
In your directive, specify your controller and inject your service:
.directive('imageHighlighter', function(highlightImageService) {
restrict: ...
scope: ...
controller: highlightImageController,
link: function(scope, element, attr, controller) ...
})
Tie everything together in a module:
var module = angular.module('imageHighlighter', [...]);
module.controller(highlightImageController);
module.directive(imageHighlighter)
module.factory(highlightImageService);
Since everything is self-contained in a module (directives, controllers, services), all one has to do to use your module is add it as a module dependency and add directives to your page:
Script:
var app = angular.module('app', ['imageHighlighter']);
Html:
<body ng-app="app">
<div image-highlighter></div>
</body>
There's another way to structure this.
Create directive Toolbar
Create directive Highlighter
Create service DataService
Create callback functions in your controller's scope
Pass these functions as attributes to your directive
After dealing with all UI related issues, your directive will execute the appropriate callbacks
The callbacks in the controller's scope will call DataService to take the appropriate action such as: Delete the comment, Save the comment
This way you can avoid calling the service directly from the directive and have it only deal with UI issues.
Here's how to pass a function to the directive: Passing a function as an attribute to a directive