├── .editorconfig ├── .gitignore ├── README.md ├── angular.json ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.e2e.json ├── package-lock.json ├── package.json ├── src ├── app │ ├── _guards │ │ ├── app-specific.can-activate.guard.ts │ │ └── index.ts │ ├── app.component.css │ ├── app.component.html │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── app.routes.ts │ ├── feature-one │ │ ├── _guards │ │ │ ├── feature-specific.can-activate.guard.ts │ │ │ └── index.ts │ │ ├── feature-one.component.css │ │ ├── feature-one.component.html │ │ ├── feature-one.component.spec.ts │ │ ├── feature-one.component.ts │ │ ├── feature-one.module.ts │ │ └── feature-one.routes.ts │ └── feature-two │ │ ├── feature-two.component.css │ │ ├── feature-two.component.html │ │ ├── feature-two.component.spec.ts │ │ ├── feature-two.component.ts │ │ ├── feature-two.module.ts │ │ └── feature-two.routes.ts ├── assets │ └── .gitkeep ├── browserslist ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── karma.conf.js ├── main.ts ├── polyfills.ts ├── styles.css ├── test.ts ├── tsconfig.app.json ├── tsconfig.spec.json └── tslint.json ├── tsconfig.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events.json 15 | speed-measure-plugin.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | yarn-error.log 41 | testem.log 42 | /typings 43 | 44 | # System Files 45 | .DS_Store 46 | Thumbs.db 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Ultimate Courses 2 | 3 | ![](https://wesleygrimes.com/assets/post_headers/routing.jpg) 4 | 5 | ## Before We Get Started 6 | 7 | This article is not intended to be a tutorial on routing in Angular. If you are new to Routing in Angular then I highly recommend you check out one of the the following resources: 8 | 9 | - [Ultimate Courses](https://bit.ly/2WubqhW) 10 | - [Official Angular Docs](https://angular.io/guide/router) 11 | 12 | ## Background 13 | 14 | The following represents a pattern that I've developed at my day job after building several enterprise Angular applications. While most online tutorials do a great job laying out the fundamentals, I had a hard time locating articles that showed recommended conventions and patterns for large and scalable applications. 15 | 16 | With this pattern you should have a clean and concise organization for all routing related concerns in your applications. 17 | 18 | ## Prerequisites 19 | 20 | For context, this article assumes you are using the following version of Angular: 21 | 22 | - Angular v7.2.6 23 | 24 | --- 25 | 26 | ## Best Practice #1 - Create a top-level Routes array file 27 | 28 | > The official [Angular docs recommend](https://angular.io/guide/router#refactor-the-routing-configuration-into-a-routing-module) creating a full-blown `app-routing.module.ts` for your top-level routing. I have found this extra layer to be unnecessary in most cases. 29 | 30 | > HOT TIP: Only register top-level routes here, if you plan to implement feature modules, then the child routes would live underneath the respective `feature.routes.ts` file. We want to keep this top-level routes file as clean as possible and follow the component tree structure. 31 | 32 | Let's go with the following approach: 33 | 34 | 1. Create a new file named `app.routes.ts` in the root `src/app` directory. This file will hold our top-level `Routes` array. We will come back later throughout the article and fill this in. For now, let's scaffold it with the following contents: 35 | 36 | ```typescript 37 | import { Routes } from '@angular/router'; 38 | 39 | export const AppRoutes: Routes = []; 40 | ``` 41 | 42 | 2. Register `AppRoutes` in the `app.module.ts` file. 43 | 44 | - Import `AppRoutes` from `app.routes.ts`. 45 | - Import `RouterModule` from `@angular/router`. 46 | - Add `RouterModule.forRoot(AppRoutes)` to your `imports` array 47 | 48 | Your updated `app.module.ts` will look similar to the following: 49 | 50 | ```typescript 51 | import { NgModule } from '@angular/core'; 52 | import { BrowserModule } from '@angular/platform-browser'; 53 | import { RouterModule } from '@angular/router'; 54 | import { AppComponent } from './app.component'; 55 | import { AppRoutes } from './app.routes'; 56 | 57 | @NgModule({ 58 | declarations: [AppComponent], 59 | imports: [BrowserModule, RouterModule.forRoot(AppRoutes)], 60 | providers: [], 61 | bootstrap: [AppComponent] 62 | }) 63 | export class AppModule {} 64 | ``` 65 | 66 | ## Best Practice #2 - Create a feature-level Routes array file 67 | 68 | In similar fashion to how we constructed the `app.routes.ts` we will create a `feature.routes.ts` to list out the individual routes for this feature module. We want to keep our routes as close to the source as possible. This will be in keeping with a clean code approach, and having a good separation of concerns. 69 | 70 | 1. Create a new file named `feature/feature.routes.ts` where `feature` matches the name of your `feature.module.ts` prefix. This file will hold our feature-level `Routes` array. Keeping in mind that you would replace `Feature` with the actual name of your module, let's scaffold it with the following contents: 71 | 72 | ```typescript 73 | import { Routes } from '@angular/router'; 74 | 75 | export const FeatureRoutes: Routes = []; 76 | ``` 77 | 78 | 2. Register `FeatureRoutes` in the `feature/feature.module.ts` file. We will make use of the `RouterModule.forChild` import so that these routes are automatically registered with lazy loading. 79 | 80 | - Import `FeatureRoutes` from `feature.routes.ts`. 81 | - Import `RouterModule` from `@angular/router`. 82 | - Add `RouterModule.forChild(FeatureRoutes)` to your `imports` array 83 | 84 | Your updated `feature/feature.module.ts` will look similar to the following: 85 | 86 | ```typescript 87 | import { CommonModule } from '@angular/common'; 88 | import { NgModule } from '@angular/core'; 89 | import { RouterModule } from '@angular/router'; 90 | import { FeatureRoutes } from './feature.routes'; 91 | 92 | @NgModule({ 93 | declarations: [], 94 | imports: [CommonModule, RouterModule.forChild(FeatureRoutes)] 95 | }) 96 | export class FeatureModule {} 97 | ``` 98 | 99 | An example of a `feature.routes.ts` file with child route(s) may look like the following: 100 | 101 | ```typescript 102 | import { Routes } from '@angular/router'; 103 | import { FeatureOneComponent } from './feature-one.component'; 104 | import { FeatureSpecificCanActivateGuard } from './_guards'; 105 | 106 | export const FeatureOneRoutes: Routes = [ 107 | { 108 | path: '', 109 | pathMatch: 'full', 110 | redirectTo: 'feature-one-component' 111 | }, 112 | { 113 | path: 'feature-one-component', 114 | component: FeatureOneComponent, 115 | canActivate: [FeatureSpecificCanActivateGuard] 116 | } 117 | ]; 118 | ``` 119 | 120 | ## Best Practice #3 - Add Lazy Loaded Features to top-level Routes file 121 | 122 | > Lazy loading is the concept of deferring load of code assets (javascript, styles) until the user actually needs to utilize the resources. This can bring large performance increases to perceived load times of your application as the entire code set doesn't have to download on first paint. 123 | 124 | > Angular provides a nice way to handle this with the `loadChildren` option for a given route. More information can be found in the [official Angular docs](https://angular.io/guide/router#lazy-loading-route-configuration). 125 | 126 | Once you've created your `app.routes.ts` and `*.routes.ts` files, you need to register any feature modules that you want to load lazily. 127 | 128 | ### Per Feature Module... 129 | 130 | Update the `AppRoutes` array in the `app.routes.ts` file to include a new route the feature: 131 | 132 | ```typescript 133 | import { Routes } from '@angular/router'; 134 | 135 | export const AppRoutes: Routes = [ 136 | { 137 | path: 'feature', 138 | loadChildren: './feature/feature.module#FeatureModule' 139 | } 140 | ]; 141 | ``` 142 | 143 | By adding the above route to the array, when the user requests `/feature` in the browser, Angular lazy loads the module using the path given and then automatically registers any routes defined in the `feature.routes.ts` `FeatureRoutes` array using the `RouterModule.forChild` import. 144 | 145 | For each additional feature module, you would add another item to the `AppRoutes` array. If you have multiple features, it might look something like the following: 146 | 147 | ```typescript 148 | import { Routes } from '@angular/router'; 149 | 150 | export const AppRoutes: Routes = [ 151 | { 152 | path: '', 153 | pathMatch: 'full', 154 | redirectTo: 'feature-one' 155 | }, 156 | { 157 | path: 'feature-one', 158 | loadChildren: './feature-one/feature-one.module#FeatureOneModule' 159 | }, 160 | { 161 | path: 'feature-two', 162 | loadChildren: './feature-two/feature-two.module#FeatureTwoModule' 163 | } 164 | ]; 165 | ``` 166 | 167 | ## Best Practice #4 - Keep Router Guards Organized 168 | 169 | Here are a few tips to keep your router guards organized. These are just guidelines, but I have found them to be very helpful. 170 | 171 | ### Name Your Guards Well 172 | 173 | Guards should use the following naming convention: 174 | 175 | - File Name: `name.function.guard.ts` 176 | - Class Name: `NameFunctionGuard` 177 | 178 | Each part being identified as: 179 | 180 | - `name` - this is the name of your guard. What are you guarding against? 181 | - `function` - this is the function your guard will be attached to. Angular supports `CanActivate`, `CanActivateChild`, `CanDeactivate`, and `Resolve`. 182 | 183 | An example of an Auth Guard that is attached to the `CanActivate` function would be named as follows: 184 | 185 | - File Name: `auth.can-activate.guard` 186 | - Class Name: `AuthCanActivateGuard` 187 | 188 | ### Group under `_guards` folder 189 | 190 | Organize all top-level guards under a folder named `src/app/_guards`. I have seen apps dump guards in the top level directory and this is messy, especially if you end up with more than a few guards. 191 | 192 | ### Use Barrel Exports 193 | 194 | > The jury is still out on whether or not using barrel exports is officially considered a "best practice" or even supported by the Angular style guide. However, I am a big fan of the clean organization this provides. This method is offered as a suggestion. 195 | 196 | Make sure that `src/app/_guards` has a nice and clean `index.ts` barrel export. Barrel exports are simply `index.ts` files that group together and export all public files from a directory. An example is as follows: 197 | 198 | ```typescript 199 | export * from './auth.can-activate.guard'; 200 | export * from './require-save.can-deactivate.guard'; 201 | ``` 202 | 203 | Without Barrel Exporting: 204 | 205 | ```typescript 206 | import { AuthCanActivateGuard } from 'src/app/_guards/auth.can-activate.guard'; 207 | import { RequireSaveCanDeactivateGuard } from 'src/app/_guards/require-save.can-deactivate.guard'; 208 | ``` 209 | 210 | With Barrel Exporting: 211 | 212 | ```typescript 213 | import { 214 | AuthCanActivateGuard, 215 | RequireSaveCanDeactivateGuard 216 | } from 'src/app/_guards'; 217 | ``` 218 | 219 | An example application with a `_guards` directory would look as follows: 220 | 221 | ![](https://wesleygrimes.com/assets/post_headers/routing_directory.png) 222 | 223 | ### Organize Feature-Specific Route Guards 224 | 225 | If you have guards that are _only_ used in a particular `FeatureRoutes` array, then store these routes underneath a folder named `_guards` underneath your feature folder. Make sure to follow the same naming conventions defined above, as well as barrel exporting. 226 | 227 | - Place guards under a folder named `_guards` underneath your feature folder 228 | - Make sure to create a barrel export `index.ts` for clean importing 229 | 230 | An example feature directory with `_guards` would look as follows: 231 | 232 | ![](https://wesleygrimes.com/assets/post_headers/routing_feature_directory.png) 233 | 234 | ## Finished Application Structure 235 | 236 | A completed application structure should look something like the following: 237 | 238 | ![](https://wesleygrimes.com/assets/post_headers/routing_completed_structure.png) 239 | 240 | --- 241 | 242 | ## Example GitHub Repository 243 | 244 | I have created a demonstration repository on GitHub. Feel free to fork, clone, and submit PRs. 245 | 246 | [https://github.com/wesleygrimes/angular-routing-best-practices](https://github.com/wesleygrimes/angular-routing-best-practices) 247 | 248 | ## Conclusion 249 | 250 | It's important to remember that I have implemented these best practices in several "real world" applications. While I have found these best practices helpful, and maintainable, I do not believe they are an end-all be-all solution to organizing routes in projects; it's just what has worked for me. I am curious as to what you all think? Please feel free to offer any suggestions, tips, or best practices you've learned when building enterprise Angular applications with routing and I will update the article to reflect as such. Happy Coding! 251 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "angular-routing-best-practices": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "prefix": "app", 11 | "schematics": {}, 12 | "architect": { 13 | "build": { 14 | "builder": "@angular-devkit/build-angular:browser", 15 | "options": { 16 | "outputPath": "dist/angular-routing-best-practices", 17 | "index": "src/index.html", 18 | "main": "src/main.ts", 19 | "polyfills": "src/polyfills.ts", 20 | "tsConfig": "src/tsconfig.app.json", 21 | "assets": [ 22 | "src/favicon.ico", 23 | "src/assets" 24 | ], 25 | "styles": [ 26 | "src/styles.css" 27 | ], 28 | "scripts": [], 29 | "es5BrowserSupport": true 30 | }, 31 | "configurations": { 32 | "production": { 33 | "fileReplacements": [ 34 | { 35 | "replace": "src/environments/environment.ts", 36 | "with": "src/environments/environment.prod.ts" 37 | } 38 | ], 39 | "optimization": true, 40 | "outputHashing": "all", 41 | "sourceMap": false, 42 | "extractCss": true, 43 | "namedChunks": false, 44 | "aot": true, 45 | "extractLicenses": true, 46 | "vendorChunk": false, 47 | "buildOptimizer": true, 48 | "budgets": [ 49 | { 50 | "type": "initial", 51 | "maximumWarning": "2mb", 52 | "maximumError": "5mb" 53 | } 54 | ] 55 | } 56 | } 57 | }, 58 | "serve": { 59 | "builder": "@angular-devkit/build-angular:dev-server", 60 | "options": { 61 | "browserTarget": "angular-routing-best-practices:build" 62 | }, 63 | "configurations": { 64 | "production": { 65 | "browserTarget": "angular-routing-best-practices:build:production" 66 | } 67 | } 68 | }, 69 | "extract-i18n": { 70 | "builder": "@angular-devkit/build-angular:extract-i18n", 71 | "options": { 72 | "browserTarget": "angular-routing-best-practices:build" 73 | } 74 | }, 75 | "test": { 76 | "builder": "@angular-devkit/build-angular:karma", 77 | "options": { 78 | "main": "src/test.ts", 79 | "polyfills": "src/polyfills.ts", 80 | "tsConfig": "src/tsconfig.spec.json", 81 | "karmaConfig": "src/karma.conf.js", 82 | "styles": [ 83 | "src/styles.css" 84 | ], 85 | "scripts": [], 86 | "assets": [ 87 | "src/favicon.ico", 88 | "src/assets" 89 | ] 90 | } 91 | }, 92 | "lint": { 93 | "builder": "@angular-devkit/build-angular:tslint", 94 | "options": { 95 | "tsConfig": [ 96 | "src/tsconfig.app.json", 97 | "src/tsconfig.spec.json" 98 | ], 99 | "exclude": [ 100 | "**/node_modules/**" 101 | ] 102 | } 103 | } 104 | } 105 | }, 106 | "angular-routing-best-practices-e2e": { 107 | "root": "e2e/", 108 | "projectType": "application", 109 | "prefix": "", 110 | "architect": { 111 | "e2e": { 112 | "builder": "@angular-devkit/build-angular:protractor", 113 | "options": { 114 | "protractorConfig": "e2e/protractor.conf.js", 115 | "devServerTarget": "angular-routing-best-practices:serve" 116 | }, 117 | "configurations": { 118 | "production": { 119 | "devServerTarget": "angular-routing-best-practices:serve:production" 120 | } 121 | } 122 | }, 123 | "lint": { 124 | "builder": "@angular-devkit/build-angular:tslint", 125 | "options": { 126 | "tsConfig": "e2e/tsconfig.e2e.json", 127 | "exclude": [ 128 | "**/node_modules/**" 129 | ] 130 | } 131 | } 132 | } 133 | } 134 | }, 135 | "defaultProject": "angular-routing-best-practices" 136 | } -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './src/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: require('path').join(__dirname, './tsconfig.e2e.json') 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | import { browser, logging } from 'protractor'; 3 | 4 | describe('workspace-project App', () => { 5 | let page: AppPage; 6 | 7 | beforeEach(() => { 8 | page = new AppPage(); 9 | }); 10 | 11 | it('should display welcome message', () => { 12 | page.navigateTo(); 13 | expect(page.getTitleText()).toEqual('Welcome to angular-routing-best-practices!'); 14 | }); 15 | 16 | afterEach(async () => { 17 | // Assert that there are no errors emitted from the browser 18 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 19 | expect(logs).not.toContain(jasmine.objectContaining({ 20 | level: logging.Level.SEVERE, 21 | } as logging.Entry)); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get(browser.baseUrl) as Promise; 6 | } 7 | 8 | getTitleText() { 9 | return element(by.css('app-root h1')).getText() as Promise; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-routing-best-practices", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "test": "ng test", 9 | "lint": "ng lint", 10 | "e2e": "ng e2e" 11 | }, 12 | "private": true, 13 | "dependencies": { 14 | "@angular/animations": "~7.2.7", 15 | "@angular/common": "~7.2.7", 16 | "@angular/compiler": "~7.2.7", 17 | "@angular/core": "~7.2.7", 18 | "@angular/forms": "~7.2.7", 19 | "@angular/platform-browser": "~7.2.7", 20 | "@angular/platform-browser-dynamic": "~7.2.7", 21 | "@angular/router": "~7.2.7", 22 | "core-js": "^2.5.4", 23 | "rxjs": "~6.4.0", 24 | "tslib": "^1.9.0", 25 | "zone.js": "~0.8.26" 26 | }, 27 | "devDependencies": { 28 | "@angular-devkit/build-angular": "~0.13.0", 29 | "@angular/cli": "~7.3.2", 30 | "@angular/compiler-cli": "~7.2.7", 31 | "@angular/language-service": "~7.2.7", 32 | "@types/node": "~8.9.4", 33 | "@types/jasmine": "~2.8.8", 34 | "@types/jasminewd2": "~2.0.3", 35 | "codelyzer": "~4.5.0", 36 | "jasmine-core": "~2.99.1", 37 | "jasmine-spec-reporter": "~4.2.1", 38 | "karma": "~3.1.1", 39 | "karma-chrome-launcher": "~2.2.0", 40 | "karma-coverage-istanbul-reporter": "~2.0.1", 41 | "karma-jasmine": "~1.1.2", 42 | "karma-jasmine-html-reporter": "^0.2.2", 43 | "protractor": "~5.4.0", 44 | "ts-node": "~7.0.0", 45 | "tslint": "~5.11.0", 46 | "typescript": "~3.2.2" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/app/_guards/app-specific.can-activate.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { 3 | ActivatedRouteSnapshot, 4 | CanActivate, 5 | RouterStateSnapshot 6 | } from '@angular/router'; 7 | 8 | @Injectable({ providedIn: 'root' }) 9 | export class AppSpecificCanActivateGuard implements CanActivate { 10 | constructor() {} 11 | 12 | canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { 13 | return true; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/app/_guards/index.ts: -------------------------------------------------------------------------------- 1 | export * from './app-specific.can-activate.guard'; 2 | -------------------------------------------------------------------------------- /src/app/app.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wesleygrimes/angular-routing-best-practices/a1b923bb7acd865330a0c13439934935e9ad44fc/src/app/app.component.css -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |

Welcome to {{ title }}!

4 | Angular Logo 9 |
10 | 11 | 15 | 16 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | 4 | describe('AppComponent', () => { 5 | beforeEach(async(() => { 6 | TestBed.configureTestingModule({ 7 | declarations: [ 8 | AppComponent 9 | ], 10 | }).compileComponents(); 11 | })); 12 | 13 | it('should create the app', () => { 14 | const fixture = TestBed.createComponent(AppComponent); 15 | const app = fixture.debugElement.componentInstance; 16 | expect(app).toBeTruthy(); 17 | }); 18 | 19 | it(`should have as title 'angular-routing-best-practices'`, () => { 20 | const fixture = TestBed.createComponent(AppComponent); 21 | const app = fixture.debugElement.componentInstance; 22 | expect(app.title).toEqual('angular-routing-best-practices'); 23 | }); 24 | 25 | it('should render title in a h1 tag', () => { 26 | const fixture = TestBed.createComponent(AppComponent); 27 | fixture.detectChanges(); 28 | const compiled = fixture.debugElement.nativeElement; 29 | expect(compiled.querySelector('h1').textContent).toContain('Welcome to angular-routing-best-practices!'); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.css'] 7 | }) 8 | export class AppComponent { 9 | title = 'angular-routing-best-practices'; 10 | } 11 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { RouterModule } from '@angular/router'; 4 | import { AppComponent } from './app.component'; 5 | import { AppRoutes } from './app.routes'; 6 | 7 | @NgModule({ 8 | declarations: [AppComponent], 9 | imports: [BrowserModule, RouterModule.forRoot(AppRoutes)], 10 | providers: [], 11 | bootstrap: [AppComponent] 12 | }) 13 | export class AppModule {} 14 | -------------------------------------------------------------------------------- /src/app/app.routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | import { AppSpecificCanActivateGuard } from './_guards'; 3 | 4 | export const AppRoutes: Routes = [ 5 | { 6 | path: '', 7 | pathMatch: 'full', 8 | redirectTo: 'feature-one' 9 | }, 10 | { 11 | path: 'feature-one', 12 | loadChildren: './feature-one/feature-one.module#FeatureOneModule' 13 | }, 14 | { 15 | path: 'feature-two', 16 | loadChildren: './feature-two/feature-two.module#FeatureTwoModule', 17 | canActivate: [AppSpecificCanActivateGuard] 18 | } 19 | ]; 20 | -------------------------------------------------------------------------------- /src/app/feature-one/_guards/feature-specific.can-activate.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { 3 | ActivatedRouteSnapshot, 4 | CanActivate, 5 | RouterStateSnapshot 6 | } from '@angular/router'; 7 | 8 | @Injectable() 9 | export class FeatureSpecificCanActivateGuard implements CanActivate { 10 | constructor() {} 11 | 12 | canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { 13 | return true; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/app/feature-one/_guards/index.ts: -------------------------------------------------------------------------------- 1 | export * from './feature-specific.can-activate.guard'; 2 | -------------------------------------------------------------------------------- /src/app/feature-one/feature-one.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wesleygrimes/angular-routing-best-practices/a1b923bb7acd865330a0c13439934935e9ad44fc/src/app/feature-one/feature-one.component.css -------------------------------------------------------------------------------- /src/app/feature-one/feature-one.component.html: -------------------------------------------------------------------------------- 1 |

2 | feature-one works! 3 |

4 | -------------------------------------------------------------------------------- /src/app/feature-one/feature-one.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { FeatureOneComponent } from './feature-one.component'; 4 | 5 | describe('FeatureOneComponent', () => { 6 | let component: FeatureOneComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ FeatureOneComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(FeatureOneComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/feature-one/feature-one.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-feature-one', 5 | templateUrl: './feature-one.component.html', 6 | styleUrls: ['./feature-one.component.css'] 7 | }) 8 | export class FeatureOneComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit() { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/app/feature-one/feature-one.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { RouterModule } from '@angular/router'; 4 | import { FeatureOneComponent } from './feature-one.component'; 5 | import { FeatureOneRoutes } from './feature-one.routes'; 6 | import { FeatureSpecificCanActivateGuard } from './_guards'; 7 | 8 | @NgModule({ 9 | declarations: [FeatureOneComponent], 10 | imports: [CommonModule, RouterModule.forChild(FeatureOneRoutes)], 11 | providers: [FeatureSpecificCanActivateGuard] 12 | }) 13 | export class FeatureOneModule {} 14 | -------------------------------------------------------------------------------- /src/app/feature-one/feature-one.routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | import { FeatureOneComponent } from './feature-one.component'; 3 | import { FeatureSpecificCanActivateGuard } from './_guards'; 4 | 5 | export const FeatureOneRoutes: Routes = [ 6 | { 7 | path: '', 8 | pathMatch: 'full', 9 | redirectTo: 'feature-one-component' 10 | }, 11 | { 12 | path: 'feature-one-component', 13 | component: FeatureOneComponent, 14 | canActivate: [FeatureSpecificCanActivateGuard] 15 | } 16 | ]; 17 | -------------------------------------------------------------------------------- /src/app/feature-two/feature-two.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wesleygrimes/angular-routing-best-practices/a1b923bb7acd865330a0c13439934935e9ad44fc/src/app/feature-two/feature-two.component.css -------------------------------------------------------------------------------- /src/app/feature-two/feature-two.component.html: -------------------------------------------------------------------------------- 1 |

2 | feature-two works! 3 |

4 | -------------------------------------------------------------------------------- /src/app/feature-two/feature-two.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { FeatureTwoComponent } from './feature-two.component'; 4 | 5 | describe('FeatureTwoComponent', () => { 6 | let component: FeatureTwoComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ FeatureTwoComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(FeatureTwoComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/feature-two/feature-two.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-feature-two', 5 | templateUrl: './feature-two.component.html', 6 | styleUrls: ['./feature-two.component.css'] 7 | }) 8 | export class FeatureTwoComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit() { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/app/feature-two/feature-two.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { RouterModule } from '@angular/router'; 4 | import { FeatureTwoComponent } from './feature-two.component'; 5 | import { FeatureTwoRoutes } from './feature-two.routes'; 6 | 7 | @NgModule({ 8 | declarations: [FeatureTwoComponent], 9 | imports: [CommonModule, RouterModule.forChild(FeatureTwoRoutes)] 10 | }) 11 | export class FeatureTwoModule {} 12 | -------------------------------------------------------------------------------- /src/app/feature-two/feature-two.routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | import { FeatureTwoComponent } from './feature-two.component'; 3 | 4 | export const FeatureTwoRoutes: Routes = [ 5 | { 6 | path: '', 7 | component: FeatureTwoComponent 8 | } 9 | ]; 10 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wesleygrimes/angular-routing-best-practices/a1b923bb7acd865330a0c13439934935e9ad44fc/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/browserslist: -------------------------------------------------------------------------------- 1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | # 5 | # For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed 6 | 7 | > 0.5% 8 | last 2 versions 9 | Firefox ESR 10 | not dead 11 | not IE 9-11 -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wesleygrimes/angular-routing-best-practices/a1b923bb7acd865330a0c13439934935e9ad44fc/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AngularRoutingBestPractices 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../coverage/angular-routing-best-practices'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags.ts'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | /*************************************************************************************************** 56 | * Zone JS is required by default for Angular itself. 57 | */ 58 | import 'zone.js/dist/zone'; // Included with Angular CLI. 59 | 60 | 61 | /*************************************************************************************************** 62 | * APPLICATION IMPORTS 63 | */ 64 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "types": [] 6 | }, 7 | "exclude": [ 8 | "test.ts", 9 | "**/*.spec.ts" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "test.ts", 12 | "polyfills.ts" 13 | ], 14 | "include": [ 15 | "**/*.spec.ts", 16 | "**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /src/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "app", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "app", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "module": "es2015", 9 | "moduleResolution": "node", 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "importHelpers": true, 13 | "target": "es5", 14 | "typeRoots": [ 15 | "node_modules/@types" 16 | ], 17 | "lib": [ 18 | "es2018", 19 | "dom" 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rulesDirectory": [ 4 | "codelyzer" 5 | ], 6 | "rules": { 7 | "array-type": false, 8 | "arrow-parens": false, 9 | "deprecation": { 10 | "severity": "warn" 11 | }, 12 | "import-blacklist": [ 13 | true, 14 | "rxjs/Rx" 15 | ], 16 | "interface-name": false, 17 | "max-classes-per-file": false, 18 | "max-line-length": [ 19 | true, 20 | 140 21 | ], 22 | "member-access": false, 23 | "member-ordering": [ 24 | true, 25 | { 26 | "order": [ 27 | "static-field", 28 | "instance-field", 29 | "static-method", 30 | "instance-method" 31 | ] 32 | } 33 | ], 34 | "no-consecutive-blank-lines": false, 35 | "no-console": [ 36 | true, 37 | "debug", 38 | "info", 39 | "time", 40 | "timeEnd", 41 | "trace" 42 | ], 43 | "no-empty": false, 44 | "no-inferrable-types": [ 45 | true, 46 | "ignore-params" 47 | ], 48 | "no-non-null-assertion": true, 49 | "no-redundant-jsdoc": true, 50 | "no-switch-case-fall-through": true, 51 | "no-use-before-declare": true, 52 | "no-var-requires": false, 53 | "object-literal-key-quotes": [ 54 | true, 55 | "as-needed" 56 | ], 57 | "object-literal-sort-keys": false, 58 | "ordered-imports": false, 59 | "quotemark": [ 60 | true, 61 | "single" 62 | ], 63 | "trailing-comma": false, 64 | "no-output-on-prefix": true, 65 | "use-input-property-decorator": true, 66 | "use-output-property-decorator": true, 67 | "use-host-property-decorator": true, 68 | "no-input-rename": true, 69 | "no-output-rename": true, 70 | "use-life-cycle-interface": true, 71 | "use-pipe-transform-interface": true, 72 | "component-class-suffix": true, 73 | "directive-class-suffix": true 74 | } 75 | } 76 | --------------------------------------------------------------------------------