SharePoint / sp-dev-docs

SharePoint & Viva Connections Developer Documentation
https://docs.microsoft.com/en-us/sharepoint/dev/
Creative Commons Attribution 4.0 International
1.24k stars 1.01k forks source link

Multiple loading of jQuery and jQuery plugins (SPFx GA) #472

Closed OlivierCC closed 2 years ago

OlivierCC commented 7 years ago

Category

Expected or Desired Behavior

Be able to use 2 jQuery plugins in 2 different web parts in the same page.

Observed Behavior

The problem is that the jQuery lib is loaded 2 times, even using the principle of external resource. With the jQuery plugins that has a side effect: when the 2nd load of jQuery, the context is reset, and so 1st plugin is "erased" and can no longer be used. Then 2 web parts with 2 plugins jQuery cause errors in the same page.

Good behavior when using scripts with external resources: the common external script must be loaded only once.

I found a workaround, but very dirty: change the jQuery code to integrate a check during the 2nd load...

Steps to Reproduce

I use two web parts that each have a jQuery plugin. Web parts use the jQuery scripts and plugins and external references. Insert these webparts in a same page

config.json file:

"jquery": {}
"path": "node_modules/jquery/dist/jquery.min.js",
"globalName": "jQuery"
},
"arctext": {}
"path": "src/javascripts/arcText/jquery.arctext.js",
"globalName": "jQuery".
"globalDependencies": ["jquery"]
},
"letterfx": {}
"path": "src/javascripts/letterfx/letterfx.js",
"globalName": "jQuery".
"globalDependencies": ["jquery"]
}

Web Part 1:

import * as $ from 'jquery';
require('arctext');
...
($ as any)('#arc').arctext({..});  <-- randomly: the artext function is not defined (if WebPart 1 is loaded 1rst)

Web Part 2:

import * as $ from 'jquery';
require('letterfx');
...
($ as any)('#AnimatedText").letterfx({..}); <-- randomly: the letterfx function is not defined  (if WebPart 2 is loaded 1rst)
patmill commented 7 years ago

Hi @OlivierCC - just checking. Is this different from this bug - https://github.com/SharePoint/sp-dev-docs/issues/336

OlivierCC commented 7 years ago

Hi @patmill, yes I think it's different because I have not the Valdek's bug with the GA version. My bug appears only with 2 web different webparts that use a different jQuery plugin, but jQuery loading itself always goes well.

ghost commented 7 years ago

Please let me know if I need to submit another issue for this. My issue stems from the same root cause of this one.

If I have jQuery loaded onto a page and CSR that uses a plugin, when I add an SPFx webpart to the page, any plugins previously on the page are overridden.

I cannot use @OlivierCC's workaround because I am using jQuery from a hosted CDN.

waldekmastykarz commented 7 years ago

@OlivierCC isn't this by design with jQuery and its plugins? Wouldn't the same happen if you loaded jQuery twice on the same page (static HTML page without SPFx) and would load plugs in between? Wouldn't the last loaded instance of jQuery override all previously loaded instances and their plugins?

ghost commented 7 years ago

@waldekmastykarz You are correct, It's best practice to not load jQuery twice. I think everyone here agrees with that. However, We have no control over that when building two independent web parts with SPFx.

waldekmastykarz commented 7 years ago

@swalker1595 you could choose to load jQuery using SPComponentLoader only after checking that it hasn't been loaded previously. Not sure if it would be sufficient to covers scenarios such as loading jQuery by another web part is currently in progress, but perhaps it's worth trying.

iclanton commented 7 years ago

@mpasarin - can you take a look at this issue?

mpasarin commented 7 years ago

@srideshpande is currently working on a better story on the usage of JQuery by multiple web parts

kmarwen commented 6 years ago

Hi all @waldekmastykarz I tried loading the script using SPComponentLoader but I always facing the issue @OlivierCC, I'm also extending your 40 spfx webparts can you tell me what was your workaround ? thx

nhadro commented 6 years ago

Is there any update on it. I am having the same issue. I have 2 webparts, both have external script entries in the config.json file and both have a requirement on jquery. One works and the other does not, they never work at the same time.

ravinleague commented 6 years ago

Guys, Have we had any update on this yet ? I have issues with presence of 2 different versions of jQuery on the same page in two different web parts.

nhadro commented 6 years ago

I had to write a separate module that would be called and load Jquery if needed and then based on the SPComponentLoader successfully loading jquery, then load the dependencies. If jquery is already loaded, it simply loads the dependencies. Hopefully this won't be needed forever, but for now I didn't see any other way around it.

kmarwen commented 6 years ago

Hi @nhadro could you please share your workaroud ?

thx

ravinleague commented 6 years ago

public render(): void {
if(typeof(window['jQuery'])=='undefined'){ SPComponentLoader.loadScript('https://code.jquery.com/jquery-2.2.4.min.js',{ globalExportsName: 'jQuery' } ).then(($: any) => { this.jQuery=$; require(‘.js’); //other js files this.initMethod() }); } else{ require(‘.js’); //other js files this.initMethod(); } }

On 8 Mar 2018, at 7:44 pm, Marwen notifications@github.com wrote:

Hi @nhadro https://github.com/nhadro could you please share your workaroud ?

thx

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/SharePoint/sp-dev-docs/issues/472#issuecomment-371435155, or mute the thread https://github.com/notifications/unsubscribe-auth/AR3Y_Pi569PP9Z3D3KmBvcrl3mDe-I2Zks5tcP14gaJpZM4MVQZG.

nhadro commented 6 years ago

The module I created is below and I used it like the following:

USAGE: JQueryLoader.LoadDependencies(["https://entegris.sharepoint.com/CDN/customJSFiles/jquery.flexslider.js"]).then(() => { this.getSliderItems(this.props); });

MODULE:

import { SPComponentLoader } from '@microsoft/sp-loader';

namespace JQueryLoader{

//If jquery is
export function LoadDependencies(dependencies: string[]) : Promise<object>{

    if(!(window as any).jQuery)
    {
        //console.log('load jquery');
        return SPComponentLoader.loadScript('https://code.jquery.com/jquery-2.1.2.min.js', { globalExportsName: 'jQuery' }).then(() => {
            return Load(dependencies);
        });
    }
    else
    {
       return Load(dependencies);
    }
}
function Load(dependencies: string[]) : Promise<object>
{
    var scripts: Promise<object>[]  = [];
    dependencies.forEach(depenency => {
        //console.log("3");
        scripts.push(SPComponentLoader.loadScript(depenency));
        //console.log("4");
    });

    return Promise.all(scripts);
}

}

export default JQueryLoader;

nhadro commented 6 years ago

With this approach I need to provide a hard coded url to my dependencies. Any way to provide a relative reference to a dependency in my code?

I have this currently, but if the location of that js file changes it breaks: JQueryLoader.LoadDependencies(["/CDN/customJSFiles/jquery.flexslider.js"]).then(() => { this.getRotatorItems(this.props); });

ravinleague commented 6 years ago

@nhadro Thanks for asking. Actually, I use Azure CDN to store all my dependencies(Third-Party JS Files, CSS, assets etc.)

  1. I first push these dependencies to CDN and retrieve URLs for all the dependeicies
  2. I then use these URLs in my code where required. The gulp task (gulp.js file) I use for pushing js files is as follows,
'use strict';
const gulp = require('gulp');
const build = require('@microsoft/sp-build-web');
var azure = require('gulp-azure-storage');

build.initialize(gulp);
gulp.task("pushjs", function() {  
    return gulp.src("src/js/**")
      .pipe(azure.upload({
          account:    "-Azure Storage Account-",
          key:        "--Key Here--",
          container:  "-Container name-"
      }));
  });

Command in terminal - gulp pushjs

Apply the same concept for pushing images and css files

nhadro commented 6 years ago

Thanks @ravinleague. I'm a consultant so in this case, if I want to reuse a webpart, I have to have them setup an Azure CDN and change the url specifically for that client. Ideally I'd just be able to reference the files in the package directly.

ravinleague commented 6 years ago

Yep, being a consultant too, I understand the pain in the CDNs and Reference URLs. Physically having the files in the App itself and referencing them via src/js/.js is the optimal way I guess.

Regards, Ravi Challa

On 28 August 2018 at 08:10, Nate notifications@github.com wrote:

Thanks @ravinleague https://github.com/ravinleague. I'm a consultant so in this case, if I want to reuse a webpart, I have to have them setup an Azure CDN and change the url specifically for that client. Ideally I'd just be able to reference the files in the package directly.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/SharePoint/sp-dev-docs/issues/472#issuecomment-416385631, or mute the thread https://github.com/notifications/unsubscribe-auth/AR3Y_G-pYAZ3cxCy2QnbpR0yUSy3Qu2uks5uVG5ggaJpZM4MVQZG .

nhadro commented 6 years ago

Yep, I appreciate the input though and the idea.

kunalvalecha commented 6 years ago

@ravinleague @nhadro Do you have any working sample you can point to.? I was not able to fix this with the solutions you guys provided. I am using flexslider and jquery

nhadro commented 6 years ago

I use it as follows:

  1. Have a shared JqueryLoader.ts that I import into my React Component. (This code is up in this thread).
  2. Then inside "componentDidMount I call this: JQueryLoader.LoadDependencies(["https://site.sharepoint.com/CDN/customJSFiles/jquery.flexslider.js"]).then(() => { this.getSliderItems(this.props); });
  3. this.getSliderItems gets the items, updates the state which calls the render and the on "componentDidMount" I initiate $("").flexslider
ravinleague commented 6 years ago

Sweet!

Regards, Ravi Challa

On 5 September 2018 at 03:48, Nate notifications@github.com wrote:

I use it as follows:

  1. Have a shared JqueryLoader.ts that I import into my React Component. (This code is up in this thread).
  2. Then inside "componentDidMount I call this: JQueryLoader.LoadDependencies(["https://site.sharepoint.com/ CDN/customJSFiles/jquery.flexslider.js https://site.sharepoint.com/CDN/customJSFiles/jquery.flexslider.js"]).then(() => { this.getSliderItems(this.props); });
  3. this.getSliderItems gets the items, updates the state which calls the render and the on "componentDidMount" I initiate $("").flexslider

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/SharePoint/sp-dev-docs/issues/472#issuecomment-418458501, or mute the thread https://github.com/notifications/unsubscribe-auth/AR3Y_EwIPTtyVaBh4JiCYrj_YEdwvugUks5uXrzzgaJpZM4MVQZG .

nhadro commented 6 years ago

I've updated my JQueryLoader to now load the script references synchronously. Many times I would have dependencies and using Promise.all simply loaded them in whatever order, so this latest change loads them in the order they a entered:

//Usage in component or webpart

JQueryLoader.LoadDependencies([ { scriptUrl: "https://cdnjs.cloudflare.com/ajax/libs/dropzone/5.5.1/min/dropzone.min.js", globalName: "jQuery", unqiueId: "dropzonejs" }, { scriptUrl: "https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js", globalName: "Popper", unqiueId: "popperjs" }, { scriptUrl: "https://tenant.sharepoint.com/sites/testextranet/SiteAssets/PortalFramework/cdn/ce-spfx-test/bootstrap.js", globalName: "jQuery", unqiueId: "bootstrapjs" }, { scriptUrl: "https://tenant.sharepoint.com/sites/testextranet/SiteAssets/PortalFramework//cdn/ce-spfx-test/bootstrap-datepicker.min.js", globalName: "jQuery", unqiueId: "bootstrapdatepicker" }

]).then(() => {
  //Load Data or do whatever

});

//JQueryLoader.ts

export interface DependencyObj { scriptUrl: string; globalName: string; unqiueId: string }

import { SPComponentLoader } from '@microsoft/sp-loader';

namespace JQueryLoader {

//If jquery is
export function LoadDependencies(dependencies: DependencyObj[]): Promise<object> {

    if (!(window as any).jQuery) {
        //console.log('load jquery');
        return SPComponentLoader.loadScript('https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js', { globalExportsName: 'jQuery' }).then(() => {
            return Load(dependencies, 0);
        });
    }
    else {
        return Load(dependencies, 0);
    }
}
function Load(dependencies: DependencyObj[], index: number): Promise<object> {
    //console.log("Index: " + index);
    //console.log("DependencyLength: " + dependencies.length);
    if (index < dependencies.length) {
        return new Promise((resolve) => {
            //console.log("loading: " + dependencies[index].scriptUrl);
            SPComponentLoader.loadScript(dependencies[index].scriptUrl, { globalExportsName: dependencies[index].globalName }).then((obj: any) => {

                (window as any)[dependencies[index].globalName] = obj;
                (window as any)[dependencies[index].unqiueId] = "Loaded";

                let newIndex = index += 1;
                //console.log("loop again: " + (dependencies.length > newIndex));
                if (dependencies.length > newIndex) {

                    Load(dependencies,newIndex).then(() => {resolve();});
                }
                else
                {
                    //console.log('resolving');
                    resolve();

                }

            })
        });
    }
    else {
       // console.log("returning");
        return Promise.resolve();
    }
}

}

export default JQueryLoader;

pkmelee337 commented 5 years ago

Hi guys, thanks for the great efforts! Altough I do think this should get fixed in the default SharePoint module loader. @srideshpande , can you give us an update on this?

In our case we could have some webparts on the page which we do not control and which also could load jQuery through from the spfx config.json externals. I therefore tweaked the JQueryLoader a little. The loader checks if jQuery is loaded and afterwards checks if jQuery has loaded the plugin. We also had the case that jquery was overwritten on another modern page when navigating, so I could not use the SPComponentLoader to load the plugins. The ComponentLoader thought the plugin was already loaded, but jQuery was overwritten and did not contain the plugin anymore. I therefore used the jQuery loadScripts implementation.

import { SPComponentLoader } from '@microsoft/sp-loader';

export interface IJQueryDependencyConfig {
    /**
     * Url of the plugin to load
     */
    scriptUrl: string;
    /**
     * Should be jQuery in
     */
    globalName: string;
    /**
     * For example, fn.owlCarousel
     */
    pluginName: string;
}

export abstract class JQueryLoader {
    public static async loadDependencies(dependencies: IJQueryDependencyConfig[] | IJQueryDependencyConfig): Promise<void> {
        let dependenciesArray: IJQueryDependencyConfig[] = (dependencies) ? (Array.isArray(dependencies) ? dependencies : [dependencies]) : [];
        if (!(window as any).jQuery) {
            await SPComponentLoader.loadScript('https://code.jquery.com/jquery-3.1.0.min.js', { globalExportsName: 'jQuery' });
        }
        await this.load(dependenciesArray);
    }
    private static async load(dependencies: IJQueryDependencyConfig[]): Promise<void> {
        for(let dependency of dependencies) {
            let pluginExists = this.pluginExists(dependency);
            if(pluginExists) continue;
            await new Promise((resolve, reject) => {
                $.getScript(dependency.scriptUrl).done((script, textStatus) => {
                    resolve();
                }).fail((jqxhr, settings, exception) => {
                    console.error(exception);
                    reject(exception);
                });
            });
        }
    }
    private static pluginExists(dependency: IJQueryDependencyConfig) {
        if(!dependency.pluginName) return false;
        let plugin = $;
        let pluginNameSplitted = dependency.pluginName.split(".");
        for(let pluginName of pluginNameSplitted) {
            plugin = plugin[pluginName];
            if(typeof(plugin) === "undefined" || plugin === null) return false;
        }
        return pluginNameSplitted.length > 0;
    }
}
Saroj-K-Panda commented 5 years ago

hello guys,

while deploying in App Catalog, I can see "https://code.jquery.com" get appended with localhost. And after deploying I am not able to see the webpart in Page.

"This client side solution will get content from the following domains: https://localhost:4321/ ; https://code.jquery.com

Can you please help me. Thanks in adavnce. -SP

AJIXuMuK commented 2 years ago

I assume SPFx Library Components is the answer for that issue.

ghost commented 2 years ago

This issue has been automatically marked as stale because it has marked as requiring author feedback but has not had any activity for 7 days. It will be closed if no further activity occurs within the next 7 days of this comment. Please see our wiki for more information: Issue List Labels: Needs Author Feedback & Issue List: No response from the original issue author

ghost commented 2 years ago

Closing issue due to no response from the original author. Please refer to our wiki for more details, including how to remediate this action if you feel this was done prematurely or in error: No response from the original issue author