├── .editorconfig ├── .gitignore ├── .vscode └── launch.json ├── DynamicContentDiagram.png ├── DynamicContentDiagram.xml ├── LICENSE ├── README.md ├── angular.json ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.e2e.json ├── package-lock.json ├── package.json ├── src ├── app │ ├── app.component.css │ ├── app.component.html │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── dynamic-content-outlet │ │ ├── dynamic-content-outlet-error.component.ts │ │ ├── dynamic-content-outlet.component.ts │ │ ├── dynamic-content-outlet.module.ts │ │ ├── dynamic-content-outlet.registry.ts │ │ └── dynamic-content-outlet.service.ts │ ├── dynamic-multiple │ │ ├── dynamic-multiple-one.component.css │ │ ├── dynamic-multiple-one.component.html │ │ ├── dynamic-multiple-one.component.ts │ │ ├── dynamic-multiple-two.component.css │ │ ├── dynamic-multiple-two.component.html │ │ ├── dynamic-multiple-two.component.ts │ │ └── dynamic-multiple.module.ts │ └── dynamic-single │ │ ├── dynamic-single-one.component.css │ │ ├── dynamic-single-one.component.html │ │ ├── dynamic-single-one.component.ts │ │ └── dynamic-single.module.ts ├── assets │ └── .gitkeep ├── browserslist ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── karma.conf.js ├── main.ts ├── polyfills.ts ├── styles.css ├── test.ts ├── tsconfig.app.json ├── tsconfig.spec.json └── tslint.json ├── tsconfig.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | /node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.sass-cache 29 | /connect.lock 30 | /coverage 31 | /libpeerconnection.log 32 | npm-debug.log 33 | yarn-error.log 34 | testem.log 35 | /typings 36 | 37 | # System Files 38 | .DS_Store 39 | Thumbs.db 40 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "chrome", 9 | "request": "launch", 10 | "name": "Launch Chrome against localhost", 11 | "url": "http://localhost:4200", 12 | "webRoot": "${workspaceFolder}" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /DynamicContentDiagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wesleygrimes/angular-dynamic-content/282ec7d7651c65ffe93ce5ee0d1dbff46779030c/DynamicContentDiagram.png -------------------------------------------------------------------------------- /DynamicContentDiagram.xml: -------------------------------------------------------------------------------- 1 | 7Vxbl6I4EP41nn3qPkC4+ehlemfP9lzO9OzZ6ccIAZlB4kLs1vn1m0CiQKLiCMqZ9qVbCgjwfVX1VSXoAEwW6z9TuJx/wD6KB4bmrwdgOjAMXXdt+o9ZNoXF0bTCEKaRX5hKhqfoJ+JnCusq8lHGbYWJYByTaFk1ejhJkEcqNpim+LV6WIBjv2JYwhBJhicPxrL138gn88LqWtrO/h5F4VxcWRfPt4DiYG7I5tDHryUTeDcAkxRjUnxarCcoZuBVcXnYs3d7YylKSJMTvgTG7PHDZwP+/Sl4/f7hx8dv/+h3fJQXGK/4A/ObJRuBQIpXiY/YIPoAjF/nEUFPS+ixva+Uc2qbk0XMdwdRHE9wjNP8XOBD5AYetWckxT9QaY/tuWgW0D38BlBK0Hrvk+lbvKijIbxAJN3QQ8QJGncy7mOGzSF/3TEGBGPzMlsON0LuJeF27B2Q9APH8gRcDQnX6SaBi8ijxgleLHHCns3QvqAwysQFy6hTNEgV2iqECR2hhjc3wTgKE7rp0Ssgah8zbCPq0SO+YxH5PruMkssd21q+RSCJMDvrbqi1w5YpQOdsWUOZrS0xZbaMrsgyjwfBEbeH2bJIPkG0ZuDV4yAIkO0p48B3hjOtJWQt26oiq8nIGpoS2a6gtfbFAVWKVYxG52WbNjAb1jDTLdkbTUvGDFgdQWYfhmzcP8gU6faykDmHIZv0DjJbu7aXuYchm/YPMuvaXjZUyIQNF+zRY6bVcLm88wsQ72g5SuiD3uEViRET+oE19oTuf4QLNLCm+Rj2fytWAI7/4PBvi4PRbt/2MiHfyi83S+mnsLI/LrYeDt1JdagWa70gCIw9GmfPbMtuqdYzzYpbGI5zL8eSoaof7K5ETvQqJc8YJZtqpXe58o7gZaPaLqCO8UBdJGZIPq68yIf5LScZzqtDtv+J37E4nvdneltkutVS0DCGcowPFTHeWSmoqxqi7oJ8cgvyJkEOLOP6Qe5KQCI/RCJCcErmOMQJjN/trLWIK8GK1hH5xs3s8zP7fG9YfHO6Lu2bbvjGd0TIhocgXBFMTbvLPmIW9wVnTSK7FMkZgSkZsckSlldimGWRJ8wPUVwVdfbQh2mlGOFV6qFDYPIOi14hROSY5sp+kqKY9qUv1Rtpn/Jh95Q7N8qVKfhqnMszN2cm0BOZqSdc10PqhDtzLdNqa+bAqgmxo5g5AKoZNKuzfAskIspTaLneMcmVhPKTUONTqq8UZdFPONsWO0scJSR/JGucq/iYRV/GeSqVYTEKyN4iLKNeESXh1zxM70wV20e8ow1uh/UiC8hFlkpKzc4mRw/Pjgpqn1D6EtGo2h9+Wrvhp5rAtpDrm6rwc40ZsFuqdyx3W9+Iise2FRWPbqv6XaMrmuQIlPUw8feoiGw+gxWKY7rJxfPeEpvPQlfZxk498y0hn1vNvRcy+1xWWaXktqh5ZkPJK4CV3aNEvWr5QtgaKyO/wmeW3HbeV2vCnPqiSPGY/KSdT0nj6KA2UL1LK3CQBsqdc/vUZ/irPNlcXmr5K6F+mbyVfFJN+cPh9bOJPON4yyYnZRMB2PF0Yt3SSQvpRDnfmxebfvRSmpbJTbTUS4TtY1hMotOTH6BHcLp5xNBHqTiW3k758JK5MvDlpgxPWBE+NdZayGa25lQcwTQVuUzl0J3NElryGp1EV0hxW+59ev66Cu84BttbbY5KLcxMV1dNkalWe3WtqxUS0GAlvbPmWaG2SKd666jUdmg7ADZW2wNOIPMjNLchG25XTgr2LiRT4zZBySvwv1lzfBJ3YhhFgrnoGylg74p2eX3wnMjqDDvDqbWyyrn7y67cArn2zCu7LZjZW1LbX2HVrKoNUHJqXFSD5RC5tgbbmnltBbYadFmXVOC2+t0DLtBfBbbkDkKhwPLbSW9Cgfdw1xcFFuvIBxT4Eq+V/Qp2fVRgW36v4qbAJ7HaPwW2jesrMNB7J8F2g2WTS0rwTPf9QFNJsK45YHheGi98oL8SbMszEgoJnr5JCd7HXW8keO+XB7aqIRN3IQk+gl0vJVieD7pJ8Ems9lCCe9AE1yW4FzPRds/64Bn0XB+oRNgApmn557ltz/tgu1Ef3GTF6zcU4Z73wc7xPvgS30j7Fez6KMLOrQ8+k9X+ibAj98EjnyHHfqqA/onhz02R5TI2VMLuIQlXMUzvv2c46Snhpa9+t/WSsQsq1Dma2yy5AdAVc3KHMfr0lRoe0gglfsxuXvVmqnjF+ChxpahgyFSj6D2KXxCjR8Vc+dtXegvg15YqdMUL3o6qKACnRw3d3P2qRfE2zu63QcC7/wE= -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Wes Grimes 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 | Ultimate Courses 2 | 3 | ![](https://wesleygrimes.com/assets/post_headers/dynamic_content_outlet_header.png) 4 | 5 | ## Overview — Dynamic Content Outlet 6 | 7 | Have you ever needed to dynamically load content or components in your Angular applications? How about in a way that the built-in structural directives  (`*ngIf*`, `*ngSwitch`) just don’t provide? Are you also in need of the optimization benefits of using Ahead-of-Time compilation? 8 | 9 | Well, I have good news for you…(And no you don’t have to be Chuck Norris!) If you stay tuned, I will help you get a solution up and running that will provide a solid way to choose from and load dynamically, at run-time, a set of predefined modules & components in your application. 10 | 11 | > This article is assumes you are building an Angular 6+ application generated using the Angular CLI. For information on using the Angular CLI check out the [official documentation](https://angular.io/cli##cli-command-reference). 12 | 13 | > This arose out of a business need for the company that I work for. What’s important to note here is that many articles and examples exist on loading content dynamically in Angular, but none that I found worked reliably when compiling Angular with the `— prod` or `— aot` flags enabled. The good news is that what I describe in this article works fantastically with Ahead-of-Time compiling. 14 | 15 | ## What We’re Going To Do 16 | 17 | We’re going to build a special module with a dynamic component outlet that can be included and used anywhere in your application. The only requirement is that you register, upfront, an array mapping your dynamic components to their parent modules. You will also add these modules to the `lazyModules` property in your `angular.json` file. By doing so, the compiler will pre-compile these modules. The compiler then splits them off into separate minified chunks and makes them available to the SystemJS loader at runtime, with AOT. 18 | 19 | --- 20 | 21 | ## Let’s Build Our Dynamic Content Outlet 22 | 23 | Assuming that you have an existing Angular 6+ CLI generated project let’s run through the following steps to scaffold the necessary parts that make up this new Dynamic Content Outlet. 24 | 25 | ### Generate the Dynamic Content Outlet Module 26 | 27 | Generate a new module named `DynamicContentOutletModule` by running the following command in your shell of choice: 28 | 29 | ```shell 30 | $ ng g m dynamic-content-outlet 31 | ``` 32 | 33 | We will come back later to this module and wire things up. 34 | 35 | ### Build the Dynamic Content Outlet Registry 36 | 37 | Create a new file underneath the newly created folder `src/app/dynamic-content-outlet` named `dynamic-content-outlet.registry.ts`. This will serve as the placeholder for array mapping component name to module path and module name. For now, it will be an empty array as follows. 38 | 39 | ```typescript 40 | interface RegistryItem { 41 | componentName: string; 42 | modulePath: string; 43 | moduleName: string; 44 | } 45 | 46 | /** 47 | * A registry array of Component Name to details 48 | * that must be updated with each new component 49 | * that you wish to load dynamically. 50 | */ 51 | 52 | export const DynamicContentOutletRegistry: RegistryItem[] = []; 53 | ``` 54 | 55 | ### Build the Dynamic Content Outlet Error Component 56 | 57 | Create a new file underneath the folder `src/app/dynamic-content-outlet/dynamic-content-outlet-error.component.ts`. This will serve as the component to be rendered anytime an error occurs attempting to load a dynamic component. You can customize the `template` property to use any custom styles or layout that you may have. The `errorMessage` input must stay the same and will be fed with the actual details of the error that occurred while attempting to dynamically render your component. 58 | 59 | ```typescript 60 | import { Component, Input } from '@angular/core'; 61 | 62 | @Component({ 63 | selector: 'app-dynamic-content-outlet-error-component', 64 | template: ` 65 |
{{ errorMessage }}
66 | ` 67 | }) 68 | export class DynamicContentOutletErrorComponent { 69 | @Input() errorMessage: string; 70 | constructor() {} 71 | } 72 | ``` 73 | 74 | ### Build the Dynamic Content Outlet Service 75 | 76 | Create a new file underneath the folder `src/app/dynamic-content-outlet/dynamic-content-outlet.service.ts`. 77 | 78 | - This service encapsulates the logic that loads dynamic components using SystemJS and renders them into the Dynamic Content Outlet. 79 | - It uses the `DynamicContentOutletRegistry` to lookup the module by `componentName`. 80 | - It also makes use of a new `static` property that we will add later on to each module we wish to dynamically load named `dynamicComponentsMap`. This allows us to get the type literal for the given `componentName` so that the `resolveComponentFactory` can instantiate the correct component. You might ask why we didn't just add a fourth property to the `DynamicContentOutletRegistry`, well this is because if we import the type in the registry, then it defeats the purpose of lazy loading these modules as the the type will be included in the main bundle. 81 | - If an error occurs, a `DynamicContentOutletErrorComponent` is rendered instead with the error message included. 82 | 83 | ```typescript 84 | import { 85 | ComponentFactoryResolver, 86 | ComponentRef, 87 | Injectable, 88 | Injector, 89 | NgModuleFactoryLoader, 90 | Type 91 | } from '@angular/core'; 92 | import { DynamicContentOutletErrorComponent } from './dynamic-content-outlet-error.component'; 93 | import { DynamicContentOutletRegistry } from './dynamic-content-outlet.registry'; 94 | 95 | type ModuleWithDynamicComponents = Type & { 96 | dynamicComponentsMap: {}; 97 | }; 98 | 99 | @Injectable() 100 | export class DynamicContentOutletService { 101 | constructor( 102 | private componentFactoryResolver: ComponentFactoryResolver, 103 | private moduleLoader: NgModuleFactoryLoader, 104 | private injector: Injector 105 | ) {} 106 | 107 | async GetComponent(componentName: string): Promise> { 108 | const modulePath = this.getModulePathForComponent(componentName); 109 | 110 | if (!modulePath) { 111 | return this.getDynamicContentErrorComponent( 112 | `Unable to derive modulePath from component: ${componentName} in dynamic-content.registry.ts` 113 | ); 114 | } 115 | 116 | try { 117 | const moduleFactory = await this.moduleLoader.load(modulePath); 118 | const moduleReference = moduleFactory.create(this.injector); 119 | const componentResolver = moduleReference.componentFactoryResolver; 120 | 121 | const componentType = (moduleFactory.moduleType as ModuleWithDynamicComponents) 122 | .dynamicComponentsMap[componentName]; 123 | 124 | const componentFactory = componentResolver.resolveComponentFactory( 125 | componentType 126 | ); 127 | return componentFactory.create(this.injector); 128 | } catch (error) { 129 | console.error(error.message); 130 | return this.getDynamicContentErrorComponent( 131 | `Unable to load module ${modulePath}. 132 | Looked up using component: ${componentName}. Error Details: ${ 133 | error.message 134 | }` 135 | ); 136 | } 137 | } 138 | 139 | private getModulePathForComponent(componentName: string) { 140 | const registryItem = DynamicContentOutletRegistry.find( 141 | i => i.componentName === componentName 142 | ); 143 | 144 | if (registryItem && registryItem.modulePath) { 145 | // imported modules must be in the format 'path#moduleName' 146 | return `${registryItem.modulePath}#${registryItem.moduleName}`; 147 | } 148 | 149 | return null; 150 | } 151 | 152 | private getDynamicContentErrorComponent(errorMessage: string) { 153 | const factory = this.componentFactoryResolver.resolveComponentFactory( 154 | DynamicContentOutletErrorComponent 155 | ); 156 | const componentRef = factory.create(this.injector); 157 | const instance = componentRef.instance; 158 | instance.errorMessage = errorMessage; 159 | return componentRef; 160 | } 161 | } 162 | ``` 163 | 164 | ### Build the Dynamic Content Outlet Component 165 | 166 | Create a new file underneath the folder `src/app/dynamic-content-outlet/dynamic-content-outlet.component.ts`. This component takes an input property named `componentName` that will call the `DynamicContentOutletService.GetComponent` method passing into it `componentName`. The service then returns an instance of that rendered and compiled component for injection into the view. The service returns an error component instance if the rendering fails for some reason. The component listens for changes via the `ngOnChanges` life-cycle method. If the `@Input() componentName: string;` is set or changes it automatically re-renders the component as necessary. It also properly handles destroying the component with the `ngOnDestroy` life-cycle method. 167 | 168 | ```typescript 169 | import { 170 | Component, 171 | ComponentRef, 172 | Input, 173 | OnChanges, 174 | OnDestroy, 175 | ViewChild, 176 | ViewContainerRef 177 | } from '@angular/core'; 178 | import { DynamicContentOutletService } from './dynamic-content-outlet.service'; 179 | 180 | @Component({ 181 | selector: 'app-dynamic-content-outlet', 182 | template: ` 183 | 184 | ` 185 | }) 186 | export class DynamicContentOutletComponent implements OnDestroy, OnChanges { 187 | @ViewChild('container', { read: ViewContainerRef }) 188 | container: ViewContainerRef; 189 | 190 | @Input() componentName: string; 191 | 192 | private component: ComponentRef<{}>; 193 | 194 | constructor(private dynamicContentService: DynamicContentOutletService) {} 195 | 196 | async ngOnChanges() { 197 | await this.renderComponent(); 198 | } 199 | 200 | ngOnDestroy() { 201 | this.destroyComponent(); 202 | } 203 | 204 | private async renderComponent() { 205 | this.destroyComponent(); 206 | 207 | this.component = await this.dynamicContentService.GetComponent( 208 | this.componentName 209 | ); 210 | this.container.insert(this.component.hostView); 211 | } 212 | 213 | private destroyComponent() { 214 | if (this.component) { 215 | this.component.destroy(); 216 | this.component = null; 217 | } 218 | } 219 | } 220 | ``` 221 | 222 | ### Finish Wiring Up Parts To The Dynamic Content Outlet Module 223 | 224 | Make sure your `src/app/dynamic-content-outlet/dynamic-content-outlet.module.ts` file looks like the following: 225 | 226 | ```typescript 227 | import { CommonModule } from '@angular/common'; 228 | import { 229 | NgModule, 230 | NgModuleFactoryLoader, 231 | SystemJsNgModuleLoader 232 | } from '@angular/core'; 233 | import { DynamicContentOutletErrorComponent } from './dynamic-content-outlet-error.component'; 234 | import { DynamicContentOutletComponent } from './dynamic-content-outlet.component'; 235 | import { DynamicContentOutletService } from './dynamic-content-outlet.service'; 236 | 237 | @NgModule({ 238 | imports: [CommonModule], 239 | declarations: [ 240 | DynamicContentOutletComponent, 241 | DynamicContentOutletErrorComponent 242 | ], 243 | exports: [DynamicContentOutletComponent], 244 | providers: [ 245 | { 246 | provide: NgModuleFactoryLoader, 247 | useClass: SystemJsNgModuleLoader 248 | }, 249 | DynamicContentOutletService 250 | ] 251 | }) 252 | export class DynamicContentOutletModule {} 253 | ``` 254 | 255 | --- 256 | 257 | ## Let’s Use Our New Dynamic Content Outlet 258 | 259 | Phew! Take a deep breath and grab a cup of coffee (french press fair trade organic dark roast). The hard work is behind you. Next we will go through the process of actually putting this new module into play! 260 | 261 | ![](https://cdn-images-1.medium.com/max/1600/1*8BhahXd-DWmGj_n-mhP-gA.jpeg) 262 | 263 | For any component that you would like dynamically rendered you need to do the following four steps. **_These steps must be followed exactly_**_._ 264 | 265 | ### 1. Prepare your module for dynamic import 266 | 267 | - Confirm that the component is listed in the `entryComponents` array in the module that the component is a part of. 268 | 269 | - Add to the module, a new `static` property called `dynamicComponentsMap`. This allows us to get the type literal for the given `componentName` so that the `resolveComponentFactory` can instantiate the correct component. 270 | 271 | > You might ask why we didn't just add a fourth property to the `DynamicContentOutletRegistry` named `componentType`; well this is because if we import the type in the registry, then it defeats the purpose of lazy loading these modules as the the type will be included in the main bundle. 272 | 273 | A prepared module might look as follows: 274 | 275 | ```typescript 276 | import { CommonModule } from '@angular/common'; 277 | import { NgModule } from '@angular/core'; 278 | import { DynamicMultipleOneComponent } from './dynamic-multiple-one.component'; 279 | import { DynamicMultipleTwoComponent } from './dynamic-multiple-two.component'; 280 | 281 | @NgModule({ 282 | declarations: [MySpecialDynamicContentComponent], 283 | imports: [CommonModule], 284 | entryComponents: [MySpecialDynamicContentComponent] 285 | }) 286 | export class MySpecialDynamicContentModule { 287 | static dynamicComponentsMap = { 288 | MySpecialDynamicContentComponent 289 | }; 290 | } 291 | ``` 292 | 293 | ### 2. Add your dynamic component(s) to the registry 294 | 295 | For any component that you would like dynamically rendered, add a new entry to the `DynamicContentOutletRegistry` array in `src/app/dynamic-content-outlet/dynamic-content-outlet.registry.ts`. 296 | 297 | The following properties must be filled out: 298 | 299 | - `componentName`: This should match exactly the name of the Component you wish to load dynamically. 300 | 301 | - `modulePath`: The absolute path to the module containing the component you wish to load dynamically. This is only the path to the module and does NOT include `moduleName` after a `#`. 302 | 303 | - `moduleName`: This is the exact name of the module. 304 | 305 | #### Example Component Mapping 306 | 307 | ```typescript 308 | { 309 | componentName: 'MySpecialDynamicContentComponent', 310 | modulePath: 'src/app/my-special-dynamic-content/my-special-dynamic-content.module', 311 | moduleName: 'MySpecialDynamicContentModule' 312 | }, 313 | ``` 314 | 315 | ### 3. Add your dynamic modules to the lazyModules array 316 | 317 | In your `angular.json` update the `projects > ** > architect > build > options > lazyModules` array and add an item for each module that you added to the registry in order for the Angular AOT compiler to detect and pre-compile your dynamic modules. If you have multiple projects in a folder, make sure you add this for the correct project you are importing and using dynamic modules in. The updated file will look similar to this: 318 | 319 | ```json 320 | { 321 | ... 322 | "projects": { 323 | "angular-dynamic-content": { 324 | ... 325 | "architect": { 326 | "build": { 327 | "builder": "@angular-devkit/build-angular:browser", 328 | "options": { 329 | ... 330 | "lazyModules": ["src/app/my-special-dynamic-content/my-special-dynamic-content.module"] 331 | }, 332 | } 333 | } 334 | } 335 | } 336 | } 337 | ``` 338 | 339 | ### Wire up the Dynamic Content Outlet Module 340 | 341 | Up to this point you have created your dynamic content outlet module and registered your components to be available in the outlet. The last thing we need to do is wire up our new `DynamicContentOutletModule` to be used in our application. In order to do so you need to: 342 | 343 | 1. Add your new `DynamicContentOutletModule` to the `imports` array of any feature module or the main `AppModule` of your Angular application. 344 | 345 | #### Example of addition to the `imports` array 346 | 347 | ```typescript 348 | @NgModule({ 349 | ... 350 | imports: [ 351 | ... 352 | DynamicContentOutletModule 353 | ], 354 | ... 355 | }) 356 | export class AppModule {} 357 | ``` 358 | 359 | 2. Add the following tag to the template of the parent component that you would like to render the dynamic content in: 360 | 361 | ```html 362 | 363 | 364 | ``` 365 | 366 | This is very similar in nature to Angular’s built-in `/` tag. 367 | 368 | 3. Happy `ng serve --prod` ing! 369 | 370 | ## Real-World Complex Example 371 | 372 | If you are interested in a more in-depth real-world example, then check out the Github Repository which will demonstrate the following: 373 | 374 | - Dynamic modules with multiple components 375 | - Demonstrating the use of on-the-fly component changes 376 | - Demonstrating that the scoped styles are loaded dynamically for each component 377 | 378 | > GitHub Repository Example [https://github.com/wesleygrimes/angular-dynamic-content](https://github.com/wesleygrimes/angular-dynamic-content) 379 | 380 | ## Conclusion 381 | 382 | Hopefully you have found this solution helpful. Here is the full GitHub repository example for you to clone and play around with. PR’s are welcome, appreciated, encouraged and accepted! 383 | 384 | ## Additional Resources 385 | 386 | I would highly recommend enrolling in the Ultimate Angular courses. It is well worth the money and I have used it as a training tool for new Angular developers. Follow the link below to signup. 387 | 388 | [Ultimate Courses: Expert online courses in JavaScript, Angular, NGRX and TypeScript](https://ultimatecourses.com/?ref=76683_ttll_neb) 389 | 390 | ## Special Thanks 391 | 392 | I want to take a moment and thank all those I was able to glean this information from. I did not come up with all this on my own, but I was able to get a working solution by combining parts from each of these articles! 393 | 394 | - [Dynamically Loading Components with Angular CLI](https://blog.angularindepth.com/dynamically-loading-components-with-angular-cli-92a3c69bcd28) 395 | 396 | - [Here is what you need to know about dynamic components in Angular](https://blog.angularindepth.com/here-is-what-you-need-to-know-about-dynamic-components-in-angular-ac1e96167f9e) 397 | 398 | - [The Need for Speed Lazy Load Non-Routable Modules in Angular](https://netbasal.com/the-need-for-speed-lazy-load-non-routable-modules-in-angular-30c8f1c33093) 399 | 400 | - Also, a huge thank you to Medium reader [ivanwonder](https://twitter.com/ivanwond) and Github user [Milan Saraiya](https://github.com/milansar) for pointing this out and providing a fork example of resolution. 401 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "angular-dynamic-content": { 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/angular-dynamic-content", 17 | "index": "src/index.html", 18 | "main": "src/main.ts", 19 | "polyfills": "src/polyfills.ts", 20 | "tsConfig": "src/tsconfig.app.json", 21 | "assets": ["src/favicon.ico", "src/assets"], 22 | "styles": ["src/styles.css"], 23 | "scripts": [], 24 | "lazyModules": [ 25 | "src/app/dynamic-multiple/dynamic-multiple.module", 26 | "src/app/dynamic-single/dynamic-single.module" 27 | ] 28 | }, 29 | "configurations": { 30 | "production": { 31 | "fileReplacements": [ 32 | { 33 | "replace": "src/environments/environment.ts", 34 | "with": "src/environments/environment.prod.ts" 35 | } 36 | ], 37 | "optimization": true, 38 | "outputHashing": "all", 39 | "sourceMap": false, 40 | "extractCss": true, 41 | "namedChunks": false, 42 | "aot": true, 43 | "extractLicenses": true, 44 | "vendorChunk": false, 45 | "buildOptimizer": true, 46 | "budgets": [ 47 | { 48 | "type": "initial", 49 | "maximumWarning": "2mb", 50 | "maximumError": "5mb" 51 | } 52 | ] 53 | } 54 | } 55 | }, 56 | "serve": { 57 | "builder": "@angular-devkit/build-angular:dev-server", 58 | "options": { 59 | "browserTarget": "angular-dynamic-content:build" 60 | }, 61 | "configurations": { 62 | "production": { 63 | "browserTarget": "angular-dynamic-content:build:production" 64 | } 65 | } 66 | }, 67 | "extract-i18n": { 68 | "builder": "@angular-devkit/build-angular:extract-i18n", 69 | "options": { 70 | "browserTarget": "angular-dynamic-content: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": ["src/styles.css"], 81 | "scripts": [], 82 | "assets": ["src/favicon.ico", "src/assets"] 83 | } 84 | }, 85 | "lint": { 86 | "builder": "@angular-devkit/build-angular:tslint", 87 | "options": { 88 | "tsConfig": ["src/tsconfig.app.json", "src/tsconfig.spec.json"], 89 | "exclude": ["**/node_modules/**"] 90 | } 91 | } 92 | } 93 | }, 94 | "angular-dynamic-content-e2e": { 95 | "root": "e2e/", 96 | "projectType": "application", 97 | "prefix": "", 98 | "architect": { 99 | "e2e": { 100 | "builder": "@angular-devkit/build-angular:protractor", 101 | "options": { 102 | "protractorConfig": "e2e/protractor.conf.js", 103 | "devServerTarget": "angular-dynamic-content:serve" 104 | }, 105 | "configurations": { 106 | "production": { 107 | "devServerTarget": "angular-dynamic-content:serve:production" 108 | } 109 | } 110 | }, 111 | "lint": { 112 | "builder": "@angular-devkit/build-angular:tslint", 113 | "options": { 114 | "tsConfig": "e2e/tsconfig.e2e.json", 115 | "exclude": ["**/node_modules/**"] 116 | } 117 | } 118 | } 119 | } 120 | }, 121 | "defaultProject": "angular-dynamic-content" 122 | } 123 | -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './src/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: require('path').join(__dirname, './tsconfig.e2e.json') 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('workspace-project App', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.getTitleText()).toEqual('Welcome to angular-dynamic-content!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getTitleText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-dynamic-content", 3 | "version": "0.0.1", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "test": "ng test", 9 | "lint": "ng lint", 10 | "e2e": "ng e2e" 11 | }, 12 | "private": true, 13 | "dependencies": { 14 | "@angular/animations": "~7.2.7", 15 | "@angular/common": "~7.2.7", 16 | "@angular/compiler": "~7.2.7", 17 | "@angular/core": "~7.2.7", 18 | "@angular/forms": "~7.2.7", 19 | "@angular/http": "~7.2.7", 20 | "@angular/platform-browser": "~7.2.7", 21 | "@angular/platform-browser-dynamic": "~7.2.7", 22 | "@angular/router": "~7.2.7", 23 | "core-js": "^2.5.4", 24 | "rxjs": "~6.4.0", 25 | "tslib": "^1.9.0", 26 | "zone.js": "~0.8.29" 27 | }, 28 | "devDependencies": { 29 | "@angular-devkit/build-angular": "^0.12.1", 30 | "@angular/cli": "~7.3.3", 31 | "@angular/compiler-cli": "~7.2.7", 32 | "@angular/language-service": "~7.2.7", 33 | "@types/jasmine": "~2.8.8", 34 | "@types/jasminewd2": "~2.0.3", 35 | "@types/node": "~8.9.4", 36 | "codelyzer": "~4.5.0", 37 | "jasmine-core": "~2.99.1", 38 | "jasmine-spec-reporter": "~4.2.1", 39 | "karma": "~3.0.0", 40 | "karma-chrome-launcher": "~2.2.0", 41 | "karma-coverage-istanbul-reporter": "~2.0.1", 42 | "karma-jasmine": "~1.1.2", 43 | "karma-jasmine-html-reporter": "^0.2.2", 44 | "protractor": "~5.4.0", 45 | "ts-node": "~7.0.0", 46 | "tslint": "~5.11.0", 47 | "typescript": "~3.2.4" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/app/app.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wesleygrimes/angular-dynamic-content/282ec7d7651c65ffe93ce5ee0d1dbff46779030c/src/app/app.component.css -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | Choose a component to render: 7 | 12 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | 4 | describe('AppComponent', () => { 5 | beforeEach(async(() => { 6 | TestBed.configureTestingModule({ 7 | declarations: [ 8 | AppComponent 9 | ], 10 | }).compileComponents(); 11 | })); 12 | 13 | it('should create the app', () => { 14 | const fixture = TestBed.createComponent(AppComponent); 15 | const app = fixture.debugElement.componentInstance; 16 | expect(app).toBeTruthy(); 17 | }); 18 | 19 | it(`should have as title 'angular-dynamic-content'`, () => { 20 | const fixture = TestBed.createComponent(AppComponent); 21 | const app = fixture.debugElement.componentInstance; 22 | expect(app.title).toEqual('angular-dynamic-content'); 23 | }); 24 | 25 | it('should render title in a h1 tag', () => { 26 | const fixture = TestBed.createComponent(AppComponent); 27 | fixture.detectChanges(); 28 | const compiled = fixture.debugElement.nativeElement; 29 | expect(compiled.querySelector('h1').textContent).toContain('Welcome to angular-dynamic-content!'); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.css'] 7 | }) 8 | export class AppComponent { 9 | components = [ 10 | 'DynamicSingleOneComponent', 11 | 'DynamicMultipleOneComponent', 12 | 'DynamicMultipleTwoComponent' 13 | ]; 14 | 15 | selectedComponent = ''; 16 | 17 | renderComponent() {} 18 | } 19 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 3 | import { BrowserModule } from '@angular/platform-browser'; 4 | import { AppComponent } from './app.component'; 5 | import { DynamicContentOutletModule } from './dynamic-content-outlet/dynamic-content-outlet.module'; 6 | 7 | @NgModule({ 8 | declarations: [AppComponent], 9 | imports: [ 10 | BrowserModule, 11 | FormsModule, 12 | ReactiveFormsModule, 13 | DynamicContentOutletModule 14 | ], 15 | bootstrap: [AppComponent] 16 | }) 17 | export class AppModule {} 18 | -------------------------------------------------------------------------------- /src/app/dynamic-content-outlet/dynamic-content-outlet-error.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-dynamic-content-outlet-error-component', 5 | template: ` 6 |
{{ errorMessage }}
7 | ` 8 | }) 9 | export class DynamicContentOutletErrorComponent { 10 | @Input() errorMessage: string; 11 | constructor() {} 12 | } 13 | -------------------------------------------------------------------------------- /src/app/dynamic-content-outlet/dynamic-content-outlet.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | ComponentRef, 4 | Input, 5 | OnChanges, 6 | OnDestroy, 7 | ViewChild, 8 | ViewContainerRef 9 | } from '@angular/core'; 10 | import { DynamicContentOutletService } from './dynamic-content-outlet.service'; 11 | 12 | @Component({ 13 | selector: 'app-dynamic-content-outlet', 14 | template: ` 15 | 16 | ` 17 | }) 18 | export class DynamicContentOutletComponent implements OnDestroy, OnChanges { 19 | @ViewChild('container', { read: ViewContainerRef }) 20 | container: ViewContainerRef; 21 | 22 | @Input() componentName: string; 23 | 24 | private component: ComponentRef<{}>; 25 | 26 | constructor(private dynamicContentService: DynamicContentOutletService) {} 27 | 28 | async ngOnChanges() { 29 | await this.renderComponent(); 30 | } 31 | 32 | ngOnDestroy() { 33 | this.destroyComponent(); 34 | } 35 | 36 | private async renderComponent() { 37 | this.destroyComponent(); 38 | 39 | this.component = await this.dynamicContentService.GetComponent( 40 | this.componentName 41 | ); 42 | this.container.insert(this.component.hostView); 43 | } 44 | 45 | private destroyComponent() { 46 | if (this.component) { 47 | this.component.destroy(); 48 | this.component = null; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/app/dynamic-content-outlet/dynamic-content-outlet.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { 3 | NgModule, 4 | NgModuleFactoryLoader, 5 | SystemJsNgModuleLoader 6 | } from '@angular/core'; 7 | import { DynamicContentOutletErrorComponent } from './dynamic-content-outlet-error.component'; 8 | import { DynamicContentOutletComponent } from './dynamic-content-outlet.component'; 9 | import { DynamicContentOutletService } from './dynamic-content-outlet.service'; 10 | 11 | @NgModule({ 12 | imports: [CommonModule], 13 | declarations: [ 14 | DynamicContentOutletComponent, 15 | DynamicContentOutletErrorComponent 16 | ], 17 | exports: [DynamicContentOutletComponent], 18 | providers: [ 19 | { 20 | provide: NgModuleFactoryLoader, 21 | useClass: SystemJsNgModuleLoader 22 | }, 23 | DynamicContentOutletService 24 | ], 25 | entryComponents: [DynamicContentOutletErrorComponent] 26 | }) 27 | export class DynamicContentOutletModule {} 28 | -------------------------------------------------------------------------------- /src/app/dynamic-content-outlet/dynamic-content-outlet.registry.ts: -------------------------------------------------------------------------------- 1 | interface RegistryItem { 2 | componentName: string; 3 | modulePath: string; 4 | moduleName: string; 5 | } 6 | 7 | /** 8 | * A mapping of Component Name to Component Type 9 | * that must be updated with each new component 10 | * that you wish to load dynamically. 11 | */ 12 | export const DynamicContentOutletRegistry: RegistryItem[] = [ 13 | { 14 | componentName: 'DynamicSingleOneComponent', 15 | modulePath: 'src/app/dynamic-single/dynamic-single.module', 16 | moduleName: 'DynamicSingleModule' 17 | }, 18 | { 19 | componentName: 'DynamicMultipleOneComponent', 20 | modulePath: 'src/app/dynamic-multiple/dynamic-multiple.module', 21 | moduleName: 'DynamicMultipleModule' 22 | }, 23 | { 24 | componentName: 'DynamicMultipleTwoComponent', 25 | modulePath: 'src/app/dynamic-multiple/dynamic-multiple.module', 26 | moduleName: 'DynamicMultipleModule' 27 | } 28 | ]; 29 | -------------------------------------------------------------------------------- /src/app/dynamic-content-outlet/dynamic-content-outlet.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ComponentFactoryResolver, 3 | ComponentRef, 4 | Injectable, 5 | Injector, 6 | NgModuleFactoryLoader, 7 | Type 8 | } from '@angular/core'; 9 | import { DynamicContentOutletErrorComponent } from './dynamic-content-outlet-error.component'; 10 | import { DynamicContentOutletRegistry } from './dynamic-content-outlet.registry'; 11 | 12 | type ModuleWithDynamicComponents = Type & { 13 | dynamicComponentsMap: {}; 14 | }; 15 | 16 | @Injectable() 17 | export class DynamicContentOutletService { 18 | constructor( 19 | private componentFactoryResolver: ComponentFactoryResolver, 20 | private moduleLoader: NgModuleFactoryLoader, 21 | private injector: Injector 22 | ) {} 23 | 24 | async GetComponent(componentName: string): Promise> { 25 | const modulePath = this.getModulePathForComponent(componentName); 26 | 27 | if (!modulePath) { 28 | return this.getDynamicContentErrorComponent( 29 | `Unable to derive modulePath from component: ${componentName} in dynamic-content.registry.ts` 30 | ); 31 | } 32 | 33 | try { 34 | const moduleFactory = await this.moduleLoader.load(modulePath); 35 | const moduleReference = moduleFactory.create(this.injector); 36 | const componentResolver = moduleReference.componentFactoryResolver; 37 | 38 | const componentType = (moduleFactory.moduleType as ModuleWithDynamicComponents) 39 | .dynamicComponentsMap[componentName]; 40 | 41 | const componentFactory = componentResolver.resolveComponentFactory( 42 | componentType 43 | ); 44 | return componentFactory.create(this.injector); 45 | } catch (error) { 46 | console.error(error.message); 47 | return this.getDynamicContentErrorComponent( 48 | `Unable to load module ${modulePath}. 49 | Looked up using component: ${componentName}. Error Details: ${ 50 | error.message 51 | }` 52 | ); 53 | } 54 | } 55 | 56 | private getModulePathForComponent(componentName: string) { 57 | const registryItem = DynamicContentOutletRegistry.find( 58 | i => i.componentName === componentName 59 | ); 60 | 61 | if (registryItem && registryItem.modulePath) { 62 | // imported modules must be in the format 'path#moduleName' 63 | return `${registryItem.modulePath}#${registryItem.moduleName}`; 64 | } 65 | 66 | return null; 67 | } 68 | 69 | private getDynamicContentErrorComponent(errorMessage: string) { 70 | const factory = this.componentFactoryResolver.resolveComponentFactory( 71 | DynamicContentOutletErrorComponent 72 | ); 73 | const componentRef = factory.create(this.injector); 74 | const instance = componentRef.instance; 75 | instance.errorMessage = errorMessage; 76 | return componentRef; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/app/dynamic-multiple/dynamic-multiple-one.component.css: -------------------------------------------------------------------------------- 1 | * { 2 | font-family: 'Courier New', Courier, monospace; 3 | font-size: 20px; 4 | color: red; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/dynamic-multiple/dynamic-multiple-one.component.html: -------------------------------------------------------------------------------- 1 |

This is the DynamicMultipleOneComponent!

2 | -------------------------------------------------------------------------------- /src/app/dynamic-multiple/dynamic-multiple-one.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-dynamic-multiple-one', 5 | templateUrl: './dynamic-multiple-one.component.html', 6 | styleUrls: ['./dynamic-multiple-one.component.css'] 7 | }) 8 | export class DynamicMultipleOneComponent implements OnInit { 9 | constructor() {} 10 | 11 | ngOnInit() {} 12 | } 13 | -------------------------------------------------------------------------------- /src/app/dynamic-multiple/dynamic-multiple-two.component.css: -------------------------------------------------------------------------------- 1 | * { 2 | font-family: 'Franklin Gothic Medium', 'Arial Narrow', Arial, sans-serif; 3 | font-size: 15px; 4 | color: blue; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/dynamic-multiple/dynamic-multiple-two.component.html: -------------------------------------------------------------------------------- 1 |

This is the DynamicMultipleTwoComponent!

2 | -------------------------------------------------------------------------------- /src/app/dynamic-multiple/dynamic-multiple-two.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-two', 5 | templateUrl: './dynamic-multiple-two.component.html', 6 | styleUrls: ['./dynamic-multiple-two.component.css'] 7 | }) 8 | export class DynamicMultipleTwoComponent implements OnInit { 9 | constructor() {} 10 | 11 | ngOnInit() {} 12 | } 13 | -------------------------------------------------------------------------------- /src/app/dynamic-multiple/dynamic-multiple.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { DynamicMultipleOneComponent } from './dynamic-multiple-one.component'; 4 | import { DynamicMultipleTwoComponent } from './dynamic-multiple-two.component'; 5 | 6 | @NgModule({ 7 | declarations: [DynamicMultipleOneComponent, DynamicMultipleTwoComponent], 8 | imports: [CommonModule], 9 | entryComponents: [DynamicMultipleOneComponent, DynamicMultipleTwoComponent] 10 | }) 11 | export class DynamicMultipleModule { 12 | static dynamicComponentsMap = { 13 | DynamicMultipleOneComponent, 14 | DynamicMultipleTwoComponent 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /src/app/dynamic-single/dynamic-single-one.component.css: -------------------------------------------------------------------------------- 1 | * { 2 | font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; 3 | font-size: 20px; 4 | color: darkgreen; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/dynamic-single/dynamic-single-one.component.html: -------------------------------------------------------------------------------- 1 |

This is the DynamicSingleOneComponent!

2 | -------------------------------------------------------------------------------- /src/app/dynamic-single/dynamic-single-one.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-dynamic-single-one', 5 | templateUrl: './dynamic-single-one.component.html', 6 | styleUrls: ['./dynamic-single-one.component.css'] 7 | }) 8 | export class DynamicSingleOneComponent implements OnInit { 9 | constructor() {} 10 | 11 | ngOnInit() {} 12 | } 13 | -------------------------------------------------------------------------------- /src/app/dynamic-single/dynamic-single.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { DynamicSingleOneComponent } from './dynamic-single-one.component'; 4 | 5 | @NgModule({ 6 | declarations: [DynamicSingleOneComponent], 7 | imports: [CommonModule], 8 | entryComponents: [DynamicSingleOneComponent] 9 | }) 10 | export class DynamicSingleModule { 11 | static dynamicComponentsMap = { 12 | DynamicSingleOneComponent 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wesleygrimes/angular-dynamic-content/282ec7d7651c65ffe93ce5ee0d1dbff46779030c/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/browserslist: -------------------------------------------------------------------------------- 1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | # 5 | # For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed 6 | 7 | > 0.5% 8 | last 2 versions 9 | Firefox ESR 10 | not dead 11 | not IE 9-11 -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wesleygrimes/angular-dynamic-content/282ec7d7651c65ffe93ce5ee0d1dbff46779030c/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AngularDynamicContent 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /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 { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | import { AppModule } from './app/app.module'; 4 | import { environment } from './environments/environment'; 5 | 6 | if (environment.production) { 7 | enableProdMode(); 8 | } 9 | 10 | platformBrowserDynamic() 11 | .bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /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/guide/browser-support 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/weak-map'; 35 | // import 'core-js/es6/set'; 36 | 37 | /** 38 | * If the application will be indexed by Google Search, the following is required. 39 | * Googlebot uses a renderer based on Chrome 41. 40 | * https://developers.google.com/search/docs/guides/rendering 41 | **/ 42 | // import 'core-js/es6/array'; 43 | 44 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 45 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 46 | 47 | /** IE10 and IE11 requires the following for the Reflect API. */ 48 | // import 'core-js/es6/reflect'; 49 | 50 | /** 51 | * Web Animations `@angular/platform-browser/animations` 52 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 53 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 54 | **/ 55 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 56 | 57 | /** 58 | * By default, zone.js will patch all possible macroTask and DomEvents 59 | * user can disable parts of macroTask/DomEvents patch by setting following flags 60 | */ 61 | 62 | // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 63 | // (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 64 | // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 65 | 66 | /* 67 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 68 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 69 | */ 70 | // (window as any).__Zone_enable_cross_context_check = true; 71 | 72 | /*************************************************************************************************** 73 | * Zone JS is required by default for Angular itself. 74 | */ 75 | import 'zone.js/dist/zone'; // Included with Angular CLI. 76 | 77 | 78 | /*************************************************************************************************** 79 | * APPLICATION IMPORTS 80 | */ 81 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "types": [] 6 | }, 7 | "exclude": ["test.ts", "**/*.spec.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /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 | } 19 | -------------------------------------------------------------------------------- /src/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "app", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "app", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "importHelpers": true, 6 | "outDir": "./dist/out-tsc", 7 | "sourceMap": true, 8 | "declaration": false, 9 | "module": "es2015", 10 | "moduleResolution": "node", 11 | "emitDecoratorMetadata": true, 12 | "experimentalDecorators": true, 13 | "target": "es5", 14 | "typeRoots": ["node_modules/@types"], 15 | "lib": ["es2018", "dom"] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "deprecation": { 15 | "severity": "warn" 16 | }, 17 | "eofline": true, 18 | "forin": true, 19 | "import-blacklist": [ 20 | true, 21 | "rxjs/Rx" 22 | ], 23 | "import-spacing": true, 24 | "indent": [ 25 | true, 26 | "spaces" 27 | ], 28 | "interface-over-type-literal": true, 29 | "label-position": true, 30 | "max-line-length": [ 31 | true, 32 | 140 33 | ], 34 | "member-access": false, 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-arg": true, 47 | "no-bitwise": true, 48 | "no-console": [ 49 | true, 50 | "debug", 51 | "info", 52 | "time", 53 | "timeEnd", 54 | "trace" 55 | ], 56 | "no-construct": true, 57 | "no-debugger": true, 58 | "no-duplicate-super": true, 59 | "no-empty": false, 60 | "no-empty-interface": true, 61 | "no-eval": true, 62 | "no-inferrable-types": [ 63 | true, 64 | "ignore-params" 65 | ], 66 | "no-misused-new": true, 67 | "no-non-null-assertion": true, 68 | "no-redundant-jsdoc": true, 69 | "no-shadowed-variable": true, 70 | "no-string-literal": false, 71 | "no-string-throw": true, 72 | "no-switch-case-fall-through": true, 73 | "no-trailing-whitespace": true, 74 | "no-unnecessary-initializer": true, 75 | "no-unused-expression": true, 76 | "no-use-before-declare": true, 77 | "no-var-keyword": true, 78 | "object-literal-sort-keys": false, 79 | "one-line": [ 80 | true, 81 | "check-open-brace", 82 | "check-catch", 83 | "check-else", 84 | "check-whitespace" 85 | ], 86 | "prefer-const": true, 87 | "quotemark": [ 88 | true, 89 | "single" 90 | ], 91 | "radix": true, 92 | "semicolon": [ 93 | true, 94 | "always" 95 | ], 96 | "triple-equals": [ 97 | true, 98 | "allow-null-check" 99 | ], 100 | "typedef-whitespace": [ 101 | true, 102 | { 103 | "call-signature": "nospace", 104 | "index-signature": "nospace", 105 | "parameter": "nospace", 106 | "property-declaration": "nospace", 107 | "variable-declaration": "nospace" 108 | } 109 | ], 110 | "unified-signatures": true, 111 | "variable-name": false, 112 | "whitespace": [ 113 | true, 114 | "check-branch", 115 | "check-decl", 116 | "check-operator", 117 | "check-separator", 118 | "check-type" 119 | ], 120 | "no-output-on-prefix": true, 121 | "use-input-property-decorator": true, 122 | "use-output-property-decorator": true, 123 | "use-host-property-decorator": true, 124 | "no-input-rename": true, 125 | "no-output-rename": true, 126 | "use-life-cycle-interface": true, 127 | "use-pipe-transform-interface": true, 128 | "component-class-suffix": true, 129 | "directive-class-suffix": true 130 | } 131 | } 132 | --------------------------------------------------------------------------------