17

Is it possible to load plain old JS or AMD modules from an Angular Controller? I have previously used RequireJS for this.

I have used AngularJS and RequireJS on a fairly large project before. I am working on a new project based on the MEAN Stack seed, and this does not use requireJS.

I am not entirely clear, but Angular has a system for loading modules -- can I load a particular piece of javascript from within my angular controller?

Is there a way to modify my module() declaration to include additional regular javascript files?

Thanks!

EDIT: To give a bit of understanding on what I am doing, I have a page that edits a few different forms. Each of these is saved into my database as a "form". Depending on the type of form, different dictionary values are mapped to different fields in different sub-views. Some of my forms have e.g dropdowns or lists of inputs. These are different, but everything else about the 'form' is handled in a generic way.

So I have this singular form controller that handles a bunch of different forms, and I am very happy with the result. The main issue comes from that each form has a separate set of data I would like to avoid loading unless I need.

I can check which form I am loading by checking my dropdown that drives my ng-include (which loads the subform).

In the short term I have just loaded everything and created namespaces under my scope to differentiate.

e.g $scope.form1 and $scope.form2 for data/rules specific to a particular form. I'd just as soon rather not load the js that I don't need.

Edit 2: http://jsfiddle.net/HB7LU/1320/

function MyCtrl($scope) {       
    $scope.doSomething = function()
    {
     //I'm used to wrapping with e.g require('jquery..... here, can I do the same thing with angular?   
        alert(" I need to run a jquery function here...");
        var xml = $(someblock);
    };
}

I've put up a fiddle with exactly what I am talking about. I want to load arbitrary JS depending on certain paths in my controller, and only when I need them.

Basically I have some larger namespaces I want to load depending on one of many options selected, and it would be expensive to just load them all.

m59
  • 43,214
  • 14
  • 119
  • 136
Yablargo
  • 3,520
  • 7
  • 37
  • 58
  • To clarify, are you wanting to lazy load non-Angular code using Angular? – m59 Dec 14 '13 at 01:39
  • Yes. I've updated my question to state this up front. I have a nested set of views where a dropdown box loads a second angular view (via include) with a second controller.. So I was having timing issues including it in the sub-template directly – Yablargo Dec 14 '13 at 01:51
  • You would need to use an Angular promise, create a script tag and add an onload listener to it that will resolve the promise so that your logic can continue after the script is ready. I lazy load entire modules into my angular app (main module). Loading Angular specific code after bootstrapping is a little more in depth, but the logic is essentially the same as I laid out above, just that you have to also register the loaded code with Angular. I'll expand on this with a posted answer if you give me a little more guidance on how to help you. – m59 Dec 14 '13 at 01:54
  • I've put up a stupid example at http://jsfiddle.net/HB7LU/1320/ – Yablargo Dec 15 '13 at 03:13
  • I guess a more likely example is let's say I have an option that loads something for a specific country, and each country has 80k of data to load in of specific validation, lists, etc. I don't want to load 80k x 100 different variations, but I want to load the country's data when I pick that particular country. – Yablargo Dec 15 '13 at 03:22

3 Answers3

19

Ok, I commented my code so that it should explain everything. If you have any further questions, just let me know. This is the solution to the issues as are further explained in your comments. Live demo here (click).

Markup:

<!DOCTYPE html>
<html ng-app="myApp" ng-controller="myCtrl">
<head>
</head>
<body>

  <button ng-click="load()">Load Foo</button>
  <!-- I'm using this to bootstrap the lazy loaded script -->
  <section ng-repeat="item in loaded" lazy="item"></section>

<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.1.5/angular.min.js"></script>  
<script src="script.js"></script>
</body>
</html>

JavaScript:

var app = angular.module('myApp', []);

app.controller('myCtrl', function($scope) {
  //array of things to load
  $scope.lazyThings = [
    {directive:'my-foo-directive', file:'foo.js'}  
  ];
  $scope.loaded = [];
  $scope.load = function() {
    var loadIndex = $scope.loaded.length;
    if ($scope.lazyThings[loadIndex]) {
      $scope.loaded.push($scope.lazyThings[loadIndex]);
    }
  }
});

app.factory('myService', function($http) {
  return {
    getJs: function(path) {

      return $http.get(path).then(function(response) {
        deferred.resolve(response.data);
      });

    }
  }
});

//this adds an attribute to kick off the directive 
//for whatever script was lazy loaded
app.directive('lazy', function($compile, $q, myService) {
  var directiveReturn = {
    restrict: 'A',
    scope: {
      lazy: '='
    },
    link: function(scope, element) {
      myService.getJs(scope.lazy.file).then(function(data) {
        return addScript(scope.lazy.file, data, scope);
      }).then(function() {
        var $span = angular.element('<span></span>').attr(scope.lazy.directive, '');
        $compile($span)(scope);
        element.append($span);
      });
    }
  }

  var scriptPromises = {};
  function addScript(file, js, scope) {
    if (!scriptPromises[file]) { //if this controller hasn't already been loaded
      var deferred = $q.defer();
      //cache promise)
      scriptPromises[file] = deferred.promise;

      //inject js into a script tag
      var script = document.createElement('script');
      script.src = 'data:text/javascript,' + encodeURI(js);
      script.onload = function() {
        //now the script is ready for use, resolve promise to add the script's directive element
        scope.$apply(deferred.resolve());
      };
      document.body.appendChild(script);
      return deferred.promise;
    }
    else { //this script has been loaded before
      return scriptPromises[loadFile]; //return the resolved promise from cache
    }
  }

  return directiveReturn;
});

app.directive('myFooDirective', function() {
  return {
    restrict: 'A',
    link: function(scope, element) {
      //put the logic from your lazy loaded "foo.js" script here
      element.text(foo.someFunction());
    }
  }
});

Sample lazy loaded script:

var foo = {
  someFunction: function() {
    return 'I am data from a lazy loaded js function!';
  }
};

There are plenty of ways that you could implement the concept I demonstrated here. Just think about how you would like to use it, and write some directives to carry it out. Angular makes most things pretty simple.

Note: Injecting the script tag is optional - but I greatly prefer that rather than executing the script directly. When using the script tag, you will be able to track all of the loaded files with the dev tools under "Resources" in the "Scripts" section.

m59
  • 43,214
  • 14
  • 119
  • 136
  • I'd recommend checking out how Labjs async loads js and executes in the desired ordered. Might be a helpful lazy load method – Michael J. Calkins Dec 15 '13 at 08:01
  • @MichaelCalkins Hmm..I don't see what you mean. It seems like that would be counter-intuitive with Angular. – m59 Dec 15 '13 at 17:40
  • Good work - the getJS method can be simplified to just: return $http.get(path) if the dfd doesnt do anything. – oooyaya Apr 29 '14 at 15:17
  • +1 Nice post. There no need to use `$q` inside your `getJs` func of `myService` as `$http` already returns a promise. E.g. `getJs: function(path) { return $http.get(path).then(function(response) { return response.data; }) ;}` – GFoley83 Jul 06 '14 at 00:39
  • @GFoley83 that's correct :) I was new with promises back then. Thanks for noting that!! – m59 Jul 06 '14 at 00:52
  • @M59 This is a great solution +1. I tried it for multiple files, to get multiple files I have added `forEach` in `load()` function now everything is fine I want to add CSS also the same way. But when I am giving the path of CSS file it is getting loaded, but it is showing the error due to CSS code it is treating CSS as js file and showing error. So what should I do to load CSS as well as js? Thanks!. – Arpit Kumar Dec 14 '16 at 05:23
  • @ArpitMeena It's mostly the same idea, just instead of creating a `script` element, you need to create a css `link` element. – m59 Dec 14 '16 at 05:25
  • I need to write a separate piece of code or the same code I can utilize? and you mean I need to first check the file type before appending it to the body? – Arpit Kumar Dec 14 '16 at 05:31
  • @ArpitMeena none of my answer is meant to be copied and pasted into an application. It's just a demonstration of how the general problem can be solved. My function `addScript` is only for javascript. You would need to think through what your goal is, the API you desire, and then you can follow the ideas in my example an adapt it to your needs. There are too many possible answers to your question. – m59 Dec 14 '16 at 05:40
9

Actually i don't know much about angular's directives and it's modular things, but with my basic knowledge i build some preloading function for loading my JS files.... loadScript function is defined in my app.js which is included in main html page.

function loadScript(src,callback){

    var script = document.createElement("script");
    script.type = "text/javascript";
    if(callback)script.onload=callback;
    document.getElementsByTagName("head")[0].appendChild(script);
    script.src = src;
}

inside controller use something like this

app.controller('myCtrl', function($scope) {
  //array of things to load are saved in a JavascriptFile
  loadScript("URL_To_JavascriptFile");
});
Sooraj ER
  • 121
  • 1
  • 6
1

Angular's module system is not like RequireJS. Angular only concerns itself with module definition and composition but not with module loading.

That means angular will not issue an HTTP request for loading any javascript, you are responsible for loading that yourself, by using <script> tags or another javascript loader, like requirejs.

Furthermore, angular requires that all modules be loaded and configured before the angular application is bootstrapped. This makes it difficult to support any sort of lazy-loading schemes for angular modules.

Making angular be able to deal with more dynamic loading of modules is a featured planned for future releases.

Daniel Tabuenca
  • 13,147
  • 3
  • 35
  • 38
  • I don't think this is quite accurate. I lazy load lots of things with Angular and it's fairly straightforward. – m59 Dec 14 '13 at 01:53
  • Can you elaborate on what kind of things you load? Of course you can load templates, and you can execute arbitrary ajax requests, but you cannot easily load additional modules. All modules have to be loaded and defined before the injector is created when bootstrapping the app. – Daniel Tabuenca Dec 14 '13 at 02:11
  • I lazy load entire module dependencies into Angular. See my answers here http://stackoverflow.com/questions/20410447/lazy-loading-angular-views-and-controllers-on-page-scroll/20413928#20413928 and here http://stackoverflow.com/questions/14855083/directive-for-lazyloading-data-in-angularjs/20415662#20415662 but those are a little more basic than loading a whole module. In order to do that, I sort of parse the module into each type (controller, filter, etc) and register them all one by one. – m59 Dec 14 '13 at 02:16
  • I stand by my comments. Yes, if you dig into the internals, violate the intent of the frameworks by saving providers that are meant to be only used during initialization, and generally hack around with things you can manage to load something after the fact. However this is is not standard angular, but rather a hack to get around the frameworks default behavior. – Daniel Tabuenca Dec 14 '13 at 06:08
  • You make it sound like a bad thing. Angular obviously needs this functionality (evidenced by the plans of the developers) and it's easy to accomplish. – m59 Dec 14 '13 at 06:12
  • 1
    The functionality is fine, I'm just saying that the way one has to implement this in current angular could be considered a hack. It works by getting ARROUND the module functionality, not working with it. For example, the script you load dynamically has to have it's controller defined in the global (window) scope. If one really needs this functionality and understands all the implications, then perhaps using the hack is fine. But I don't think this should be considered an easy, or recommended practice in the general case. – Daniel Tabuenca Dec 14 '13 at 16:54
  • m59 -- could I load something like, lets say, jquery as an angular module and load on demand? Or should I just go with requireJS – Yablargo Dec 15 '13 at 03:10
  • 1
    @Yablargo you definitely don't want to load jQuery after Angular is bootstrapped because Angular actually uses jQuery if it's available and otherwise fallsback to its own jqLite implementation. I hear of people often using RequireJS with Angular, so you might want to go with that...personally, I either avoid jQuery altogether or just go ahead and include it. I only lazy load other Angular functionality when needed. I actually wouldn't recommend lazy loading anything unless you really need to. In my case, it is a must. – m59 Dec 15 '13 at 03:16
  • Right. I was using it as an arbitrary example. What I actually need is a bunch of selection-specific structures/funcs that will get loaded and is really way too big to get everything. – Yablargo Dec 15 '13 at 03:42