angularjs directives using templateUrl - initialisation sequence - javascript

I have scenario where I want to send a broadcast event in one controller and have the controller for a directive receive the message. The event is sent immediately on controller startup, and the issue is that because the directive is loading the view using templateUrl, it happens asynchronously, so the event is broadcast before the directive controller is intialised. This problem doesn't happen if the view is in the main page body, but I guess there could still be an issue with controller initialisation order.
I am using the following code in my main controller:
$rootScope.$broadcast("event:myevent");
I have reproduced the issue here: http://jsfiddle.net/jugglingcats/7Wf8N.
You can see in the Javascript console that the main controller is initialised before the controller for the directive, and it never sees the event.
So my question is whether there is a way to wait until all controllers are initialised before broadcasting an event?
Many thanks

I have created a working version. I actually feel that it is a very unclean way to do it, but I could not come up with something better: http://jsfiddle.net/7Wf8N/3/
What I did is this: In the directive I added some code which will increase a counter in $rootScope upon initialization. I use a counter because as you said, you want to wait for more than one controller:
$rootScope.initialized = ( $rootScope.initialized||0 ) +1;
In the "RegularCtrl" I added a watch on this counter and if the counter reaches the correct value (everything is initialized) I send the event:
$rootScope.$watch('initialized', function() {
if ( $rootScope.initialized == 1 ) {
$rootScope.$broadcast("event:myevent");
}
});

Are you ng-view? If so, you have the $viewContentLoaded event available. This will fire after all the dom is loaded.
http://docs.angularjs.org/api/ngRoute.directive:ngView
function MyCtrl($scope, $rootScope) {
$scope.$on('$viewContentLoaded', function() {
$rootScope.$broadcast('event:myevent');
});
}
If you aren't using ng-view, you could just set a variable and use data-binding to your directive.

Related

$(document).ready alternative for AngularJS

I'm using a template called Gentelella and I'm trying to implement AngularJS into it. However, I'm having an issue with a certain Javascript file. In the end of this file, a $(document).ready function is called which initialises Javascript code that makes some changes in the HTML code. The issue is that the $(document).ready function is called too early, before the HTML is fully loaded.
This issue occurs probably because I'm using ngRoute and this injects the template html file into the ng-view of the index.html. When this happens, the DOM probably already announces a document ready before AngularJS has injected the template (=HTML).
So basically, I just need to find a way to call some code in a Javascript file once AngularJS has injected the template.
I attached some code to gain some insight into the issue:
Snippet of the custom.min.js
$(document).ready(function () {
init_sparklines(), init_flot_chart(), init_sidebar(), init_wysiwyg(), init_InputMask(), ...
});
Snippet of the main.js:
.config(function($routeProvider, $httpProvider) {
$routeProvider.when('/', {
templateUrl : 'dash.html',
controller : 'dash',
controllerAs: 'controller'
}).when('/login', {
templateUrl : 'login.html',
controller : 'navigation',
controllerAs: 'controller'
}).when('/plain_page', {
templateUrl : 'plain_page.html',
controller : 'dash',
controllerAs: 'controller'
}).otherwise('/');
$httpProvider.defaults.headers.common["X-Requested-With"] = 'XMLHttpRequest';
})
Thanks in advance!
Many jQuery plugins depend on a workflow of 1. draw the DOM. 2. run an init() function to set up code against those DOM elements.
That workflow fares poorly in Angular, because the DOM isn't static: Angular sets up and destroys DOM nodes on its own lifecycle, which can overwrite event bindings or DOM changes made outside Angular. Document ready isn't particularly useful when you're using Angular, because all it indicates is that Angular itself is ready to start running.
To use Angular effectively, you have to get into the habit of initing code only when it's actually needed. So instead of a big bucket of init_foo(); init_bar(); on document.ready, you should have a Foo directive with its own init code, and a Bar directive with its own specific init code, and so on. Each of those directives should only modify the DOM created by that specific directive. This is the only safe way to ensure that the DOM elements you need to modify actually exist, and that you're not creating conflicts or unexpected interdependencies between directives.
To take one example: I'm guessing your init_flot_chart() crawls down through the DOM looking for a particular element inside of which it'll draw a flot chart. Instead of that top-down approach, create a directive:
angular.module('yourApp')
.directive('flotWrapper', function () {
return {
template: "<div></div>",
scope: {
data: '#'
},
link: function(scope, elem, attrs) {
var options = {}; // or could pass this in as an attribute if desired
$.plot(elem, scope.data, options); // <-- this calls flot on the directive's element; no DOM crawling necessary
}
};
});
which you use like this:
<flot-wrapper data="{{theChartData}}"></flot-wrapper>
...where theChartData is an object containing whatever data is to be drawn in the chart. (You can add other attributes to pass in whatever other parameters you like, such as the flot options, a title, etc.)
When Angular draws that flotWrapper directive, it first creates the DOM element(s) in the directive template, and then runs whatever is in its link function against the template's root element. (The flot library itself can be included via a plain old <script> tag so its plot function is available when the directive needs it.)
(Note that this wouldn't update automatically if the contents of theChartData change; a more elaborate example which also watches for changes and responds appropriately can be seen here.)
As you are using ngRoute, your controller will run when the page is loaded.
You can call an init() method when the controller starts to do whatever you want.
function MyCtrl($scope) {
function init() {
console.log('controller started!');
}
init();
}
As a side note, it is a best practice recommanded in John Papa Angular's guide.
Another possibily is to use the ng-init directive, for example:
<div ng-controller="MyCtrl" ng-init="myFunction()">...</div>
There is a lifecycle hooks named $postLink() in recent version of angular.js, Called after this controller's element and its children have been linked. Similar to the post-link function this hook can be used to set up DOM event handlers and do direct DOM manipulation.Check the guide
This is related to angular digest cycle, it's about how angular works underneath the hood, data binding etc. There are great tutorials explaining this.
To solve your problem, use $timeout, it will make the code execute on the next cycle:
app.controller('Controller', function ($scope, $timeout) {
$scope.$on('$viewContentLoaded', function(event) {
$timeout(function() {
init_sparklines(), init_flot_chart(), init_sidebar(), init_wysiwyg(), init_InputMask(), ...
},0);
});
});
$document.ready(function() {
$scope.$on('$viewContentLoaded', function() {
$timeout(function() {
init_sparklines(), init_flot_chart(), init_sidebar(), init_wysiwyg(), init_InputMask(), ...
})
})
})
update Notice: sry guys, this is not a rigorous solution, plz see another answer I wrote

Directive Update then run Query to Update Dom on Click

I've looked at more than 10 different directive questions and none have worked for me thus far. I'm trying to have my controller recognize site.selectedUnit has changed which updates selectedChannel in my custom directive. The first time works great, but after that the ng-click does not change the view info. The selectedChannel is registered and stored on my chatList controller, but the http requests are not called again.
If i click on a unit in site.html, the selectedChannel for the unit number changes in the chatList view, so I'm guessing the http requests are not being called again.
How do I add a listener to either the directive or my chatList controller so I can call the http requests again when selectedChannel changes?
Best I can tell from the code you've provided, you need to make a few changes
site.controller
Add a definition for $scope.site otherwise site.selectedUnit will only be available in the context that it is defined in the HTML via ng-click.
$scope.site = {
selectedUnit: null
};
chatList.directive
Remove the isolate scope and inject the selectedChannel property via bindToController so that it will be accessible in the controller.
.directive('chatList', function() {
return {
restrict: 'E',
bindToController: {
selectedChannel: '='
},
templateUrl: 'chatList.html',
controller: 'ChatListController',
controllerAs: 'chatList'
};
})
chatList.controller
Watch for changes to selectedChannel.id to make your $http.get() call
vm.tenants = [];
$scope.$watch(angular.bind(vm.selectedChannel.id, function() {
// make http.get call here
}))
You have other issues as well that are too many to list so here's a working plunker

How to put listen over $destroy while using controllerAs syntax in angular

Now I'm getting rid of $scope dependency from my angular controller to ensuring that I could easily migrate my code to Angular2. My current angular version is 1.4.X.
While doing the same thing there is place while I placed $destroy listener over my controller scope like $scope.$on('$destory', function() ....).
I can see $on method only available on $scope of controller, but how can I achieve it without using $scope dependency.
If you are using angular 1.5+ they added lifecycle hooks that you can opt into on your controller. No $scope needed. Just add a function called $onDestroy() to your controller and it will be called when your controller is being clean up:
$onDestroy() - Called on a controller when its containing scope is
destroyed. Use this hook for releasing external resources, watches and
event handlers.
Example from http://blog.thoughtram.io/angularjs/2016/03/29/exploring-angular-1.5-lifecycle-hooks.html:
function MyCmpController($element) {
var clickHandler = function () {
// do something
};
this.$onInit = function () {
$element.on('click', clickHandler);
};
this.$onDestroy = function () {
$element.off('click', clickHandler);
};
}
After some research I found that the $destroy event is emitted while removing DOM & on that same DOM scope it broadcasts $destroy event, but at the same time same that event has been propagated on DOM which is going to be removed.
You know there is one dependency $element, if you inject that in controller will give you the DOM where you have ng-controller directive place. So inject $element inside your controller & then place listener over it like below
$element.on('$destroy', function(){
//write clean up code here
});
Mine solution will work till 1.4.X version. For 1.5.3+ version you
could use Angular lifecycle hook which is $onDestroy like shown by
#pgreen2 in above answer. Thanks :)

Add AngularJS directive at runtime

I am creating a game where the first thing that needs to happen is some state is loaded in from an external JSON file - the contents of one of my directives are dependent on this data being available - because of this, I would like to delay applying the directive until after the data has loaded. I have written the following:
window.addEventListener('mythdataLoaded', function (e) {
// Don't try to create characters until mythdata has loaded
quest.directive('character', function() {
return {
restrict: 'A',
scope: {
character: '#'
},
controller: 'CharacterCtrl',
templateUrl: 'partials/character.html',
replace: true,
link: function(scope, element) {
$(document).on('click', '#'+scope.character, function () {
$('#'+scope.character+'-popup').fadeToggle();
});
}
};
});
});
// Load in myth data
var myth_data;
$.getJSON("js/mythdata_playtest.json", function(json) {
myth_data = json;
window.dispatchEvent(new Event('mythdataLoaded'));
});
However, it appears that my directive's link function never runs - I'm thinking this is because angular has already executed the part of it's cycle where directives are compiled/linked by the time this directive gets added. Is there some way to force angular to compile this directive after it is created? I googled around a bit, and some people suggested adding $compile to the link function for similar issues - but the link function is never run, so that doesn't work for this case. Thanks!
It seems to me it would be better to always configure the directive, to do the JSON call in the directive, and attach logic to the element in the JSON call's success handler. This would, if I understand you correctly, do what you want.
AngularJS is meant as a framework, not a library, so using it in the way you mentioned is not recommended. Exactly as you mentioned, AngularJS does a lot of things for you when it runs. AngularJS, by default, runs on document loaded, and your $.getJSON callback arrives after that. When AngularJS runs it does all its magic with compiling the content and all that.
As a sidenote, it's also more the Angular way to use $http over $.getJSON.
I think you're thinking about this the wrong way. A major ideology in angular is that you set up declarative elements and let it react to the state of the scope.
What I think you might want to do is pass in what you need through the directive scope, and use other angular built in directives to hide or show your default ("non directive") state until the scope gets set from the controller for example.
Example:
You want a box to be hidden until an api call comes back. Your directive sets special styles on your element (not hidden). Instead of delaying to dynamically set your directive, you can pass in a scope var with a default value and use something like ng-show="data.ready" in your directive template to handle the actual dom stuff.

AngularJS Directive Isolate Scope Not Updating Parent

I've got a directive that has a model bound 2-ways, when calling a save() method via ng-click the parent scope isn't updated unless I call $scope.$apply() which then throws the $apply already in progress error.
I'm using ngResource, and the event has a listener calling $scope.model.$save();
Is there a work-around for this? Or am I doing something completely wrong?
.directive('editable', function(){
return {
restrict: 'AE',
templateUrl: '/assets/partials/editable.html',
scope: {
value: '=editable',
field: '#fieldType'
},
controller: function($scope){
...
$scope.save = function(){
$scope.value = $scope.editor.value;
$scope.$emit('saved');
$scope.toggleEditor();
};
}
};
})
UPDATE
It looks like it is updating the parent after all but that the emit is being fired before the digest has finished completing. I can force it to the end of the stack using $timeout but it feels a bit hacky. Is there a better way?
$scope.$on('saved', function(){
$timeout(function(){
$scope.contact.$update();
}, 0);
});
How are you calling $scope.save ? If you use one of the angular directives, like ng-click, angular will automatically run a digest cycle for you and therefore you don't need to call $scope.$apply(). Internally, angular checks if a digest is already in progress before starting another cycle, so it will handle the issue of digest already in progress for you if you use one of the built-in directives. For example, put this in your directive's template...
<button ng-click="save()">Save</button>
If you need to call save from the controller or link function, you can do a little hack to prevent the 'digest already in progress' error. Use the $timeout service to defer the call to the end of the call stack...
$scope.save();
$timeout(function() {
$scope.$apply();
}, 0);
We are setting the timeout to 0, so there is no real delay, but this is still enough to push the $apply call to the end of the current call stack, which allows the digest that is in progress to finish first and prevents the error. This is not ideal and could imply a larger issue with your design, but sometimes you just have to make it work

Categories

Resources