I've been using directives in AngularJS which build a HTML element with data fetched from the $scope of the controller. I have my controller set a $scope.ready=true variable when it has fetched it's JSON data from the server. This way the directive won't have to build the page over and over each time data is fetched.
Here is the order of events that occur:
The controller page loads a route and fires the controller function.
The page scans the directives and this particular directive is fired.
The directive builds the element and evaluates its expressions and goes forward, but when the directive link function is fired, it waits for the controller to be "ready".
When ready, an inner function is fired which then continues building the partial.
This works, but the code is messy. My question is that is there an easier way to do this? Can I abstract my code so that it gets fired after my controller fires an event? Instead of having to make this onReady inner method.
Here's what it looks like (its works, but it's messy hard to test):
angular.module('App', []).directive('someDirective',function() {
return {
link : function($scope, element, attrs) {
var onReady = function() {
//now lets do the normal stuff
};
var readyKey = 'ready';
if($scope[readyKey] != true) {
$scope.$watch(readyKey, function() {
if($scope[readyKey] == true) {
onReady();
}
});
}
else {
onReady();
}
}
};
});
You could use $scope.$emit in your controller and $rootScope.on("bradcastEventName",...); in your directive. The good point is that directive is decoupled and you can pull it out from project any time. You can reuse same pattern for all directives and other "running" components of your app to respond to this event.
There are two issues that I have discovered:
Having any XHR requests fire in the background will not prevent the template from loading.
There is a difference between having the data be applied to the $scope variable and actually having that data be applied to the bindings of the page (when the $scope is digested). So if you set your data to the scope and then fire an event to inform the partial that the scope is ready then this won't ensure that the data binding for that partial is ready.
So to get around this, then the best solution is to:
Use this plugin to manage the event handling between the controller and any directives below:
https://github.com/yearofmoo/AngularJS-Scope.onReady
Do not put any data into your directive template HTML that you expect the JavaScript function to pickup and use. So if for example you have a link that looks like this:
<a data-user-id="{{ user_id }}" href="/path/to/:user_id/page">My Page</a>
Then the problem is that the directive will have to prepare the :user_id value from the data-user-id attribute, get the href value and replace the data. This means that the directive will have to continuously check the data-user-id attribute to see if it's there (by checking the attrs hash every few moments).
Instead, place a different scope variable directly into the URL
My Page
And then place this in your directive:
$scope.whenReady(function() {
$scope.directive_user_id = $scope.user_id;
});
Related
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'm using this http://plnkr.co/edit/sqXVCJ?p=preview on my Angular UI accordion. I've put the directive attribute on the anchor (which is in the template) and I've overriden the template to remove the ng-click="isOpen = !isOpen" and replaced it with a function call "callSubmit". All the accordions have views loaded into them, all of the views have forms as well. The purpose of the callSubmit function is to submit the form which works fine but I get the error above.
I've read through various posts regrding adding the $timeout service (which didnt't work) and adding the safe apply method which gave me recursion and digest errors.
I'm not sure what else I can try, the function works fine, but I just keep getting that error.
!--- button
<button type="submit" class="btn btn-default hidden" id="btnSubmit_step1" ng-click="submitForm()">Trigger validation</button>
!-- function
$scope.callSubmit = function() {
document.getElementById('btnSubmit_step1').click();
};
edit: The reason that I have been triggering the button click from the controller is due to the fact that the form method is in another scope & a different controller.
So if I use broadCast from the rootScope like below. I get the broadcast event to the other controller without issue. The controller that receives the broadcast has the form in it's scope but when I try to execute the function I get nothing, no form submission at all.
$$scope.callSubmit = function() {
//document.getElementById('btnSubmit_step1').click();
$rootScope.$broadcast('someEvent', [1,2,3]);
};
$scope.$on('someEvent', function(event, mass) {
$scope.submitForm();
});
Don't click the button from a controller.
$scope.callSubmit = function() {
$scope.submitForm();
};
The documentation is clear...
Do not use controllers to:
Manipulate DOM — Controllers should contain only business logic.
Putting any presentation logic into Controllers significantly affects
its testability. Angular has databinding for most cases and directives
to encapsulate manual DOM manipulation.
Clicking a button on a page is manipulating the DOM.
related to this question: How do I create a dynamic nav which gets allowed menu items passed to it?
Basically, I have a nav outside of any views. The nav needs to display menu items which a user has access to.
I create the nav markup with a directive like so:
<tree family="treeFamily"></tree>
Where treeFamily is the data which will be used to build the navigation menu.
However, since my nav is outside of any views, it doesn't have a controller, so there is no scope variable called treeFamily. Which means the directive doesn't get any data to create a navigation.
I originally thought I could just inject a service with the data for the menu items, but then there is no way that I can see to tell an angular directive to use data taken from an injected service for binding.
The only other way that seems to be possible is to have a $rootScope variable called treeFamily and have the directive generated markup bind to that instead.
I still think you want to have a look at angular-ui router, as mentioned I in your previous question
https://github.com/angular-ui/ui-router
However, the way I'd do this without angular-ui-router is to create the service, then just inject the service in to the directive when you declare that, and use the data in there as per http://docs.angularjs.org/guide/directive.
For example:
angular.module('yourModule').service('yourService', function() {
// define your service
});
angular.module('yourModule').directive('yourDirective', function(yourService) {
return {
link: function postLink(scope, element, attrs) {
// you can now define your directive and access your yourService service
}
};
});
If you don't want to use a $rootScope variable here is a slightly hacky solution but you could get the scope by the element.
Example.
Say your data is applied to a test controller so you have a element like this
<div id="test" ng-controller="test">
You could do this example using jQuery (not required)
$('#test').scope().treeFamily
There it is you have access to the scope that you need to get your data from, demo in progress.
Demo: http://jsfiddle.net/hq26h/
In the demo the random directive is accessing the treeFamily data from the test controller when the directive is outside the controller.
If you wan't your service data to be bindable, you can do this
app.directive('something', function( $someNavDataService ) {
return function( $scope ) {
$scope.navData = $someNavDataService;
};
});
I created a custom directive in angular so that I can fade out a form on submit and replace it with a template with a custom message.
The desired workflow is as follows:
The user completes the form and clicks submit.
The controller updates the model with a new object and emits a "formSubmitted" event with some args.
The directive listens for the event and fades out the form.
The directive loads a partial html filled with the args from the event.
I resolved the first 3 steps, but I wasn't able to get around the final step (I don't want to hardcode the html as Strings, I want to pull it from another file if possible).
How can it be done?
Edit: some sample code (simplified):
This is the form:
<section class="hero-unit {{containerClass}}" tk-jq-replace-children>
<h2>{{formTitle}}</h2>
<form class="form-search" name="solform" novalidate>
<fieldset>
...
This is the controller:
if( formWasSavedOk ){
$scope.formSubmittedMsg = msg;
//Here emits the custom event
$scope.$emit( 'solicitud:formSubmitted' );
}
This is the directive:
.directive( 'tkJqReplaceChildren', function( $compile ){
return {
link: function( $scope, $iElement/*, $iAttrs*/ ){
//The directive listens for the event
$scope.$on( 'solicitud:formSubmitted', function( /*event*/ ){
$iElement
.children()
.fadeOut(1000)
.promise()
.done(function(){
var $detachedElments = $(this).detach();
//The html is compiled and added to the DOM
$iElement.html( $compile( "<h2>{{formSubmittedMsg}}</h2>" )($scope) );
$scope.$apply();
});
});
}
};
});
<h2>{{formSubmittedMsg}}</h2> is the code I want to pull from app/partials/create/createdOk.html (it is way more than just a header, that's why I want to load it from a file)
I'm not sure if you are looking for the $http service. I have created a plunker http://plnkr.co/edit/13kFLh9RTsIlO4TaFIFQ?p=preview, which doesn't cover the first three steps, but covers the 4th step you need.
In the plunker click on the text "Click here to submit the form", and notice the new text is is inserted. This new text is from the external file called tmpl.html. In the firebug console, you can notice a get call after you clicked the text, to fetch the tmpl.html
I believe the "Angular way" to fetch an external html snippet would be to use the ng-include directive:
<ng-include
src="{string}"
[onload="{string}"]
[autoscroll="{string}"]>
</ng-include>
As for why your directive didn't work, my guess is that you're fetching the html at the directive's link phase, rather than compile phase. There's a nice explanation on the difference on this answer, but it's pretty much this:
If you are going to make DOM transformation, it should be compile if
you want to add some features are behavior changes, it should be in
link.
I would recommend moving most of that logic away from the directive, to the controller: fetching the data using a $resource or $http service, and then passing the results to the newly created scope of the ng-directive.