├── apps ├── .gitkeep ├── main-app │ ├── src │ │ ├── assets │ │ │ └── .gitkeep │ │ ├── app │ │ │ ├── pages │ │ │ │ └── simple-page │ │ │ │ │ ├── simple-page.component.scss │ │ │ │ │ ├── simple-page.component.html │ │ │ │ │ └── simple-page.component.ts │ │ │ ├── web-component-host │ │ │ │ ├── web-component-host.component.scss │ │ │ │ ├── web-component-host.component.html │ │ │ │ ├── web-component-host.module.ts │ │ │ │ └── web-component-host.component.ts │ │ │ ├── main-app.component.ts │ │ │ ├── main-app.component.scss │ │ │ ├── main-app.component.html │ │ │ └── app.module.ts │ │ ├── styles.scss │ │ ├── favicon.ico │ │ ├── environments │ │ │ ├── environment.prod.ts │ │ │ └── environment.ts │ │ ├── test-setup.ts │ │ ├── index.html │ │ ├── main.ts │ │ └── polyfills.ts │ ├── tsconfig.editor.json │ ├── tsconfig.app.json │ ├── tsconfig.spec.json │ ├── tsconfig.json │ ├── jest.config.js │ ├── .browserslistrc │ └── .eslintrc.json └── wc-app │ ├── src │ ├── assets │ │ └── .gitkeep │ ├── app │ │ ├── pages │ │ │ ├── first │ │ │ │ ├── first.component.scss │ │ │ │ ├── first.component.html │ │ │ │ ├── first.component.ts │ │ │ │ └── first.component.spec.ts │ │ │ ├── home │ │ │ │ ├── home.component.scss │ │ │ │ ├── home.component.html │ │ │ │ ├── home.component.ts │ │ │ │ └── home.component.spec.ts │ │ │ └── second │ │ │ │ ├── second.component.scss │ │ │ │ ├── second.component.html │ │ │ │ ├── second.component.ts │ │ │ │ └── second.component.spec.ts │ │ ├── components │ │ │ └── links │ │ │ │ ├── links.component.scss │ │ │ │ ├── links.component.html │ │ │ │ ├── links.component.ts │ │ │ │ └── links.component.spec.ts │ │ ├── wc-app.component.scss │ │ ├── wc-app.component.html │ │ ├── app-routing.module.ts │ │ ├── app.module.ts │ │ ├── wc-app.component.ts │ │ └── utils │ │ │ ├── noop-location.strategy.ts │ │ │ └── multi-location.strategy.ts │ ├── test-setup.ts │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── styles.scss │ ├── favicon.ico │ ├── index.html │ ├── main.ts │ └── polyfills.ts │ ├── tsconfig.editor.json │ ├── tsconfig.app.json │ ├── tsconfig.spec.json │ ├── tsconfig.json │ ├── .browserslistrc │ ├── jest.config.js │ └── .eslintrc.json ├── libs └── .gitkeep ├── tools ├── generators │ └── .gitkeep └── tsconfig.tools.json ├── .prettierrc ├── .prettierignore ├── jest.preset.js ├── jest.config.js ├── .vscode └── extensions.json ├── .editorconfig ├── tsconfig.base.json ├── .gitignore ├── .eslintrc.json ├── nx.json ├── README.md ├── decorate-angular-cli.js ├── package.json └── angular.json /apps/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /libs/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tools/generators/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/main-app/src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/wc-app/src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } -------------------------------------------------------------------------------- /apps/wc-app/src/app/pages/first/first.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/wc-app/src/app/pages/home/home.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/wc-app/src/app/components/links/links.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/wc-app/src/app/pages/second/second.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/wc-app/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular'; 2 | -------------------------------------------------------------------------------- /apps/main-app/src/app/pages/simple-page/simple-page.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/wc-app/src/app/pages/home/home.component.html: -------------------------------------------------------------------------------- 1 |

Home Page

2 | -------------------------------------------------------------------------------- /apps/wc-app/src/app/pages/first/first.component.html: -------------------------------------------------------------------------------- 1 |

First Page

2 | -------------------------------------------------------------------------------- /apps/wc-app/src/app/pages/second/second.component.html: -------------------------------------------------------------------------------- 1 |

Second Page

2 | -------------------------------------------------------------------------------- /apps/main-app/src/app/pages/simple-page/simple-page.component.html: -------------------------------------------------------------------------------- 1 |

Simple Page

2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Add files here to ignore them from prettier formatting 2 | 3 | /dist 4 | /coverage 5 | -------------------------------------------------------------------------------- /jest.preset.js: -------------------------------------------------------------------------------- 1 | const nxPreset = require('@nrwl/jest/preset'); 2 | 3 | module.exports = { ...nxPreset }; 4 | -------------------------------------------------------------------------------- /apps/main-app/src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /apps/wc-app/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | }; 4 | -------------------------------------------------------------------------------- /apps/wc-app/src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /apps/wc-app/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcellkiss/angular-web-component-example/HEAD/apps/wc-app/src/favicon.ico -------------------------------------------------------------------------------- /apps/main-app/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcellkiss/angular-web-component-example/HEAD/apps/main-app/src/favicon.ico -------------------------------------------------------------------------------- /apps/main-app/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | wcAppScriptPath: '/wc-app/main.js', 4 | }; 5 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const { getJestProjects } = require('@nrwl/jest'); 2 | 3 | module.exports = { projects: [...getJestProjects(), '/apps/wc-app'] }; 4 | -------------------------------------------------------------------------------- /apps/main-app/src/app/web-component-host/web-component-host.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | button { 3 | height: 30px; 4 | padding: 0 10px; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /apps/main-app/tsconfig.editor.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["**/*.ts"], 4 | "compilerOptions": { 5 | "types": ["jest", "node"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /apps/wc-app/tsconfig.editor.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["**/*.ts"], 4 | "compilerOptions": { 5 | "types": ["jest", "node"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /apps/wc-app/src/app/components/links/links.component.html: -------------------------------------------------------------------------------- 1 | Links: home page | 2 | first page | 3 | second page 4 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "angular.ng-template", 4 | "nrwl.angular-console", 5 | "esbenp.prettier-vscode", 6 | "firsttris.vscode-jest-runner" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /apps/main-app/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": [] 6 | }, 7 | "files": ["src/main.ts", "src/polyfills.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /apps/wc-app/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": [] 6 | }, 7 | "files": ["src/main.ts", "src/polyfills.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /apps/wc-app/src/app/components/links/links.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'wc-app-links', 5 | templateUrl: './links.component.html', 6 | styleUrls: ['./links.component.scss'], 7 | }) 8 | export class LinksComponent {} 9 | -------------------------------------------------------------------------------- /apps/wc-app/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "files": ["src/test-setup.ts"], 9 | "include": ["**/*.spec.ts", "**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /apps/main-app/src/app/main-app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'main-app', 5 | templateUrl: './main-app.component.html', 6 | styleUrls: ['./main-app.component.scss'], 7 | }) 8 | export class MainAppComponent { 9 | title = 'main-app'; 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /apps/main-app/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "files": ["src/test-setup.ts"], 9 | "include": ["**/*.spec.ts", "**/*.test.ts", "**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /tools/tsconfig.tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "../dist/out-tsc/tools", 5 | "rootDir": ".", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": ["node"], 9 | "importHelpers": false 10 | }, 11 | "include": ["**/*.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /apps/main-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.app.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | }, 12 | { 13 | "path": "./tsconfig.editor.json" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /apps/wc-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.app.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | }, 12 | { 13 | "path": "./tsconfig.editor.json" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /apps/wc-app/src/app/pages/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'wc-app-home', 5 | templateUrl: './home.component.html', 6 | styleUrls: ['./home.component.scss'] 7 | }) 8 | export class HomeComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit(): void { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /apps/main-app/src/app/main-app.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: block; 3 | padding: 20px; 4 | font-family: Arial, Helvetica, sans-serif; 5 | 6 | span.hl { 7 | font-style: italic; 8 | color: darkred; 9 | } 10 | 11 | .router-container { 12 | border: 1px solid green; 13 | // background-color: rgba(0, 255, 0, 0.1); 14 | padding: 10px; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /apps/wc-app/src/app/pages/first/first.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'wc-app-first', 5 | templateUrl: './first.component.html', 6 | styleUrls: ['./first.component.scss'] 7 | }) 8 | export class FirstComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit(): void { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /apps/wc-app/src/app/pages/second/second.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'wc-app-second', 5 | templateUrl: './second.component.html', 6 | styleUrls: ['./second.component.scss'] 7 | }) 8 | export class SecondComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit(): void { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /apps/wc-app/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | WcApp 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /apps/main-app/src/app/pages/simple-page/simple-page.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'main-app-simple-page', 5 | templateUrl: './simple-page.component.html', 6 | styleUrls: ['./simple-page.component.scss'], 7 | }) 8 | export class SimplePageComponent implements OnInit { 9 | constructor() {} 10 | 11 | ngOnInit(): void {} 12 | 13 | ngAfterViewInit(): void {} 14 | } 15 | -------------------------------------------------------------------------------- /apps/wc-app/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | import { WcAppModule } from './app/app.module'; 4 | import { environment } from './environments/environment'; 5 | 6 | if (environment.production) { 7 | enableProdMode(); 8 | } 9 | 10 | platformBrowserDynamic() 11 | .bootstrapModule(WcAppModule) 12 | .catch((err) => console.error(`Error in wc-app`, err)); 13 | -------------------------------------------------------------------------------- /apps/main-app/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular/setup-jest'; 2 | 3 | import { getTestBed } from '@angular/core/testing'; 4 | import { 5 | BrowserDynamicTestingModule, 6 | platformBrowserDynamicTesting, 7 | } from '@angular/platform-browser-dynamic/testing'; 8 | 9 | getTestBed().resetTestEnvironment(); 10 | getTestBed().initTestEnvironment( 11 | BrowserDynamicTestingModule, 12 | platformBrowserDynamicTesting(), 13 | { teardown: { destroyAfterEach: false } } 14 | ); 15 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "importHelpers": true, 11 | "target": "es2015", 12 | "module": "esnext", 13 | "lib": ["es2017", "dom"], 14 | "skipLibCheck": true, 15 | "skipDefaultLibCheck": true, 16 | "baseUrl": ".", 17 | "paths": {} 18 | }, 19 | "exclude": ["node_modules", "tmp"] 20 | } 21 | -------------------------------------------------------------------------------- /apps/main-app/src/app/web-component-host/web-component-host.component.html: -------------------------------------------------------------------------------- 1 |

Web Component Host Page

2 | 3 | This is the Web Component: 4 |
5 | 11 | 12 |
13 |

@Input

14 | 17 | 18 |

@Output

19 |

20 | Message from WebComponent: {{ outputMessage }} 21 |

22 | -------------------------------------------------------------------------------- /apps/main-app/src/app/web-component-host/web-component-host.module.ts: -------------------------------------------------------------------------------- 1 | import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { WebComponentHostComponent } from './web-component-host.component'; 4 | 5 | const routes: Routes = [ 6 | { 7 | path: '**', 8 | component: WebComponentHostComponent, 9 | }, 10 | ]; 11 | 12 | @NgModule({ 13 | declarations: [WebComponentHostComponent], 14 | imports: [RouterModule.forChild(routes)], 15 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 16 | }) 17 | export class WebComponentHostModule {} 18 | -------------------------------------------------------------------------------- /apps/main-app/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | MainApp 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /apps/main-app/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | import { AppModule } from './app/app.module'; 4 | import { environment } from './environments/environment'; 5 | 6 | if (environment.production) { 7 | enableProdMode(); 8 | } 9 | 10 | // Include Web Component script 11 | const scriptEl = document.createElement('script'); 12 | scriptEl.src = environment.wcAppScriptPath; 13 | document.head.appendChild(scriptEl); 14 | 15 | platformBrowserDynamic() 16 | .bootstrapModule(AppModule) 17 | .catch((err) => console.error(err)); 18 | -------------------------------------------------------------------------------- /apps/wc-app/src/app/wc-app.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | border: 1px solid rgba(0, 255, 255, 1); 3 | background-color: rgba(0, 255, 255, 0.1); 4 | padding: 20px; 5 | display: block; 6 | font-family: Arial, Helvetica, sans-serif; 7 | 8 | input { 9 | height: 30px; 10 | width: 300px; 11 | border-radius: 5px; 12 | padding: 0 10px; 13 | border: 1px solid gray; 14 | } 15 | 16 | button { 17 | height: 30px; 18 | padding: 0 10px; 19 | } 20 | 21 | .router-outlet-container { 22 | padding: 10px; 23 | border: 1px solid blue; 24 | background-color: rgba(0, 0, 255, 0.1); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /apps/main-app/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'main-app', 3 | preset: '../../jest.preset.js', 4 | setupFilesAfterEnv: ['/src/test-setup.ts'], 5 | globals: { 6 | 'ts-jest': { 7 | stringifyContentPathRegex: '\\.(html|svg)$', 8 | 9 | tsconfig: '/tsconfig.spec.json', 10 | }, 11 | }, 12 | coverageDirectory: '../../coverage/apps/main-app', 13 | snapshotSerializers: [ 14 | 'jest-preset-angular/build/serializers/no-ng-attributes', 15 | 'jest-preset-angular/build/serializers/ng-snapshot', 16 | 'jest-preset-angular/build/serializers/html-comment', 17 | ], 18 | transform: { '^.+\\.(ts|js|html)$': 'jest-preset-angular' }, 19 | }; 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | /node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.angular/cache 29 | /.sass-cache 30 | /connect.lock 31 | /coverage 32 | /libpeerconnection.log 33 | npm-debug.log 34 | yarn-error.log 35 | testem.log 36 | /typings 37 | 38 | # System Files 39 | .DS_Store 40 | Thumbs.db 41 | -------------------------------------------------------------------------------- /apps/wc-app/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false, 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /apps/wc-app/src/app/pages/home/home.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { HomeComponent } from './home.component'; 4 | 5 | describe('HomeComponent', () => { 6 | let component: HomeComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ HomeComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(HomeComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /apps/wc-app/src/app/pages/first/first.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { FirstComponent } from './first.component'; 4 | 5 | describe('FirstComponent', () => { 6 | let component: FirstComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ FirstComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(FirstComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /apps/wc-app/src/app/components/links/links.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { LinksComponent } from './links.component'; 4 | 5 | describe('LinksComponent', () => { 6 | let component: LinksComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ LinksComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(LinksComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /apps/wc-app/src/app/pages/second/second.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SecondComponent } from './second.component'; 4 | 5 | describe('SecondComponent', () => { 6 | let component: SecondComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ SecondComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(SecondComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /apps/main-app/.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # For the full list of supported browsers by the Angular framework, please see: 6 | # https://angular.io/guide/browser-support 7 | 8 | # You can see what browsers were selected by your queries by running: 9 | # npx browserslist 10 | 11 | last 1 Chrome version 12 | last 1 Firefox version 13 | last 2 Edge major versions 14 | last 2 Safari major versions 15 | last 2 iOS major versions 16 | Firefox ESR 17 | not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line. 18 | -------------------------------------------------------------------------------- /apps/wc-app/.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # For the full list of supported browsers by the Angular framework, please see: 6 | # https://angular.io/guide/browser-support 7 | 8 | # You can see what browsers were selected by your queries by running: 9 | # npx browserslist 10 | 11 | last 1 Chrome version 12 | last 1 Firefox version 13 | last 2 Edge major versions 14 | last 2 Safari major versions 15 | last 2 iOS major versions 16 | Firefox ESR 17 | not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line. 18 | -------------------------------------------------------------------------------- /apps/wc-app/src/app/wc-app.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Web Component App

3 | 4 | Routing in Web Component App: 5 |
6 | 7 | 8 |
9 | 10 |
11 | 12 |
13 |
14 |
15 |
16 |

@Input

17 | 18 | message from host: {{ message }} 19 | 20 |
21 |
22 |

@Output

23 | 24 |
25 |
26 | 27 |
28 | 29 |
30 |
31 |
32 |
33 | -------------------------------------------------------------------------------- /apps/main-app/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false, 7 | wcAppScriptPath: 'http://localhost:4210/main.js', 8 | }; 9 | 10 | /* 11 | * For easier debugging in development mode, you can import the following file 12 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 13 | * 14 | * This import should be commented out in production mode because it will have a negative impact 15 | * on performance if an error is thrown. 16 | */ 17 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 18 | -------------------------------------------------------------------------------- /apps/wc-app/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'wc-app', 3 | preset: '../../jest.preset.js', 4 | setupFilesAfterEnv: ['/src/test-setup.ts'], 5 | globals: { 6 | 'ts-jest': { 7 | tsConfig: '/tsconfig.spec.json', 8 | stringifyContentPathRegex: '\\.(html|svg)$', 9 | astTransformers: { 10 | before: [ 11 | 'jest-preset-angular/build/InlineFilesTransformer', 12 | 'jest-preset-angular/build/StripStylesTransformer', 13 | ], 14 | }, 15 | }, 16 | }, 17 | coverageDirectory: '../../coverage/apps/wc-app', 18 | snapshotSerializers: [ 19 | 'jest-preset-angular/build/AngularNoNgAttributesSnapshotSerializer.js', 20 | 'jest-preset-angular/build/AngularSnapshotSerializer.js', 21 | 'jest-preset-angular/build/HTMLCommentSerializer.js', 22 | ], 23 | }; 24 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["**/*"], 4 | "plugins": ["@nrwl/nx"], 5 | "overrides": [ 6 | { 7 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 8 | "rules": { 9 | "@nrwl/nx/enforce-module-boundaries": [ 10 | "error", 11 | { 12 | "enforceBuildableLibDependency": true, 13 | "allow": [], 14 | "depConstraints": [ 15 | { 16 | "sourceTag": "*", 17 | "onlyDependOnLibsWithTags": ["*"] 18 | } 19 | ] 20 | } 21 | ] 22 | } 23 | }, 24 | { 25 | "files": ["*.ts", "*.tsx"], 26 | "extends": ["plugin:@nrwl/nx/typescript"], 27 | "rules": {} 28 | }, 29 | { 30 | "files": ["*.js", "*.jsx"], 31 | "extends": ["plugin:@nrwl/nx/javascript"], 32 | "rules": {} 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /apps/main-app/src/app/main-app.component.html: -------------------------------------------------------------------------------- 1 |

Main App

2 | 3 |

4 | This is the index.html file of the main-app, which 5 | renders main-app's 6 | AppComponent. 7 |

8 | 9 |

10 | This AppComponent renders a 11 | router-outlet, where the default route is the 12 | WebComponentHost 13 |

14 | 15 |

16 | WebComponentHost renders the 17 | wc-app as a Web Component `<wc-app>` 20 |

21 | 22 |
23 | Routing in Main App: 24 |
25 | Links: 26 | Web Component Host page | 27 | Simple page 28 |
29 |
30 | 31 |
32 | -------------------------------------------------------------------------------- /apps/wc-app/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "extends": [ 8 | "plugin:@nrwl/nx/angular", 9 | "plugin:@angular-eslint/template/process-inline-templates" 10 | ], 11 | "rules": { 12 | "@angular-eslint/directive-selector": [ 13 | "error", 14 | { 15 | "type": "attribute", 16 | "prefix": "wcApp", 17 | "style": "camelCase" 18 | } 19 | ], 20 | "@angular-eslint/component-selector": [ 21 | "error", 22 | { 23 | "type": "element", 24 | "prefix": "wc-app", 25 | "style": "kebab-case" 26 | } 27 | ] 28 | } 29 | }, 30 | { 31 | "files": ["*.html"], 32 | "extends": ["plugin:@nrwl/nx/angular-template"], 33 | "rules": {} 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /apps/main-app/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "extends": [ 8 | "plugin:@nrwl/nx/angular", 9 | "plugin:@angular-eslint/template/process-inline-templates" 10 | ], 11 | "rules": { 12 | "@angular-eslint/directive-selector": [ 13 | "error", 14 | { 15 | "type": "attribute", 16 | "prefix": "mainApp", 17 | "style": "camelCase" 18 | } 19 | ], 20 | "@angular-eslint/component-selector": [ 21 | "error", 22 | { 23 | "type": "element", 24 | "prefix": "main-app", 25 | "style": "kebab-case" 26 | } 27 | ] 28 | } 29 | }, 30 | { 31 | "files": ["*.html"], 32 | "extends": ["plugin:@nrwl/nx/angular-template"], 33 | "rules": {} 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /apps/wc-app/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { LocationStrategy } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { RouterModule, Routes } from '@angular/router'; 4 | import { FirstComponent } from './pages/first/first.component'; 5 | import { HomeComponent } from './pages/home/home.component'; 6 | import { SecondComponent } from './pages/second/second.component'; 7 | import { NoopLocationStrategy } from './utils/noop-location.strategy'; 8 | 9 | const routes: Routes = [ 10 | { 11 | path: '', 12 | component: HomeComponent, 13 | }, 14 | { 15 | path: 'first', 16 | component: FirstComponent, 17 | }, 18 | { 19 | path: 'second', 20 | component: SecondComponent, 21 | }, 22 | ]; 23 | 24 | @NgModule({ 25 | imports: [ 26 | RouterModule.forRoot(routes, { 27 | // enableTracing: true, 28 | }), 29 | ], 30 | providers: [{ provide: LocationStrategy, useClass: NoopLocationStrategy }], 31 | exports: [RouterModule], 32 | }) 33 | export class AppRoutingModule {} 34 | -------------------------------------------------------------------------------- /apps/main-app/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { RouterModule } from '@angular/router'; 4 | import { MainAppComponent } from './main-app.component'; 5 | import { SimplePageComponent } from './pages/simple-page/simple-page.component'; 6 | 7 | @NgModule({ 8 | declarations: [MainAppComponent], 9 | imports: [ 10 | BrowserModule, 11 | RouterModule.forRoot( 12 | [ 13 | { 14 | path: 'web-component-host', 15 | loadChildren: () => 16 | import('./web-component-host/web-component-host.module').then( 17 | (m) => m.WebComponentHostModule 18 | ), 19 | }, 20 | { 21 | path: 'simple-page', 22 | component: SimplePageComponent, 23 | }, 24 | { path: '**', redirectTo: '/web-component-host' }, 25 | ], 26 | { initialNavigation: 'enabled' } 27 | ), 28 | ], 29 | providers: [], 30 | bootstrap: [MainAppComponent], 31 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 32 | }) 33 | export class AppModule {} 34 | -------------------------------------------------------------------------------- /apps/wc-app/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Injector, NgModule } from '@angular/core'; 2 | import { createCustomElement } from '@angular/elements'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { BrowserModule } from '@angular/platform-browser'; 5 | import { AppRoutingModule } from './app-routing.module'; 6 | import { LinksComponent } from './components/links/links.component'; 7 | import { FirstComponent } from './pages/first/first.component'; 8 | import { HomeComponent } from './pages/home/home.component'; 9 | import { SecondComponent } from './pages/second/second.component'; 10 | import { WcAppComponent } from './wc-app.component'; 11 | 12 | @NgModule({ 13 | declarations: [ 14 | WcAppComponent, 15 | HomeComponent, 16 | FirstComponent, 17 | SecondComponent, 18 | LinksComponent, 19 | ], 20 | imports: [BrowserModule, AppRoutingModule, FormsModule], 21 | providers: [], 22 | // bootstrap: [AppComponent], 23 | }) 24 | export class WcAppModule { 25 | constructor(private injector: Injector) { 26 | // Create custom element 27 | const AppElement = createCustomElement(WcAppComponent, { injector }); 28 | // Register the custom element with the browser. 29 | customElements.define('wc-app', AppElement); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "implicitDependencies": { 3 | "package.json": { 4 | "dependencies": "*", 5 | "devDependencies": "*" 6 | }, 7 | ".eslintrc.json": "*" 8 | }, 9 | "affected": { 10 | "defaultBase": "master" 11 | }, 12 | "npmScope": "playground-nx-wc-app", 13 | "tasksRunnerOptions": { 14 | "default": { 15 | "runner": "@nrwl/workspace/tasks-runners/default", 16 | "options": { 17 | "cacheableOperations": ["build", "lint", "test", "e2e"], 18 | "parallel": 1 19 | } 20 | } 21 | }, 22 | "targetDependencies": { 23 | "build": [ 24 | { 25 | "target": "build", 26 | "projects": "dependencies" 27 | } 28 | ] 29 | }, 30 | "cli": { 31 | "defaultCollection": "@nrwl/angular" 32 | }, 33 | "defaultProject": "main-app", 34 | "generators": { 35 | "@nrwl/angular": { 36 | "application": { 37 | "linter": "eslint" 38 | }, 39 | "library": { 40 | "linter": "eslint" 41 | }, 42 | "storybook-configuration": { 43 | "linter": "eslint" 44 | } 45 | }, 46 | "@nrwl/angular:application": { 47 | "style": "scss", 48 | "linter": "eslint", 49 | "unitTestRunner": "jest", 50 | "e2eTestRunner": "cypress" 51 | }, 52 | "@nrwl/angular:library": { 53 | "style": "scss", 54 | "linter": "eslint", 55 | "unitTestRunner": "jest" 56 | }, 57 | "@nrwl/angular:component": { 58 | "style": "scss" 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular Web Component Example - nx app 2 | 3 | The goal of this project is to showcase an nx workspace, where the main-app (`main-app`) uses a web-component, which is the result of the build process of another app (`wc-app`) 4 | 5 | ## Getting started 6 | 7 | Run `npx nx serve main-app` and `npx nx serve wc-app` 8 | 9 | Playground app will run on `port 4200` and wc-app will run on `port 4210`. 10 | 11 | Open `http://localhost:4200` in the browser and have a look at it. 12 | 13 | Note: main-app has to be manually refreshed in the browser if a change is made to wc-app. 14 | 15 | ## Build process 16 | 17 | The build process of the `wc-app` uses an npm plugin called `ngx-build-plus` to extend the default behaviour. 18 | 19 | We use two flags of the plugin: 20 | 21 | - singleBuild 22 | - keepStyles 23 | 24 | You can find both of these settings in the `angular.json` file. 25 | 26 | ## Useful links 27 | 28 | - [Multiple Angular Apps on a single page](https://medium.com/swlh/multiple-angular-apps-on-a-single-page-9f49bc863177) 29 | - [ngx-build-plus](https://github.com/manfredsteyer/ngx-build-plus) 30 | - [Official Angular Elements Docs](https://angular.io/guide/elements) 31 | - [Building Web Components with Angular](https://buddy.works/tutorials/building-web-components-with-angular#installing-setting-up-angular-elements) 32 | - [Routing artcile](https://medium.com/@timon.grassl/how-to-use-routing-in-angular-web-components-c6a76449cdb) 33 | - [ngx routing plugin](https://www.npmjs.com/package/ngx-elements-router) 34 | - [Routing within a Web Component](https://github.com/fboeller/ngx-elements-router) 35 | 36 | ## Feedback 37 | 38 | Did I forget something? Drop a PR or a message: [marcellkiss@budacode.com](mailto:marcellkiss@budacode.com) 39 | -------------------------------------------------------------------------------- /apps/wc-app/src/app/wc-app.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | EventEmitter, 4 | Input, 5 | OnChanges, 6 | OnInit, 7 | Output, 8 | SimpleChanges, 9 | } from '@angular/core'; 10 | import { Router, RoutesRecognized } from '@angular/router'; 11 | 12 | @Component({ 13 | selector: 'wc-app', 14 | templateUrl: './wc-app.component.html', 15 | styleUrls: ['./wc-app.component.scss'], 16 | }) 17 | export class WcAppComponent implements OnInit, OnChanges { 18 | // Routing i/o 19 | @Input() route: string; 20 | @Output() routeChange: EventEmitter = new EventEmitter(); 21 | 22 | // Test i/o 23 | @Input() message: string; 24 | @Output() submitEvent: EventEmitter = new EventEmitter(); 25 | 26 | public inputModel: string; 27 | 28 | constructor(private router: Router) {} 29 | 30 | ngOnInit() { 31 | this.forwardRouteChanges(); 32 | } 33 | 34 | ngOnChanges(changes: SimpleChanges): void { 35 | console.log(`ngOnChanges in Web Component`); 36 | 37 | // React on route changes 38 | if (changes['route']) { 39 | console.log( 40 | `>>> [WebComponent]: Incoming route change: ${changes['route'].currentValue}` 41 | ); 42 | this.router.navigateByUrl(changes['route'].currentValue, { 43 | state: { fromPlatform: true }, 44 | }); 45 | } 46 | } 47 | 48 | private forwardRouteChanges() { 49 | this.router.events.subscribe((event) => { 50 | if ( 51 | event instanceof RoutesRecognized && 52 | (!this.isFromPlatform() || this.isRedirect(event)) 53 | ) { 54 | console.log( 55 | `>>> [WebComponent]: Outgoing route change: ${event.urlAfterRedirects}` 56 | ); 57 | this.routeChange.next(event.urlAfterRedirects); 58 | } 59 | }); 60 | } 61 | 62 | public onSubmit() { 63 | this.submitEvent.next(this.inputModel); 64 | } 65 | 66 | isFromPlatform(): boolean { 67 | return this.router.getCurrentNavigation()?.extras?.state?.fromPlatform; 68 | } 69 | 70 | isRedirect(event: RoutesRecognized): boolean { 71 | return event.url !== event.urlAfterRedirects; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /apps/main-app/src/app/web-component-host/web-component-host.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnDestroy, OnInit } from '@angular/core'; 2 | import { ActivatedRoute, Router } from '@angular/router'; 3 | import { Subscription } from 'rxjs'; 4 | 5 | @Component({ 6 | selector: 'main-app-web-component-host', 7 | templateUrl: './web-component-host.component.html', 8 | styleUrls: ['./web-component-host.component.scss'], 9 | }) 10 | export class WebComponentHostComponent implements OnInit, OnDestroy { 11 | public message = ''; 12 | public outputMessage = ''; 13 | public routeInput: string; 14 | private subs: Subscription[] = []; 15 | private WEB_COMPONENT_BASE_URL = '/web-component-host'; 16 | 17 | constructor(private route: ActivatedRoute, private router: Router) {} 18 | 19 | ngOnInit(): void { 20 | this.syncRouteInput(); 21 | } 22 | 23 | ngOnDestroy(): void { 24 | this.subs.forEach((sub) => sub.unsubscribe()); 25 | } 26 | 27 | private syncRouteInput() { 28 | const routeSub = this.route.url.subscribe((url) => { 29 | // Pass on just the relative part, not the whole 30 | this.routeInput = this.router.url.split(this.WEB_COMPONENT_BASE_URL)[1]; 31 | }); 32 | 33 | this.subs.push(routeSub); 34 | } 35 | 36 | public onSubmitEvent(submitValue: string) { 37 | console.log({ submitValue }); 38 | this.outputMessage = `${submitValue}`; 39 | } 40 | 41 | public onRouteChange(relativeRoute: string): void { 42 | console.log(`>>> [Platform]: new route arrived`, relativeRoute); 43 | if (relativeRoute && relativeRoute.startsWith('/')) { 44 | this.router.navigateByUrl(this.WEB_COMPONENT_BASE_URL + relativeRoute, { 45 | replaceUrl: true, //event.replaceUrl || false, 46 | }); 47 | } else { 48 | console.warn(`Invalid router event.`, event); 49 | } 50 | } 51 | 52 | public onChangeMessage() { 53 | const words = [ 54 | 'Hallo Welt!', 55 | 'Moin Welt!', 56 | 'Grüß Gott Welt!', 57 | 'Szia Világ!', 58 | 'Hello World!', 59 | 'Hola Mundo!', 60 | 'Bonjour le monde!', 61 | 'Ciao mondo!', 62 | ]; 63 | const randomIndex = Math.floor(Math.random() * words.length); 64 | this.message = `${words[randomIndex]}`; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /apps/wc-app/src/app/utils/noop-location.strategy.ts: -------------------------------------------------------------------------------- 1 | import { 2 | APP_BASE_HREF, 3 | LocationChangeListener, 4 | LocationStrategy, 5 | PlatformLocation, 6 | } from '@angular/common'; 7 | import { Inject, Injectable, Optional } from '@angular/core'; 8 | 9 | /** 10 | * Acts as a noop location strategy that does not modify the browser url. 11 | * Should be used for a RouterModule in a micro frontend. 12 | * That way, the RouterModule of the platform is in charge of modifying the browser url. 13 | * 14 | * ``` 15 | * imports: [ 16 | * RouterModule.forRoot([ 17 | * { path: 'my-route', component: SomeComponent }, 18 | * { path: '**', component: NoComponent } 19 | * ]) 20 | * ], 21 | * providers: [ 22 | * { provide: LocationStrategy, useClass: NoopLocationStrategy }, 23 | * ] 24 | * ``` 25 | */ 26 | @Injectable() 27 | export class NoopLocationStrategy extends LocationStrategy { 28 | private readonly baseHref: string; 29 | 30 | constructor( 31 | private platformLocation: PlatformLocation, 32 | @Optional() @Inject(APP_BASE_HREF) baseHref?: string 33 | ) { 34 | super(); 35 | this.baseHref = baseHref || this.platformLocation.getBaseHrefFromDOM(); 36 | if (!this.baseHref) { 37 | throw new Error( 38 | `No base href set. Please provide a value for the APP_BASE_HREF token or add a base element to the document.` 39 | ); 40 | } 41 | } 42 | 43 | onPopState(_fn: LocationChangeListener): void {} 44 | 45 | getBaseHref(): string { 46 | return this.baseHref; 47 | } 48 | 49 | path(_includeHash: boolean = false): string { 50 | return ''; 51 | } 52 | 53 | prepareExternalUrl(internal: string): string { 54 | if (this.baseHref.endsWith('/') && internal.startsWith('/')) { 55 | return this.baseHref.substring(0, this.baseHref.length - 1) + internal; 56 | } else if (this.baseHref.endsWith('/') || internal.startsWith('/')) { 57 | return this.baseHref + internal; 58 | } else { 59 | return `${this.baseHref}/${internal}`; 60 | } 61 | } 62 | 63 | pushState( 64 | _state: any, 65 | _title: string, 66 | _path: string, 67 | _queryParams: string 68 | ): void {} 69 | 70 | replaceState( 71 | _state: any, 72 | _title: string, 73 | _path: string, 74 | _queryParams: string 75 | ): void {} 76 | 77 | forward(): void {} 78 | 79 | back(): void {} 80 | } 81 | -------------------------------------------------------------------------------- /apps/wc-app/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** 22 | * By default, zone.js will patch all possible macroTask and DomEvents 23 | * user can disable parts of macroTask/DomEvents patch by setting following flags 24 | * because those flags need to be set before `zone.js` being loaded, and webpack 25 | * will put import in the top of bundle, so user need to create a separate file 26 | * in this directory (for example: zone-flags.ts), and put the following flags 27 | * into that file, and then add the following code before importing zone.js. 28 | * import './zone-flags'; 29 | * 30 | * The flags allowed in zone-flags.ts are listed here. 31 | * 32 | * The following flags will work for all browsers. 33 | * 34 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 35 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 36 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 37 | * 38 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 39 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 40 | * 41 | * (window as any).__Zone_enable_cross_context_check = true; 42 | * 43 | */ 44 | 45 | /*************************************************************************************************** 46 | * Zone JS is required by default for Angular itself. 47 | */ 48 | import 'zone.js/dist/zone'; // Included with Angular CLI. 49 | 50 | /*************************************************************************************************** 51 | * APPLICATION IMPORTS 52 | */ 53 | -------------------------------------------------------------------------------- /apps/main-app/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** 22 | * By default, zone.js will patch all possible macroTask and DomEvents 23 | * user can disable parts of macroTask/DomEvents patch by setting following flags 24 | * because those flags need to be set before `zone.js` being loaded, and webpack 25 | * will put import in the top of bundle, so user need to create a separate file 26 | * in this directory (for example: zone-flags.ts), and put the following flags 27 | * into that file, and then add the following code before importing zone.js. 28 | * import './zone-flags'; 29 | * 30 | * The flags allowed in zone-flags.ts are listed here. 31 | * 32 | * The following flags will work for all browsers. 33 | * 34 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 35 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 36 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 37 | * 38 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 39 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 40 | * 41 | * (window as any).__Zone_enable_cross_context_check = true; 42 | * 43 | */ 44 | 45 | /*************************************************************************************************** 46 | * Zone JS is required by default for Angular itself. 47 | */ 48 | import 'zone.js/dist/zone'; // Included with Angular CLI. 49 | 50 | /*************************************************************************************************** 51 | * APPLICATION IMPORTS 52 | */ 53 | -------------------------------------------------------------------------------- /decorate-angular-cli.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file decorates the Angular CLI with the Nx CLI to enable features such as computation caching 3 | * and faster execution of tasks. 4 | * 5 | * It does this by: 6 | * 7 | * - Patching the Angular CLI to warn you in case you accidentally use the undecorated ng command. 8 | * - Symlinking the ng to nx command, so all commands run through the Nx CLI 9 | * - Updating the package.json postinstall script to give you control over this script 10 | * 11 | * The Nx CLI decorates the Angular CLI, so the Nx CLI is fully compatible with it. 12 | * Every command you run should work the same when using the Nx CLI, except faster. 13 | * 14 | * Because of symlinking you can still type `ng build/test/lint` in the terminal. The ng command, in this case, 15 | * will point to nx, which will perform optimizations before invoking ng. So the Angular CLI is always invoked. 16 | * The Nx CLI simply does some optimizations before invoking the Angular CLI. 17 | * 18 | * To opt out of this patch: 19 | * - Replace occurrences of nx with ng in your package.json 20 | * - Remove the script from your postinstall script in your package.json 21 | * - Delete and reinstall your node_modules 22 | */ 23 | 24 | const fs = require('fs'); 25 | const os = require('os'); 26 | const cp = require('child_process'); 27 | const isWindows = os.platform() === 'win32'; 28 | let output; 29 | try { 30 | output = require('@nrwl/workspace').output; 31 | } catch (e) { 32 | console.warn('Angular CLI could not be decorated to enable computation caching. Please ensure @nrwl/workspace is installed.'); 33 | process.exit(0); 34 | } 35 | 36 | /** 37 | * Symlink of ng to nx, so you can keep using `ng build/test/lint` and still 38 | * invoke the Nx CLI and get the benefits of computation caching. 39 | */ 40 | function symlinkNgCLItoNxCLI() { 41 | try { 42 | const ngPath = './node_modules/.bin/ng'; 43 | const nxPath = './node_modules/.bin/nx'; 44 | if (isWindows) { 45 | /** 46 | * This is the most reliable way to create symlink-like behavior on Windows. 47 | * Such that it works in all shells and works with npx. 48 | */ 49 | ['', '.cmd', '.ps1'].forEach(ext => { 50 | if (fs.existsSync(nxPath + ext)) fs.writeFileSync(ngPath + ext, fs.readFileSync(nxPath + ext)); 51 | }); 52 | } else { 53 | // If unix-based, symlink 54 | cp.execSync(`ln -sf ./nx ${ngPath}`); 55 | } 56 | } 57 | catch(e) { 58 | output.error({ title: 'Unable to create a symlink from the Angular CLI to the Nx CLI:' + e.message }); 59 | throw e; 60 | } 61 | } 62 | 63 | try { 64 | symlinkNgCLItoNxCLI(); 65 | require('@nrwl/cli/lib/decorate-cli').decorateCli(); 66 | output.log({ title: 'Angular CLI has been decorated to enable computation caching.' }); 67 | } catch(e) { 68 | output.error({ title: 'Decoration of the Angular CLI did not complete successfully' }); 69 | } 70 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground-nx-wc-app", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "ng": "nx", 7 | "postinstall": "node ./decorate-angular-cli.js && ngcc --properties es2015 browser module main", 8 | "nx": "nx", 9 | "start": "run-p start:main start:wc", 10 | "start:main": "ng serve main-app", 11 | "start:wc": "ng serve wc-app", 12 | "build:main": "npx nx run main-app:build:production", 13 | "build:wc": "npx nx run wc-app:build:production", 14 | "build": "npx nx run main-app:build:production && npx nx run wc-app:build:production", 15 | "test": "ng test", 16 | "lint": "nx workspace-lint && ng lint", 17 | "e2e": "ng e2e", 18 | "affected:apps": "nx affected:apps", 19 | "affected:libs": "nx affected:libs", 20 | "affected:build": "nx affected:build", 21 | "affected:e2e": "nx affected:e2e", 22 | "affected:test": "nx affected:test", 23 | "affected:lint": "nx affected:lint", 24 | "affected:dep-graph": "nx affected:dep-graph", 25 | "affected": "nx affected", 26 | "format": "nx format:write", 27 | "format:write": "nx format:write", 28 | "format:check": "nx format:check", 29 | "update": "nx migrate latest", 30 | "workspace-generator": "nx workspace-generator", 31 | "dep-graph": "nx dep-graph", 32 | "help": "nx help" 33 | }, 34 | "private": true, 35 | "dependencies": { 36 | "@angular/animations": "13.1.2", 37 | "@angular/common": "13.1.2", 38 | "@angular/compiler": "13.1.2", 39 | "@angular/core": "13.1.2", 40 | "@angular/elements": "13.1.2", 41 | "@angular/forms": "13.1.2", 42 | "@angular/platform-browser": "13.1.2", 43 | "@angular/platform-browser-dynamic": "13.1.2", 44 | "@angular/router": "13.1.2", 45 | "@nrwl/angular": "13.4.4", 46 | "ngx-build-plus": "^13.0.1", 47 | "rxjs": "~7.5.2", 48 | "tslib": "^2.3.1", 49 | "zone.js": "0.11.4" 50 | }, 51 | "devDependencies": { 52 | "@angular-devkit/build-angular": "13.1.3", 53 | "@angular-eslint/eslint-plugin": "13.0.1", 54 | "@angular-eslint/eslint-plugin-template": "13.0.1", 55 | "@angular-eslint/template-parser": "13.0.1", 56 | "@angular/cli": "13.1.3", 57 | "@angular/compiler-cli": "13.1.2", 58 | "@angular/language-service": "13.1.2", 59 | "@nrwl/cli": "13.4.4", 60 | "@nrwl/cypress": "13.4.4", 61 | "@nrwl/eslint-plugin-nx": "13.4.4", 62 | "@nrwl/jest": "13.4.4", 63 | "@nrwl/linter": "13.4.4", 64 | "@nrwl/tao": "13.4.4", 65 | "@nrwl/workspace": "13.4.4", 66 | "@types/jest": "27.4.0", 67 | "@types/node": "17.0.8", 68 | "@typescript-eslint/eslint-plugin": "~5.3.0", 69 | "@typescript-eslint/parser": "~5.3.0", 70 | "cypress": "^9.2.1", 71 | "dotenv": "11.0.0", 72 | "eslint": "8.6.0", 73 | "eslint-config-prettier": "8.3.0", 74 | "eslint-plugin-cypress": "^2.12.1", 75 | "jest": "27.4.7", 76 | "jest-preset-angular": "11.0.1", 77 | "ng-packagr": "13.1.3", 78 | "npm-run-all": "^4.1.5", 79 | "postcss": "^8.3.9", 80 | "postcss-import": "^14.0.2", 81 | "postcss-preset-env": "^6.7.0", 82 | "postcss-url": "^10.1.1", 83 | "prettier": "2.5.1", 84 | "ts-jest": "27.1.2", 85 | "ts-node": "~10.4.0", 86 | "typescript": "4.5.4" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /apps/wc-app/src/app/utils/multi-location.strategy.ts: -------------------------------------------------------------------------------- 1 | import { 2 | APP_BASE_HREF, 3 | Location, 4 | LocationChangeListener, 5 | LocationStrategy, 6 | PlatformLocation, 7 | } from '@angular/common'; 8 | import { Inject, Injectable, Optional } from '@angular/core'; 9 | 10 | // TODO: Fix location change on navigation arrow click, increased navigationId and routing depth > 1 11 | @Injectable() 12 | export class MultiLocationStrategy extends LocationStrategy { 13 | private _baseHref: string; 14 | 15 | constructor( 16 | private _platformLocation: PlatformLocation, 17 | @Optional() @Inject(APP_BASE_HREF) href?: string 18 | ) { 19 | super(); 20 | 21 | if (href == null) { 22 | href = this._platformLocation.getBaseHrefFromDOM(); 23 | } 24 | 25 | if (href == null) { 26 | throw new Error( 27 | `No base href set. Please provide a value for the APP_BASE_HREF token or add a base element to the document.` 28 | ); 29 | } 30 | 31 | this._baseHref = href; 32 | } 33 | 34 | onPopState(fn: LocationChangeListener): void { 35 | this._platformLocation.onPopState(fn); 36 | this._platformLocation.onHashChange(fn); 37 | } 38 | 39 | getBaseHref(): string { 40 | return this._baseHref; 41 | } 42 | 43 | prepareExternalUrl(internal: string, state?: any): string { 44 | return this.checkPathAndInternal(this.path(), internal, state); 45 | } 46 | 47 | checkPathAndInternal(path: string, internal: string, state: any): string { 48 | const openParenthesisIndex = internal.indexOf('('); 49 | const closedParenthesisIndex = internal.indexOf(')', openParenthesisIndex); 50 | const outlet = internal.substring( 51 | openParenthesisIndex, 52 | closedParenthesisIndex + 1 53 | ); 54 | 55 | //Get navigationId from RouterState 56 | let navId = 0; 57 | if (state) { 58 | navId = state.navigationId; 59 | } 60 | 61 | // add internal route to base href 62 | if (path.startsWith('/') && path.length === 1) { 63 | return internal; 64 | } 65 | 66 | if (path.includes('(')) { 67 | //There's an outlet in the path => remove if necessary or on initial navigation 68 | return navId === -1 || navId === 1 69 | ? path 70 | : path.replace(/ *\([^)]*\) */g, ''); 71 | } 72 | 73 | if (path === internal) { 74 | //don't append to prevent dublicated path e.g. /foo/foo 75 | return path; 76 | } 77 | 78 | if (outlet.length < 1) { 79 | return internal; 80 | } else { 81 | // append internal url to path 82 | return path + outlet; 83 | } 84 | } 85 | 86 | path(includeHash: boolean = false): string { 87 | const pathname = 88 | this._platformLocation.pathname + 89 | Location.normalizeQueryParams(this._platformLocation.search); 90 | const hash = this._platformLocation.hash; 91 | return hash && includeHash ? `${pathname}${hash}` : pathname; 92 | } 93 | 94 | pushState(state: any, title: string, url: string, queryParams: string) { 95 | const externalUrl = this.prepareExternalUrl( 96 | url + Location.normalizeQueryParams(queryParams), 97 | state 98 | ); 99 | this._platformLocation.pushState(state, title, externalUrl); 100 | } 101 | 102 | replaceState(state: any, title: string, url: string, queryParams: string) { 103 | const externalUrl = this.prepareExternalUrl( 104 | url + Location.normalizeQueryParams(queryParams), 105 | state 106 | ); 107 | this._platformLocation.replaceState(state, title, externalUrl); 108 | } 109 | 110 | forward(): void { 111 | this._platformLocation.forward(); 112 | } 113 | 114 | back(): void { 115 | this._platformLocation.back(); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "projects": { 4 | "main-app": { 5 | "projectType": "application", 6 | "root": "apps/main-app", 7 | "sourceRoot": "apps/main-app/src", 8 | "prefix": "main-app", 9 | "architect": { 10 | "build": { 11 | "builder": "@angular-devkit/build-angular:browser", 12 | "options": { 13 | "outputPath": "dist/apps/main-app", 14 | "index": "apps/main-app/src/index.html", 15 | "main": "apps/main-app/src/main.ts", 16 | "polyfills": "apps/main-app/src/polyfills.ts", 17 | "tsConfig": "apps/main-app/tsconfig.app.json", 18 | "aot": true, 19 | "assets": [ 20 | "apps/main-app/src/favicon.ico", 21 | "apps/main-app/src/assets" 22 | ], 23 | "styles": ["apps/main-app/src/styles.scss"], 24 | "scripts": [] 25 | }, 26 | "configurations": { 27 | "production": { 28 | "fileReplacements": [ 29 | { 30 | "replace": "apps/main-app/src/environments/environment.ts", 31 | "with": "apps/main-app/src/environments/environment.prod.ts" 32 | } 33 | ], 34 | "optimization": true, 35 | "outputHashing": "all", 36 | "sourceMap": false, 37 | "namedChunks": false, 38 | "extractLicenses": true, 39 | "vendorChunk": false, 40 | "buildOptimizer": true, 41 | "budgets": [ 42 | { 43 | "type": "initial", 44 | "maximumWarning": "2mb", 45 | "maximumError": "5mb" 46 | }, 47 | { 48 | "type": "anyComponentStyle", 49 | "maximumWarning": "6kb", 50 | "maximumError": "10kb" 51 | } 52 | ] 53 | } 54 | } 55 | }, 56 | "serve": { 57 | "builder": "@angular-devkit/build-angular:dev-server", 58 | "options": { 59 | "browserTarget": "main-app:build", 60 | "liveReload": false 61 | }, 62 | "configurations": { 63 | "production": { 64 | "browserTarget": "main-app:build:production" 65 | } 66 | } 67 | }, 68 | "extract-i18n": { 69 | "builder": "@angular-devkit/build-angular:extract-i18n", 70 | "options": { 71 | "browserTarget": "main-app:build" 72 | } 73 | }, 74 | "lint": { 75 | "builder": "@nrwl/linter:eslint", 76 | "options": { 77 | "lintFilePatterns": [ 78 | "apps/main-app/src/**/*.ts", 79 | "apps/main-app/src/**/*.html" 80 | ] 81 | }, 82 | "outputs": ["{options.outputFile}"] 83 | }, 84 | "test": { 85 | "builder": "@nrwl/jest:jest", 86 | "outputs": ["coverage/apps/main-app"], 87 | "options": { 88 | "jestConfig": "apps/main-app/jest.config.js", 89 | "passWithNoTests": true 90 | } 91 | } 92 | }, 93 | "tags": [] 94 | }, 95 | "wc-app": { 96 | "projectType": "application", 97 | "root": "apps/wc-app", 98 | "sourceRoot": "apps/wc-app/src", 99 | "prefix": "wc-app", 100 | "architect": { 101 | "build": { 102 | "builder": "ngx-build-plus:browser", 103 | "options": { 104 | "singleBundle": true, 105 | "outputPath": "dist/apps/main-app/wc-app", 106 | "index": "apps/wc-app/src/index.html", 107 | "main": "apps/wc-app/src/main.ts", 108 | "polyfills": "apps/wc-app/src/polyfills.ts", 109 | "tsConfig": "apps/wc-app/tsconfig.app.json", 110 | "aot": true, 111 | "assets": ["apps/wc-app/src/favicon.ico", "apps/wc-app/src/assets"], 112 | "styles": ["apps/wc-app/src/styles.scss"], 113 | "scripts": [] 114 | }, 115 | "configurations": { 116 | "production": { 117 | "fileReplacements": [ 118 | { 119 | "replace": "apps/wc-app/src/environments/environment.ts", 120 | "with": "apps/wc-app/src/environments/environment.prod.ts" 121 | } 122 | ], 123 | "optimization": true, 124 | "outputHashing": "none", 125 | "sourceMap": false, 126 | "namedChunks": false, 127 | "extractLicenses": true, 128 | "vendorChunk": false, 129 | "buildOptimizer": true, 130 | "budgets": [ 131 | { 132 | "type": "initial", 133 | "maximumWarning": "2mb", 134 | "maximumError": "5mb" 135 | }, 136 | { 137 | "type": "anyComponentStyle", 138 | "maximumWarning": "6kb", 139 | "maximumError": "10kb" 140 | } 141 | ] 142 | } 143 | } 144 | }, 145 | "serve": { 146 | "builder": "ngx-build-plus:dev-server", 147 | "options": { 148 | "singleBundle": true, 149 | "browserTarget": "wc-app:build", 150 | "port": 4210 151 | }, 152 | "configurations": { 153 | "production": { 154 | "browserTarget": "wc-app:build:production" 155 | } 156 | } 157 | }, 158 | "extract-i18n": { 159 | "builder": "@angular-devkit/build-angular:extract-i18n", 160 | "options": { 161 | "browserTarget": "wc-app:build" 162 | } 163 | }, 164 | "lint": { 165 | "builder": "@nrwl/linter:eslint", 166 | "options": { 167 | "lintFilePatterns": [ 168 | "apps/wc-app/src/**/*.ts", 169 | "apps/wc-app/src/**/*.html" 170 | ] 171 | }, 172 | "outputs": ["{options.outputFile}"] 173 | }, 174 | "test": { 175 | "builder": "@nrwl/jest:jest", 176 | "outputs": ["coverage/apps/wc-app"], 177 | "options": { 178 | "jestConfig": "apps/wc-app/jest.config.js", 179 | "passWithNoTests": true 180 | } 181 | } 182 | }, 183 | "tags": [] 184 | } 185 | } 186 | } 187 | --------------------------------------------------------------------------------