Angular Lazy Load External JS files with Angular Universal - javascript

https://maps.googleapis.com/maps/api/js?libraries=places&key=XXX
This is inside index.html file, but i want to lazy load this script, because that module is lazy loaded and it's really not necessary for all users.
I can't use trick with directly accessing the DOM and appending script el. because I want to use Angular Universal ( SSR ).

You can access the DOM even if you are using SSR. Add this to your lazy loaded module module or one of the components of your lazy loaded module
import {DOCUMENT} from "#angular/common";
import {Renderer2} from '#angular/core';
constructor(#Inject(DOCUMENT) private document: any, private renderer: Renderer2)
{
}
constructor()
{
const scriptElt = this.renderer.createElement('script');
this.renderer.setAttribute(scriptElt, 'type', 'text/javascript');
this.renderer.setAttribute(scriptElt, 'src', 'yourJSFile.js');
this.renderer.appendChild(this.document.head, scriptElt);
}

Related

Generate appropriate Angular Element dynamically without bloating the build size?

Summary:
When createCustomElement() is called multiple times inside a switch case, all of the components will be included in the build, instead of just the one that will actually be used. This increases the build size with duplicate code.
Details
I have an Angular 11 app with a multi-site architecture. There is a project in the angular.json for each site, so they can be built independently and generate their own dist bundles based on the appropriate environment.ts file that contains a "siteCode".
For one of the big components in my site -- let's call it myWidget -- I also export it as a generic web component (a.k.a "Angular Element", a.k.a. "custom element") for other sites to consume. So I have myWidget in a sub-project of the main app, and it also has its own projects listed in angular.json. Meaning I can run a build that should contain just myWidget for a given site (along with the core Angular framework, obviously).
app.module.ts of myWidget sub-project (simplified):
import { MyWidgetSite1Component } from './my-widget/site-widgets/my-widget-site1.component';
import { MyWidgetSite2Component } from './my-widget/site-widgets/my-widget-site2.component';
import { MyWidgetSite3Component } from './my-widget/site-widgets/my-widget-site3.component';
#NgModule({
declarations: [AppComponent],
imports: [MyWidgetModule]
})
export class AppModule {
constructor(private injector: Injector) {
//Create generic web component version of myWidget. Use correct child version per site.
switch (environment.siteCode) {
case "site1": {
const myWidgetCustomElement = createCustomElement(MyWidgetSite1Component , { injector: this.injector });
customElements.define('site1-myWidget', myWidgetCustomElement);
break;
}
case "site2": {
const myWidgetCustomElement = createCustomElement(MyWidgetSite2Component , { injector: this.injector });
customElements.define('site2-myWidget', myWidgetCustomElement);
break;
}
case "site3": {
const myWidgetCustomElement = createCustomElement(MyWidgetSite3Component , { injector: this.injector });
customElements.define('site3-myWidget', myWidgetCustomElement);
break;
}
}
}
}
Problem: it includes all three of those components in the build, instead of just the one that will be used for that site (verified with webpack bundle analyzer).
Futher background
The three site-specific myWidget components here all inherit from a common base component where all the real logic is, so they are nearly-identical. I'm doing this so I can load the appropriate CSS files for that site and bundle them inside the exported MyWidget web component as component-specific CSS. It uses shadowDom encapsulation and this way the web component is completely sealed off from the parent page that it's inserted into. So the components in question look like this:
my-widget-site1.component.ts
#Component({
selector: 'my-widget-site1',
templateUrl: '../my-widget/my-widget.component.html', //Shares base myWidget template
//First file does nothing but import appropriate site's stylesheets from main project. It would
//have been better to directly include them here, but that resulted in odd build errors.
styleUrls: [
'./my-widget-site1.component.scss',
'../my-widget/my-widget.component.scss'],
encapsulation: ViewEncapsulation.ShadowDom
})
export class MyWidgetSite1Component extends MyWidgetComponent implements OnInit {
//Do not add any code here
}
my-widget-site1.component.scss
#import '../../../../../../src/app/sites/site1/theme.scss';
#import '../../../../../../src/styles.scss';
#import '../../../../../../src/app/sites/site1/styles.scss';
Conclusion
I can think of a few general ideas to solve this:
1) Some trick to load the desired component dynamically instead of in a switch case?
I haven't been able to find anything. It seems as long as I have to import the component, it will be included in the build.
2) Avoid having multiple versions of the component per site entirely?
I would love to do this, but I don't know how to get around the CSS problem. The appropriate CSS files for a given site need to be bundled into this component at build time so they are encapsulated in the shadow-root of the web component and not built as a separate CSS file that gets imported into the global scope of the consuming page. That means I can't just list them in the "styles" section of the project build in angular.json
I tried to do a dynamic #import statement on the scss but that doesn't seem possible.
Can you script something into the build process somehow to choose the right scss files at build time? I would have no idea where to start with something like that.
I figured out an interesting solution to this.
I can get rid of the need for multiple component files and effectively pull off a dynamic #import by using a 'shortcut' to point at the necessary scss files instead of the full path:
#import "theme";
#import "styles";
#import "site-styles";
You can configure the folders it will find the specified files in via angular.json:
"stylePreprocessorOptions": {
"includePaths": [
"src/app/sites/site1/", //theme.scss and site-styles.scss are here
"src/" //styles.scss is here
]
}
So now I can use one component that always has the same imports, but at build time it will actually use the right file for the site being built.
Info about the shortcut syntax: https://www.digitalocean.com/community/tutorials/angular-shortcut-to-importing-styles-files-in-components

import third party JS library in open source LWC

After wasting significant amount of time on how to import jQuery,
I got below 2 ways
in HTML with local path or CDN:
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
IN JS with local path or CDN:
var script = document.createElement('script');
script.src = 'https://code.jquery.com/jquery-3.4.1.min.js';
script.type = 'text/javascript';
document.getElementsByTagName('head')[0].appendChild(script);
But unfortunately both of the above methods won't work in LWC and there is no documentation available on how to do the same.
Below approach works fine in my index.html page to import jQuery in my lwc project.
<script src="./resources/js/jquery-3.5.1.min.js"></script>
I also wasted so many hour on how to import CSS in lwc as there is no documentation available on importing third party CSS also but some how I manged to import css by using below code
constructor() {
super();
const styles = document.createElement('link');
styles.href = './resources/css/bootstrap.css';
styles.rel = 'stylesheet';
this.template.appendChild(styles);
}
So I tried some similar approach to import JS and this doesn't give any errors at the console log but the same doesn't work at all, tried in both constructor and connectedCallback but no luck.
connectedCallback() {
const jq = document.createElement('SCRIPT');
jq.src = './resources/js/jquery-3.5.1.min.js';
jq.type = 'text/javascript';
this.template.appendChild(jq);
}
if anyone has any idea about how to import the JS library in open source LWC then please do let me know, would highly appreciate your help.
First thing that we need to do is to add the JQuery JS file in the
Static Resource. The Static Resource should look something like
below:
JQuery Static Resource
Include JQuery in LWC (Lightning Web Component)
In the JS Controller, we first need to import loadScript() method from
lightning/platformResourceLoader module. We can also include loadStyle
method if we want to include an external CSS file.
import { loadScript, loadStyle } from
'lightning/platformResourceLoader';
Import the Static Resource that we created earlier as well. Check
this implementation to include Static Resource in Lightning Web
Component.
Finally, in the renderedCallback method, call loadScript that we
imported earlier and pass a reference to the current template (this)
and the Static Resource. This method returns the Promise. The then()
function will be called if the Promise is resolved. The catch()
function will be executed if there is an error. We are using
renderedCallback as we want to load the Scripts once the component
is loaded and rendered on UI.
Now, to access the JQuery method in Lightning Web Component, there
is some catch. Ideally, there are multiple ways to call JQuery
methods. The most common one is below:
$(".className").hide();
This will hide the element with class=”className”. But in Lightning
Web Component, we must use it like below:
$(this.template.querySelector('.className')).hide();
That is all. We just need to reference the elements using
queryLocator. Then, we can call the JQuery methods.
import { LightningElement } from 'lwc';
import { loadScript, loadStyle } from
'lightning/platformResourceLoader';
import jQuery from '#salesforce/resourceUrl/JQuery';
lwcJquery.js
import { LightningElement } from 'lwc';
import { loadScript, loadStyle } from
'lightning/platformResourceLoader';
import jQuery from '#salesforce/resourceUrl/JQuery';
export default class LwcJquery extends LightningElement {
renderedCallback(){
loadScript(this, jQuery)
.then(() => {
console.log('JQuery loaded.');
})
.catch(error=>{
console.log('Failed to load the JQuery : ' +error);
});
}
slideIt(event){
$(this.template.querySelector('.panel')).slideToggle("slow");
}
slideRight(event){
$(this.template.querySelector('.innerDiv')).animate({left:
'275px'});
}
}
Here, I have used slideToggle() and animate() JQuery methods to add
some animation.

Load new modules dynamically in run-time with Angular CLI & Angular 5

Currently I'm working on a project which is being hosted on a clients server. For new 'modules' there is no intention to recompile the entire application. That said, the client wants to update the router/lazy loaded modules in runtime. I've tried several things out but I can't get it to work. I was wondering if any of you knows what I could still try or what I missed.
One thing I noticed, most of the resources I tried, using angular cli, are being bundled into seperate chunks by webpack by default when building the application. Which seems logical as it makes use of the webpack code splitting. but what if the module is not known yet at compile time (but a compiled module is stored somewhere on a server)? The bundling does not work because it can't find the module to import. And Using SystemJS will load up UMD modules whenever found on the system, but are also bundled in a seperate chunk by webpack.
Some resources I already tried;
dynamic-remote-component-loader
module-loading
Loading modules from different server at runtime
How to load dynamic external components into Angular application
Implementing a plugin architecture / plugin system / pluggable framework in Angular 2, 4, 5, 6
Angular 5 - load modules (that are not known at compile time) dynamically at run-time
https://medium.com/#nikolasleblanc/building-an-angular-4-component-library-with-the-angular-cli-and-ng-packagr-53b2ade0701e
Some several other relating this topic.
Some code I already tried and implement, but not working at this time;
Extending router with normal module.ts file
this.router.config.push({
path: "external",
loadChildren: () =>
System.import("./module/external.module").then(
module => module["ExternalModule"],
() => {
throw { loadChunkError: true };
}
)
});
Normal SystemJS Import of UMD bundle
System.import("./external/bundles/external.umd.js").then(modules => {
console.log(modules);
this.compiler.compileModuleAndAllComponentsAsync(modules['External'])
.then(compiled => {
const m = compiled.ngModuleFactory.create(this.injector);
const factory = compiled.componentFactories[0];
const cmp = factory.create(this.injector, [], null, m);
});
});
Import external module, not working with webpack (afaik)
const url = 'https://gist.githubusercontent.com/dianadujing/a7bbbf191349182e1d459286dba0282f/raw/c23281f8c5fabb10ab9d144489316919e4233d11/app.module.ts';
const importer = (url:any) => Observable.fromPromise(System.import(url));
console.log('importer:', importer);
importer(url)
.subscribe((modules) => {
console.log('modules:', modules, modules['AppModule']);
this.cfr = this.compiler
.compileModuleAndAllComponentsSync(modules['AppModule']);
console.log(this.cfr,',', this.cfr.componentFactories[0]);
this.external.createComponent(this.cfr.componentFactories[0], 0);
});
Use SystemJsNgModuleLoader
this.loader.load('app/lazy/lazy.module#LazyModule')
.then((moduleFactory: NgModuleFactory<any>) => {
console.log(moduleFactory);
const entryComponent = (<any>moduleFactory.moduleType).entry;
const moduleRef = moduleFactory.create(this.injector);
const compFactory = moduleRef.componentFactoryResolver
.resolveComponentFactory(entryComponent);
});
Tried loading a module made with rollup
this.http.get(`./myplugin/${metadataFileName}`)
.map(res => res.json())
.map((metadata: PluginMetadata) => {
// create the element to load in the module and factories
const script = document.createElement('script');
script.src = `./myplugin/${factoryFileName}`;
script.onload = () => {
//rollup builds the bundle so it's attached to the window
//object when loaded in
const moduleFactory: NgModuleFactory<any> =
window[metadata.name][metadata.moduleName + factorySuffix];
const moduleRef = moduleFactory.create(this.injector);
//use the entry point token to grab the component type that
//we should be rendering
const compType = moduleRef.injector.get(pluginEntryPointToken);
const compFactory = moduleRef.componentFactoryResolver
.resolveComponentFactory(compType);
// Works perfectly in debug, but when building for production it
// returns an error 'cannot find name Component of undefined'
// Not getting it to work with the router module.
}
document.head.appendChild(script);
}).subscribe();
Example with SystemJsNgModuleLoader only works when the Module is already provided as 'lazy' route in the RouterModule of the app (which turns it into a chunk when built with webpack)
I found a lot of discussion about this topic on StackOverflow here and there and provided solutions seem really good of loading modules/components dynamically if known up front. but none is fitting for our use case of the project. Please let me know what I can still try or dive into.
Thanks!
EDIT: I've found; https://github.com/kirjs/angular-dynamic-module-loading and will give this a try.
UPDATE: I've created a repository with an example of loading modules dynamically using SystemJS (and using Angular 6); https://github.com/lmeijdam/angular-umd-dynamic-example
I was facing the same problem. As far as I understand it until now:
Webpack puts all resources in a bundle and replaces all System.import with __webpack_require__. Therefore, if you want to load a module dynamically at runtime by using SystemJsNgModuleLoader, the loader will search for the module in the bundle. If the module does not exist in the bundle, you will get an error. Webpack is not going to ask the server for that module. This is a problem for us, since we want to load a module that we do not know at build/compile time.
What we need is loader that will load a module for us at runtime (lazy and dynamic). In my example, I am using SystemJS and Angular 6 / CLI.
Install SystemJS: npm install systemjs –save
Add it to angular.json: "scripts": [ "node_modules/systemjs/dist/system.src.js"]
app.component.ts
import { Compiler, Component, Injector, ViewChild, ViewContainerRef } from '#angular/core';
import * as AngularCommon from '#angular/common';
import * as AngularCore from '#angular/core';
declare var SystemJS;
#Component({
selector: 'app-root',
template: '<button (click)="load()">Load</button><ng-container #vc></ng-container>'
})
export class AppComponent {
#ViewChild('vc', {read: ViewContainerRef}) vc;
constructor(private compiler: Compiler,
private injector: Injector) {
}
load() {
// register the modules that we already loaded so that no HTTP request is made
// in my case, the modules are already available in my bundle (bundled by webpack)
SystemJS.set('#angular/core', SystemJS.newModule(AngularCore));
SystemJS.set('#angular/common', SystemJS.newModule(AngularCommon));
// now, import the new module
SystemJS.import('my-dynamic.component.js').then((module) => {
this.compiler.compileModuleAndAllComponentsAsync(module.default)
.then((compiled) => {
let moduleRef = compiled.ngModuleFactory.create(this.injector);
let factory = compiled.componentFactories[0];
if (factory) {
let component = this.vc.createComponent(factory);
let instance = component.instance;
}
});
});
}
}
my-dynamic.component.ts
import { NgModule, Component } from '#angular/core';
import { CommonModule } from '#angular/common';
import { Other } from './other';
#Component({
selector: 'my-dynamic-component',
template: '<h1>Dynamic component</h1><button (click)="LoadMore()">LoadMore</button>'
})
export class MyDynamicComponent {
LoadMore() {
let other = new Other();
other.hello();
}
}
#NgModule({
declarations: [MyDynamicComponent],
imports: [CommonModule],
})
export default class MyDynamicModule {}
other.component.ts
export class Other {
hello() {
console.log("hello");
}
}
As you can see, we can tell SystemJS what modules already exist in our bundle. So we do not need to load them again (SystemJS.set). All other modules that we import in our my-dynamic-component (in this example other) will be requested from the server at runtime.
I've used the https://github.com/kirjs/angular-dynamic-module-loading solution with Angular 6's library support to create an application I shared on Github. Due to company policy it needed to be taken offline. As soon as discussions are over regarding the example project source I will share it on Github!
UPDATE: repo can be found ; https://github.com/lmeijdam/angular-umd-dynamic-example
I have tested in Angular 6, below solution works for dynamically loading a module from an external package or an internal module.
1. If you want to dynamically load a module from a library project or a package:
I have a library project "admin" (or you can use a package) and an application project "app".
In my "admin" library project, I have AdminModule and AdminRoutingModule.
In my "app" project:
a. Make change in tsconfig.app.json:
"compilerOptions": {
"module": "esNext",
},
b. In app-routing.module.ts:
const routes: Routes = [
{
path: 'admin',
loadChildren: async () => {
const a = await import('admin')
return a['AdminModule'];
}
},
{
path: '',
redirectTo: '',
pathMatch: 'full'
}
];
#NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule {
}
2. if you want to load a module from the same project.
There are 4 different options:
a. In app-routing.module.ts:
const routes: Routes = [
{
path: 'example',
/* Options 1: Use component */
// component: ExampleComponent, // Load router from component
/* Options 2: Use Angular default lazy load syntax */
loadChildren: './example/example.module#ExampleModule', // lazy load router from module
/* Options 3: Use Module */
// loadChildren: () => ExampleModule, // load router from module
/* Options 4: Use esNext, you need to change tsconfig.app.json */
/*
loadChildren: async () => {
const a = await import('./example/example.module')
return a['ExampleModule'];
}
*/
},
{
path: '',
redirectTo: '',
pathMatch: 'full'
}
];
#NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule {
}
``
Do it with angular 6 library and rollup do the trick. I've just experiment with it and i can share standalone angular AOT module with the main app without rebuild last.
In angular library set angularCompilerOptions.skipTemplateCodegen to false and after build library you will get module factory.
After that build an umd module with rollup like this:
rollup dist/plugin/esm2015/lib/plugin.module.ngfactory.js --file src/assets/plugin.module.umd.js --format umd --name plugin
Load text source umd bundle in main app and eval it with module context
Now you can access to ModuleFactory from exports object
Here https://github.com/iwnow/angular-plugin-example you can find how develop plugin with standalone building and AOT
I believe this is possible using SystemJS to load a UMD bundle if you build and run your main application using webpack. I used a solution that uses ng-packagr to build a UMD bundle of the dynamic plugin/addon module. This github demonstrates the procedure described:
https://github.com/nmarra/dynamic-module-loading
Yes, you can lazy load modules using by referring them as modules in the router. Here is an example https://github.com/start-angular/SB-Admin-BS4-Angular-6
First couple all the components that you are using into a single module
Now refer that module in the router and angular will lazy load your module into the view.

How to lazyload library in Angular 4 module?

I have an app with multiple modules.
One of the modules is to visualize pdf. I use pdf.js which is pretty greedy and the vendor.js is somehow big because of this.
Is there a way to lazyload the library at the same time I lazy load the pdf-module ?
I've noticed this answer, but it doesn't feel as right.
Load external js script dynamically in Angular 2
I am not trying to lazyload a module but an external library.
If you want to lazy load external libraries such as jquery, jspdf you can create some service like:
lazy-loading-library.service.ts
import { Injectable, Inject } from '#angular/core';
import { Observable } from 'rxjs/Observable';
import { ReplaySubject } from 'rxjs/ReplaySubject';
import { DOCUMENT } from '#angular/platform-browser';
#Injectable()
export class LazyLoadingLibraryService {
private loadedLibraries: { [url: string]: ReplaySubject<any> } = {};
constructor(#Inject(DOCUMENT) private readonly document: any) { }
public loadJs(url: string): Observable<any> {
if (this.loadedLibraries[url]) {
return this.loadedLibraries[url].asObservable();
}
this.loadedLibraries[url] = new ReplaySubject();
const script = this.document.createElement('script');
script.type = 'text/javascript';
script.src = url;
script.onload = () => {
this.loadedLibraries[url].next('');
this.loadedLibraries[url].complete();
};
this.document.body.appendChild(script);
return this.loadedLibraries[url].asObservable();
}
}
And whenever you need some external library just use this service that will load library only once:
app.component.ts
export class AppComponent {
constructor(private service: LazyLoadingLibraryService) {}
loadJQuery() {
this.service.loadJs('https://code.jquery.com/jquery-3.2.1.min.js').subscribe(() => {
console.log(`jQuery version ${jQuery.fn.jquery} has been loaded`);
});
}
loadJsPdf() {
this.service.loadJs('https://cdnjs.cloudflare.com/ajax/libs/jspdf/1.3.5/jspdf.min.js').subscribe(() => {
console.log(`JsPdf library has been loaded`);
});
}
Plunker Example
If you're looking for lazy loading angular module then these questions might be helpful for you:
How to manually lazy load a module?
How to lazy load Angular 2 components in a TabView (PrimeNG)?
I hope you're using Angular CLI.
Install pdfjs-dist package:
npm install pdfjs-dist
Install types for it:
npm install #types/pdfjs-dist --save-dev
Add the following import statement to your lazy loaded module file:
import 'pdfjs-dist';
The last step will embed pdf.js source code in the lazy loaded bundle when you run ng build.
You should be able to access the global PDFJS variable from your code.
Hope this helps.

How to include external JavaScript libraries in Angular 2?

I am trying to include an external JS library in my Angular 2 app and trying to make all the methods in that JS file as a service in Angular 2 app.
For eg: lets say my JS file contains.
var hello = {
helloworld : function(){
console.log('helloworld');
},
gmorning : function(){
console.log('good morning');
}
}
So I am trying to use this JS file and reuse all the methods in this object and add it to a service, so that my service has public methods, which in turn calls this JS methods. I am trying to reuse the code, without reimplementing all the methods in my typescript based Angular 2 app. I am dependent on an external library, which I cant modify.
Please help, thank you in advance.
With ES6, you could export your variable:
export var hello = {
(...)
};
and import it like this into another module:
import {hello} from './hello-module';
assuming that the first module is located into the hello-module.js file and in the same folder than the second one. It's not necessary to have them in the same folder (you can do something like that: import {hello} from '../folder/hello-module';). What is important is that the folder is correctly handled by SystemJS (for example with the configuration in the packages block).
When using external libs which are loaded into the browser externally (e.g. by the index.html) you just need to say your services/component that it is defined via "declare" and then just use it. For example I recently used socket.io in my angular2 component:
import { Component, Input, Observable, AfterContentInit } from angular2/angular2';
import { Http } from 'angular2/http';
//needed to use socket.io! io is globally known by the browser!
declare var io:any;
#Component({
selector: 'my-weather-cmp',
template: `...`
})
export class WeatherComp implements AfterContentInit{
//the socket.io connection
public weather:any;
//the temperature stream as Observable
public temperature:Observable<number>;
//#Input() isn't set yet
constructor(public http: Http) {
const BASE_URL = 'ws://'+location.hostname+':'+location.port;
this.weather = io(BASE_URL+'/weather');
//log any messages from the message event of socket.io
this.weather.on('message', (data:any) =>{
console.log(data);
});
}
//#Input() is set now!
ngAfterContentInit():void {
//add Observable
this.temperature = Observable.fromEvent(this.weather, this.city);
}
}

Categories

Resources