11

I would like to use Plupload in an Angular2 component and access the Plupload JavaScript file from a CDN. I want it specific to a component so that it is not downloaded if it is not required - I want it to be in a lazy loaded module. How can I do this?

Now fully answered on this page!

The result of this quest, which included offering and awarding bounties to two people who worked hard with me on it, is as follows:

  1. Example of using Plupload with Angular 2 and TypeScript
  2. How to Lazy load a script from a CDN in Angular 2
  3. Example of how to use Plupload in a lazy loaded module
  4. How to use a lazy loaded script in Angular 2

    (See edit history for the ugly details that used to make up this question.)

Reid
  • 3,170
  • 2
  • 23
  • 37
  • Hi Reid. Do you still need help with this? – AngularChef Feb 21 '17 at 18:17
  • @AngularFrance It is not yet solved. So I would like someone to supply the answer. If you have it, that would be swell. Otherwise if nobody else comes up with a relatively complete response, I will be working on it in the next few days and if successful, post my answer. – Reid Feb 22 '17 at 04:58
  • I'd like to take a stab at it. You mention a Pupload CDN script file and having no problem using Pupload in a regular JS environment, can you provide the URL for the CDN script file and maybe a link to a plain JS Pupload example that you'd like to integrate with Angular? – AngularChef Feb 22 '17 at 08:46
  • @laser has updated their answer and perhaps they have it nailed. Will investigate over the next few hours. – Reid Feb 22 '17 at 19:07
  • @AngularFrance I updated the original post and there you see a working javascript example, using a CDN. – Reid Feb 22 '17 at 22:53
  • Ok. For future reference, it would be best to have an "official" answer (as per Stackoverflow jargon) to the question. Maybe mark one of the current answers as *the* answer or post one yourself. – AngularChef Feb 23 '17 at 08:05
  • @AngularFrance I am still seeking a working answer. – Reid Feb 23 '17 at 18:42
  • I am confused. I thought laser's answer was the one given he got the bounty (I didn't read the comments down there). What's wrong with laser's proposed solution? – AngularChef Feb 23 '17 at 18:47
  • @laser had some ideas and appeared to put some effort into it, so I gave them the bounty points which were about to expire. Their answer was not satisfactory, however. What is needed is a working example, not a bunch of ideas that everyone is going to have to try to fit together. – Reid Feb 23 '17 at 19:03
  • @AngularFrance see preceding comment. Also, if there is a mechanism to arbitrarily award bounty points to someone who posts an answer I like, and you post an answer I find suitable answer, I will award it 50 points. – Reid Feb 23 '17 at 19:13

3 Answers3

3

Here's the overview of what you need to do to create a lazy-loaded Plupload feature while loading Plupload from a CDN:

  1. When the feature is needed (e.g. user clicks a button or visits a page), dynamically add a <script> tag to the page to load the Plupload library from a CDN.
  2. Wait until the library is loaded to proceed (or you could get a "plupload is undefined" error).
  3. Display the UI to interact with Plupload in one of your Angular templates. In its simplest form, this UI consists of two buttons: "Select files" and "Upload files".
  4. Initialize Plupload and wire it up to the UI.

Complete, working code: https://plnkr.co/edit/4t39Rod4YNAOrHmZdxqc?p=preview

Please take note of the following points in my implementation:

  • Regarding #2. A better way to check whether Plupload has finished loading would be to poll the global namespace for the existence of the plupload variable. As long as window.plupload does not exist, it means the library hasn't been loaded yet and that we should NOT proceed. For simplicity my code just waits for one second and proceeds.
  • Number 4 can prove a bit tricky. Plupload makes a heavy use of direct DOM access to wire its API to the HTML (e.g. document.getElementById('filelist')). This is something Angular discourages and that you should try avoiding whenever possible. More specifically direct DOM access is used in the following places:
    • To tell Plupload which DOM element should trigger the "Select files" dialog (what they call the browse_button config option). For this I could not avoid the direct DOM reference and I used the @ViewChild decorator to get a hold of the "Select Files" button.
    • To display selected files in the template. For this I converted the Plupload syntax into the regular Angular syntax. I push selected files to a class property called fileList which I display in the template using a standard *ngFor.
    • The "Upload Files" button triggers some code that does the actual uploading and refreshes the UI to show upload progress. Once more, I converted this to regular Angular syntax using event binding and data binding.

Let me know if you have any questions.

AngularChef
  • 13,797
  • 8
  • 53
  • 69
  • One issue is loading plupload in the index.html. Please fix that and update your link in your answer. A second thing is I can't award you 50 points because I have already awarded a bounty on this answer, so I am compelled to award you 100 points, per my commitment to awarding you points. SO won't let me do that until 24 hours after I post it, which I have. Third, since your answer is not in the requested form (with lazy loaded components), and laser (another answerer) was the first to do that, I will be checking his as the answer, unless someone else has a super answer. Thank you. – Reid Feb 24 '17 at 19:10
  • You may be the super answerer, as laser's answer has a type error upon clicking the upload button, whereas yours works fine. If you could also port it to the lazy loading plunker, minimally modifying the plunker linked to in my question, that would be super and clear proof. Otherwise I will have to port it myself to see. – Reid Feb 24 '17 at 19:51
  • Hey. Not sure what you mean by "your answer is not with lazy-loaded component". The Pupload component is clearly lazy-loaded (aka loaded on demand) in my Plunkr. – AngularChef Feb 25 '17 at 01:43
  • A lazy-loaded Angular 2 component is a component that is not loaded until/unless it is called. The plunker I link to in my question has a lazy loaded component. In all cases the app component is loaded if the application is run. As in the case of your Plunker. – Reid Feb 25 '17 at 02:19
  • Add this to your answer: https://plnkr.co/edit/fq7X1w9N5j0TTcMzYm5e?p=preview - or I will have to add my own answer ;) - unless you have a better equivalent. And if you could come up with a version that loads plupload from ngOnInit() (plunker forking from that link) rather than from a button, that would be really cool. It is actually clunky to not have it load with the component. – Reid Feb 25 '17 at 07:50
  • Ahhhh, got it. You mean a "lazy-loaded module" (there's no such thing as a "lazy-loaded component"). I have updated my Plunkr as per your instructions: https://plnkr.co/edit/4t39Rod4YNAOrHmZdxqc?p=preview – AngularChef Feb 25 '17 at 08:47
  • Thank you for the bounty, Reid! – AngularChef Feb 25 '17 at 18:58
  • Running into a little issue if I toggle between eager and lazy a few times on you plunker. Perhaps you want some sort of subscription destroy? – Reid Feb 25 '17 at 21:11
  • 1
    Right, I just fixed the Plunkr. There was an issue with how the ` – AngularChef Feb 25 '17 at 22:22
2

In this approach no need for any extra loader modules.

See example (check console for Woohoo): http://plnkr.co/edit/gfUs4Uhe8kMGzPxwpBay?p=preview updated plunker: https://plnkr.co/edit/leG062tg7uX8sLrA0i2i?p=preview

You can lazyload some js by adding the script url to you document:

Create a my-lazy-load.function.ts:

export function lazyload(url) {
    // based on https://friendlybit.com/js/lazy-loading-asyncronous-javascript/

    let scripts = document.getElementsByTagName('script');
    for (let i = scripts.length; i--;) {
        if (scripts[i].src.match(url)) return true;
    }

    let s = document.createElement('script');
    s.type = 'text/javascript';
    s.async = true;
    s.src = url;
    let x = document.getElementsByTagName('script')[0];
    x.parentNode.insertBefore(s, x);

    return true;
}

In your component that you want to add plupload:

import {lazyload} from "./my-lazy-load.function.ts";

export class MyComponent  implements OnInit {

    pluploadInterval:number = null;
    hasPlupload: boolean = false;

    ngOnInit() {
        lazyload("https://cdnjs.cloudflare.com/ajax/libs/plupload/2.3.1/plupload.full.min.js");

        this.pluploadInterval = window.setInterval(()=>{
            if(window.plupload) { // you can check existence of plupload object any way you want

                // Woohoo, I am ready to be used

                this.hasPlupload = true; // everything is run outside ngZone, wrap it if angular2 is not reacting to changes, or change window.setInterval to setInterval
                window.clearInterval(this.pluploadInterval); // don't forget to clean interval
            }
        }, 100); // timeinterval can vary 
....

The browser will load this automatically.

Notice if(plupload) it assumes that there is global object plupload that the script adds (I do not know if it truely added, check your working example in pure javascript). As it is jquery extension you can check it's prototype like this: jQuery test for whether an object has a method?

OLD HISTORICAL: @Reid here is plunker: https://plnkr.co/edit/zDWWQbTQUSHBqCsrUMUi?p=preview the plupload is actually loaded, but added to require with define("plupload", ['./moxie'], extract); I am not sure at the moment how to extract from there and which package require is belong to... the code for finding correct module loader belongs to plupload itself, here it is (from plupload.dev.js):

if (typeof define === "function" && define.amd) {
    define("plupload", ['./moxie'], extract);
} else if (typeof module === "object" && module.exports) {
    module.exports = extract(require('./moxie'));
} else {
    global.plupload = extract(global.moxie);
}
Community
  • 1
  • 1
laser
  • 570
  • 4
  • 20
  • Thank you, but what I am looking for is the way to use Plupload, sourced from a CDN, in a TypeScript / Angular 2 environment - and do that in a lazy loading manner. So this is a chunk of interrelated things that all have to come together in the client side of the solution. – Reid Feb 19 '17 at 04:38
  • @Reid the code above lazy loading any js from anywhere (including plupload from internet). You just normally write your plupload application as it was already there (if you have declarations that is good, if not you can add "declare let :any;" in header and use as it was declared. – laser Feb 19 '17 at 08:33
  • Once I have a solution, if no one else posts before me, I will post a solution. What I am looking for is not some ideas but a step by step answer. Granted, your background may make the code and ideas you have offered seem like all that is needed to understand it, but I don't have that background. This is the kind of detail I am looking for as an answer and I will provide if I find it: http://stackoverflow.com/questions/23279259/mvc-5-asp-net-identity-2-capture-users-preference-for-remember-me-in-externa/23304354#23304354 – Reid Feb 19 '17 at 23:24
  • How do you know when the script has finished loading and you can begin using the framework? – Zze Feb 21 '17 at 22:32
  • @Zze it's just jquery extension through prototype... check that $("#uploader").plupload exists put setInterval and patiently wait. – laser Feb 21 '17 at 22:49
  • @Reid for my answer you don't need to do anything (nor any extra knowledge is required). Just use this function in any lazy-loaded (or not if you want) module file. Run function and wait for object. – laser Feb 21 '17 at 22:54
  • I've updated the original post. I think that what is needed is a working example as described in the updated post, as without seeing the full picture it just isn't coming together for me. – Reid Feb 22 '17 at 22:43
  • @Reid updated, I do not have time right now how to correctly extract plupload from module loader, you need to change `if(plupload) {` or `if(window.plupload) {` to correct way of extracting. – laser Feb 22 '17 at 23:54
  • I've awarded you the bounty, as you tried and the points were about to expire. If you can get around to getting it to work on plunker that would be of value to the community. Thanks. – Reid Feb 23 '17 at 00:11
  • @Reid actually, old plunker I fork from Zze and it turned out to be angular beta.0. Now I took official plunker from angular team, and it uses systemjs, thus window.plupload started to work immediately. Whither it will work for your local development, purely depend on your loaders for modules. – laser Feb 23 '17 at 07:58
  • If you want to take it across the finish line, I put in a plunker link in the updated question and that link has a lazy loaded component that has your lazy loaded script in it and working. Just needs, in my opinion, making the script in the updated question to be put in place and made functional. Anyway, thank you for the lazy loading outside script code. – Reid Feb 24 '17 at 02:24
  • @Reid ok, I put your call for uploader into plunker, now it is working also. Check for added plunker (it is only one function that is added) and minor change to lazyloading function to check if we already have plupload, then do not load it anymore. – laser Feb 24 '17 at 12:46
  • Cool. I expect to make your answer the answer in the next two days. Note that I promised AngularFrance some bounty points if he came up with the answer regarding using the plupload script, which he posted before you did, though you were the first to put it into the form I requested. I was required to make his bounty 100 as second bounties are like that, per SO. But you will get love from the community. You could clean up this answer a bit though so that a first-time reader doesn't have to sort through what the answer is. – Reid Feb 24 '17 at 19:02
  • Update: upon further research, it looks like AngularFrance has the answer, which, though it is not in exactly the correct form, seems to be working. So barring information that says his answer is incorrect, I will likely award his answer as the answer, as your answer, upon clicking the upload button, has "Uncaught TypeError: Cannot read property 'replace' of undefined", whereas his does not. – Reid Feb 24 '17 at 19:46
1

I think that your best bet is to use the Require.js Library so that you can dynamically load your scripts from within your components.

The small trade off is that you will have to add this 18KB library to your index.html page (CDN), however this could save you huge amounts of loading if your 3rd party libraries are massive.

I have no experience with using plupload, so instead I put together the following plunkr which uses an external animation library, drawn from a CDN. The plunkr animates a number from 0 - 100.

https://plnkr.co/edit/fJCtezsERYHOYplLh7Jo?p=preview


index.html
<script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.3/require.min.js"></script>

component.ts

ngOnInit(){
    // Dynamically loads the framework from the CDN.
    require(["https://cdnjs.cloudflare.com/ajax/libs/gsap/1.19.1/TweenLite.min.js"],
    // This function is then called ONCE LOAD IS COMPLETE
    function (common) {
      // Do greensock things
      var demo = {score:0},
      scoreDisplay = document.getElementById("scoreDisplay");

      //create a tween that changes the value of the score property of the demo object from 0 to 100 over the course of 20 seconds.
      var tween = TweenLite.to(demo, 20, {score:100, onUpdate:showScore})

      //each time the tween updates this function will be called.
      function showScore() {
        scoreDisplay.innerHTML = demo.score.toFixed(2);
      }  
    });
  }

What I like about this approach, is that in the onLoad callback from require, the syntax is unchanged from a normal implementation of the library, so you can just copy paste your currently working code into the callback.

Zze
  • 18,229
  • 13
  • 85
  • 118
  • Thank you for your effort. I am thinking I will try to do it without an additional library at this time. I suspect that this can only be adequately answered by someone who uses Plupload in Angular2. – Reid Feb 22 '17 at 05:08
  • This solution could have potential problem (except that it also not lazy loaded). I experience in angular2 beta, that some module loaders conflicts with angular2 (`require` is in global scope), when you have different in tsconfig.json. Hopefully, not a problem anymore. – laser Feb 22 '17 at 11:32