37 |
38 |
--------------------------------------------------------------------------------
/src/polyfills.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Web Animations `@angular/platform-browser/animations`
3 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
4 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
5 | **/
6 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`.
7 |
8 | /**
9 | * By default, zone.js will patch all possible macroTask and DomEvents
10 | * user can disable parts of macroTask/DomEvents patch by setting following flags
11 | */
12 |
13 | // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
14 | // (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
15 | // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
16 |
17 | /*
18 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
19 | * with the following flag, it will bypass `zone.js` patch for IE/Edge
20 | */
21 | // (window as any).__Zone_enable_cross_context_check = true;
22 |
23 | /***************************************************************************************************
24 | * Zone JS is required by default for Angular itself.
25 | */
26 | import 'zone.js/dist/zone'; // Included with Angular CLI.
27 |
28 | /***************************************************************************************************
29 | * APPLICATION IMPORTS
30 | */
31 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "rulesDirectory": ["node_modules/codelyzer"],
3 | "rules": {
4 | "arrow-return-shorthand": true,
5 | "callable-types": true,
6 | "class-name": true,
7 | "deprecation": {
8 | "severity": "warn"
9 | },
10 | "forin": true,
11 | "import-blacklist": [true, "rxjs/Rx"],
12 | "interface-over-type-literal": true,
13 | "label-position": true,
14 | "member-access": false,
15 | "member-ordering": [
16 | true,
17 | {
18 | "order": ["static-field", "instance-field", "static-method", "instance-method"]
19 | }
20 | ],
21 | "no-arg": true,
22 | "no-bitwise": true,
23 | "no-console": [true, "debug", "info", "time", "timeEnd", "trace"],
24 | "no-construct": true,
25 | "no-debugger": true,
26 | "no-duplicate-super": true,
27 | "no-empty": false,
28 | "no-empty-interface": true,
29 | "no-eval": true,
30 | "no-inferrable-types": [true, "ignore-params"],
31 | "no-misused-new": true,
32 | "no-non-null-assertion": true,
33 | "no-redundant-jsdoc": true,
34 | "no-shadowed-variable": true,
35 | "no-string-literal": false,
36 | "no-string-throw": true,
37 | "no-switch-case-fall-through": true,
38 | "no-unnecessary-initializer": true,
39 | "no-unused-expression": true,
40 | "no-use-before-declare": true,
41 | "no-var-keyword": true,
42 | "object-literal-sort-keys": false,
43 | "prefer-const": true,
44 | "radix": true,
45 | "triple-equals": [true, "allow-null-check"],
46 | "unified-signatures": true,
47 | "variable-name": false,
48 | "whitespace": [true, "check-branch", "check-decl", "check-operator", "check-separator", "check-type"],
49 | "no-output-on-prefix": true,
50 | "no-inputs-metadata-property": true,
51 | "no-outputs-metadata-property": true,
52 | "no-host-metadata-property": true,
53 | "no-input-rename": true,
54 | "no-output-rename": true,
55 | "use-lifecycle-interface": true,
56 | "use-pipe-transform-interface": true,
57 | "component-class-suffix": true,
58 | "directive-class-suffix": true
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "herodevs-packages",
3 | "version": "0.0.0",
4 | "scripts": {
5 | "ng": "ng",
6 | "precommit": "lint-staged",
7 | "start": "ng serve",
8 | "build": "ng build",
9 | "test": "ng test",
10 | "lint": "ng lint",
11 | "e2e": "ng e2e",
12 | "build-lazy": "ng build lazy",
13 | "build-dynamic": "ng build dynamicService",
14 | "npm-pack-lazy": "cd dist/loader && npm pack",
15 | "npm-pack-dynamic": "cd dist/dynamic && npm pack",
16 | "package-lazy": "npm run build-lazy && npm run npm-pack-lazy",
17 | "package-dynamic": "npm run build-dynamic && npm run npm-pack-dynamic",
18 | "package": "rm -rf dist/ && npm run package-dynamic && npm run package-lazy"
19 | },
20 | "private": false,
21 | "dependencies": {
22 | "@angular/animations": "^8.0.0",
23 | "@angular/common": "^8.0.0",
24 | "@angular/compiler": "^8.0.0",
25 | "@angular/core": "^8.0.0",
26 | "@angular/forms": "^8.0.0",
27 | "@angular/platform-browser": "^8.0.0",
28 | "@angular/platform-browser-dynamic": "^8.0.0",
29 | "@angular/router": "^8.0.0",
30 | "core-js": "^2.5.4",
31 | "rxjs": "~6.5.2",
32 | "zone.js": "~0.9.1"
33 | },
34 | "devDependencies": {
35 | "@angular-devkit/build-angular": "~0.800.0",
36 | "@angular-devkit/build-ng-packagr": "~0.800.0",
37 | "@angular/cli": "~8.0.2",
38 | "@angular/compiler-cli": "^8.0.0",
39 | "@angular/language-service": "^8.0.0",
40 | "@types/jasmine": "~2.8.8",
41 | "@types/jasminewd2": "~2.0.3",
42 | "@types/node": "~8.9.4",
43 | "codelyzer": "^5.0.1",
44 | "husky": "1.3.1",
45 | "jasmine-core": "~2.99.1",
46 | "jasmine-spec-reporter": "~4.2.1",
47 | "karma": "~3.0.0",
48 | "karma-chrome-launcher": "~2.2.0",
49 | "karma-coverage-istanbul-reporter": "~2.0.1",
50 | "karma-jasmine": "~1.1.2",
51 | "karma-jasmine-html-reporter": "^0.2.2",
52 | "lint-staged": "8.1.0",
53 | "ng-packagr": "^5.1.0",
54 | "prettier": "1.16.1",
55 | "protractor": "~5.4.0",
56 | "ts-node": "~7.0.0",
57 | "tsickle": "^0.35.0",
58 | "tslib": "^1.9.0",
59 | "tslint": "~5.11.0",
60 | "typescript": "~3.4.5"
61 | },
62 | "lint-staged": {
63 | "*.{ts,tsx}": [
64 | "prettier --parser typescript --writeprettier --parser typescript --write",
65 | "git add"
66 | ]
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/projects/lazy/src/lib/hero-loader.directive.ts:
--------------------------------------------------------------------------------
1 | import {
2 | AfterViewInit,
3 | Directive,
4 | EventEmitter,
5 | Injector,
6 | Input,
7 | NgModuleFactory,
8 | NgModuleFactoryLoader,
9 | OnChanges,
10 | OnDestroy,
11 | Output,
12 | SimpleChanges,
13 | ViewContainerRef,
14 | } from '@angular/core';
15 | import { BehaviorSubject, combineLatest, Observable, Subscription } from 'rxjs';
16 | import { distinctUntilChanged, filter, first } from 'rxjs/operators';
17 | import {
18 | DynamicComponentService,
19 | ICreatedModule,
20 | ICreatedComponentInterface,
21 | } from '@herodevs/dynamic-component-service';
22 |
23 | // @ts-ignore
24 | @Directive({
25 | selector: 'hero-loader, [hero-loader]',
26 | })
27 | export class HeroLoaderDirective implements AfterViewInit, OnChanges, OnDestroy {
28 | // @ts-ignore
29 | @Input() moduleName: string;
30 |
31 | @Output() init = new EventEmitter();
32 |
33 | componentRef: ICreatedComponentInterface;
34 |
35 | changesBS = new BehaviorSubject(null);
36 |
37 | // Need to wait until the component view has inited
38 | afterInitBS = new BehaviorSubject(false);
39 |
40 | /**
41 | * This observable fires once the component has been init'd
42 | * and once the changes come through
43 | * and once the changes that has an input value that is a function
44 | *
45 | * It only fires once. If the input changes, this observable
46 | * will not fire again.
47 | */
48 | // @ts-ignore
49 | action$ = combineLatest(
50 | this.changesBS.asObservable().pipe(
51 | filter((val: SimpleChanges) => {
52 | return val && val.moduleName && val.moduleName.currentValue;
53 | }),
54 | first(),
55 | ),
56 | this.afterInitBS.asObservable().pipe(
57 | filter((init) => init),
58 | distinctUntilChanged(),
59 | ),
60 | );
61 |
62 | subs: Subscription[] = [
63 | this.action$.subscribe(() => {
64 | // Uses the loader function to lazy load and compile a module.
65 | this.loader
66 | .load(this.moduleName)
67 | .then((compiledModule: NgModuleFactory) => {
68 | if (this.destroyed) return {};
69 | return this.lazy.createAndAttachModuleAsync(compiledModule, this.injector, { vcr: this.vcr });
70 | })
71 | .then(({ moduleRef, componentRef }: ICreatedModule) => {
72 | this.componentRef = componentRef;
73 | this.init.emit(componentRef);
74 | });
75 | }),
76 | ];
77 |
78 | destroyed = false;
79 |
80 | constructor(
81 | private lazy: DynamicComponentService,
82 | private vcr: ViewContainerRef,
83 | private injector: Injector,
84 | private loader: NgModuleFactoryLoader,
85 | ) {}
86 |
87 | ngAfterViewInit() {
88 | this.afterInitBS.next(true);
89 | }
90 |
91 | ngOnChanges(changes: SimpleChanges): void {
92 | this.changesBS.next(changes);
93 | }
94 |
95 | ngOnDestroy(): void {
96 | this.destroyed = true;
97 | this.subs.forEach((s) => s.unsubscribe());
98 |
99 | // If the component has init'd, destroy it.
100 | if (this.componentRef) {
101 | this.componentRef.detach();
102 | }
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/projects/dynamic-service/README.MD:
--------------------------------------------------------------------------------
1 | # DynamicComponentService
2 |
3 | A service that makes dynamically creating components easy.
4 |
5 | When you use DynamicComponentService to create a component, you are given
6 | backed an `ICreatedComponentInterface` which allows you to do the
7 | following three things:
8 |
9 | - `.next()` - Push new data into the component's Inputs, as well
10 | as provide callbacks for the Outputs. This is how you pass new
11 | down into the created component.
12 | - `.detach()` - Allows you to detach the component from the DOM
13 | as well as destroys the component. This is how you destroy the
14 | created component.
15 | - `.componentRef` - This is a pointer to the created component.
16 | This is of type `ComponentRef`. In other words, this is the
17 | instance of the component that is returned when you call `new`
18 | on the class definition. The `.componentRef` is the `this` of
19 | the component.
20 |
21 | ### How to use it
22 |
23 | #### Installation
24 |
25 | Start by installing it correctly:
26 |
27 | ```bash
28 | npm install @herodevs/dynamic-af
29 | ```
30 |
31 | #### Inject the Service
32 |
33 | Now you need to inject the service into your component, or into
34 | another service of your own. You do that by adding it to the
35 | constructor of your component/service, like so:
36 |
37 | ```typescript
38 | export class MyCoolComponent {
39 | constructor(private dynamicService: DynamicComponentService) {
40 | // ...
41 | }
42 | }
43 | ```
44 |
45 | #### Call `createAndAttachComponentSync`
46 |
47 | Now that you have the service, you can call the `createAndAttachComponentSync`
48 | method to create a component and have it attached to the DOM.
49 | Here is an example of what that looks like:
50 |
51 | ```typescript
52 | const ref = dynamicService.createAndAttachComponentSync(FooComponent, { vcr: this.viewContainerRef });
53 | ```
54 |
55 | You must pass the `createAndAttachComponentSync` method two
56 | parameters. First, you need to pass the class of the component
57 | that you want to dynamically create. The second is an
58 | object that matches the `CreateComponentOptions` interface:
59 |
60 | ```typescript
61 | interface CreateComponentOptions {
62 | module?: NgModuleRef;
63 | context?: { [key: string]: any };
64 | vcr?: ViewContainerRef;
65 | }
66 | ```
67 |
68 | Here are what each of those represents:
69 |
70 | - `vcr (optional, but not really)` - This is the `ViewContainerRef`
71 | where you want to attach the createdComponent. If you don't
72 | provide a `vcr`, the service will have no choice but to attach
73 | your component to the bottom of the `document.body`. So it
74 | is recommended that you DEFINITELY provide a `vcr`.
75 | - `context (optional)` - This is an object that has keys
76 | that match the names of the Inputs/Outputs of the component
77 | being created. If your component being created has an
78 | input named `name`, then you can pass a `context` with
79 | a `name` property to provide a name. Eg: `{name: 'Your Name'}`.
80 | This will pass the value `Your Name` into the Input
81 | of you component.
82 | - `module (optional)` - This is a reference to the module
83 | that the component belongs to. You only need to pass this
84 | if you manually lazily loaded the component and module.
85 | Otherwise you can not pass this.
86 |
87 | #### Updating input/output values
88 |
89 | Once you have the `ref` to your created component, you can
90 | call `next(newContext)` to pass in new values to your
91 | inputs/outputs of your component. Here is an example of
92 | updating an input value one second for a component
93 | that has `@Input() count`:
94 |
95 | ```typescript
96 | const ref = dynamicService.createAndAttachComponentSync(FooComponent, { vcr: this.viewContainerRef });
97 |
98 | let count = 0;
99 | ref.next({ count: count++ });
100 |
101 | setInterval(() => {
102 | ref.next({ count: count++ });
103 | }, 1000);
104 | ```
105 |
106 | Once a second the created component will get a new `count`
107 | via it's input.
108 |
--------------------------------------------------------------------------------
/projects/lazy/README.MD:
--------------------------------------------------------------------------------
1 | # `` for lazy loading in Angular
2 |
3 | Every Angular app is different and has different needs. Yet Angular only provides
4 | one method for lazily loading code: using the `loadChildren` piece in the routes
5 | for any given module. By using the `loadChildren` piece of a route, you are telling
6 | Angular and the Angular CLI to help you out and lazily load that piece of the app
7 | when the associated route is hit. This is a very efficient tool that we should
8 | all be using.
9 |
10 | #### HOWEVER!!!
11 |
12 | Some of us need more flexibility when lazily loading modules. Some modules need
13 | to be triggered to load on events BESIDES route change. Maybe a `click` or a
14 | `mouseover`. Maybe when the user has admin rights, or when they don't have
15 | admin rights. This is why we built ``. Using this component, combined
16 | with an `ngIf`, you can trigger lazy loading of a module for just about any
17 | scenario that you can think of.
18 |
19 | #### How does it work?
20 |
21 | To do this, we utilize the exact same pieces of Angular that `loadChildren` from
22 | routes uses. But we do it in a different way. Let's look at how it works.
23 |
24 | ## Getting Started
25 |
26 | Start by installing the right node module:
27 |
28 | ```bash
29 | npm install @herodevs/hero-loader
30 | ```
31 |
32 | At this point, we have all that we need to get started. We only need to do some
33 | configuring. We need to do the following:
34 |
35 | 1. Tell Angular to create a separate bundle for the module that we intend to
36 | lazy load.
37 | 2. Import `HeroLoaderModule` where we intend to use this lazy loading.
38 | 3. Tell `` to load that bundle when needed.
39 |
40 | Let's do this one at a time.
41 |
42 | ### Create a separate bundle for our module
43 |
44 | Open your `angular.json` file. In that file, look for the nested property
45 | `projects..architect.build.options` where ``
46 | is the name of your project. Once you have the build options property in
47 | sight, add the `lazyModules` property to the options:
48 |
49 | ```json
50 | "options": {
51 | ...
52 | "lazyModules": [ "src/app/test/test.module" ]
53 | }
54 | ```
55 |
56 | In the above example, you are telling the Angular CLI to prepare a separate
57 | bundle for `TestModule` in the file `src/app/test/test.module.ts`. You will
58 | notice that this looks a lot like the `loadChildren` syntax for a route.
59 | That's because this `lazyModules` property is doing the same thing that
60 | the `loadChildren` property does in a route. Now the Angular CLI knows to
61 | create a separate bundle for the `TestModule`.
62 |
63 | ### Import `HeroLoaderModule`
64 |
65 | In your app, you need to add `HeroLoaderModule` to the imports of one of your
66 | app's NgModules
67 |
68 | ```typescript
69 | @NgModule({
70 | imports: [HeroLoaderModule],
71 | })
72 | export class AppModule {}
73 | ```
74 |
75 | Now your app knows about the `HeroLoaderModule` and you can use the ``
76 | component to lazy load the `TestModule`.
77 |
78 | ### Use `` in our app
79 |
80 | The following is an example of how to use `` to load our `TestModule`.
81 |
82 | ```html
83 |
Hover to load TestModule
84 |
85 | ```
86 |
87 | When you hover the `
` above, the `ngIf` will turn on the `` component
88 | which will then load the `TestModule` and it will use whatever component is
89 | listed in the `TestModule.bootstrap` property and attach that component to the
90 | inside of the `` component.
91 |
92 | Consider that `TestModule` looks as follows:
93 |
94 | ```typescript
95 | @NgModule({
96 | declarations: [TestComponent],
97 | bootstrap: [TestComponent],
98 | })
99 | export class TestModule {}
100 | ```
101 |
102 | Using `` to load the `TestModule` will the `TestComponent` inside of the
103 | the `` component that you added to your template.
104 |
105 | ## Question
106 |
107 | - [Report an Issue](https://github.com/herodevs/herodevs-packages/issues)
108 | - [View Past Issues](https://github.com/herodevs/herodevs-packages/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aclosed+)
109 |
--------------------------------------------------------------------------------
/angular.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "version": 1,
4 | "newProjectRoot": "projects",
5 | "projects": {
6 | "main": {
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/main",
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": ["src/app/bar/bar.module"]
25 | },
26 | "configurations": {
27 | "production": {
28 | "fileReplacements": [
29 | {
30 | "replace": "src/environments/environment.ts",
31 | "with": "src/environments/environment.prod.ts"
32 | }
33 | ],
34 | "optimization": true,
35 | "outputHashing": "all",
36 | "sourceMap": false,
37 | "extractCss": true,
38 | "namedChunks": false,
39 | "aot": true,
40 | "extractLicenses": true,
41 | "vendorChunk": false,
42 | "buildOptimizer": true
43 | }
44 | }
45 | },
46 | "serve": {
47 | "builder": "@angular-devkit/build-angular:dev-server",
48 | "options": {
49 | "browserTarget": "main:build"
50 | },
51 | "configurations": {
52 | "production": {
53 | "browserTarget": "main:build:production"
54 | }
55 | }
56 | },
57 | "extract-i18n": {
58 | "builder": "@angular-devkit/build-angular:extract-i18n",
59 | "options": {
60 | "browserTarget": "main:build"
61 | }
62 | },
63 | "test": {
64 | "builder": "@angular-devkit/build-angular:karma",
65 | "options": {
66 | "main": "src/test.ts",
67 | "polyfills": "src/polyfills.ts",
68 | "tsConfig": "src/tsconfig.spec.json",
69 | "karmaConfig": "src/karma.conf.js",
70 | "styles": ["src/styles.css"],
71 | "scripts": [],
72 | "assets": ["src/favicon.ico", "src/assets"]
73 | }
74 | },
75 | "lint": {
76 | "builder": "@angular-devkit/build-angular:tslint",
77 | "options": {
78 | "tsConfig": ["src/tsconfig.app.json", "src/tsconfig.spec.json"],
79 | "exclude": ["**/node_modules/**"]
80 | }
81 | }
82 | }
83 | },
84 | "main-e2e": {
85 | "root": "e2e/",
86 | "projectType": "application",
87 | "architect": {
88 | "e2e": {
89 | "builder": "@angular-devkit/build-angular:protractor",
90 | "options": {
91 | "protractorConfig": "e2e/protractor.conf.js",
92 | "devServerTarget": "main:serve"
93 | },
94 | "configurations": {
95 | "production": {
96 | "devServerTarget": "main:serve:production"
97 | }
98 | }
99 | },
100 | "lint": {
101 | "builder": "@angular-devkit/build-angular:tslint",
102 | "options": {
103 | "tsConfig": "e2e/tsconfig.e2e.json",
104 | "exclude": ["**/node_modules/**"]
105 | }
106 | }
107 | }
108 | },
109 | "lazy": {
110 | "root": "projects/lazy",
111 | "sourceRoot": "projects/lazy/src",
112 | "projectType": "library",
113 | "prefix": "hero",
114 | "architect": {
115 | "build": {
116 | "builder": "@angular-devkit/build-ng-packagr:build",
117 | "options": {
118 | "tsConfig": "projects/lazy/tsconfig.lib.json",
119 | "project": "projects/lazy/ng-package.json"
120 | }
121 | },
122 | "test": {
123 | "builder": "@angular-devkit/build-angular:karma",
124 | "options": {
125 | "main": "projects/lazy/src/test.ts",
126 | "tsConfig": "projects/lazy/tsconfig.spec.json",
127 | "karmaConfig": "projects/lazy/karma.conf.js"
128 | }
129 | },
130 | "lint": {
131 | "builder": "@angular-devkit/build-angular:tslint",
132 | "options": {
133 | "tsConfig": ["projects/lazy/tsconfig.lib.json", "projects/lazy/tsconfig.spec.json"],
134 | "exclude": ["**/node_modules/**"]
135 | }
136 | }
137 | }
138 | },
139 | "dynamicService": {
140 | "root": "projects/dynamic-service",
141 | "sourceRoot": "projects/dynamic-service/src",
142 | "projectType": "library",
143 | "prefix": "lib",
144 | "architect": {
145 | "build": {
146 | "builder": "@angular-devkit/build-ng-packagr:build",
147 | "options": {
148 | "tsConfig": "projects/dynamic-service/tsconfig.lib.json",
149 | "project": "projects/dynamic-service/ng-package.json"
150 | }
151 | },
152 | "test": {
153 | "builder": "@angular-devkit/build-angular:karma",
154 | "options": {
155 | "main": "projects/dynamic-service/src/test.ts",
156 | "tsConfig": "projects/dynamic-service/tsconfig.spec.json",
157 | "karmaConfig": "projects/dynamic-service/karma.conf.js"
158 | }
159 | },
160 | "lint": {
161 | "builder": "@angular-devkit/build-angular:tslint",
162 | "options": {
163 | "tsConfig": [
164 | "projects/dynamic-service/tsconfig.lib.json",
165 | "projects/dynamic-service/tsconfig.spec.json"
166 | ],
167 | "exclude": ["**/node_modules/**"]
168 | }
169 | }
170 | }
171 | }
172 | },
173 | "defaultProject": "lazyloading"
174 | }
175 |
--------------------------------------------------------------------------------
/projects/dynamic-service/src/lib/dynamic-component.service.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ApplicationRef,
3 | Compiler,
4 | ComponentFactory,
5 | ComponentFactoryResolver,
6 | ComponentRef,
7 | Injectable,
8 | Injector,
9 | NgModuleFactory,
10 | NgModuleRef,
11 | Type,
12 | ViewContainerRef,
13 | } from '@angular/core';
14 | import { BehaviorSubject, Subscription, Observable } from 'rxjs';
15 |
16 | export interface InternalNgModuleRef extends NgModuleRef {
17 | _bootstrapComponents: Type[];
18 | }
19 |
20 | export interface ICreatedComponentInterface {
21 | next: (data: { [key: string]: any }) => void;
22 | detach: () => void;
23 | componentRef: ComponentRef;
24 | }
25 |
26 | export interface ICreatedModule {
27 | moduleRef: NgModuleRef;
28 | componentRef?: ICreatedComponentInterface;
29 | }
30 |
31 | interface CreateComponentOptions {
32 | module?: NgModuleRef;
33 | context?: { [key: string]: any };
34 | vcr?: ViewContainerRef;
35 | }
36 |
37 | interface CreateAttachModuleOptions {
38 | context?: { [key: string]: any };
39 | vcr?: ViewContainerRef;
40 | }
41 |
42 | @Injectable({
43 | providedIn: 'root',
44 | })
45 | export class DynamicComponentService {
46 | constructor(
47 | private _compiler: Compiler,
48 | private cfr: ComponentFactoryResolver,
49 | private appRef: ApplicationRef,
50 | private injector: Injector,
51 | ) {}
52 |
53 | /**
54 | *
55 | * @param compiledModule - This is a module that is compiled by the JIT or AOT compiler
56 | * @param injector - An injector that the
57 | */
58 | createModuleSync(compiledModule: NgModuleFactory, injector: Injector): ICreatedModule {
59 | // Now that the module is loaded and compiled, create an instance of it.
60 | const moduleRef = compiledModule.create(injector) as NgModuleRef;
61 |
62 | return {
63 | moduleRef,
64 | };
65 | }
66 |
67 | createModuleAsync(compiledModule: NgModuleFactory, injector: Injector): Promise {
68 | return new Promise((res, rej) => {
69 | try {
70 | res(this.createModuleSync(compiledModule, injector));
71 | } catch {
72 | rej();
73 | }
74 | });
75 | }
76 |
77 | createAndAttachModuleSync(
78 | compiledModule: NgModuleFactory,
79 | injector: Injector,
80 | { vcr, context = {} }: CreateAttachModuleOptions = {},
81 | ): ICreatedModule {
82 | // Create an instance of the module from the moduleFactory
83 | const createdModule = this.createModuleSync(compiledModule, injector);
84 |
85 | // Take the bootstrap component from that module.
86 | // Using any, as in AngularV8 the InternalNgModuleRef no longer gets exported.
87 | const type = (createdModule.moduleRef as any)._bootstrapComponents[0];
88 |
89 | // The first time they try this and screw up, they will get this warning. This won't happen in prod.
90 | if (!type) {
91 | warn(`Module '${typeof createdModule.moduleRef}' has no bootstrap component.
92 | You must fix this before calling 'dynamicComponentService.createAndAttachModule'.`);
93 | }
94 |
95 | const createdComponent = this.createAndAttachComponentSync(type, {
96 | context,
97 | module: createdModule.moduleRef,
98 | vcr,
99 | });
100 |
101 | return {
102 | moduleRef: createdModule.moduleRef,
103 | componentRef: createdComponent,
104 | };
105 | }
106 |
107 | createAndAttachModuleAsync(
108 | compiledModule: NgModuleFactory,
109 | injector: Injector,
110 | { vcr, context = {} }: CreateAttachModuleOptions = {},
111 | ): Promise {
112 | return new Promise((res, rej) => {
113 | try {
114 | res(this.createAndAttachModuleSync(compiledModule, injector, { vcr, context }));
115 | } catch {
116 | rej('Error created and attaching module async.');
117 | }
118 | });
119 | }
120 |
121 | private getComponentFactory(type: any, module?: NgModuleRef): ComponentFactory {
122 | if (module) {
123 | return module.componentFactoryResolver.resolveComponentFactory(type);
124 | } else {
125 | return this.cfr.resolveComponentFactory(type);
126 | }
127 | }
128 |
129 | /**
130 | *
131 | *
132 | @param type - A type of a component that we want to create.
133 | * @param injector - The injector from the parent container component.
134 | * @param vcr - The view container ref from the calling component.
135 | * @param context - An object that has properties that match the Input and Output names from the
136 | * component that is being created.
137 | * @param module - For components that were not lazily loaded, the type existed a build and thus, the
138 | * ViewContainerRef will have access to it's factory. But for those that were
139 | * lazily loaded, we will need to get their factory from the module that they are
140 | * declared in (which module was also lazily loaded and compiled, during which
141 | * process it received access to the components factory).
142 | */
143 | createAndAttachComponentSync(
144 | type: any,
145 | { context, module, vcr }: CreateComponentOptions = {},
146 | ): ICreatedComponentInterface {
147 | // Use the module to get the component factory, so that we can create an instance of the component.
148 | const factory = this.getComponentFactory(type, module);
149 |
150 | // Create an instance of the component, and add it to the DOM
151 | let componentRef;
152 | if (vcr) {
153 | // This call to createComponent will create and attach the instance to the vcr (the view element).
154 | componentRef = vcr.createComponent(factory);
155 | } else {
156 | // Manually create an instance of the component
157 | componentRef = factory.create(this.injector);
158 |
159 | // Attach it to the app
160 | this.appRef.attachView(componentRef.hostView);
161 |
162 | // Attach the instance to the end of the body
163 | document.body.appendChild((componentRef.hostView as any).rootNodes[0]);
164 | warn(`Since no 'ViewContainerRef' was provided to 'DynamicComponentService.createAndAttachComponent',
165 | the component is being attached to the root of the . This is not recommended.`);
166 | }
167 |
168 | // Take the context and search for keys that match the names of the outputs
169 | // on the component. Track the
170 | const subscriptions = this._wireOutputs(factory, componentRef, context);
171 |
172 | // Place the incoming context into a stream. This stream will be returned to the caller,
173 | // and the caller can send in a new context at will by calling `.next(newContact)` on
174 | // this BehaviorSubject.
175 | const context$ = new BehaviorSubject(context);
176 |
177 | // Subscribe to the new observable for updated input values
178 | subscriptions.push(
179 | context$.subscribe((_context) => {
180 | // When a new values comes through this stream, match up the key names to the input/output
181 | // names on the component and update those values on the component.
182 | factory.inputs.forEach((i) => {
183 | if (_context[i.propName] !== undefined) {
184 | componentRef.instance[i.propName] = _context[i.propName];
185 | }
186 | });
187 | }),
188 | );
189 |
190 | // This function will be returned to the caller, to be called when their context is destroyed
191 | const detach = () => {
192 | // We only need to manually detach if we didn't get a vcr
193 | if (!vcr) {
194 | this.appRef.detachView(componentRef.hostView);
195 | }
196 |
197 | // Destroy our instance of the component
198 | componentRef.destroy();
199 |
200 | // Go through each subscription from the context and unsubscribe
201 | subscriptions.map((s: Subscription) => {
202 | if (!s.closed) {
203 | s.unsubscribe();
204 | }
205 | });
206 | };
207 |
208 | // This function will be returned to the caller, to be called when there are new values for the inputs
209 | const next = (data: any) => {
210 | context$.next(data);
211 | };
212 |
213 | return {
214 | detach,
215 | next,
216 | componentRef,
217 | };
218 | }
219 |
220 | /**
221 | * Same as `createComponent` function, but wraps call in a promise
222 | */
223 | createAndAttachComponentAsync(
224 | type: any,
225 | { context, module, vcr }: CreateComponentOptions = {},
226 | ): Promise {
227 | return new Promise((res, rej) => {
228 | try {
229 | res(this.createAndAttachComponentSync(type, { context, module, vcr }));
230 | } catch {
231 | rej('Error creating component async');
232 | }
233 | });
234 | }
235 |
236 | // Internal function to add event emitters for each of the provided outputs
237 | private _wireOutputs(
238 | factory: ComponentFactory,
239 | componentRef: any,
240 | context: { [key: string]: any },
241 | ): Array {
242 | const subscriptions: Subscription[] = [];
243 | factory.outputs.forEach((o) => {
244 | if (context[o.propName] && context[o.propName] instanceof Function) {
245 | subscriptions.push(componentRef.instance[o.propName].subscribe(context[o.propName]));
246 | }
247 | });
248 | return subscriptions;
249 | }
250 | }
251 |
252 | function checkNotEmpty(value: any, exportName: string): any {
253 | if (!value) {
254 | throw new Error(`Cannot find '${exportName}'`);
255 | }
256 | return value;
257 | }
258 |
259 | function warn(msg) {
260 | console.warn(msg.replace(/\s{2,}/g, ' '));
261 | }
262 |
--------------------------------------------------------------------------------