I am just learning Angular and I have some questions regarding the architecture of my app.
The project I will be working on will be using allot of external libraries: jQuery, jQuery.ui, jsPlumb and so on, with different loading times.
I know that each code related to these libraries will have to be handled inside directives.
I worked with Backbone which uses Require JS effectively - on each view, I could set what libraries I need and the view would be loaded as soon as those libraries are available.
Now, on angular side - what would be the correct way of handling this issue?
From the top of my head, I am thinking of:
Place checks inside the router - checking if the desired libraries for a certain route are loaded.
Place checks inside each directive - for example if one directive uses jsPlumb, place a check inside and return the directives content when that library is loaded - I believe this could generate problems when interacting with other directives on the same view, which require multiple libraries with different loading times.
Load angular only after every other library is loaded - that would lead to long loading times.
What's the best way to handle all those issues?
You can create a factory to load the external library you need. Return a deferred object for the library's script after it loads. Here is one I used for d3 library:
var d3js = angular.module('d3', []);
d3js.factory('d3Service', ['$document', '$q', '$rootScope', '$window',
function($document, $q, $rootScope, $window) {
var d = $q.defer();
function onScriptLoad() {
// Load client in the browser
$rootScope.$apply(function() { d.resolve($window.d3); });
}
// Create a script tag with d3 as the source
// and call our onScriptLoad callback when it
// has been loaded
var scriptTag = $document[0].createElement('script');
scriptTag.type = 'text/javascript';
scriptTag.async = true;
scriptTag.src = 'lib/d3.v3.js';
scriptTag.onreadystatechange = function () {
if (this.readyState == 'complete') onScriptLoad();
}
scriptTag.onload = onScriptLoad;
var s = $document[0].getElementsByTagName('body')[0];
s.appendChild(scriptTag);
return {
d3: function() { return d.promise; }
};
}]);
then in your directive, use then function of the deferred to wait until it's ready
d3Service.d3().then(function(d3) {
// place code using d3 library here
}
If your directive is needing access to multiple libraries, you can chain the deferreds.
d3Service.d3().then(function(d3) {
someOtherLibSvc.getLib().then(function(lib){
// place code using d3 library and someOtherLibrary here
}
}
To avoid this chaining check out bluebird and use Promise.join, Angular comes with $q automatically so I just used that here.
Note: if you just load JQuery before angular, then angular.element will already reference JQuery. So you don't need to do this for JQuery, just load it in your main html page before Angular
In short go
http://slides.com/thomasburleson/using-requirejs-with-angularjs#/
route.
However now suffer having to put the defines and requires everywhere. For a large application this may get messy if not planned carefully. Use grunt requirejs plugin for builds because without this things would be wash.
I'm not sure this is the "correct" way to do this, but here is perhaps a simpler way to handle external libraries (in this case d3.js) within Angular 1.x code.
This is the same basic idea as #aarosil's answer (use a factory), but using fewer dependencies on other services - for what that's worth.
var app = angular.module('SomeApp', []);
app.factory('LibraryFactory', function () {
var factory = {};
factory.getD3 = function(callback) {
if(!window.d3) {
var script = document.createElement("script");
script.src = "https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.9/d3.min.js";
document.head.appendChild(script);
script.onload = function() {
callback(window.d3);
};
} else {
callback(window.d3);
}
};
//factory.getSomeOtherLibrary = ...ditto
return factory;
});
And to use the factory (eg, in a controller):
app.controller('SomeTrialWithD3Ctrl', function ($scope, LibraryFactory) {
LibraryFactory.getD3(function main(d3) {
// place code using d3 library here
});
});
And of course, the callbacks can be chained if you need multiple libraries at once.
Related
I have the same issue with this post Pass Angular scope variable to Javascript . But I can't achive my solution with their answers.
My Angular Controller
angular.module('App').controller('HomeController', [
'$rootScope', '$scope', '$state', '$timeout', 'ReportService', 'MsgService',
function($rootScope, $scope, $state, $timeout, ReportService, MsgService) {
$scope.$on('$viewContentLoaded', function() {
console.log('HomeController');
$scope.get_locations();
});
// get locations
$scope.get_locations = function() {
var data = {};
// call http get to my api
MsgService.get_all_locations(data, function(response) {
if (response.code == 1) { // success
$scope.locations_array = response.data; // data that I want to access to script
} else {
alert(response.message);
}
});
}
}
]);
My Html
<div id="map" ng-controller="HomeController">{{locations_array}}</div> // {{locations_array}} scope have the result that I want
<script type="text/javascript">
$(document).ready(function() {
var data = $('[ng-controller="HomeController"]').scope().$parent.locations_array;
console.log(data); // underfined
//var $element = $('#map');
// var scope = angular.element($element).scope();
// console.dir(scope.$parent.locations_array); // underfined
});
</script>
I tried access from browser develop tool then It can access scope. But My code can't access this.
How to solve this?
The immediate problem here is a timing issue - you are trying to read the locations_array value off the scope long before the value is populated.
The sequence of events is something like this:
ready event for document triggers, and before Angular has even thought about starting, your inline JS code runs, trying to read the value from the scope, which doesn't exist yet.
Angular bootstraps your Angular application in response to the document's ready event (this may be before #1, depending on the order of scripts on the page). This will call the HomeController constructor, that only sets up a listener for the $viewContentLoaded event.
The $viewContentLoaded event gets broadcast, and you initiate an asynchronous request for the locations.
When that returns with the locations some time later, it populates them on the scope.
Don't rely on .scope()
In addition to the timing issues, there is another major problem with your solution - it relies on the debug information being included by AngularJS. Obviously, it is by default, but it is possible to disable this debug information for significant performance gains in production.
If someone else comes along, possibly after you have left, and tries to disable debug information to improve performance or for some other reason (it is a recommended practice in production), it will stop .scope() from working.
So by relying on .scope(), you are making it so that disabling debug info, a best practice and performance booster, is not possible now or in the future for your app, because it will break things. And it won't be at all obvious to that developer that it would break anything.
So relying on .scope() for anything other than debugging should always be a very last resort.
So what do I do instead?
Like I mentioned, this is a timing problem - you need to wait until the locations are eventually loaded before running code that relies on them.
Luckily, we have many options in JS to deal with asynchronous values - callbacks, promises, RxJS observables, etc. Pick your favourite.
Example: using a global promise
In your controller, create a promise on the global scope (icky, but it needs to be outside Angular somewhere), and resolve that promise with the location data when it is loaded.
var resolveLocations;
window.locationsPromise = new Promise(function (resolve) {
resolveLocations = resolve;
});
angular.module('App').controller('HomeController', [
'$rootScope', '$scope', '$state', '$timeout', 'ReportService', 'MsgService',
function($rootScope, $scope, $state, $timeout, ReportService, MsgService) {
$scope.$on('$viewContentLoaded', function() {
console.log('HomeController');
$scope.get_locations();
});
// get locations
$scope.get_locations = function() {
var data = {};
// call http get to my api
MsgService.get_all_locations(data, function(response) {
if (response.code == 1) { // success
resolveLocations(response.data); // resolve the promise
$scope.locations_array = response.data; // data that I want to access to script
} else {
alert(response.message);
}
});
}
}
]);
Then, your normal (non-angular) javascript (which needs to run after your Angular javascript file is loaded) could use that promise to do something with the data when available:
<script type="text/javascript">
$(document).ready(function() {
window.locationsPromise.then(function (locations_array) {
console.dir(locations_array);
// do something with the data
});
});
</script>
There is probably a better way
Without knowing why you think you need access to this data outside of Angular, it's hard to say for sure, but there are likely other better ways of handling the interplay between Angular code and other Javascript code that depends on it.
Maybe you create a directive to integrate a jQuery plugin, or another service, or whatever, but since AngularJS code is just normal JS, there is no need to think of them as separate from each other. You just have to get the timing right so you have the data available. Good luck!
I'm building a small two-language app with the use of angular-translate. I want to have a language switcher in every view (controller). I'm trying to figure out how to put the code responsible for language switching into every controller. The code looks like this:
var langSwitch = $Scope.setLang = function (langKey) {
$translate.use(langKey);
};
So far I've figured that I can create a factory that looks like this:
app.factory('langSwitch', function ($rootScope, $translate) {
var langSwitch = $rootScope.setLang = function (langKey) {
$translate.use(langKey);
};
return langSwitch;
});
and inject it into controllers in this maner:
app.controller('HomeCtrl', function (langSwitch) {
// normal controller code here
});
This works but 1) I'm using $rootScope and I have a feeling this is bad practice & 2) jsHint screams that "langSwitch" is not defined. Maybe there is a simpler way to make the function global without putting it into every controller?
I'm still pretty new to Angular so don't scream at me :) Thanks.
edit
My view:
<button ng-click="setLang('en_GB')">English</button>
<button ng-click="setLang('pl_PL')">Polish</button>
Although you got the idea, you overcomplicated things a bit. You could declare the service as follows:
app.service('langSwitch', function ($translate) {
this.setLang = function (langKey) {
$translate.use(langKey);
};
});
And then inject langSwitch in the controller responsible for lang switching, as you already did. No need to inject $rootScope in the service.
You don't need $rootScope indeed unless you need to process some global events in your application. All services and factories in angular are singletons by default. That means once it created, it will be passed as the same instance in every place it is declared as a dependency. So if you want to share data and functionality between different controllers - the services will suit fine. You can change your factory code to:
app.factory('langSwitch', function($translate) {
return {
setLang: function(langKey) {
$trasnlate.use(langKey);
};
};
});
I recently had a question on how I could load jQuery when I moved to a new page
using AngularJS ui-router. Here's the answer that I accepted:
resolve:{
jquery: function($q){
if (typeof jQuery === 'undefined') {
var deferred = $q.defer();
var script = document.createElement('script');
script.setAttribute("src", "http://code.jquery.com/jquery-2.0.3.min.js");
document.getElementsByTagName("head")[0].appendChild(script);
var wait = setInterval(function(){
if (typeof jQuery === 'function') {
console.log('jQuery:', typeof jQuery)
deferred.resolve();
clearInterval(wait);
}
}, 100);
return deferred.promise;
}
}
}
Can someone tell me how I can extend this so I get the jQuery script and also another
local script from my server. The important thing for me is that the second script would need to download after jQuery as it depends on it.
The second script I need to get is /Scripts/pagedown/markdown.js
Please note I am not looking for a solution that uses an external library like require.js. I have only one time I am doing this so using an external library would be overkill.
Try using http://requirejs.org/ since it's the best solution here.
jquery-deps.js:
define(['jquery-min'], function (dep1) {
// Your local script here. Dependent file will be fetched here if needed
return someObject; //For example, jQuery object
});
You can add this in router resove:
require(['jquery-min'], function(dependency) {
// jquery will be fetched first, then more scripts will be executed.
// Add more scripts
});
Take a look at requireJS docs, it's easy to implement and don't include js files using plain javascript just because it's not safe and not cool enough.
Convert the script loading process into a more generic, standalone function meant for reuse:
var injectJs = function(url, name, deferred) {
var script = document.createElement('script');
script.setAttribute("src", url);
document.getElementsByTagName("head")[0].appendChild(script);
var wait = setInterval(function(){
if (typeof window[name] === 'function') {
deferred.resolve();
clearInterval(wait);
}
}, 100);
}
Create another promise for markdown.js:
var mdDeferred = $q.defer();
Then, instead of returning the jQuery loading promise from the function, add a callback to it:
jqDeferred.promise.then(function(){
... and inside of that callback, use injectJS again to load the markdown promise, resolving its promise once it has.
Note: you need to pass the name (a string) of the object to check for in as the third object. For jQuery, it is 'jQuery'. Find out what object will be found on the window object once the script loads and use that.
Finally, it's the markdown promise that should now be returned from the resolve function:
return mdDeferred.promise;
Review this demo to see a working example, which uses Underscore in place of markdown.js.
Please consider the following angularjs code for a controller:
(function (app) {
var controller = function ($scope, $state, datacontext) {
$scope.$parent.manageTitle = "Account Management";
$scope.accounts = [];
var init = function () {
getRecords();
};
var getRecords = function () {
return datacontext.getAccounts().then(function (data) {
$scope.$apply(function () {
$scope.accounts = data;
});
});
};
init();
};
app.controller("accountsCtrl", ["$scope", "$state", "datacontext", controller]);
})(angular.module("app"));
Removing the $scope.$apply wrapper and leaving just the "$scope.accounts = data" in the getRecords method breaks the code. The data is retrieved but the ng-repeat directive in the html is not automatically updated. I'm trying to get my arms around the entire $apply/$digest model, but it sure seems to be that the $apply should NOT be required in this case.
Am I doing something wrong?
Thanks.
<------------------------------------------ EDIT ---------------------------------------->
Ok, thanks for the responses. Here is the datacontext. It uses Breeze. I still can't figure out what the problem is - - I just don't see why $apply is required in the code, above.
(function (app) {
var datacontext = function () {
'use strict';
breeze.config.initializeAdapterInstance('modelLibrary', 'backingStore', true);
breeze.config.initializeAdapterInstance("ajax", "angular", true);
breeze.NamingConvention.camelCase.setAsDefault();
var service;
var manager = new breeze.EntityManager('api/ProximityApi');
var entityQuery = breeze.EntityQuery;
var queryFailed = function (error) {
};
var querySuccess = function (data) {
return data.results;
};
var getAccounts = function () {
var orderBy = 'accountName';
return entityQuery.from('Accounts')
.select('id, accountName')
.orderBy(orderBy)
.using(manager)
.execute()
.then(querySuccess, queryFailed);
};
service = {
getAccounts: getAccounts
};
return service;
};
app.factory('datacontext', [datacontext]);
})(angular.module('app'));
Thanks again!
Thanks for your answers. Jared - you're right on the money. By default, Breeze does not use angular $q promises, but uses third-party Q.js promises instead. Therefore, I needed $apply to synchronize the VM to the view. Recently however, the Breeze folks created angular.breeze.js, which allows the Breeze code to use angular promises, instead. By including the angular.breeze module in the application, all Breeze code will use native angular promises and $http instead.
This solved my problem and I could remove the $apply call.
See: http://www.breezejs.com/documentation/breeze-angular-service
The reason that you need to use the $apply function is the result of using Breeze to to return the data. the $apply function is used to get angular to run a digest on all the internal watches and update the scope accordingly. This is not needed when all changes occur in the angular scope as it does this digest automatically. In your code, because you are using Breeze the changes are taking place outside the angular scope, thus you will need to get angular to manually run the digest, and this is true for anything that takes place out side of angular (jQuery, other frameworks ect...). It is true that Breeze is using promises to update the data, however Angular does not know how to handle the changes after the promise returns because it is out side the scope. If you were using an angular service with promises then the view would be updated automatically. If your code is working correctly as is then it would be the correct way to use $apply in this way.
The only thing I might suggest is to change the way you are calling the apply to make sure that it will only run if another digest is not currently in progress as this can cause digest errors. I suggest you call the function as such:
if(!$scope.$$phase){$scope.$apply(function () {
$scope.accounts = data;
});
Or the other option would be to write a custom wrapper around the $apply function like this SafeApply
I would like to include the Facebook javascript libraries in an Angular project such that all the facebook API calls (login, logout, etc) are encapsulated inside a service. But because of the async nature of the FB library my code seems overly verbose and I have several calls to $rootScope.apply() which I'm not sure is best practice.
Right now I have something like this:
app.factory('Facebook', function($rootScope, $window, $q){
var FBdefer = $q.defer();
var FBpromise = FBdefer.promise;
$window.fbAsyncInit = function(){
$rootScope.$apply(function(){
FB.init(/* FB init code here*/);
FBdefer.resolve(FB);
}
}
var fb_service_api = {
login: function(){
var deferred = $q.defer();
FBPromise.then(function(FB){
FB.login(function(response){
$rootScope.$apply(
deferred.resolve(response)
);
});
}
return deferred.promise.
}
}
return fb_service_api;
})
Looking for a good design pattern here that fits well with the angular framework.
I think that you may be able to solve this using Misko Hevery's approach here to delay loading of your controller(s) until the Facebook XHR calls have 'resolved'.
This seems to have solved all of my asynchronous data loading issues and will probably mean you can just delete your $apply() calls too.