├── .vscode └── settings.json ├── CONTRIBUTING.md ├── modules └── angular-dynamic │ ├── src │ ├── state_mapper.ts │ ├── existing_ng_module_factory_loader.ts │ ├── interfaces.ts │ ├── explicit_ng_module_factory_loader.ts │ ├── stateful_module.ts │ ├── metadata.ts │ ├── directives │ │ ├── sa_form_control.ts │ │ ├── sa_state_outlet.ts │ │ └── sa_lazy_component_outlet.ts │ ├── component_type_loader.ts │ ├── state_mapper_factory.ts │ └── angular_reflector.ts │ ├── spec │ ├── shared_service_module.ts │ ├── common_js_ng_module_factory_loader.spec.ts │ ├── component_type_loader.spec.ts │ ├── sa_lazy_component_outlet.spec.ts │ ├── common_js_ng_module_factory_loader.ts │ ├── sa_form_control.spec.ts │ ├── widget_module.ts │ └── sa_state_outlet.spec.ts │ ├── tsconfig.dist.json │ ├── .npmignore │ ├── rollup.config.js │ ├── tests.js │ ├── tsconfig.json │ ├── index.ts │ ├── LICENSE │ ├── tslint.json │ ├── CHANGELOG.md │ ├── package.json │ └── README.md ├── .editorconfig ├── .gitignore ├── LICENSE ├── package.json ├── tslint.json └── README.md /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Open to suggestions, but keep in mind its a bit early for bug reports and minor feature requests yet (except for angular-dynamic). 4 | -------------------------------------------------------------------------------- /modules/angular-dynamic/src/state_mapper.ts: -------------------------------------------------------------------------------- 1 | import { StateChange } from './interfaces'; 2 | 3 | /** 4 | * If a component inherits from this class, the implemented get and set methods will be used. 5 | */ 6 | export abstract class StateMapper { 7 | abstract getState(): any; 8 | abstract setState(state: any): StateChange; 9 | } 10 | 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 4 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.json] 14 | indent_size = 2 15 | 16 | [*.md] 17 | insert_final_newline = false 18 | trim_trailing_whitespace = false 19 | -------------------------------------------------------------------------------- /modules/angular-dynamic/spec/shared_service_module.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NgModule, ModuleWithProviders } from '@angular/core'; 2 | 3 | @Injectable() 4 | export class TestService { 5 | } 6 | 7 | @NgModule({ 8 | }) 9 | export class SharedServiceModule { 10 | static forRoot() { 11 | return { 12 | ngModule: SharedServiceModule, 13 | providers: [ TestService ] 14 | }; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /modules/angular-dynamic/src/existing_ng_module_factory_loader.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NgModuleFactory, NgModuleFactoryLoader, getModuleFactory } from '@angular/core'; 2 | 3 | /** 4 | * Relies on modules that are already loaded. 5 | */ 6 | @Injectable() 7 | export class ExistingNgModuleLoader implements NgModuleFactoryLoader { 8 | load(path: string): Promise> { 9 | return Promise.resolve(getModuleFactory(path)); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /modules/angular-dynamic/tsconfig.dist.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "declaration": true, 5 | "stripInternal": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "module": "es2015", 9 | "moduleResolution": "node", 10 | "noEmitOnError": false, 11 | "outDir": "./release", 12 | "sourceMap": true, 13 | "inlineSources": true, 14 | "lib": ["es2015", "dom"], 15 | "target": "es5", 16 | "skipLibCheck": true 17 | }, 18 | "files": [ 19 | "index.ts" 20 | ], 21 | "angularCompilerOptions": { 22 | "strictMetadataEmit": false, 23 | "genDir": "./release" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /modules/angular-dynamic/.npmignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | tmp 29 | typings 30 | -------------------------------------------------------------------------------- /modules/angular-dynamic/rollup.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | entry: './release/index.js', 3 | dest: './release/bundles/angular-dynamic.umd.js', 4 | format: 'umd', 5 | moduleName: 'sharpangles.angular-dynamic', 6 | external: [ 7 | '@angular/core', 8 | '@angular/forms' 9 | ], 10 | globals: { 11 | '@angular/core': 'ng.core', 12 | '@angular/forms': 'ng.forms', 13 | 'rxjs/Observable': 'Rx', 14 | 'rxjs/BehaviorSubject': 'Rx', 15 | 'rxjs/Subscriber': 'Rx', 16 | 'rxjs/scheduler/queue': 'Rx.Scheduler', 17 | 'rxjs/operator/observeOn': 'Rx.Observable.prototype', 18 | 'rxjs/operator/scan': 'Rx.Observable.prototype', 19 | 'rxjs/operator/withLatestFrom': 'Rx.Observable' 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /modules/angular-dynamic/src/interfaces.ts: -------------------------------------------------------------------------------- 1 | import { AbstractControl } from '@angular/forms'; 2 | 3 | export interface StateChange { 4 | control?: AbstractControl; 5 | state: TState; 6 | } 7 | 8 | /** 9 | * A serializable reference to a component. 10 | */ 11 | export interface TypeReference { 12 | /** The module name to use for resolving relative references. Use this for static values provided inside another module, such as in unit tests. */ 13 | moduleId?: string; 14 | 15 | /** The module name, using the same format as lazy loaded modules. */ 16 | moduleName?: string; 17 | 18 | /** The name of the component type listed in the entryComponents of the referenced NgModule. */ 19 | componentTypeName?: string; 20 | } 21 | -------------------------------------------------------------------------------- /modules/angular-dynamic/tests.js: -------------------------------------------------------------------------------- 1 | require('ts-node/register'); 2 | require('core-js/es7/reflect'); 3 | require('zone.js/dist/zone-node.js'); 4 | require('zone.js/dist/long-stack-trace-zone.js'); 5 | require('zone.js/dist/proxy.js'); 6 | require('zone.js/dist/sync-test.js'); 7 | require('zone.js/dist/async-test.js'); 8 | require('zone.js/dist/fake-async-test.js'); 9 | const Jasmine = require('jasmine'); 10 | 11 | const runner = new Jasmine(); 12 | 13 | global.jasmine = runner.jasmine; 14 | 15 | require('zone.js/dist/jasmine-patch.js'); 16 | 17 | const { getTestBed } = require('@angular/core/testing'); 18 | const { ServerTestingModule, platformServerTesting } = require('@angular/platform-server/testing'); 19 | 20 | getTestBed().initTestEnvironment(ServerTestingModule, platformServerTesting()); 21 | 22 | runner.loadConfig({ 23 | spec_dir: 'spec', 24 | spec_files: [ '**/*.spec.ts' ] 25 | }); 26 | 27 | runner.execute(); 28 | -------------------------------------------------------------------------------- /modules/angular-dynamic/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "stripInternal": true, 5 | "emitDecoratorMetadata": true, 6 | "experimentalDecorators": true, 7 | "moduleResolution": "node", 8 | "noEmitOnError": false, 9 | "lib": ["es2015", "dom"], 10 | "target": "es5", 11 | "skipLibCheck": true, 12 | "strictNullChecks": true, 13 | "noUnusedLocals": true, 14 | "inlineSources": true, 15 | "inlineSourceMap": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "downlevelIteration": true, 18 | "declaration": true, 19 | "module": "commonjs", 20 | "outDir": "./release", 21 | "rootDir": ".", 22 | "types": [ 23 | "node", 24 | "jasmine" 25 | ] 26 | }, 27 | "exclude": [ 28 | "node_modules", 29 | "release" 30 | ], 31 | "angularCompilerOptions": { 32 | "strictMetadataEmit": false 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /modules/angular-dynamic/spec/common_js_ng_module_factory_loader.spec.ts: -------------------------------------------------------------------------------- 1 | import { StatefulModule } from '../src/stateful_module'; 2 | import { CommonJsNgModuleLoader } from './common_js_ng_module_factory_loader'; 3 | import { NgModuleFactory } from '@angular/core'; 4 | import { TestBed, async } from '@angular/core/testing'; 5 | import { WidgetModule } from './widget_module'; 6 | 7 | describe('CommonJsNgModuleLoader', () => { 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({ 10 | imports: [ StatefulModule.forRoot() ], 11 | providers: [CommonJsNgModuleLoader] 12 | }); 13 | }); 14 | 15 | it('should load', async(async () => { 16 | let loader: CommonJsNgModuleLoader = TestBed.get(CommonJsNgModuleLoader); 17 | let widgetModule: NgModuleFactory = await loader.load('../spec/widget_module#WidgetModule'); 18 | expect(widgetModule instanceof NgModuleFactory).toBeTruthy(); 19 | })); 20 | }); 21 | -------------------------------------------------------------------------------- /modules/angular-dynamic/src/explicit_ng_module_factory_loader.ts: -------------------------------------------------------------------------------- 1 | import { Compiler, Injectable, NgModuleFactory, NgModuleFactoryLoader, Type } from '@angular/core'; 2 | 3 | 4 | /** 5 | * Explicitly set the module and its module id. 6 | */ 7 | @Injectable() 8 | export class ExplicitNgModuleLoader implements NgModuleFactoryLoader { 9 | register(path: string, moduleType: Type) { 10 | this.registered.set(path, moduleType); 11 | } 12 | 13 | private registered = new Map>(); 14 | 15 | constructor(private _compiler: Compiler) { 16 | } 17 | 18 | load(path: string): Promise> { 19 | return this.loadAndCompile(path); 20 | } 21 | 22 | private loadAndCompile(path: string): Promise> { 23 | let type = this.registered.get(path); 24 | if (!type) 25 | throw new Error('Module not registered.'); 26 | return this._compiler.compileModuleAsync(type); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /modules/angular-dynamic/index.ts: -------------------------------------------------------------------------------- 1 | // export { CommonJsNgModuleLoader, CommonJsNgModuleLoaderConfig } from './src/common_js_ng_module_factory_loader'; 2 | export { ExistingNgModuleLoader } from './src/existing_ng_module_factory_loader'; 3 | export { ExplicitNgModuleLoader } from './src/explicit_ng_module_factory_loader'; 4 | export { ComponentTypeLoader, TypeReferenceUrlResolver, TYPE_REFERENCE_URL_RESOLVER } from './src/component_type_loader'; 5 | export { StateChange, TypeReference } from './src/interfaces'; 6 | export { ControlDecorator, StateDecorator, StatefulDecorator, Control, State, Stateful } from './src/metadata'; 7 | export { StateMapperFactory } from './src/state_mapper_factory'; 8 | export { StateMapper } from './src/state_mapper'; 9 | export { StatefulModule } from './src/stateful_module'; 10 | export { SaStateOutlet } from './src/directives/sa_state_outlet'; 11 | export { SaLazyComponentOutlet } from './src/directives/sa_lazy_component_outlet'; 12 | export { SaFormControl } from './src/directives/sa_form_control'; 13 | export { AngularReflector } from './src/angular_reflector'; 14 | -------------------------------------------------------------------------------- /modules/angular-dynamic/src/stateful_module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, ModuleWithProviders } from '@angular/core'; 2 | import { ReactiveFormsModule } from '@angular/forms'; 3 | import { SaStateOutlet } from './directives/sa_state_outlet'; 4 | import { SaFormControl } from './directives/sa_form_control'; 5 | import { SaLazyComponentOutlet } from './directives/sa_lazy_component_outlet'; 6 | import { ComponentTypeLoader } from './component_type_loader'; 7 | import { StateMapperFactory } from './state_mapper_factory'; 8 | import { AngularReflector } from './angular_reflector'; 9 | 10 | @NgModule({ 11 | declarations: [SaStateOutlet, SaLazyComponentOutlet, SaFormControl], 12 | imports: [ReactiveFormsModule], 13 | exports: [SaStateOutlet, SaLazyComponentOutlet, SaFormControl, ReactiveFormsModule] 14 | }) 15 | export class StatefulModule { 16 | static forRoot() { 17 | return { 18 | ngModule: StatefulModule, 19 | providers: [ 20 | ComponentTypeLoader, 21 | StateMapperFactory, 22 | AngularReflector 23 | ] 24 | }; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | .nyc 5 | .nyc_output 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 19 | .grunt 20 | 21 | # Compiled binary addons (http://nodejs.org/api/addons.html) 22 | build/Release 23 | 24 | # Users Environment Variables 25 | .lock-wscript 26 | 27 | # OS generated files # 28 | .DS_Store 29 | ehthumbs.db 30 | Icon? 31 | Thumbs.db 32 | 33 | # Node Files # 34 | node_modules 35 | /bower_components 36 | 37 | # Typing TSD # 38 | /src/typings/tsd/ 39 | /typings/ 40 | /tsd_typings/ 41 | 42 | # Dist # 43 | /dist 44 | /public/__build__/ 45 | /src/*/__build__/ 46 | __build__/** 47 | .webpack.json 48 | 49 | #doc 50 | /doc 51 | 52 | # IDE # 53 | .idea/ 54 | *.swp 55 | !/typings/custom.d.ts 56 | 57 | # Build Artifacts # 58 | release 59 | dist 60 | /node_modules/ 61 | lerna-debug.log 62 | /lib/ 63 | ngfactory 64 | output 65 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Eric Nilsen 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 | -------------------------------------------------------------------------------- /modules/angular-dynamic/spec/component_type_loader.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentTypeLoader } from '../src/component_type_loader'; 2 | import { StatefulModule } from '../src/stateful_module'; 3 | import { TypeReference } from '../src/interfaces'; 4 | import { CommonJsNgModuleLoader } from './common_js_ng_module_factory_loader'; 5 | import { NgModuleFactoryLoader } from '@angular/core'; 6 | import { async, TestBed } from '@angular/core/testing'; 7 | 8 | describe('ComponentTypeLoader', () => { 9 | beforeEach(() => { 10 | TestBed.configureTestingModule({ 11 | imports: [ StatefulModule.forRoot() ], 12 | providers: [ComponentTypeLoader, { provide: NgModuleFactoryLoader, useClass: CommonJsNgModuleLoader }] 13 | }); 14 | }); 15 | 16 | it('should load', async(async () => { 17 | let loader: ComponentTypeLoader = TestBed.get(ComponentTypeLoader); 18 | let result = await loader.resolveAsync({ moduleName: '../spec/widget_module#WidgetModule', componentTypeName: 'Widget' }); 19 | expect(result && result.component && result.component.name).toBe('Widget', 'Could not load the Widget component type'); 20 | })); 21 | }); 22 | -------------------------------------------------------------------------------- /modules/angular-dynamic/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Eric Nilsen 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 | 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sharpangles/platform", 3 | "version": "1.0.0", 4 | "description": "monorepo for sharpangles development", 5 | "keywords": [ 6 | "Angular", 7 | "Enterprise" 8 | ], 9 | "author": "Eric Nilsen", 10 | "license": "MIT", 11 | "repository": {}, 12 | "devDependencies": { 13 | "@angular/animations": "5.0.0", 14 | "@angular/common": "5.0.0", 15 | "@angular/compiler": "5.0.0", 16 | "@angular/compiler-cli": "5.0.0", 17 | "@angular/core": "5.0.0", 18 | "@angular/forms": "5.0.0", 19 | "@angular/http": "5.0.0", 20 | "@angular/platform-browser": "5.0.0", 21 | "@angular/platform-browser-dynamic": "5.0.0", 22 | "@angular/platform-server": "5.0.0", 23 | "@types/jasmine": "^2.5.52", 24 | "@types/node": "^7.0.31", 25 | "core-js": "^2.4.1", 26 | "cpy-cli": "^1.0.1", 27 | "jasmine": "^2.6.0", 28 | "lerna": "^2.0.0-rc.5", 29 | "nyc": "^11.0.2", 30 | "rimraf": "^2.6.1", 31 | "rollup": "^0.43.0", 32 | "rxjs": "^5.4.0", 33 | "ts-node": "^3.0.6", 34 | "tslib": "1.7.1", 35 | "tslint": "^5.4.3", 36 | "typescript": "^2.3.4", 37 | "uglify-js": "^3.0.15", 38 | "zone.js": "^0.8.12" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "class-name": true, 4 | "comment-format": [ 5 | true, 6 | "check-space" 7 | ], 8 | "indent": [ 9 | true, 10 | "spaces" 11 | ], 12 | "no-duplicate-variable": true, 13 | "no-eval": true, 14 | "no-internal-module": true, 15 | "no-trailing-whitespace": true, 16 | "no-var-keyword": true, 17 | "one-line": [ 18 | true, 19 | "check-open-brace", 20 | "check-whitespace" 21 | ], 22 | "quotemark": [ 23 | true, 24 | "single" 25 | ], 26 | "semicolon": [true, "always"], 27 | "triple-equals": [ 28 | true, 29 | "allow-null-check" 30 | ], 31 | "typedef-whitespace": [ 32 | true, 33 | { 34 | "call-signature": "nospace", 35 | "index-signature": "nospace", 36 | "parameter": "nospace", 37 | "property-declaration": "nospace", 38 | "variable-declaration": "nospace" 39 | } 40 | ], 41 | "variable-name": [ 42 | true, 43 | "ban-keywords", 44 | "allow-leading-underscore" 45 | ], 46 | "whitespace": [ 47 | true, 48 | "check-branch", 49 | "check-decl", 50 | "check-operator", 51 | "check-separator", 52 | "check-type" 53 | ] 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /modules/angular-dynamic/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "class-name": true, 4 | "comment-format": [ 5 | true, 6 | "check-space" 7 | ], 8 | "indent": [ 9 | true, 10 | "spaces" 11 | ], 12 | "no-duplicate-variable": true, 13 | "no-eval": true, 14 | "no-internal-module": true, 15 | "no-trailing-whitespace": true, 16 | "no-var-keyword": true, 17 | "one-line": [ 18 | true, 19 | "check-open-brace", 20 | "check-whitespace" 21 | ], 22 | "quotemark": [ 23 | true, 24 | "single" 25 | ], 26 | "semicolon": [true, "always"], 27 | "triple-equals": [ 28 | true, 29 | "allow-null-check" 30 | ], 31 | "typedef-whitespace": [ 32 | true, 33 | { 34 | "call-signature": "nospace", 35 | "index-signature": "nospace", 36 | "parameter": "nospace", 37 | "property-declaration": "nospace", 38 | "variable-declaration": "nospace" 39 | } 40 | ], 41 | "variable-name": [ 42 | true, 43 | "ban-keywords", 44 | "allow-leading-underscore" 45 | ], 46 | "whitespace": [ 47 | true, 48 | "check-branch", 49 | "check-decl", 50 | "check-operator", 51 | "check-separator", 52 | "check-type" 53 | ] 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /modules/angular-dynamic/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # 1.0.0 (2017-11-06) 3 | Move markForCheck inside directives. 4 | 5 | 6 | # 1.0.0-alpha.8 (2017-09-19) 7 | Bug fix when props exist in both ng4 and ng5 prop meta storage. 8 | 9 | 10 | # 1.0.0-alpha.7 (2017-09-19) 11 | Minor fix, plus moved commonjs factory loader to specs for now to get rid of static analysis warnings. Can re-add in separate library later if needed. 12 | 13 | 14 | # 1.0.0-alpha.6 (2017-09-19) 15 | Get stuff working with 'static' dynamic (ie. ng cli with AOT). 16 | In this mode, there is no need to register any specific NgModuleFactoryLoader. 17 | Ng-cli basically transforms your typescript to plug its factories in during AOT. 18 | It discovers components by using the router, so you'll have to add fake (or possibly real) routes. 19 | In the root module, add routes like: { path: 'makeupsomething', loadChildren: '../../relative-path-to/some.module#SomeModule', component: SomeModule }. 20 | Obviously this goes against the grain of what this library is trying to do, but sometimes you want it to run in an ng-cli devops environment, perhaps in anticipation of a better way later that wont cause you to change library code. 21 | - Don't use a @scopename absolute reference in loadChildren, its not supported by ng cli yet. 22 | - The component type is required, so if there are multiple types add multiple paths. Remember, this is just a hack to make it work in the static analysis culture without changing library code. 23 | - It adds an unfortunate private member access to get the component type, but only when the NgModule attribute is gone (ie. webpack output from the cli). 24 | 25 | 26 | # 1.0.0-alpha.5 (2017-08-30) 27 | Angular 5 28 | 29 | 30 | # 1.0.0-alpha.4 (2017-06-15) 31 | Added more NgModuleFactoryLoaders. 32 | - Existing: Simply wraps getModuleFactory from @angular/core 33 | - Explicit: Use this to register types directly as 'dynamic' (even though you have them statically resolved). Useful for running tests and samples with webpack/ng-cli. 34 | 35 | 36 | # 1.0.0-alpha.3 (2017-06-14) 37 | State is now supplied in StateChange. 38 | 39 | 40 | # 1.0.0-alpha.2 (2017-06-13) 41 | Export @Control, @State, and @Stateful. 42 | 43 | 44 | # 1.0.0-alpha.1 (2017-06-13) 45 | Removed dependency on angular internal Reflector. 46 | 47 | 48 | # 1.0.0-alpha.0 (2017-04-13) 49 | -------------------------------------------------------------------------------- /modules/angular-dynamic/spec/sa_lazy_component_outlet.spec.ts: -------------------------------------------------------------------------------- 1 | import { SaLazyComponentOutlet } from '../src/directives/sa_lazy_component_outlet'; 2 | import { StatefulModule } from '../src/stateful_module'; 3 | import { TypeReference } from '../src/interfaces'; 4 | import { CommonJsNgModuleLoader } from './common_js_ng_module_factory_loader'; 5 | import { Component, ViewChild, Input, NgModuleFactoryLoader } from '@angular/core'; 6 | import { async, TestBed } from '@angular/core/testing'; 7 | import { By } from '@angular/platform-browser'; 8 | import { Widget } from './widget_module'; 9 | import { TestService } from './shared_service_module'; 10 | 11 | @Component({ 12 | selector: 'testComponent', 13 | template: ` 14 | 15 |
Loading...
16 |
17 | `, 18 | viewProviders: [TestService] 19 | }) 20 | class TestComponent { 21 | @Input() widgetReference: TypeReference; 22 | @ViewChild(SaLazyComponentOutlet) outlet: SaLazyComponentOutlet; 23 | } 24 | 25 | 26 | describe('SaLazyComponentOutlet', () => { 27 | let moduleConfig = { 28 | declarations: [TestComponent], 29 | imports: [StatefulModule.forRoot()], 30 | entryComponents: [TestComponent], 31 | providers: [{ provide: NgModuleFactoryLoader, useClass: CommonJsNgModuleLoader }] 32 | }; 33 | beforeEach(async(async () => { 34 | TestBed.configureTestingModule(moduleConfig); 35 | await TestBed.compileComponents(); 36 | })); 37 | it('should transition from loading to a lazily loaded widget', async(async () => { 38 | let fixture = TestBed.createComponent(TestComponent); 39 | fixture.detectChanges(); 40 | await fixture.whenStable(); 41 | let testComponent = fixture.componentInstance; 42 | let loadingElement = fixture.debugElement.query(By.css('.loading')); 43 | expect(loadingElement && loadingElement.name === 'div').toBeTruthy('The loading element is not present prior to loading the widget'); 44 | testComponent.widgetReference = { moduleName: '../spec/widget_module#WidgetModule', componentTypeName: 'Widget' }; 45 | fixture.detectChanges(); 46 | await fixture.whenStable(); 47 | let widget: Widget = testComponent.outlet.componentRef && testComponent.outlet.componentRef.instance; 48 | expect(widget instanceof Widget).toBeTruthy(); 49 | expect(widget.injector.get(TestService)).toBeTruthy('The injector was not wired up correctly'); 50 | expect(widget.testService).toBeTruthy('The shared srevice is not accessible to the dynamic child'); 51 | })); 52 | }); 53 | -------------------------------------------------------------------------------- /modules/angular-dynamic/spec/common_js_ng_module_factory_loader.ts: -------------------------------------------------------------------------------- 1 | import { Compiler, Injectable, Optional, NgModuleFactory, NgModuleFactoryLoader } from '@angular/core'; 2 | 3 | const _SEPARATOR = '#'; 4 | 5 | const FACTORY_CLASS_SUFFIX = 'NgFactory'; 6 | 7 | /** 8 | * Configuration for CommonJsNgModuleLoader. 9 | * token. 10 | * 11 | * @experimental 12 | */ 13 | export abstract class CommonJsNgModuleLoaderConfig { 14 | /** 15 | * Prefix to add when computing the name of the factory module for a given module name. 16 | */ 17 | factoryPathPrefix: string; 18 | 19 | /** 20 | * Suffix to add when computing the name of the factory module for a given module name. 21 | */ 22 | factoryPathSuffix: string; 23 | } 24 | 25 | const DEFAULT_CONFIG: CommonJsNgModuleLoaderConfig = { 26 | factoryPathPrefix: '', 27 | factoryPathSuffix: '.ngfactory', 28 | }; 29 | 30 | /** 31 | * NgModuleFactoryLoader that uses CommonJs to load NgModuleFactory 32 | * @experimental 33 | */ 34 | @Injectable() 35 | export class CommonJsNgModuleLoader implements NgModuleFactoryLoader { 36 | private _config: CommonJsNgModuleLoaderConfig; 37 | 38 | constructor(private _compiler: Compiler, @Optional() config?: CommonJsNgModuleLoaderConfig) { 39 | this._config = config || DEFAULT_CONFIG; 40 | } 41 | 42 | load(path: string): Promise> { 43 | const offlineMode = this._compiler instanceof Compiler; 44 | return offlineMode ? this.loadFactory(path) : this.loadAndCompile(path); 45 | } 46 | 47 | private loadAndCompile(path: string): Promise> { 48 | let [module, exportName] = path.split(_SEPARATOR); 49 | if (exportName === undefined) { 50 | exportName = 'default'; 51 | } 52 | 53 | let type = require(module)[exportName]; 54 | return this._compiler.compileModuleAsync(checkNotEmpty(type, module, exportName)); 55 | } 56 | 57 | private loadFactory(path: string): Promise> { 58 | let [module, exportName] = path.split(_SEPARATOR); 59 | let factoryClassSuffix = FACTORY_CLASS_SUFFIX; 60 | if (exportName === undefined) { 61 | exportName = 'default'; 62 | factoryClassSuffix = ''; 63 | } 64 | 65 | let factory = require(this._config.factoryPathPrefix + module + this._config.factoryPathSuffix)[exportName + factoryClassSuffix]; 66 | return checkNotEmpty(factory, module, exportName); 67 | } 68 | } 69 | 70 | function checkNotEmpty(value: any, modulePath: string, exportName: string): any { 71 | if (!value) { 72 | throw new Error(`Cannot find '${exportName}' in '${modulePath}'`); 73 | } 74 | return value; 75 | } 76 | -------------------------------------------------------------------------------- /modules/angular-dynamic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sharpangles/angular-dynamic", 3 | "version": "1.0.0", 4 | "description": "Dynamic component loading for angular", 5 | "main": "./bundles/angular-dynamic.umd.js", 6 | "module": "./index.js", 7 | "scripts": { 8 | "test": "nyc node tests.js", 9 | "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0", 10 | "clean:pre": "rimraf release", 11 | "clean:post": "npm run clean:ngfactory && npm run clean:artifacts", 12 | "clean:ngfactory": "rimraf release/**/*.ngfactory.ts", 13 | "clean:artifacts": "rimraf release/node_modules release/release", 14 | "copy": "cpy LICENSE package.json README.md release", 15 | "build:js": "ngc -p tsconfig.dist.json", 16 | "build:umd": "rollup -c rollup.config.js", 17 | "build:uglify": "uglifyjs -c --screw-ie8 --comments -o ./release/bundles/angular-dynamic.min.umd.js ./release/bundles/angular-dynamic.umd.js", 18 | "bundle:main": "npm run build:js && npm run build:umd && npm run build:uglify", 19 | "prebuild": "npm run test && npm run clean:pre", 20 | "postbuild": "npm run clean:post && npm run copy", 21 | "build": "npm run bundle:main", 22 | "version": "npm run changelog && git add CHANGELOG.md" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/sharpangles/platform.git" 27 | }, 28 | "keywords": [ 29 | "Angular", 30 | "Enterprise", 31 | "Dynamic", 32 | "Lazy Loading" 33 | ], 34 | "author": "Eric Nilsen", 35 | "license": "MIT", 36 | "bugs": { 37 | "url": "https://github.com/sharpangles/platform/issues" 38 | }, 39 | "homepage": "https://github.com/sharpangles/platform/tree/master", 40 | "peerDependencies": { 41 | "@angular/core": "^4.0.0 || ^5.0.0", 42 | "@angular/forms": "^4.0.0", 43 | "rxjs": "^5.3.0" 44 | }, 45 | "devDependencies": { 46 | "@angular/animations": "5.0.0", 47 | "@angular/common": "5.0.0", 48 | "@angular/compiler": "5.0.0", 49 | "@angular/compiler-cli": "5.0.0", 50 | "@angular/core": "5.0.0", 51 | "@angular/forms": "5.0.0", 52 | "@angular/http": "5.0.0", 53 | "@angular/platform-browser": "5.0.0", 54 | "@angular/platform-browser-dynamic": "5.0.0", 55 | "@angular/platform-server": "5.0.0", 56 | "@types/jasmine": "^2.5.52", 57 | "@types/node": "^7.0.31", 58 | "core-js": "^2.4.1", 59 | "cpy-cli": "^1.0.1", 60 | "jasmine": "^2.6.0", 61 | "lerna": "^2.0.0-rc.5", 62 | "nyc": "^11.0.2", 63 | "rimraf": "^2.6.1", 64 | "rollup": "^0.43.0", 65 | "rxjs": "^5.4.0", 66 | "ts-node": "^3.0.6", 67 | "tslib": "1.7.1", 68 | "tslint": "^5.4.3", 69 | "typescript": "^2.3.4", 70 | "uglify-js": "^3.0.15", 71 | "zone.js": "^0.8.12", 72 | "conventional-changelog-cli": "^1.3.1", 73 | "jasmine-marbles": "^0.0.2" 74 | }, 75 | "nyc": { 76 | "extension": [ 77 | ".ts" 78 | ], 79 | "exclude": [ 80 | "spec/**/*.spec" 81 | ], 82 | "include": [ 83 | "src/**/*.ts" 84 | ] 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /modules/angular-dynamic/spec/sa_form_control.spec.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, forwardRef, ViewChild, ViewChildren, QueryList, ElementRef, Renderer2, NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormControl, FormGroup, FormArray, NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms'; 4 | import { TestBed, async } from '@angular/core/testing'; 5 | import { StatefulModule } from '../src/stateful_module'; 6 | 7 | @Component({ 8 | selector: 'child', 9 | template: ``, 10 | providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => ChildControl), multi: true }] 11 | }) 12 | export class ChildControl implements ControlValueAccessor { 13 | constructor(private _renderer: Renderer2) { 14 | } 15 | 16 | @ViewChild('inputElement') private _inputElement: ElementRef; 17 | 18 | onChange = (_: any) => { }; 19 | onTouched = () => { }; 20 | 21 | writeValue(obj: any): void { 22 | this._renderer.setProperty(this._inputElement.nativeElement, 'value', obj); 23 | } 24 | 25 | registerOnChange(fn: (_: any) => void): void { 26 | this.onChange = fn; 27 | } 28 | registerOnTouched(fn: () => void): void { 29 | this.onTouched = fn; 30 | } 31 | 32 | control = new FormControl(); 33 | @Input() value: string; 34 | } 35 | 36 | @Component({ 37 | selector: 'test-cmp', 38 | template: ` 39 |
40 |

41 |
42 | ` 43 | }) 44 | export class TestComponent { 45 | @ViewChildren(ChildControl) children: QueryList; 46 | 47 | childValues: any = ['hi', 'whatever']; 48 | 49 | control: FormGroup = new FormGroup({ 50 | list: new FormArray([]) 51 | }); 52 | 53 | submit(event: Event) { 54 | console.log(this.control.value); 55 | console.log(this.control.valid); 56 | event.preventDefault(); 57 | } 58 | } 59 | 60 | @NgModule({ 61 | declarations: [ChildControl, TestComponent], 62 | imports: [StatefulModule, CommonModule], 63 | exports: [StatefulModule, CommonModule, ChildControl, TestComponent], 64 | entryComponents: [TestComponent] 65 | }) 66 | export class TestModule { 67 | } 68 | 69 | describe('SaFormControl', () => { 70 | let moduleConfig = { 71 | imports: [TestModule] 72 | }; 73 | beforeEach(async(async () => { 74 | TestBed.configureTestingModule(moduleConfig); 75 | await TestBed.compileComponents(); 76 | })); 77 | it('should connect an explicit child control to a parent', async(async () => { 78 | let fixture = TestBed.createComponent(TestComponent); 79 | fixture.changeDetectorRef.markForCheck(); 80 | fixture.detectChanges(); 81 | await fixture.whenStable(); 82 | let testComponent = fixture.componentInstance; 83 | expect(testComponent.children.length).toBeTruthy(); 84 | })); 85 | }); 86 | -------------------------------------------------------------------------------- /modules/angular-dynamic/src/metadata.ts: -------------------------------------------------------------------------------- 1 | import { TypeDecorator, Injector, Type } from '@angular/core'; 2 | import { StateChange } from './interfaces'; 3 | import { ANNOTATIONS } from './angular_reflector'; 4 | 5 | export interface StatefulDecorator extends TypeDecorator { } 6 | 7 | export interface StateDecorator { 8 | (bindingPropertyName?: string): any; 9 | new (bindingPropertyName?: string): any; 10 | } 11 | 12 | export function State(bindingPropertyName?: string): PropertyDecorator { 13 | // This does not invoke all of angular annotations. It's just enough to get StatefulMetadtaFactory to work. 14 | // AOT cant seem to understand makePropDecorator if copying ng metadata.Makes us use a little magic string stuff for now tokeep it simple. 15 | return function (target: Object, propertyKey: string | symbol) { 16 | const meta = (Reflect).getOwnMetadata('propMetadata', target.constructor) || {}; 17 | meta['State'] = bindingPropertyName && meta.hasOwnProperty(bindingPropertyName) && meta['State'] || []; 18 | meta['State'].unshift({ type: 'State', propertyName: propertyKey, bindingPropertyName: bindingPropertyName }); 19 | (Reflect).defineMetadata('propMetadata', meta, target.constructor); 20 | }; 21 | } 22 | 23 | 24 | export interface ControlDecorator { 25 | (bindingPropertyName?: string): any; 26 | new (bindingPropertyName?: string): any; 27 | } 28 | 29 | export function Control(bindingPropertyName?: string): PropertyDecorator { 30 | // This does not invoke all of angular annotations. It's just enough to get StatefulMetadtaFactory to work. 31 | // AOT cant seem to understand makePropDecorator if copying ng metadata.Makes us use a little magic string stuff for now tokeep it simple. 32 | return function (target: Object, propertyKey: string | symbol) { 33 | const meta = (Reflect).getOwnMetadata('propMetadata', target.constructor) || {}; 34 | meta['Control'] = bindingPropertyName && meta.hasOwnProperty(bindingPropertyName) && meta['Control'] || []; 35 | meta['Control'].unshift({ type: 'Control', propertyName: propertyKey, bindingPropertyName: bindingPropertyName }); 36 | (Reflect).defineMetadata('propMetadata', meta, target.constructor); 37 | }; 38 | } 39 | 40 | 41 | export interface StatefulDecorator { 42 | (obj: Stateful): TypeDecorator; 43 | new (obj: Stateful): Stateful; 44 | } 45 | 46 | export interface Stateful { 47 | getState: (injector: Injector, component: any) => any; 48 | setState: (injector: Injector, component: any, state: any) => StateChange; 49 | } 50 | 51 | export function Stateful(stateful: Stateful) { 52 | // This does not invoke all of angular annotations. It's just enough to get StatefulMetadtaFactory to work. 53 | // AOT cant seem to understand makePropDecorator if copying ng metadata.Makes us use a little magic string stuff for now tokeep it simple. 54 | return function TypeDecorator(cls: Type) { 55 | // just set both for ng4 and 5. 56 | const annotations = (Reflect).getOwnMetadata('annotations', cls) || []; 57 | annotations.push(stateful); 58 | (Reflect).defineMetadata('annotations', annotations, cls); 59 | if (!cls[ANNOTATIONS]) 60 | cls[ANNOTATIONS] = []; 61 | cls[ANNOTATIONS].push(stateful); 62 | return cls; 63 | }; 64 | } 65 | -------------------------------------------------------------------------------- /modules/angular-dynamic/spec/widget_module.ts: -------------------------------------------------------------------------------- 1 | import { StateChange } from '../src/interfaces'; 2 | import { StateMapper } from '../src/state_mapper'; 3 | import { Control, Stateful, State } from '../src/metadata'; 4 | import { Component, NgModule, Input, Injector } from '@angular/core'; 5 | import { FormControl } from '@angular/forms'; 6 | import { SharedServiceModule, TestService } from './shared_service_module'; 7 | 8 | export interface IWidgetState { 9 | someValue: string; 10 | } 11 | 12 | /** AOT still checks the Stateful decorator, so we have to export functions for it. */ 13 | export function getState(injector: Injector, component: WidgetCustomizingStateProjection) { 14 | return { someValue: component.someValue }; 15 | } 16 | export function setState(injector: Injector, component: any, state: IWidgetState): StateChange { 17 | (component).someValue = state ? state.someValue.toUpperCase() : null; 18 | return { state: state }; 19 | } 20 | 21 | @Component({ 22 | selector: 'widgetCustomizingStateProjection', 23 | template: `WidgetCustomizingStateProjection: {{ someValue || "no state" }}` 24 | }) 25 | @Stateful({ 26 | getState: getState, 27 | setState: setState 28 | }) 29 | export class WidgetCustomizingStateProjection implements IWidgetState { 30 | @Input() someValue: any; 31 | } 32 | 33 | @Component({ 34 | selector: 'WidgetUsingStateDecorator', 35 | template: `WidgetUsingStateDecorator: {{ someValue || "no state" }}`, 36 | }) 37 | export class WidgetUsingStateDecorator implements IWidgetState { 38 | @State() someValue: any; 39 | } 40 | 41 | @Component({ 42 | selector: 'widgetUsingInputDecorator', 43 | template: `WidgetUsingInputDecorator: {{ someValue || "no state" }}`, 44 | }) 45 | export class WidgetUsingInputDecorator implements IWidgetState { 46 | @Input() someValue: any; 47 | } 48 | 49 | @Component({ 50 | selector: 'widget', 51 | template: `Widget: {{ someValue || "no state" }}`, 52 | }) 53 | export class WidgetUsingInheritance extends StateMapper { 54 | getState() { 55 | return { someValue: this.someValue }; 56 | } 57 | setState(state: IWidgetState): StateChange { 58 | this.someValue = state ? state.someValue.toUpperCase() : null; 59 | return { state: state }; 60 | } 61 | 62 | someValue: any; 63 | } 64 | 65 | @Component({ 66 | selector: 'widget', 67 | template: `Widget: {{ someValue || "no state" }}`, 68 | }) 69 | export class Widget implements IWidgetState { 70 | constructor(public injector: Injector, public testService: TestService) { 71 | } 72 | 73 | @Input() someValue: any; 74 | } 75 | 76 | @Component({ 77 | selector: 'widgetWithControl', 78 | template: `WidgetUsingInputDecorator: {{ someValue || "no state" }}`, 79 | }) 80 | export class WidgetWithControl implements IWidgetState { 81 | @Control() control: FormControl = new FormControl(); 82 | @Input() someValue: any; 83 | } 84 | 85 | let DYNAMIC_COMPONENTS = [WidgetCustomizingStateProjection, WidgetUsingStateDecorator, WidgetUsingInputDecorator, WidgetUsingInheritance, Widget, WidgetWithControl]; 86 | 87 | @NgModule({ 88 | declarations: [DYNAMIC_COMPONENTS], 89 | imports: [SharedServiceModule], 90 | exports: [SharedServiceModule, DYNAMIC_COMPONENTS], 91 | entryComponents: [DYNAMIC_COMPONENTS] 92 | }) 93 | export class WidgetModule { 94 | } 95 | -------------------------------------------------------------------------------- /modules/angular-dynamic/src/directives/sa_form_control.ts: -------------------------------------------------------------------------------- 1 | import { Directive, Input, OnChanges, SimpleChanges, Optional, Self } from '@angular/core'; 2 | import { FormGroup, FormArray, AbstractControl, NgControl, FormGroupName, FormArrayName } from '@angular/forms'; 3 | 4 | 5 | /** 6 | * Separates concerns of forms, allowing child components to build their control, while letting parents manage their membership and transactional behaviors. 7 | * WARNING: This uses a hack to replace the built-in form ngOnChanges method with a substitute. 8 | * Proposal: https://github.com/angular/angular/issues/10195 9 | */ 10 | @Directive({ 11 | selector: '[saFormControl]' 12 | }) 13 | export class SaFormControl implements OnChanges { 14 | constructor( @Optional() @Self() formGroupName: FormGroupName, @Optional() @Self() formArrayName: FormArrayName, @Optional() @Self() ngControl: NgControl) { 15 | this.otherDirective = (formGroupName ? formGroupName : formArrayName ? formArrayName : ngControl); 16 | this.oldOnChanges = this.otherDirective.ngOnChanges; 17 | this.otherDirective.ngOnChanges = (changes: SimpleChanges) => this.newOnChanges(changes); 18 | } 19 | 20 | @Input() saFormControl: AbstractControl; 21 | 22 | /** The naming directive. */ 23 | private otherDirective: any; 24 | 25 | /** The original ngOnChanges method that was overridden on the naming directive. */ 26 | private oldOnChanges: (changes: SimpleChanges) => any; 27 | 28 | /** Holds onto changes that went to the original naming directive. Undefined means it was never called, null means it is already handled. */ 29 | private delayedChanges?: SimpleChanges; 30 | 31 | /** An ngOnChanges hack on the naming directive that delays its call until this ngOnChanges runs first. Otherwise the naming directive will set up its control too soon. */ 32 | private newOnChanges(changes: SimpleChanges) { 33 | if (!this.delayedChanges) 34 | this.delayedChanges = changes; 35 | } 36 | 37 | ngOnChanges(changes: SimpleChanges) { 38 | if (changes.saFormControl && this.otherDirective._parent) { 39 | if (changes.saFormControl.previousValue instanceof AbstractControl) 40 | this.remove(); 41 | if (changes.saFormControl.currentValue instanceof AbstractControl) 42 | this.add(); 43 | if (!(changes.saFormControl.previousValue instanceof AbstractControl) && !(changes.saFormControl.currentValue instanceof AbstractControl)) 44 | return; 45 | } 46 | if (this.delayedChanges) { 47 | this.oldOnChanges.call(this.otherDirective, this.delayedChanges); 48 | delete this.delayedChanges; 49 | } 50 | } 51 | 52 | private add(): void { 53 | if (this.otherDirective._parent.control instanceof FormGroup) 54 | (this.otherDirective._parent.control).addControl(this.otherDirective.name, this.saFormControl); 55 | else 56 | (this.otherDirective._parent.control).insert(parseInt(this.otherDirective.name), this.saFormControl); 57 | } 58 | 59 | private remove(): void { 60 | if (this.otherDirective._parent.control instanceof FormGroup) 61 | (this.otherDirective._parent.control).removeControl(this.otherDirective.name); 62 | else 63 | (this.otherDirective._parent.control).removeAt(parseInt(this.otherDirective.name)); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /modules/angular-dynamic/src/directives/sa_state_outlet.ts: -------------------------------------------------------------------------------- 1 | import { Input, Output, Directive, OnChanges, SimpleChange, EventEmitter, ComponentRef, ChangeDetectorRef } from '@angular/core'; 2 | import { StateChange } from '../interfaces'; 3 | import { StateMapperFactory } from '../state_mapper_factory'; 4 | 5 | export abstract class SaStateOutletBase implements OnChanges { 6 | constructor(private stateMapperFactory: StateMapperFactory, private cdr: ChangeDetectorRef) { 7 | } 8 | 9 | /** 10 | * When https://github.com/angular/angular/issues/8563 is implemented, we should be able to query the component directly and could get rid of the ComponentRef input. 11 | * This will also allow us to bring back the 'default' child component concept via ng-content. 12 | */ 13 | @Input() saStateOutlet: any; 14 | 15 | @Output() saStateOutletStateSet = new EventEmitter(false); 16 | 17 | protected abstract get componentRef(): ComponentRef | undefined; 18 | 19 | ngOnChanges(changes: { [key: string]: SimpleChange; }): any { 20 | if (!this.componentRef) 21 | return; 22 | let stateful = this.stateMapperFactory.get(this.componentRef.componentType); 23 | let stateChange = stateful.setState(this.componentRef.injector, this.componentRef.instance, this.saStateOutlet); 24 | this.saStateOutletStateSet.emit(stateChange); 25 | this.cdr.markForCheck(); 26 | } 27 | 28 | getState() { 29 | if (!this.componentRef) 30 | return; 31 | let stateful = this.stateMapperFactory.get(this.componentRef.componentType); 32 | return stateful.getState(this.componentRef.injector, this.componentRef.instance); 33 | } 34 | } 35 | 36 | /** 37 | * Projects state into a child component. 38 | */ 39 | @Directive({ 40 | // selector: "[saStateOutlet][saStateOutletComponentRef]", 41 | selector: '[saStateOutlet]', 42 | exportAs: 'saStateOutlet' 43 | }) 44 | export class SaStateOutlet extends SaStateOutletBase { 45 | constructor(stateMapperFactory: StateMapperFactory, cdr: ChangeDetectorRef) { 46 | super(stateMapperFactory, cdr); 47 | } 48 | 49 | @Input() saStateOutletComponentRef?: ComponentRef; 50 | protected get componentRef(): ComponentRef | undefined { return this.saStateOutletComponentRef; } 51 | } 52 | 53 | // /** 54 | // * Projects state into a child component. 55 | // */ 56 | // @Directive({ 57 | // selector: "[saStateOutlet]:not([saStateOutletComponentRef])", 58 | // exportAs: 'saStateOutlet' 59 | // }) 60 | // export class SaStateOutletImplicit extends SaStateOutletBase implements OnDestroy { 61 | // constructor(stateMapperFactory: StateMapperFactory, changeDetectorRef: ChangeDetectorRef, @Self() saLazyComponentOutlet: SaLazyComponentOutlet) { 62 | // super(stateMapperFactory); 63 | // if (saLazyComponentOutlet) { 64 | // this.subscription = saLazyComponentOutlet.componentChanged.subscribe(c => { 65 | // this.lazyComponentRef = c.componentRef; 66 | // changeDetectorRef.detectChanges(); 67 | // }); 68 | // } 69 | // } 70 | 71 | // private subscription: any; 72 | // private lazyComponentRef?: ComponentRef; 73 | // protected get componentRef(): ComponentRef | undefined { return this.lazyComponentRef; } 74 | 75 | // ngOnDestroy() { 76 | // if (this.subscription) { 77 | // this.subscription.unsubscribe(); 78 | // } 79 | // } 80 | // } 81 | -------------------------------------------------------------------------------- /modules/angular-dynamic/src/component_type_loader.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NgModuleFactory, NgModuleFactoryLoader, NgModule, Type, InjectionToken, Optional, Inject, getModuleFactory, resolveForwardRef, ComponentFactoryResolver } from '@angular/core'; 2 | import { TypeReference } from './interfaces'; 3 | import { AngularReflector } from './angular_reflector'; 4 | 5 | /** 6 | * Implementations asyncrhonously resolve the type reference to a url. 7 | * This gives an opportunity to involve global bootstrappers, configure a module loader, or handle internal references with moduleId (similar to UrlTree from the router or UrlResolver from the compiler), 8 | */ 9 | export interface TypeReferenceUrlResolver { 10 | getUrl(typeReference: TypeReference): Promise; 11 | } 12 | 13 | export let TYPE_REFERENCE_URL_RESOLVER: InjectionToken = new InjectionToken('TypeReferenceUrlResolver'); 14 | 15 | /** 16 | * Creates a ComponentFactory from an ITypeMetadata. 17 | * @todo Keep an eye out for anything exposed on router to accomplish this. 18 | */ 19 | @Injectable() 20 | export class ComponentTypeLoader { 21 | constructor(public componentFactoryResolver: ComponentFactoryResolver, @Optional() private moduleFactoryLoader?: NgModuleFactoryLoader, @Optional() @Inject(TYPE_REFERENCE_URL_RESOLVER) private typeReferenceUrlResolver?: TypeReferenceUrlResolver) { 22 | } 23 | 24 | /** 25 | * @todo This method overlaps alot with the router. At the time this was written, the router stuff wasnt very isolable. Should reinvestigate now that its been a few major versions of ng. 26 | * See @angular/common location, UrlTree parsing, etc... to bring back relative module stuff. Although then we add router dependency 27 | */ 28 | async resolveAsync(typeReference: TypeReference): Promise<{ module: NgModuleFactory, component: Type }> { 29 | if (!typeReference.componentTypeName) { 30 | throw new Error('A component name is required.'); 31 | } 32 | let url = this.typeReferenceUrlResolver ? await this.typeReferenceUrlResolver.getUrl(typeReference) : typeReference.moduleName!; 33 | let ngModuleFactory = this.moduleFactoryLoader ? await this.moduleFactoryLoader.load(url) : getModuleFactory(url); 34 | let annotations = new AngularReflector().annotations(ngModuleFactory.moduleType); 35 | let component: Type; 36 | let ngModule: NgModule = annotations.find((m: any) => m.entryComponents || m.bootstrap); 37 | if (ngModule) { 38 | if (!ngModule.entryComponents && !ngModule.bootstrap) { 39 | throw new Error('The type was not a module with entryComponents'); 40 | } 41 | component = this.findComponent(ngModule, typeReference.componentTypeName)!; 42 | } 43 | else { 44 | // Gets things working with ng-cli AOT since we lose NgModule annotations. 45 | const entry = Array.from((this.componentFactoryResolver)._factories.entries()).find(e => e[0].name === typeReference.componentTypeName); 46 | if (!entry) 47 | throw new Error('Component is not an entryComponent on the dynamically loaded module.'); 48 | component = entry[0]; 49 | } 50 | return { module: ngModuleFactory, component: component }; 51 | } 52 | 53 | private findComponent(ngModule: NgModule, componentTypeName: string): Type | undefined { 54 | let flattenedEntryComponents = this.flatten(ngModule.entryComponents || ngModule.bootstrap); 55 | return flattenedEntryComponents.find(entryComponent => resolveForwardRef(entryComponent).name === componentTypeName); 56 | } 57 | 58 | /** @todo or https://github.com/angular/angular/blob/7764c5c697df8ceddf6e4a177b338ff9b9c7212a/packages/compiler/src/metadata_resolver.ts#L1056 */ 59 | private flatten(list: Array): T[] { 60 | return list.reduce((flat: any[], item: T | T[]): T[] => (flat).concat(Array.isArray(item) ? this.flatten(item) : item), []); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @sharpangles 2 | 3 | Angular for the Enterprise. 4 | 5 | Currently, the angular ecosystem is mostly focused on a consumer-facing culture, where small apps and sites are downloaded by a high percentage of unique users and every downloaded bit counts. 6 | Sharpangles is in the beginning stages of offering an alternative ecosystem geared for the future, where 5G and gigabit connections will make static analysis feel like a premature optimization. 7 | Even in the present day, enterprise scenarios rarely care if their consistent user base is caching 500k vs 1MB of javascript, especially when the latter enables them to push new user interface to their users on the fly. 8 | We embrace the dynamic nature of ES6+ where module loading may not always be performed by a statically-anyalizable import statement at the top of a file. 9 | Unfortunately, it touches alot more than just the module loading process. 10 | 11 | ## What works: 12 | ### [@sharpangles/angular-dynamic](https://github.com/sharpangles/platform/tree/master/modules/angular-dynamic) 13 | Dynamic component loading for Angular. Enables dynamic state and loading capabilities through a set of directives not achievable simply through the use of SystemJsNgModuleLoader. 14 | 15 | ## What is in progress: 16 | 17 | Everything discussed below. The progress for this lives in separate public but unstable branches until it achieves some initial stable alpha usability. Much of it is still in a creative phase, although it is starting to solidify. 18 | 19 | 20 | ### Bootstrapping 21 | When dynamic libraries are loaded, they often need a means to inject low-level configuration into the executing environment before they are made available. 22 | A simple example is in loading a library that requires a polyfill. Since the library is dynamic, the environment knows nothing about the library or the polyfill let alone how and where to load it. 23 | This requires modeling dynamic dependency graphs inside the executing environment. When the library is loaded, it needs to inject new configuration into features already running in the environment. 24 | Perhaps it appends further configuration into the active SystemJS instance. 25 | 26 | Status: The majority of a first workable version is in @sharpangles/platform-* in a WIP branch. 27 | 28 | ### Libraries and Packaging 29 | Libraries that participate in dynamic capabilities need to expose a variety of endpoints for various use-cases that contain the ability to configure the environment. 30 | Ideally, this means we can still package each use-case into a single minified build output. 31 | However, that output would need a way to inject features into the running environment prior to even being processed by a module loader. 32 | It also means that packaging requires a context for minification and tree-shaking. Libraries and Entrypoints provide the means to understand and describe this context. 33 | 34 | Status: Mostly contained in the 'libraries' feature inside platform-global in a WIP branch. 35 | It will rely on using only simple minifications for now, but ultimately extend into maintaining intermediate varable maps per context to permit cross-dynamic-library minification. 36 | 37 | ### DevOps 38 | Enabling complex scenarios like this require new approaches to devops. Dynamic libraries imply a larger ecosystem of code, with numerous overlapping development contexts and rapid switching between them. 39 | The number of potential develop, build and test scenarios grows exponentially, to the point they must be actively monitored. DevOps can no longer be an imperative sequence of command-line scripts. 40 | It needs to evolve into a declarative operational model of the system, whether that system is a developers machine, a development shop, a build cluster, or perhaps even the executing environment itself. 41 | It doesn't demonize imperative in favor of declarative, it simply separates those approaches into layers. 42 | In the end, the system should be as capable of managing an automated home full of IoT devices just as well as a developers machine. 43 | 44 | Status: @sharpangles/ops (still adapting from devops-build) is getting the majority of attention. 45 | -------------------------------------------------------------------------------- /modules/angular-dynamic/src/state_mapper_factory.ts: -------------------------------------------------------------------------------- 1 | import { Type, Injectable, Input, Injector } from '@angular/core'; 2 | import { Stateful } from './metadata'; 3 | import { StateChange } from './interfaces'; 4 | import { StateMapper } from './state_mapper'; 5 | import { AngularReflector } from './angular_reflector'; 6 | 7 | @Injectable() 8 | export class StateMapperFactory { 9 | private statefulsByType: Map, Stateful> = new Map, Stateful>(); 10 | 11 | get(typeOrFunc: Type) { 12 | let result = this.statefulsByType.get(typeOrFunc); 13 | if (!result) 14 | result = this.load(typeOrFunc); 15 | return result; 16 | } 17 | 18 | set(typeOrFunc: Type, loader: Stateful) { 19 | this.statefulsByType.set(typeOrFunc, loader); 20 | } 21 | 22 | private load(typeOrFunc: Type): Stateful { 23 | let stateful: Stateful = new AngularReflector().annotations(typeOrFunc).find(a => a.getState || a.setState); 24 | if (stateful) 25 | return stateful; 26 | stateful = this.createDefault(typeOrFunc); 27 | this.set(typeOrFunc, stateful); 28 | return stateful; 29 | } 30 | 31 | private stringMap(map: { [key: string]: any }, callback: /*(V, K) => void*/ Function) { 32 | for (let prop in map) { 33 | if (map.hasOwnProperty(prop)) 34 | callback(map[prop], prop); 35 | } 36 | } 37 | 38 | /** @todo Overrides for default getState implementation supporting ngrx state, deep-cloning, etc... */ 39 | protected createDefault(typeOrFunc: Type) { 40 | let statePairs: [string, string][] = []; 41 | let inputPairs: [string, string][] = []; 42 | let propertyMetadata = new AngularReflector().propMetadata(typeOrFunc); 43 | let controlProp: string; 44 | 45 | this.stringMap(propertyMetadata, (metadata: any[], propName: string) => { 46 | metadata.forEach(a => { 47 | if (a.type === 'State') 48 | statePairs.push([a.propertyName, a.bindingPropertyName || a.propertyName]); 49 | else if (a.type === 'Control') 50 | controlProp = a.propertyName; 51 | else if (a instanceof Input) { 52 | let statePropName = (a).bindingPropertyName ? (a).bindingPropertyName : propName; 53 | inputPairs.push([propName, statePropName]); 54 | } 55 | }); 56 | }); 57 | 58 | let pairs = statePairs.length > 0 ? statePairs : inputPairs; 59 | 60 | return { 61 | getState: (injector: Injector, component: any) => { 62 | if (component instanceof StateMapper) { 63 | let result = component.getState(); 64 | if (result != null) 65 | return result; 66 | } 67 | let state: { [key: string]: any } = {}; 68 | for (let pair of pairs) { 69 | let componentValue = component[pair[1]]; 70 | state[pair[0]] = componentValue; 71 | // state[pair[0]] = this._copier.deepCopy(componentValue); 72 | } 73 | return state; 74 | }, 75 | setState: function (injector: Injector, component: any, state: any): StateChange { 76 | if (component instanceof StateMapper) { 77 | let result = component.setState(state); 78 | if (result != null) 79 | return result; 80 | } 81 | for (let pair of pairs) { 82 | let stateValue = state ? state[pair[1]] : null; 83 | if (stateValue !== undefined) 84 | component[pair[0]] = stateValue; 85 | } 86 | let stateChanged: StateChange = { state: state }; 87 | if (controlProp) 88 | stateChanged.control = component[controlProp]; 89 | return stateChanged; 90 | } 91 | }; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /modules/angular-dynamic/src/directives/sa_lazy_component_outlet.ts: -------------------------------------------------------------------------------- 1 | import { Directive, Input, Output, OnChanges, EventEmitter, SimpleChanges, NgModuleFactory, ViewContainerRef, TemplateRef, Type, ComponentRef, EmbeddedViewRef, NgModuleRef, ComponentFactoryResolver, ReflectiveInjector, forwardRef, ChangeDetectorRef } from '@angular/core'; 2 | import { TypeReference } from '../interfaces'; 3 | import { ComponentTypeLoader } from '../component_type_loader'; 4 | 5 | /** 6 | * Dynamically loads a component based on a provided TypeReference. 7 | * Combines the benefits of ngTemplateOutlet and ngComponentOutlet while dynamically loading type metadata from state. 8 | * Additionally, componentRef is exposed for use with other directives, such as saStateOutlet. 9 | * ngComponentOutlet can also be used via pipes on a ComponentTypeLoader. 10 | * When a component is not loaded or set, either the provided template or ng-content is projected. 11 | */ 12 | @Directive({ 13 | selector: '[saLazyComponentOutlet]', 14 | exportAs: 'saLazyComponentOutlet' 15 | }) 16 | export class SaLazyComponentOutlet implements OnChanges { 17 | constructor(private viewContainer: ViewContainerRef, private componentTypeLoader: ComponentTypeLoader, private _template: TemplateRef, private cdr: ChangeDetectorRef) { 18 | } 19 | 20 | @Input() saLazyComponentOutlet: TypeReference; 21 | @Input() saLazyComponentOutletContext: Object; 22 | @Input() saLazyComponentOutletTemplate: TemplateRef; 23 | @Input() saLazyComponentOutletContent: any[][]; 24 | 25 | @Output() componentChanged = new EventEmitter(true); 26 | 27 | private _resolution: Promise<{ module: NgModuleFactory, component: Type }> | null; 28 | 29 | componentRef?: ComponentRef; 30 | moduleRef?: NgModuleRef; 31 | viewRef?: EmbeddedViewRef; 32 | 33 | private _moduleFactory: NgModuleFactory; 34 | 35 | ngOnChanges(changes: SimpleChanges): void { 36 | if (changes.saLazyComponentOutlet && changes.saLazyComponentOutlet.previousValue || !this.componentRef && changes.saLazyComponentOutletTemplate) 37 | this._clear(); 38 | if (!this.componentRef) 39 | this._enableTemplate(); 40 | if (changes.saLazyComponentOutlet && changes.saLazyComponentOutlet.currentValue) 41 | this._loadReference(); 42 | } 43 | 44 | private _loadReference() { 45 | let resolution = this.componentTypeLoader.resolveAsync(this.saLazyComponentOutlet); 46 | this._resolution = resolution; 47 | this._resolution.then(result => this._onComponentResolved(resolution, result.module, result.component)); 48 | } 49 | 50 | private _clear() { 51 | if (this.componentRef) { 52 | this.viewContainer.remove(this.viewContainer.indexOf(this.componentRef.hostView)); 53 | this.viewContainer.clear(); 54 | delete this.componentRef; 55 | this.componentChanged.emit(this); 56 | } 57 | if (this.viewRef) { 58 | this.viewContainer.remove(this.viewContainer.indexOf(this.viewRef)); 59 | delete this.viewRef; 60 | } 61 | this.cdr.markForCheck(); 62 | } 63 | 64 | private _enableTemplate() { 65 | if (this.viewRef) 66 | this.viewContainer.remove(this.viewContainer.indexOf(this.viewRef)); 67 | this.viewRef = this.viewContainer.createEmbeddedView(this.saLazyComponentOutletTemplate || this._template, this.saLazyComponentOutletContext); 68 | this.cdr.markForCheck(); 69 | } 70 | 71 | private _onComponentResolved(resolution: Promise, module: NgModuleFactory, component: Type) { 72 | if (this._resolution !== resolution) // Make sure a change didnt 'cancel' this. 73 | return; 74 | let injector = this.viewContainer.injector; 75 | if (this._moduleFactory !== module) { 76 | this._moduleFactory = module; 77 | if (this.moduleRef) 78 | this.moduleRef.destroy(); 79 | this.moduleRef = module ? module.create(injector) : undefined; 80 | } 81 | if (this.moduleRef) 82 | injector = this.moduleRef.injector; 83 | let componentFactory = injector.get(ComponentFactoryResolver).resolveComponentFactory(component); 84 | this._clear(); 85 | injector = ReflectiveInjector.resolveAndCreate([{ provide: forwardRef(() => SaLazyComponentOutlet), useValue: this }], injector); 86 | this.componentRef = this.viewContainer.createComponent(componentFactory, this.viewContainer.length, injector, this.saLazyComponentOutletContent); 87 | this.componentChanged.emit(this); 88 | this.cdr.markForCheck(); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /modules/angular-dynamic/spec/sa_state_outlet.spec.ts: -------------------------------------------------------------------------------- 1 | import { SaLazyComponentOutlet } from '../src/directives/sa_lazy_component_outlet'; 2 | import { StatefulModule } from '../src/stateful_module'; 3 | import { TypeReference, StateChange } from '../src/interfaces'; 4 | import { CommonJsNgModuleLoader } from './common_js_ng_module_factory_loader'; 5 | import { Component, ViewChild, NgModuleFactoryLoader } from '@angular/core'; 6 | import { FormGroup } from '@angular/forms'; 7 | import { async, TestBed } from '@angular/core/testing'; 8 | 9 | @Component({ 10 | selector: 'testComponent', 11 | template: ` 12 |
13 |

LazyComponentOutlet and StateOutlet with @Input mappings

14 | 15 |
Loading...
16 |
17 |
18 |
19 |

LazyComponentOutlet and StateOutlet with @State mappings (also sets componentRef explicitly)

20 | 21 |
Loading...
22 |
23 |
24 |
25 |

LazyComponentOutlet and StateOutlet with custom @Stateful

26 | 27 |
Loading...
28 |
29 |
30 |
31 |

LazyComponentOutlet and StateOutlet with inheritance

32 | 33 |
Loading...
34 |
35 |
36 |
37 |

LazyComponentOutlet and StateOutlet with forms, wired by event

38 | 39 |
Loading...
40 |
41 |
42 | ` 43 | }) 44 | class TestComponent { 45 | stateForInput = { someValue: 'stateForInput' }; 46 | stateForState = { someValue: 'stateForState' }; 47 | stateForStateful = { someValue: 'stateForStateful' }; 48 | stateForInheritance = { someValue: 'stateForInheritance' }; 49 | stateForForms = { someValue: 'stateForForms' }; 50 | 51 | @ViewChild('lazy1') lazyForInput: SaLazyComponentOutlet; 52 | @ViewChild('lazy2') lazyForState: SaLazyComponentOutlet; 53 | @ViewChild('lazy3') lazyForStateful: SaLazyComponentOutlet; 54 | @ViewChild('lazy4') lazyForInheritance: SaLazyComponentOutlet; 55 | @ViewChild('lazy5') lazyForForms: SaLazyComponentOutlet; 56 | 57 | widgetCustomizingStateProjectionReference = { moduleName: '../spec/widget_module#WidgetModule', componentTypeName: 'WidgetCustomizingStateProjection' }; 58 | widgetUsingStateDecoratorReference = { moduleName: '../spec/widget_module#WidgetModule', componentTypeName: 'WidgetUsingStateDecorator' }; 59 | widgetUsingInputDecoratorReference = { moduleName: '../spec/widget_module#WidgetModule', componentTypeName: 'WidgetUsingInputDecorator' }; 60 | widgetUsingInheritanceReference = { moduleName: '../spec/widget_module#WidgetModule', componentTypeName: 'WidgetUsingInheritance' }; 61 | widgetWithControlReference = { moduleName: '../spec/widget_module#WidgetModule', componentTypeName: 'WidgetWithControl' }; 62 | 63 | control: FormGroup = new FormGroup({ 64 | }); 65 | 66 | onStateSet(stateChange: StateChange) { 67 | if (stateChange.control) { 68 | this.control.addControl('child', stateChange.control); 69 | } 70 | } 71 | } 72 | 73 | describe('SaStateOutlet', () => { 74 | let moduleConfig = { 75 | declarations: [TestComponent], 76 | imports: [StatefulModule.forRoot()], 77 | entryComponents: [TestComponent], 78 | providers: [{ provide: NgModuleFactoryLoader, useClass: CommonJsNgModuleLoader }] 79 | }; 80 | let testComponent: TestComponent; 81 | beforeEach(async(async () => { 82 | TestBed.configureTestingModule(moduleConfig); 83 | await TestBed.compileComponents(); 84 | let fixture = TestBed.createComponent(TestComponent); 85 | fixture.detectChanges(); 86 | await fixture.whenStable(); 87 | fixture.detectChanges(); // 2 stages of changes, via componentRef. 88 | await fixture.whenStable(); 89 | testComponent = fixture.componentInstance; 90 | })); 91 | it('should set state using @Input()', () => { 92 | expect(testComponent.lazyForInput.componentRef && testComponent.lazyForInput.componentRef.instance.someValue).toBe('stateForInput'); 93 | }); 94 | it('should set state using @State()', () => { 95 | expect(testComponent.lazyForState.componentRef && testComponent.lazyForState.componentRef.instance.someValue).toBe('stateForState'); 96 | }); 97 | it('should set state using @Stateful()', () => { 98 | expect(testComponent.lazyForStateful.componentRef && testComponent.lazyForStateful.componentRef.instance.someValue).toBe('stateForStateful'.toUpperCase()); 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /modules/angular-dynamic/README.md: -------------------------------------------------------------------------------- 1 | # @sharpangles/angular-dynamic 2 | 3 | ## Dynamic Component Loading for Angular. 4 | 5 | # 6 | ### NOTE: This is just getting started and is not quite enough to take you to the ultimate dynamic dashboard without alot of work on your own. Several more pieces are on their way to being open sourced, including global bootstrapping of dependencies and packages (including global and module based), a layout mechanism that supports declarative and imperative manipulation through dynamic layout hierarchies, some love for ngrx (effects bootstrapping), a full sample, docs, etc... 7 | # 8 | 9 | While @angular/router works great for explicitly coded transitions, sometimes component hierarchies are determined from external state. 10 | The router does provide some mechanisms to dynamically create routes, lazy load modules, and map state, however sometimes you don't want to bring along history, urls, and knowledge on the child-side of its involvement in routing. 11 | Additionally, without lifecycle hooks during the loading process it is difficult to separate concerns across other hierarchical structures, such as forms. 12 | 13 | This library provides the directives needed to manage the process of loading a child component and mapping its hierarchical information. 14 | - saLazyComponentOutlet: A directive that loads dynamic child components by combining features from ngTemplateOutlet, ngComponentOutlet, and dynamic type resolution via ComponentTypeLoader. 15 | - saStateOutlet: A directive that projects state into dynamically loaded components. Typically used in conjunction with saLazyComponentOutlet. 16 | - saFormControl: A directive that separates concerns of control building from parent form membership and submission. 17 | 18 | Example usage with both dynamic loading and state projection: 19 | ```html 20 | 21 |
Loading...
22 |
23 | ``` 24 | 25 | - By default, state is mapped to @Inputs based on matching json properties. If @Input() is used with a parameter, that parameter determines the expected name of the json property. 26 | - When state needs to be decoupled from inputs, use the @State decorator instead. If any @State decorator is present, they will be used exclusively. 27 | - Additionally, a component can extend StateMapper to provide its own instance-level state mapping. 28 | - Finally, a component can have its mapping set directly by type on the StateMapperFactory. This is useful for third-party components with more complex mapping requirements. If the child is aware of being dynamic, it can use the @Stateful class decorator. 29 | 30 | ## Forms 31 | When composing a parent control from child controls the child should be responsible for its structure while the parent should be responsible for its place in the overall interaction scenario with the user. 32 | Take an address as an example. It should define a control containing the lines, city, state, and zip. Perhaps it even adds async validators to a postal API. 33 | However, it should not be responsible for determining the transactional role it plays in submission. 34 | Additionally, the parent is what knows if it is the home address, work address, or a member in a list of shipping addresses. 35 | This separation is supported across the dynamic boundary by the following: 36 | - A @Control() decorator that can be used on a property for the child component. 37 | - The SaFormControl directive, which can be useful even without dynamic loading (see [this proposal](https://github.com/angular/angular/issues/10195)). 38 | 39 | Example of saFormControl: 40 | ```html 41 |
42 |

43 |
44 | 45 | ``` 46 | 47 | ## Module loading 48 | The goal of this library is to provide the means to push new components to the client that could have been written at the time the user started navigating the site. 49 | It is common to have a shared kernel between the initially loaded modules and the dynamically injected ones, including angular at the very least. 50 | This library is agnostic of module loading techniques. However, currently there is no mechanism to create staged static compilations. 51 | For now this leads to less optimal minification. However, size is often a much lower priority than dynamic content for enterprise applications. 52 | Here are suggestions on module and minification scenarios: 53 | 54 | #### Use simple minifications (i.e. google closure compiler in simple mode) and ES6 module loading (either native or SystemJS). 55 | Take a scenario with a single static bundle and a single lazy loaded dynamic bundle which relies on sharing code with the static bundle. 56 | The static bundle cannot use advanced minification scenarios that rename public members used by the dynamic bundle. 57 | Additionally, the dynamic bundle should not include the shared code. One exception is AOT for shared components. 58 | The static bundle may only generate AOT output for what it uses. The dynamic bundle may use additional generated AOT. 59 | Since these per-directive AOT outputs are not easily separated between the static and dynamic, it is often simpler to live with some potential duplication. 60 | A module loader like systemJS will simply ignore the second generated output. 61 | 62 | #### Dynamic builds / hot module reloading in production. 63 | With this approach you can rebuild the entire library of all static and dynamic modules with every change, then reload the modules user-side. 64 | This approach allows you to produce more optimal builds with static analysis (i.e. webpack) at the cost of effectively reloading the user. 65 | If your scenario uses dynamic components targeting specific tenants or audiences, it may be less optimal unless you produce separate builds for each target audience. 66 | 67 | #### Raise community voice for minifiers to allow staged compilation. 68 | Minifiers that are deterministic (always produce the same output) could be provided an input configuration representing the previous build. 69 | Perhaps a better option would be to support importing mappings generated from a previous build, enabling incremental build scenarios. 70 | Since the first stage would not know about the second stage it would lead to slightly larger packages than static analysis alone, but they would be much smaller than simple minification scenarios. 71 | Google's closure compiler already has the ability to output mappings, but not yet to input them. 72 | Lets hope bundling moves with the ES6 evolution towards dynamic modules. 73 | -------------------------------------------------------------------------------- /modules/angular-dynamic/src/angular_reflector.ts: -------------------------------------------------------------------------------- 1 | import { Type, Injectable, TypeDecorator } from '@angular/core'; 2 | 3 | /** 4 | * Update: this is still here for ng4. Its better in ng5, so some of this goes away eventually. Adapted to support ng4 and 5. 5 | * 6 | * 7 | * Below is adapted from the angular reflector: https://github.com/angular/angular/blob/14fd78fd85b3dd230742eaaeab65f75226f37167/packages/core/src/reflection/reflection_capabilities.ts 8 | * This avoids any private references to the unexported reflector, at the cost of some duplication. 9 | * Although it doesn't involve a private reference, it still needs to be treated and maintained as such, since the inheritence behavior and tsickle decorator treatment is still an internal implementation. 10 | * 11 | * Items work reading or that could cause changes here: 12 | * https://github.com/google/closure-compiler/issues/2065 13 | * https://github.com/angular/angular/issues/14786 14 | * https://github.com/Microsoft/TypeScript/issues/11435 15 | * 16 | * Note: these changes would affect any library with decorators that want to use closure compiler, so a reflector like this would be needed even for non-angular cases. 17 | * Since this reflection stuff is affected by TS design issues (https://github.com/Microsoft/TypeScript/issues/11435), is useful for all custom decorators, etc... 18 | * it would be awesome if angular would expose this as part of the public API. 19 | * 20 | * @license 21 | * Copyright Google Inc. All Rights Reserved. 22 | * 23 | * Use of this source code is governed by an MIT-style license that can be 24 | * found in the LICENSE file at https://angular.io/license 25 | */ 26 | @Injectable() 27 | export class AngularReflector { 28 | private _ownAnnotations(typeOrFunc: Type, parentCtor: any): any[] | null { 29 | // Prefer the direct API. 30 | if ((typeOrFunc).annotations && (typeOrFunc).annotations !== parentCtor.annotations) { 31 | let annotations = (typeOrFunc).annotations; 32 | if (typeof annotations === 'function' && annotations.annotations) { 33 | annotations = annotations.annotations; 34 | } 35 | return annotations; 36 | } 37 | 38 | // API of tsickle for lowering decorators to properties on the class. 39 | if ((typeOrFunc).decorators && (typeOrFunc).decorators !== parentCtor.decorators) { 40 | return convertTsickleDecoratorIntoMetadata((typeOrFunc).decorators); 41 | } 42 | 43 | let ng5results: any[] | undefined; 44 | 45 | // NG5 API for metadata created by invoking the decorators. 46 | if (typeOrFunc.hasOwnProperty(ANNOTATIONS)) { 47 | ng5results = (typeOrFunc as any)[ANNOTATIONS]; 48 | } 49 | 50 | // NG4 API for metadata created by invoking the decorators. 51 | if (Reflect && (Reflect).getOwnMetadata) { 52 | const ng4Results = (Reflect).getOwnMetadata('annotations', typeOrFunc); 53 | return ng5results ? ng4Results ? ng5results.concat(ng4Results) : ng5results : ng4Results; 54 | } 55 | 56 | return null; 57 | } 58 | 59 | annotations(typeOrFunc: Type): any[] { 60 | if (typeof typeOrFunc !== 'function') { 61 | return []; 62 | } 63 | const parentCtor = getParentCtor(typeOrFunc); 64 | const ownAnnotations = this._ownAnnotations(typeOrFunc, parentCtor) || []; 65 | const parentAnnotations = parentCtor !== Object ? this.annotations(parentCtor) : []; 66 | return parentAnnotations.concat(ownAnnotations); 67 | } 68 | 69 | private _ownPropMetadata(typeOrFunc: any, parentCtor: any): { [key: string]: any[] } | null { 70 | // Prefer the direct API. 71 | if ((typeOrFunc).propMetadata && 72 | (typeOrFunc).propMetadata !== parentCtor.propMetadata) { 73 | let propMetadata = (typeOrFunc).propMetadata; 74 | if (typeof propMetadata === 'function' && propMetadata.propMetadata) { 75 | propMetadata = propMetadata.propMetadata; 76 | } 77 | return propMetadata; 78 | } 79 | 80 | 81 | // API of tsickle for lowering decorators to properties on the class. 82 | if ((typeOrFunc).propDecorators && 83 | (typeOrFunc).propDecorators !== parentCtor.propDecorators) { 84 | const propDecorators = (typeOrFunc).propDecorators; 85 | const propMetadata = <{ [key: string]: any[] }>{}; 86 | Object.keys(propDecorators).forEach(prop => { 87 | propMetadata[prop] = convertTsickleDecoratorIntoMetadata(propDecorators[prop]); 88 | }); 89 | return propMetadata; 90 | } 91 | 92 | let ng5results: any[] | undefined; 93 | 94 | // NG5 API for metadata created by invoking the decorators. 95 | if (typeOrFunc.hasOwnProperty(PROP_METADATA)) { 96 | ng5results = (typeOrFunc as any)[PROP_METADATA]; 97 | } 98 | 99 | // NG4 API for metadata created by invoking the decorators. 100 | if (Reflect && (Reflect).getOwnMetadata) { 101 | const ng4Results = (Reflect).getOwnMetadata('propMetadata', typeOrFunc); 102 | return ng5results ? ng4Results ? Object.assign({}, ng5results, ng4Results) : ng5results : ng4Results; 103 | } 104 | return null; 105 | } 106 | 107 | propMetadata(typeOrFunc: any): { [key: string]: any[] } { 108 | if (typeof typeOrFunc !== 'function') { 109 | return {}; 110 | } 111 | const parentCtor = getParentCtor(typeOrFunc); 112 | const propMetadata: { [key: string]: any[] } = {}; 113 | if (parentCtor !== Object) { 114 | const parentPropMetadata = this.propMetadata(parentCtor); 115 | Object.keys(parentPropMetadata).forEach((propName) => { 116 | propMetadata[propName] = parentPropMetadata[propName]; 117 | }); 118 | } 119 | const ownPropMetadata = this._ownPropMetadata(typeOrFunc, parentCtor); 120 | if (ownPropMetadata) { 121 | Object.keys(ownPropMetadata).forEach((propName) => { 122 | const decorators: any[] = []; 123 | if (propMetadata.hasOwnProperty(propName)) { 124 | decorators.push(...propMetadata[propName]); 125 | } 126 | decorators.push(...ownPropMetadata[propName]); 127 | propMetadata[propName] = decorators; 128 | }); 129 | } 130 | return propMetadata; 131 | } 132 | } 133 | 134 | function getParentCtor(ctor: Function): Type { 135 | const parentProto = Object.getPrototypeOf(ctor.prototype); 136 | const parentCtor = parentProto ? parentProto.constructor : null; 137 | // Note: We always use `Object` as the null value 138 | // to simplify checking later on. 139 | return parentCtor || Object; 140 | } 141 | 142 | function convertTsickleDecoratorIntoMetadata(decoratorInvocations: any[]): any[] { 143 | if (!decoratorInvocations) { 144 | return []; 145 | } 146 | return decoratorInvocations.map(decoratorInvocation => { 147 | const decoratorType = decoratorInvocation.type; 148 | const annotationCls = decoratorType.annotationCls; 149 | const annotationArgs = decoratorInvocation.args ? decoratorInvocation.args : []; 150 | return new annotationCls(...annotationArgs); 151 | }); 152 | } 153 | 154 | 155 | 156 | export const ANNOTATIONS = '__annotations__'; 157 | export const PARAMETERS = '__paramaters__'; 158 | export const PROP_METADATA = '__prop__metadata__'; 159 | 160 | /** 161 | * @suppress {globalThis} 162 | */ 163 | export function makeDecorator( 164 | name: string, props?: (...args: any[]) => any, parentClass?: any, 165 | chainFn?: (fn: Function) => void): 166 | { new(...args: any[]): any; (...args: any[]): any; (...args: any[]): (cls: any) => any; } { 167 | const metaCtor = makeMetadataCtor(props); 168 | 169 | function DecoratorFactory(objOrType: any): (cls: any) => any { 170 | if (this instanceof DecoratorFactory) { 171 | metaCtor.call(this, objOrType); 172 | return this; 173 | } 174 | 175 | const annotationInstance = new (DecoratorFactory)(objOrType); 176 | const TypeDecorator: TypeDecorator = function TypeDecorator(cls: Type) { 177 | // Use of Object.defineProperty is important since it creates non-enumerable property which 178 | // prevents the property is copied during subclassing. 179 | const annotations = cls.hasOwnProperty(ANNOTATIONS) ? 180 | (cls as any)[ANNOTATIONS] : 181 | Object.defineProperty(cls, ANNOTATIONS, { value: [] })[ANNOTATIONS]; 182 | annotations.push(annotationInstance); 183 | return cls; 184 | }; 185 | if (chainFn) chainFn(TypeDecorator); 186 | return TypeDecorator; 187 | } 188 | 189 | if (parentClass) { 190 | DecoratorFactory.prototype = Object.create(parentClass.prototype); 191 | } 192 | 193 | DecoratorFactory.prototype.ngMetadataName = name; 194 | (DecoratorFactory).annotationCls = DecoratorFactory; 195 | return DecoratorFactory as any; 196 | } 197 | 198 | function makeMetadataCtor(props?: (...args: any[]) => any): any { 199 | return function ctor(...args: any[]) { 200 | if (props) { 201 | const values = props(...args); 202 | for (const propName in values) { 203 | this[propName] = values[propName]; 204 | } 205 | } 206 | }; 207 | } 208 | 209 | export function makeParamDecorator( 210 | name: string, props?: (...args: any[]) => any, parentClass?: any): any { 211 | const metaCtor = makeMetadataCtor(props); 212 | function ParamDecoratorFactory(...args: any[]): any { 213 | if (this instanceof ParamDecoratorFactory) { 214 | metaCtor.apply(this, args); 215 | return this; 216 | } 217 | const annotationInstance = new (ParamDecoratorFactory)(...args); 218 | 219 | (ParamDecorator).annotation = annotationInstance; 220 | return ParamDecorator; 221 | 222 | function ParamDecorator(cls: any, unusedKey: any, index: number): any { 223 | // Use of Object.defineProperty is important since it creates non-enumerable property which 224 | // prevents the property is copied during subclassing. 225 | const parameters = cls.hasOwnProperty(PARAMETERS) ? 226 | (cls as any)[PARAMETERS] : 227 | Object.defineProperty(cls, PARAMETERS, { value: [] })[PARAMETERS]; 228 | 229 | // there might be gaps if some in between parameters do not have annotations. 230 | // we pad with nulls. 231 | while (parameters.length <= index) { 232 | parameters.push(null); 233 | } 234 | 235 | (parameters[index] = parameters[index] || []).push(annotationInstance); 236 | return cls; 237 | } 238 | } 239 | if (parentClass) { 240 | ParamDecoratorFactory.prototype = Object.create(parentClass.prototype); 241 | } 242 | ParamDecoratorFactory.prototype.ngMetadataName = name; 243 | (ParamDecoratorFactory).annotationCls = ParamDecoratorFactory; 244 | return ParamDecoratorFactory; 245 | } 246 | 247 | export function makePropDecorator( 248 | name: string, props?: (...args: any[]) => any, parentClass?: any): any { 249 | const metaCtor = makeMetadataCtor(props); 250 | 251 | function PropDecoratorFactory(...args: any[]): any { 252 | if (this instanceof PropDecoratorFactory) { 253 | metaCtor.apply(this, args); 254 | return this; 255 | } 256 | 257 | const decoratorInstance = new (PropDecoratorFactory)(...args); 258 | 259 | return function PropDecorator(target: any, name: string) { 260 | const constructor = target.constructor; 261 | // Use of Object.defineProperty is important since it creates non-enumerable property which 262 | // prevents the property is copied during subclassing. 263 | const meta = constructor.hasOwnProperty(PROP_METADATA) ? 264 | (constructor as any)[PROP_METADATA] : 265 | Object.defineProperty(constructor, PROP_METADATA, { value: {} })[PROP_METADATA]; 266 | meta[name] = meta.hasOwnProperty(name) && meta[name] || []; 267 | meta[name].unshift(decoratorInstance); 268 | }; 269 | } 270 | 271 | if (parentClass) { 272 | PropDecoratorFactory.prototype = Object.create(parentClass.prototype); 273 | } 274 | 275 | PropDecoratorFactory.prototype.ngMetadataName = name; 276 | (PropDecoratorFactory).annotationCls = PropDecoratorFactory; 277 | return PropDecoratorFactory; 278 | } 279 | --------------------------------------------------------------------------------