├── .github └── workflows │ └── nodejs.yml ├── .gitignore ├── .vscode └── tasks.json ├── LICENSE ├── README.md ├── angular.json ├── img └── example.gif ├── package-lock.json ├── package.json ├── src ├── app │ ├── app.component.css │ ├── app.component.html │ ├── app.component.ts │ ├── app.module.ts │ ├── component-selectors.ts │ ├── dynamic-components.service.ts │ ├── hmr-module-helper.ts │ ├── lazy-components │ │ ├── a │ │ │ ├── a.component.css │ │ │ ├── a.component.ts │ │ │ └── a.module.ts │ │ └── b │ │ │ ├── b.module.ts │ │ │ ├── b1.component.css │ │ │ ├── b1.component.ts │ │ │ ├── b2.component.css │ │ │ └── b2.component.ts │ ├── lazy-routes │ │ └── c │ │ │ ├── c.component.css │ │ │ ├── c.component.ts │ │ │ └── c.module.ts │ └── lazy.component.css ├── assets │ └── loading.gif ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── index.html ├── karma.conf.js ├── main.ts ├── polyfills.ts ├── styles.css ├── tsconfig.app.json └── tsconfig.spec.json └── tsconfig.json /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [10.x, 12.x] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - run: npm ci 28 | - run: npm run build --if-present 29 | # - run: npm test 30 | env: 31 | CI: true 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | dist/* -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "start", 9 | "problemMatcher": [ 10 | "$tsc-watch" 11 | ], 12 | "isBackground": true 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Andrew Miller 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # angular-hmr-lazy-components 2 | This example shows how Angular HMR can be used to automatically reload lazy routes and lazy (dynamically-loaded) components. This can be extremely helpful for large Angular apps that take a while to JIT compile when they reload. 3 | 4 | Notice in the animation below that moments after a change is made in the a.component.ts source code, just the "A" components already showing in the app are reloaded. 5 | ![Image of example in action](https://github.com/wags1999/angular-hmr-lazy-components/blob/master/img/example.gif) 6 | 7 | # Reloading Lazy Routes 8 | 9 | ## How It Works 10 | Most of the magic is in [hmr-module-helper.ts](https://github.com/wags1999/angular-hmr-lazy-components/blob/master/src/app/hmr-module-helper.ts). Each module that is going to be lazy-loaded through the router will use this helper class to enable HMR for that module. Here's how this is used in the [c module](https://github.com/wags1999/angular-hmr-lazy-components/blob/master/src/app/lazy-routes/c/c.module.ts): 11 | 12 | export class CModule { 13 | constructor(moduleRef: NgModuleRef) { 14 | HmrModuleHelper.enableHmrRouterNgModule(module, moduleRef); 15 | } 16 | } 17 | 18 | HmrModuleHelper.enableHmrNodeModule(module); 19 | 20 | # Reloading Dynamically-Loaded Components 21 | 22 | ## How It Works 23 | Reloading dynamically-loaded components takes a little bit more. 24 | * [dynamic-component.service.ts](https://github.com/wags1999/angular-hmr-lazy-components/blob/master/src/app/dynamic-components.service.ts) - This service is responsible for dynamically loading and opening components, keeping track of what components are open, closing components, and reloading components. 25 | * [hmr-module-helper.ts](https://github.com/wags1999/angular-hmr-lazy-components/blob/master/src/app/hmr-module-helper.ts). Each module that is going to be dynamically loaded will use this helper class to enable HMR for that module. This also wires things up with the DynamicComponentService to destroy and reload the components when the module is hot-reloaded. Here's how this is used in [a module](https://github.com/wags1999/angular-hmr-lazy-components/blob/master/src/app/lazy-components/a/a.module.ts): 26 | 27 | export class AModule { 28 | constructor(moduleRef: NgModuleRef) { 29 | HmrModuleHelper.enableHmrDynamicNgModule(module, moduleRef); 30 | } 31 | } 32 | 33 | HmrModuleHelper.enableHmrNodeModule(module); 34 | 35 | ## Instructions 36 | Besides the above files, there are a few other things to point out: 37 | * You'll need to list each module that you want to dynamically load in the `lazyModules` setting of your [angular.json](https://github.com/wags1999/angular-hmr-lazy-components/blob/master/angular.json) file: 38 | 39 | "lazyModules": [ 40 | "src/app/lazy-components/a/a.module", 41 | "src/app/lazy-components/b/b.module" 42 | ] 43 | 44 | * You'll need to enable hmr in `serve` section of your [angular.json](https://github.com/wags1999/angular-hmr-lazy-components/blob/master/angular.json) file: 45 | 46 | "options": { 47 | "browserTarget": "demo:build", 48 | "hmr": true, 49 | "hmrWarning": false 50 | }, 51 | 52 | * Setup your environments files so you can detect in code if hmr is on. Details can be found on the [Angular CLI wiki](https://github.com/angular/angular-cli/wiki/stories-configure-hmr#add-environment-for-hmr). 53 | * If you aren't using/importing the @angular/router, you'll need to configure a provider for the `NgModuleFactoryLoader` in your [app.module.ts](https://github.com/wags1999/angular-hmr-lazy-components/blob/master/src/app/app.module.ts): 54 | 55 | providers: [ 56 | {provide: NgModuleFactoryLoader, useClass: SystemJsNgModuleLoader}, 57 | DynamicComponentsService 58 | ], 59 | 60 | ## Remarks 61 | * Because the DynamicComponentsService uses the components' selectors, all of the dynamic loading works in a Prod (AOT) build. 62 | * You can't run this example on StackBlitz, as it doesn't seem to honor the lazyModules setting in the angular.json file. 63 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "demo": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "prefix": "app", 11 | "schematics": {}, 12 | "architect": { 13 | "build": { 14 | "builder": "@angular-devkit/build-angular:browser", 15 | "options": { 16 | "outputPath": "dist/demo", 17 | "index": "src/index.html", 18 | "main": "src/main.ts", 19 | "polyfills": "src/polyfills.ts", 20 | "tsConfig": "src/tsconfig.app.json", 21 | "assets": [ 22 | "src/favicon.ico", 23 | "src/assets" 24 | ], 25 | "styles": [ 26 | "src/styles.css" 27 | ], 28 | "scripts": [], 29 | "lazyModules": [ 30 | "src/app/lazy-components/a/a.module", 31 | "src/app/lazy-components/b/b.module" 32 | ] 33 | }, 34 | "configurations": { 35 | "production": { 36 | "fileReplacements": [ 37 | { 38 | "replace": "src/environments/environment.ts", 39 | "with": "src/environments/environment.prod.ts" 40 | } 41 | ], 42 | "optimization": true, 43 | "outputHashing": "all", 44 | "sourceMap": false, 45 | "extractCss": true, 46 | "namedChunks": false, 47 | "aot": true, 48 | "extractLicenses": true, 49 | "vendorChunk": false, 50 | "buildOptimizer": true 51 | } 52 | } 53 | }, 54 | "serve": { 55 | "builder": "@angular-devkit/build-angular:dev-server", 56 | "options": { 57 | "browserTarget": "demo:build", 58 | "hmr": true, 59 | "hmrWarning": false 60 | }, 61 | "configurations": { 62 | "production": { 63 | "browserTarget": "demo:build:production" 64 | } 65 | } 66 | }, 67 | "extract-i18n": { 68 | "builder": "@angular-devkit/build-angular:extract-i18n", 69 | "options": { 70 | "browserTarget": "demo:build" 71 | } 72 | }, 73 | "test": { 74 | "builder": "@angular-devkit/build-angular:karma", 75 | "options": { 76 | "main": "src/test.ts", 77 | "polyfills": "src/polyfills.ts", 78 | "tsConfig": "src/tsconfig.spec.json", 79 | "karmaConfig": "src/karma.conf.js", 80 | "styles": [ 81 | "styles.css" 82 | ], 83 | "scripts": [], 84 | "assets": [ 85 | "src/favicon.ico", 86 | "src/assets" 87 | ] 88 | } 89 | }, 90 | "lint": { 91 | "builder": "@angular-devkit/build-angular:tslint", 92 | "options": { 93 | "tsConfig": [ 94 | "src/tsconfig.app.json", 95 | "src/tsconfig.spec.json" 96 | ], 97 | "exclude": [ 98 | "**/node_modules/**" 99 | ] 100 | } 101 | } 102 | } 103 | } 104 | }, 105 | "defaultProject": "demo" 106 | } -------------------------------------------------------------------------------- /img/example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wags1999/angular-hmr-lazy-components/9b16281c41ad4adac1f6bdb4ceb793b95346bf5f/img/example.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular", 3 | "version": "0.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "@angular/common": "^7.0.1", 7 | "@angular/compiler": "^7.0.1", 8 | "@angular/core": "^7.0.1", 9 | "@angular/forms": "^7.0.1", 10 | "@angular/platform-browser": "^7.0.1", 11 | "@angular/platform-browser-dynamic": "^7.0.1", 12 | "@angular/router": "^7.0.1", 13 | "core-js": "^2.5.7", 14 | "rxjs": "^6.3.3", 15 | "zone.js": "^0.8.26" 16 | }, 17 | "scripts": { 18 | "ng": "ng", 19 | "start": "ng serve", 20 | "build": "ng build", 21 | "test": "ng test", 22 | "lint": "ng lint", 23 | "e2e": "ng e2e" 24 | }, 25 | "devDependencies": { 26 | "@angular-devkit/build-angular": "^0.13.0", 27 | "@angular/cli": "~7.0.2", 28 | "@angular/compiler-cli": "~7.0.0", 29 | "@angular/language-service": "~7.0.0", 30 | "@types/jasmine": "~2.8.8", 31 | "@types/jasminewd2": "~2.0.3", 32 | "@types/node": "~8.9.4", 33 | "codelyzer": "~4.5.0", 34 | "jasmine-core": "~2.99.1", 35 | "jasmine-spec-reporter": "~4.2.1", 36 | "karma": "~3.0.0", 37 | "karma-chrome-launcher": "~2.2.0", 38 | "karma-coverage-istanbul-reporter": "~2.0.1", 39 | "karma-jasmine": "~1.1.2", 40 | "karma-jasmine-html-reporter": "^0.2.2", 41 | "protractor": "~5.4.0", 42 | "ts-node": "~7.0.0", 43 | "tslint": "~5.11.0", 44 | "typescript": "~3.1.1" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/app/app.component.css: -------------------------------------------------------------------------------- 1 | h1, p, button, li { 2 | font-family: Arial, Helvetica, sans-serif; 3 | } -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 |

Angular HMR Lazy Components

2 |

3 | This example shows how Angular HMR can be used to reload lazy routes and lazy 4 | (dynamically-loaded) components. 5 |

6 |

Lazy-Loaded Dynamic Components Example:

7 |

8 | Instructions: 9 |

10 | 19 | 20 |   21 |   22 | 23 |

24 |
25 | 26 |

Lazy-Loaded Routes Example:

27 |

28 | Instructions: 29 |

30 | 35 | Click me to navigate to the lazy route C 36 | 37 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, NgModuleFactoryLoader, Injector, ComponentFactoryResolver, Type, ViewChild, ViewContainerRef } from '@angular/core'; 2 | import { ComponentSelectors } from './component-selectors'; 3 | import { DynamicComponentsService } from './dynamic-components.service'; 4 | 5 | @Component({ 6 | selector: 'my-app', 7 | templateUrl: './app.component.html', 8 | styleUrls: [ './app.component.css' ] 9 | }) 10 | export class AppComponent { 11 | 12 | constructor( 13 | private dynamicComponentSvc: DynamicComponentsService 14 | ) {} 15 | 16 | @ViewChild('componentOutlet', { read: ViewContainerRef }) outlet: ViewContainerRef; 17 | 18 | loadA() { 19 | this.dynamicComponentSvc.createComponent({ 20 | modulePath:'src/app/lazy-components/a/a.module#AModule', 21 | selectorName: ComponentSelectors.AComponent, 22 | outlet: this.outlet 23 | }); 24 | } 25 | 26 | loadB1() { 27 | this.dynamicComponentSvc.createComponent({ 28 | modulePath:'src/app/lazy-components/b/b.module#BModule', 29 | selectorName: ComponentSelectors.B1Component, 30 | outlet: this.outlet 31 | }); 32 | } 33 | 34 | loadB2() { 35 | this.dynamicComponentSvc.createComponent({ 36 | modulePath:'src/app/lazy-components/b/b.module#BModule', 37 | selectorName: ComponentSelectors.B2Component, 38 | outlet: this.outlet 39 | }); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, NgModuleFactoryLoader, SystemJsNgModuleLoader } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { RouterModule, Routes } from '@angular/router'; 5 | 6 | import { AppComponent } from './app.component'; 7 | import { DynamicComponentsService } from './dynamic-components.service'; 8 | 9 | export const appRoutes: Routes = [ 10 | { path: 'c', loadChildren: 'src/app/lazy-routes/c/c.module#CModule' } 11 | ]; 12 | 13 | @NgModule({ 14 | imports: [ BrowserModule, FormsModule, RouterModule.forRoot(appRoutes) ], 15 | providers: [ 16 | //the followiung is needed if you aren't importing the RouterModule 17 | //{provide: NgModuleFactoryLoader, useClass: SystemJsNgModuleLoader}, 18 | DynamicComponentsService 19 | ], 20 | declarations: [ AppComponent ], 21 | bootstrap: [ AppComponent ] 22 | }) 23 | export class AppModule { } 24 | -------------------------------------------------------------------------------- /src/app/component-selectors.ts: -------------------------------------------------------------------------------- 1 | export enum ComponentSelectors { 2 | AComponent = 'my-a', 3 | B1Component = 'my-b1', 4 | B2Component = 'my-b2', 5 | } -------------------------------------------------------------------------------- /src/app/dynamic-components.service.ts: -------------------------------------------------------------------------------- 1 | import { NgModuleFactoryLoader, Injector, ViewContainerRef, ComponentFactoryResolver, Type, Injectable, ComponentRef, ElementRef } from "@angular/core"; 2 | import { ComponentSelectors } from "./component-selectors"; 3 | 4 | @Injectable() 5 | export class DynamicComponentsService { 6 | 7 | constructor( 8 | private loader: NgModuleFactoryLoader, 9 | private injector: Injector 10 | ) { } 11 | 12 | /** 13 | * This map keeps track of the components that are open. 14 | * This is important to be able to reload components when their modules are hot-reloaded. 15 | */ 16 | private openComponents = new Map, CreateComponentRequest>(); 17 | 18 | /** 19 | * Dynamically create an Angular component using an NgModuleFactoryLoader 20 | * @param request Contains the info required to dynamically load an Angular component 21 | */ 22 | public createComponent(request: CreateComponentRequest): Promise { 23 | return this.loader 24 | .load(request.modulePath) 25 | .then((moduleFactory) => { 26 | const module = moduleFactory.create(this.injector); 27 | const componentFactoryResolver = module.componentFactoryResolver; 28 | const factoryClass = this.getFactoryClass(componentFactoryResolver, request.selectorName); 29 | if (!factoryClass) throw new Error(`Unrecognized component name: ${request.selectorName}`); 30 | const componentFactory = componentFactoryResolver.resolveComponentFactory(factoryClass); 31 | const componentRef = request.outlet.createComponent(componentFactory, request.index, module.injector); 32 | this.openComponents.set(componentRef, request); 33 | }) 34 | .catch(err => { 35 | throw err; 36 | }); 37 | } 38 | 39 | /** 40 | * Given an ComponentFactoryResolver, returns the Type that has the given selector. 41 | * We use selectors to find the component because those don't get changed during optimization/minification 42 | * @param componentFactoryResolver A ComponentFactoryResolver for a given module 43 | * @param selector The selector to find 44 | */ 45 | private getFactoryClass(componentFactoryResolver: ComponentFactoryResolver, selector: string): Type { 46 | const factories = Array.from(componentFactoryResolver['_factories'].keys()); 47 | const factoryClass = >factories.find((x: any) => componentFactoryResolver['_factories'].get(x).selector === selector); 48 | return factoryClass; 49 | } 50 | 51 | /** 52 | * Finds all open components of a specific type, and reloads them. This is used in the HMR process. 53 | * @param componentTypes The type of component to reload. 54 | */ 55 | public reloadComponents(componentTypes: Array>) { 56 | //get an array of componentRefs that should be reloaded 57 | let componentsToReload = Array.from(this.openComponents.keys()). 58 | filter(c => componentTypes.some(t => c.instance instanceof t)); 59 | let reloadRequests = new Array(); 60 | 61 | //build the list of reload requests 62 | for (let componentToReload of componentsToReload) { 63 | let request = this.openComponents.get(componentToReload); 64 | //find out what the current index of the component is in the view 65 | request.index = request.outlet.indexOf(componentToReload.hostView); 66 | reloadRequests.push(request); 67 | } 68 | 69 | //close and re-load the components 70 | componentsToReload.forEach(c => this.closeComponentRef(c)); 71 | reloadRequests.forEach(r => this.createComponent(r)); 72 | } 73 | 74 | /** 75 | * Destroy/close the component passed in 76 | * @param component The component to destroy/close 77 | */ 78 | public closeComponent(component: any) { 79 | let componentRef = Array.from(this.openComponents.keys()).find(c => c.instance === component); 80 | this.closeComponentRef(componentRef); 81 | } 82 | 83 | /** 84 | * Destroy/close the componentRef passed in 85 | * @param componentRef The componentRef to destroy/close 86 | */ 87 | protected closeComponentRef(componentRef: ComponentRef) { 88 | componentRef.destroy(); 89 | this.openComponents.delete(componentRef); 90 | } 91 | } 92 | 93 | export interface CreateComponentRequest { 94 | modulePath: string; 95 | selectorName: ComponentSelectors; 96 | outlet: ViewContainerRef; 97 | index?: number; 98 | } 99 | -------------------------------------------------------------------------------- /src/app/hmr-module-helper.ts: -------------------------------------------------------------------------------- 1 | import { NgModuleRef, Compiler, NgModule, Type, NgZone } from "@angular/core"; 2 | import { NgModuleResolver } from "@angular/compiler"; 3 | import { Router } from "@angular/router"; 4 | import { environment } from "../environments/environment"; 5 | import { DynamicComponentsService } from "./dynamic-components.service"; 6 | import { appRoutes } from "./app.module"; 7 | 8 | /** 9 | * This class contains common functionality that allows component modules to hot-reload in the app without reloading the whole app. 10 | * 11 | * ### Example 12 | * 13 | * export class MyModule { 14 | * constructor(moduleRef: NgModuleRef) { 15 | * HmrModuleHelper.enableHmrNgModule(module, moduleRef); 16 | * } 17 | * } 18 | * 19 | * HmrModuleHelper.enableHmrNodeModule(module); 20 | * 21 | * ### Remarks 22 | * 23 | * The source code for the webpack Hot Module Replacement is available at 24 | * https://github.com/webpack/webpack/blob/v4.9.2/lib/HotModuleReplacement.runtime.js 25 | **/ 26 | export class HmrModuleHelper { 27 | 28 | /** 29 | * Call this method from your module file (but outside your module class) to enable hot-module-reload. 30 | * @param nodeModuleRef The module to enable HMR on. Just pass the "module" reference from the module you're calling from. 31 | */ 32 | public static enableHmrNodeModule(nodeModuleRef: NodeModule): void { 33 | if (environment.hmr) { 34 | (nodeModuleRef).hot.accept(); 35 | } 36 | } 37 | 38 | /** 39 | * Call this method from the constructor of modules that will be dynamically loaded (not through the router) to make it available for hot-reload. 40 | * @param nodeModuleRef The module to enable HMR on. Just pass the "module" reference from the module you're calling from. 41 | * @param moduleRef An NgModuleRef that points to the Angular module to enable HMR on. 42 | */ 43 | public static enableHmrDynamicNgModule(nodeModuleRef: NodeModule, moduleRef: NgModuleRef): void { 44 | 45 | //only add a Dispose handler if this environment supports hmr and we haven't already created a disposeHandler 46 | if (environment.hmr && (nodeModuleRef).hot._disposeHandlers.length == 0) { 47 | 48 | let compiler = moduleRef.injector.get(Compiler); 49 | let dynamicComponentSvc = moduleRef.injector.get(DynamicComponentsService); 50 | let zone = moduleRef.injector.get(NgZone); 51 | 52 | (nodeModuleRef).hot.addDisposeHandler(() => { 53 | try { 54 | zone.run(() => { 55 | //get the metadata for this module 56 | let metaData: NgModule = ((compiler as any)._metadataResolver._ngModuleResolver as NgModuleResolver).resolve((moduleRef)._moduleType); 57 | 58 | //get a list of all component types that are part of this module (they should all be listed as entryComponents) 59 | let componentTypes = >>metaData.entryComponents; 60 | 61 | //ask all components of all affected types to reload 62 | dynamicComponentSvc.reloadComponents(componentTypes); 63 | 64 | //clear the Angular cache that knows about this component, so its reloaded 65 | for (let declarations of metaData.declarations) { 66 | let dec = >declarations; 67 | compiler.clearCacheFor(dec); 68 | } 69 | 70 | compiler.clearCacheFor((moduleRef)._moduleType); 71 | }); 72 | 73 | } catch (error) { 74 | console.error(error); 75 | throw error; 76 | } 77 | }); 78 | } 79 | } 80 | 81 | /** 82 | * Call this method from the constructor of modules that will be lazy loaded through the router to make it available for hot-reload. 83 | * @param nodeModuleRef The module to enable HMR on. Just pass the "module" reference from the module you're calling from. 84 | * @param moduleRef An NgModuleRef that points to the Angular module to enable HMR on. 85 | */ 86 | public static enableHmrRouterNgModule(nodeModuleRef: NodeModule, moduleRef: NgModuleRef): void { 87 | 88 | //only add a Dispose handler if this environment supports hmr and we haven't already created a disposeHandler 89 | if (environment.hmr && (nodeModuleRef).hot._disposeHandlers.length == 0) { 90 | 91 | let compiler = moduleRef.injector.get(Compiler); 92 | let zone = moduleRef.injector.get(NgZone); 93 | let router = moduleRef.injector.get(Router); 94 | 95 | (nodeModuleRef).hot.addDisposeHandler(() => { 96 | try { 97 | zone.run(() => { 98 | //get the metadata for this module 99 | let metaData: NgModule = ((compiler as any)._metadataResolver._ngModuleResolver as NgModuleResolver).resolve((moduleRef)._moduleType); 100 | 101 | //clear the Angular cache that knows about this component, so its reloaded 102 | for (let declarations of metaData.declarations) { 103 | let dec = >declarations; 104 | compiler.clearCacheFor(dec); 105 | } 106 | 107 | compiler.clearCacheFor((moduleRef)._moduleType); 108 | 109 | //tell the router to reset its config - this causes it to purge the previously loaded module 110 | router.resetConfig(appRoutes); 111 | 112 | //tell the router to re-load the current route 113 | router.navigateByUrl(router.url); 114 | }); 115 | 116 | } catch (error) { 117 | console.error(error); 118 | throw error; 119 | } 120 | }); 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/app/lazy-components/a/a.component.css: -------------------------------------------------------------------------------- 1 | .lazy-component { 2 | border: 1px solid #40A8CC; 3 | } -------------------------------------------------------------------------------- /src/app/lazy-components/a/a.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { ComponentSelectors } from '../../component-selectors'; 3 | import { DynamicComponentsService } from '../../dynamic-components.service'; 4 | 5 | @Component({ 6 | selector: ComponentSelectors.AComponent, 7 | template: ` 8 |
9 | 10 | 11 | 12 | {{content}} 13 |
14 | `, 15 | styleUrls: [ '../../lazy.component.css', './a.component.css' ] 16 | }) 17 | export class AComponent { 18 | loading: boolean = true; 19 | content: string; 20 | 21 | constructor( 22 | private dynamicComponentSvc: DynamicComponentsService 23 | ) {} 24 | 25 | ngOnInit() { 26 | setTimeout(() => { 27 | this.loading = false; 28 | this.content = 'A'; 29 | //uncomment this next line to see this component dynamically reload 30 | //this.content = 'A1' 31 | },3000) 32 | } 33 | 34 | closeMe() { 35 | this.dynamicComponentSvc.closeComponent(this); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/app/lazy-components/a/a.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, NgModuleRef } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { AComponent } from './a.component'; 5 | import { HmrModuleHelper } from '../../hmr-module-helper'; 6 | 7 | @NgModule({ 8 | imports: [CommonModule], 9 | declarations: [AComponent], 10 | entryComponents: [AComponent] 11 | }) 12 | export class AModule { 13 | constructor(moduleRef: NgModuleRef) { 14 | HmrModuleHelper.enableHmrDynamicNgModule(module, moduleRef); 15 | } 16 | } 17 | 18 | HmrModuleHelper.enableHmrNodeModule(module); 19 | -------------------------------------------------------------------------------- /src/app/lazy-components/b/b.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, NgModuleRef } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { B1Component } from './b1.component'; 5 | import { B2Component } from './b2.component'; 6 | import { HmrModuleHelper } from '../../hmr-module-helper'; 7 | 8 | @NgModule({ 9 | imports: [CommonModule], 10 | declarations: [B1Component,B2Component], 11 | entryComponents: [B1Component,B2Component] 12 | }) 13 | export class BModule { 14 | constructor(moduleRef: NgModuleRef) { 15 | HmrModuleHelper.enableHmrDynamicNgModule(module, moduleRef); 16 | } 17 | } 18 | 19 | HmrModuleHelper.enableHmrNodeModule(module); -------------------------------------------------------------------------------- /src/app/lazy-components/b/b1.component.css: -------------------------------------------------------------------------------- 1 | .lazy-component { 2 | border: 1px solid rgb(204, 64, 64); 3 | } -------------------------------------------------------------------------------- /src/app/lazy-components/b/b1.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { ComponentSelectors } from '../../component-selectors'; 3 | import { DynamicComponentsService } from '../../dynamic-components.service'; 4 | 5 | @Component({ 6 | selector: ComponentSelectors.B1Component, 7 | template: ` 8 |
9 | 10 | 11 | 12 | {{content}} 13 |
14 | `, 15 | styleUrls: [ '../../lazy.component.css', './b1.component.css' ] 16 | }) 17 | export class B1Component { 18 | loading: boolean = true; 19 | content: string; 20 | 21 | constructor( 22 | private dynamicComponentSvc: DynamicComponentsService 23 | ) {} 24 | 25 | ngOnInit() { 26 | setTimeout(() => { 27 | this.loading = false; 28 | this.content = 'B1'; 29 | },3000) 30 | } 31 | 32 | closeMe() { 33 | this.dynamicComponentSvc.closeComponent(this); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/app/lazy-components/b/b2.component.css: -------------------------------------------------------------------------------- 1 | .lazy-component { 2 | border: 1px solid rgb(64, 204, 122); 3 | } -------------------------------------------------------------------------------- /src/app/lazy-components/b/b2.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { ComponentSelectors } from '../../component-selectors'; 3 | import { DynamicComponentsService } from '../../dynamic-components.service'; 4 | 5 | @Component({ 6 | selector: ComponentSelectors.B2Component, 7 | template: ` 8 |
9 | 10 | 11 | 12 | {{content}} 13 |
14 | `, 15 | styleUrls: [ '../../lazy.component.css', './b2.component.css' ] 16 | }) 17 | export class B2Component { 18 | loading: boolean = true; 19 | content: string; 20 | 21 | constructor( 22 | private dynamicComponentSvc: DynamicComponentsService 23 | ) {} 24 | 25 | ngOnInit() { 26 | setTimeout(() => { 27 | this.loading = false; 28 | this.content = 'B2'; 29 | },3000) 30 | } 31 | 32 | closeMe() { 33 | this.dynamicComponentSvc.closeComponent(this); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/app/lazy-routes/c/c.component.css: -------------------------------------------------------------------------------- 1 | .lazy-component { 2 | border: 1px solid #40A8CC; 3 | } -------------------------------------------------------------------------------- /src/app/lazy-routes/c/c.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { ComponentSelectors } from '../../component-selectors'; 3 | import { DynamicComponentsService } from '../../dynamic-components.service'; 4 | 5 | @Component({ 6 | selector: 'my-c', 7 | template: ` 8 |
9 | 10 | 11 | 12 | {{content}} 13 |
14 | `, 15 | styleUrls: [ '../../lazy.component.css', './c.component.css' ] 16 | }) 17 | export class CComponent { 18 | loading: boolean = true; 19 | content: string; 20 | 21 | ngOnInit() { 22 | setTimeout(() => { 23 | this.loading = false; 24 | this.content = 'C'; 25 | //uncomment this next line to see this component dynamically reload 26 | //this.content = 'C1' 27 | },3000) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/app/lazy-routes/c/c.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, NgModuleRef, ModuleWithProviders } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { RouterModule, Routes } from '@angular/router'; 4 | 5 | import { CComponent } from './c.component'; 6 | import { HmrModuleHelper } from '../../hmr-module-helper'; 7 | 8 | const routes: Routes = [ 9 | { path: '', component: CComponent } 10 | ] 11 | const routing: ModuleWithProviders = RouterModule.forChild(routes); 12 | 13 | @NgModule({ 14 | imports: [CommonModule, routing], 15 | declarations: [CComponent], 16 | entryComponents: [CComponent] 17 | }) 18 | export class CModule { 19 | constructor(moduleRef: NgModuleRef) { 20 | HmrModuleHelper.enableHmrRouterNgModule(module, moduleRef); 21 | } 22 | } 23 | 24 | HmrModuleHelper.enableHmrNodeModule(module); 25 | -------------------------------------------------------------------------------- /src/app/lazy.component.css: -------------------------------------------------------------------------------- 1 | .lazy-component { 2 | box-shadow: 2px 2px 1px 0 #cccccc, 0 0 2px 0 #cccccc !important; 3 | width: 50px; 4 | height: 50px; 5 | text-align: center; 6 | line-height: 50px; 7 | font-family: Arial, Helvetica, sans-serif; 8 | margin: 10px; 9 | } 10 | 11 | .lazy-component .loadingImage { 12 | width: 50px; 13 | height: 50px; 14 | } -------------------------------------------------------------------------------- /src/assets/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wags1999/angular-hmr-lazy-components/9b16281c41ad4adac1f6bdb4ceb793b95346bf5f/src/assets/loading.gif -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | hmr: false 4 | }; -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: false, 3 | hmr: true 4 | }; -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Angular App 4 | 5 | 6 | 7 | loading 8 | 9 | -------------------------------------------------------------------------------- /src/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../coverage'), 20 | reports: ['html', 'lcovonly'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false 30 | }); 31 | }; -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import './polyfills'; 2 | 3 | import { enableProdMode } from '@angular/core'; 4 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 5 | 6 | import { AppModule } from './app/app.module'; 7 | import { environment } from './environments/environment.prod'; 8 | 9 | platformBrowserDynamic().bootstrapModule(AppModule).then(ref => { 10 | // Ensure Angular destroys itself on hot reloads. 11 | if (window['ngRef']) { 12 | window['ngRef'].destroy(); 13 | } 14 | window['ngRef'] = ref; 15 | 16 | // Otherwise, log the boot error 17 | }).catch(err => console.error(err)); -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | // import 'core-js/es6/symbol'; 23 | // import 'core-js/es6/object'; 24 | // import 'core-js/es6/function'; 25 | // import 'core-js/es6/parse-int'; 26 | // import 'core-js/es6/parse-float'; 27 | // import 'core-js/es6/number'; 28 | // import 'core-js/es6/math'; 29 | // import 'core-js/es6/string'; 30 | // import 'core-js/es6/date'; 31 | // import 'core-js/es6/array'; 32 | // import 'core-js/es6/regexp'; 33 | // import 'core-js/es6/map'; 34 | // import 'core-js/es6/set'; 35 | 36 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 37 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 38 | 39 | /** IE10 and IE11 requires the following to support `@angular/animation`. */ 40 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 41 | 42 | 43 | /** Evergreen browsers require these. **/ 44 | import 'core-js/es6/reflect'; 45 | import 'core-js/es7/reflect'; 46 | 47 | 48 | /** ALL Firefox browsers require the following to support `@angular/animation`. **/ 49 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 50 | 51 | 52 | 53 | /*************************************************************************************************** 54 | * Zone JS is required by Angular itself. 55 | */ 56 | import 'zone.js/dist/zone'; // Included with Angular CLI. 57 | 58 | 59 | /*************************************************************************************************** 60 | * APPLICATION IMPORTS 61 | */ 62 | 63 | /** 64 | * Date, currency, decimal and percent pipes. 65 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 66 | */ 67 | // import 'intl'; // Run `npm install --save intl`. -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | /* Add application styles & imports to this file! */ -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app" 5 | }, 6 | "exclude": [ 7 | "test.ts", 8 | "**/*.spec.ts" 9 | ] 10 | } -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "test.ts", 12 | "polyfills.ts" 13 | ], 14 | "include": [ 15 | "**/*.spec.ts", 16 | "**/*.d.ts" 17 | ] 18 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "module": "es2015", 9 | "moduleResolution": "node", 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "target": "es5", 13 | "typeRoots": [ 14 | "node_modules/@types" 15 | ], 16 | "lib": [ 17 | "es6", 18 | "dom" 19 | ] 20 | } 21 | } --------------------------------------------------------------------------------