` without closing tag!
26 | * The only tags that allows self-closing are the one that does not require a closing tag in first place:
27 | these are the void elements that do not not accept content `
`, `
`, `
![]()
`, `
`, `
`, `
`
28 | (and others).
29 |
30 | ## Templates
31 |
32 | In accordance with the [Angular style guide](https://angular.io/guide/styleguide), HTML templates should be extracted in
33 | separate files, when more than 3 lines.
34 |
35 | Only use inline templates sparingly in very simple components with less than 3 lines of HTML.
36 |
37 | ## Enforcement
38 |
39 | Coding rules enforcement and basic sanity checks are done in this project by [HTMLHint](http://htmlhint.com).
40 |
--------------------------------------------------------------------------------
/src/app/auth/credentials.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 |
3 | export interface Credentials {
4 | // Customize received credentials here
5 | username: string;
6 | token: string;
7 | }
8 |
9 | const credentialsKey = 'credentials';
10 |
11 | /**
12 | * Provides storage for authentication credentials.
13 | * The Credentials interface should be replaced with proper implementation.
14 | */
15 | @Injectable({
16 | providedIn: 'root'
17 | })
18 | export class CredentialsService {
19 |
20 | private _credentials: Credentials | null = null;
21 |
22 | constructor() {
23 | const savedCredentials = sessionStorage.getItem(credentialsKey) || localStorage.getItem(credentialsKey);
24 | if (savedCredentials) {
25 | this._credentials = JSON.parse(savedCredentials);
26 | }
27 | }
28 |
29 | /**
30 | * Checks is the user is authenticated.
31 | * @return True if the user is authenticated.
32 | */
33 | isAuthenticated(): boolean {
34 | return !!this.credentials;
35 | }
36 |
37 | /**
38 | * Gets the user credentials.
39 | * @return The user credentials or null if the user is not authenticated.
40 | */
41 | get credentials(): Credentials | null {
42 | return this._credentials;
43 | }
44 |
45 | /**
46 | * Sets the user credentials.
47 | * The credentials may be persisted across sessions by setting the `remember` parameter to true.
48 | * Otherwise, the credentials are only persisted for the current session.
49 | * @param credentials The user credentials.
50 | * @param remember True to remember credentials across sessions.
51 | */
52 | setCredentials(credentials?: Credentials, remember?: boolean) {
53 | this._credentials = credentials || null;
54 |
55 | if (credentials) {
56 | const storage = remember ? localStorage : sessionStorage;
57 | storage.setItem(credentialsKey, JSON.stringify(credentials));
58 | } else {
59 | sessionStorage.removeItem(credentialsKey);
60 | localStorage.removeItem(credentialsKey);
61 | }
62 | }
63 |
64 | }
65 |
--------------------------------------------------------------------------------
/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit, OnDestroy } from '@angular/core';
2 | import { Router, NavigationEnd, ActivatedRoute } from '@angular/router';
3 | import { Title } from '@angular/platform-browser';
4 | import { TranslateService } from '@ngx-translate/core';
5 | import { merge } from 'rxjs';
6 | import { filter, map, switchMap } from 'rxjs/operators';
7 |
8 | import { environment } from '@env/environment';
9 | import { Logger, UntilDestroy, untilDestroyed } from '@shared';
10 | import { I18nService } from '@app/i18n';
11 |
12 | const log = new Logger('App');
13 |
14 | @UntilDestroy()
15 | @Component({
16 | selector: 'app-root',
17 | templateUrl: './app.component.html',
18 | styleUrls: ['./app.component.scss']
19 | })
20 | export class AppComponent implements OnInit, OnDestroy {
21 |
22 | constructor(private router: Router,
23 | private activatedRoute: ActivatedRoute,
24 | private titleService: Title,
25 | private translateService: TranslateService,
26 | private i18nService: I18nService) { }
27 |
28 | ngOnInit() {
29 | // Setup logger
30 | if (environment.production) {
31 | Logger.enableProductionMode();
32 | }
33 |
34 | log.debug('init');
35 |
36 |
37 | // Setup translations
38 | this.i18nService.init(environment.defaultLanguage, environment.supportedLanguages);
39 |
40 | const onNavigationEnd = this.router.events.pipe(filter(event => event instanceof NavigationEnd));
41 |
42 | // Change page title on navigation or language change, based on route data
43 | merge(this.translateService.onLangChange, onNavigationEnd)
44 | .pipe(
45 | map(() => {
46 | let route = this.activatedRoute;
47 | while (route.firstChild) {
48 | route = route.firstChild;
49 | }
50 | return route;
51 | }),
52 | filter(route => route.outlet === 'primary'),
53 | switchMap(route => route.data),
54 | untilDestroyed(this)
55 | )
56 | .subscribe(event => {
57 | const title = event['title'];
58 | if (title) {
59 | this.titleService.setTitle(this.translateService.instant(title));
60 | }
61 | });
62 | }
63 |
64 | ngOnDestroy() {
65 | this.i18nService.destroy();
66 | }
67 |
68 | }
69 |
--------------------------------------------------------------------------------
/docs/routing.md:
--------------------------------------------------------------------------------
1 | # Browser routing
2 |
3 | To allow navigation without triggering a server request, Angular now use by default the
4 | [HTML5 pushState](https://developer.mozilla.org/en-US/docs/Web/API/History_API#Adding_and_modifying_history_entries)
5 | API enabling natural URL style (like `localhost:4200/home/`), in opposition to Angular 1 which used the *hashbang* hack
6 | routing style (like `localhost:4200/#/home/`).
7 |
8 | This change has several consequences you should know of, be sure to read the
9 | [browser URL styles](https://angular.io/docs/ts/latest/guide/router.html#!#browser-url-styles) notice to fully
10 | understand the differences between the two approaches.
11 |
12 | In short:
13 |
14 | - It is only supported on modern browsers (IE10+), a [polyfill](https://github.com/Modernizr/Modernizr/wiki/HTML5-Cross-Browser-Polyfills#html5-history-api-pushstate-replacestate-popstate)
15 | is required for older browsers.
16 |
17 | - You have the option to perform *server-side rendering* later if you need to increase your app perceived performance.
18 |
19 | - You need to [configure URL rewriting](#server-configuration) on your server so that all routes serve your index file.
20 |
21 | It is still possible to revert to the hash strategy, but unless you have specific needs, you should stick with the
22 | default HTML5 routing mode.
23 |
24 | ## Server configuration
25 |
26 | To allow your angular application working properly as a *Single Page Application* (SPA) and allow bookmarking or
27 | refreshing any page, you need some configuration on your server, otherwise you will be running into troubles.
28 |
29 | > Note that during development, the live reload server already supports SPA mode.
30 |
31 | The basic idea is simply to serve the `index.html` file for every request aimed at your application.
32 |
33 | Here is an example on how to perform this on an [Express](http://expressjs.com) NodeJS server:
34 |
35 | ```js
36 | // Put this in your `server.js` file, after your other rules (APIs, static files...)
37 | app.get('/*', function(req, res) {
38 | res.sendFile(__dirname + '/index.html')
39 | });
40 | ```
41 |
42 | For other servers like [Nginx](https://www.nginx.com/blog/creating-nginx-rewrite-rules/) or
43 | [Apache](http://httpd.apache.org/docs/2.0/misc/rewriteguide.html), you may look for how to perform *URL rewriting*.
44 |
--------------------------------------------------------------------------------
/src/app/auth/credentials.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed, inject } from '@angular/core/testing';
2 |
3 | import { CredentialsService, Credentials } from './credentials.service';
4 |
5 | const credentialsKey = 'credentials';
6 |
7 | describe('CredentialsService', () => {
8 | let credentialsService: CredentialsService;
9 |
10 | beforeEach(() => {
11 | TestBed.configureTestingModule({
12 | providers: [CredentialsService]
13 | });
14 |
15 | credentialsService = TestBed.inject(CredentialsService);
16 | });
17 |
18 | afterEach(() => {
19 | // Cleanup
20 | localStorage.removeItem(credentialsKey);
21 | sessionStorage.removeItem(credentialsKey);
22 | });
23 |
24 | describe('setCredentials', () => {
25 | it('should authenticate user if credentials are set', () => {
26 | // Act
27 | credentialsService.setCredentials({ username: 'me', token: '123' });
28 |
29 | // Assert
30 | expect(credentialsService.isAuthenticated()).toBe(true);
31 | expect((credentialsService.credentials as Credentials).username).toBe('me');
32 | });
33 |
34 | it('should clean authentication', () => {
35 | // Act
36 | credentialsService.setCredentials();
37 |
38 | // Assert
39 | expect(credentialsService.isAuthenticated()).toBe(false);
40 | });
41 |
42 | it('should persist credentials for the session', () => {
43 | // Act
44 | credentialsService.setCredentials({ username: 'me', token: '123' });
45 |
46 | // Assert
47 | expect(sessionStorage.getItem(credentialsKey)).not.toBeNull();
48 | expect(localStorage.getItem(credentialsKey)).toBeNull();
49 | });
50 |
51 | it('should persist credentials across sessions', () => {
52 | // Act
53 | credentialsService.setCredentials({ username: 'me', token: '123' }, true);
54 |
55 | // Assert
56 | expect(localStorage.getItem(credentialsKey)).not.toBeNull();
57 | expect(sessionStorage.getItem(credentialsKey)).toBeNull();
58 | });
59 |
60 | it('should clear user authentication', () => {
61 | // Act
62 | credentialsService.setCredentials();
63 |
64 | // Assert
65 | expect(credentialsService.isAuthenticated()).toBe(false);
66 | expect(credentialsService.credentials).toBeNull();
67 | expect(sessionStorage.getItem(credentialsKey)).toBeNull();
68 | expect(localStorage.getItem(credentialsKey)).toBeNull();
69 | });
70 | });
71 | });
72 |
--------------------------------------------------------------------------------
/src/app/@shared/logger.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Logger, LogLevel, LogOutput } from './logger.service';
2 |
3 | const logMethods = ['log', 'info', 'warn', 'error'];
4 |
5 | describe('Logger', () => {
6 | let savedConsole: any[];
7 | let savedLevel: LogLevel;
8 | let savedOutputs: LogOutput[];
9 |
10 | beforeAll(() => {
11 | savedConsole = [];
12 | logMethods.forEach((m) => {
13 | savedConsole[m] = console[m];
14 | console[m] = () => {};
15 | });
16 | savedLevel = Logger.level;
17 | savedOutputs = Logger.outputs;
18 | });
19 |
20 | beforeEach(() => {
21 | Logger.level = LogLevel.Debug;
22 | });
23 |
24 | afterAll(() => {
25 | logMethods.forEach((m) => { console[m] = savedConsole[m]; });
26 | Logger.level = savedLevel;
27 | Logger.outputs = savedOutputs;
28 | });
29 |
30 | it('should create an instance', () => {
31 | expect(new Logger()).toBeTruthy();
32 | });
33 |
34 | it('should add a new LogOutput and receives log entries', () => {
35 | // Arrange
36 | const outputSpy = jest.fn();
37 | const log = new Logger('test');
38 |
39 | // Act
40 | Logger.outputs.push(outputSpy);
41 |
42 | log.debug('d');
43 | log.info('i');
44 | log.warn('w');
45 | log.error('e', { error: true });
46 |
47 | // Assert
48 | expect(outputSpy).toHaveBeenCalled();
49 | expect(outputSpy.mock.calls.length).toBe(4);
50 | expect(outputSpy).toHaveBeenCalledWith('test', LogLevel.Debug, 'd');
51 | expect(outputSpy).toHaveBeenCalledWith('test', LogLevel.Info, 'i');
52 | expect(outputSpy).toHaveBeenCalledWith('test', LogLevel.Warning, 'w');
53 | expect(outputSpy).toHaveBeenCalledWith('test', LogLevel.Error, 'e', { error: true });
54 | });
55 |
56 | it('should add a new LogOutput and receives only production log entries', () => {
57 | // Arrange
58 | const outputSpy = jest.fn();
59 | const log = new Logger('test');
60 |
61 | // Act
62 | Logger.outputs.push(outputSpy);
63 | Logger.enableProductionMode();
64 |
65 | log.debug('d');
66 | log.info('i');
67 | log.warn('w');
68 | log.error('e', { error: true });
69 |
70 | // Assert
71 | expect(outputSpy).toHaveBeenCalled();
72 | expect(outputSpy.mock.calls.length).toBe(2);
73 | expect(outputSpy).toHaveBeenCalledWith('test', LogLevel.Warning, 'w');
74 | expect(outputSpy).toHaveBeenCalledWith('test', LogLevel.Error, 'e', { error: true });
75 | });
76 | });
77 |
--------------------------------------------------------------------------------
/src/app/auth/authentication.guard.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed } from '@angular/core/testing';
2 | import { Router, ActivatedRouteSnapshot } from '@angular/router';
3 |
4 | import { CredentialsService } from './credentials.service';
5 | import { MockCredentialsService } from './credentials.service.mock';
6 | import { AuthenticationGuard } from './authentication.guard';
7 |
8 | describe('AuthenticationGuard', () => {
9 | let authenticationGuard: AuthenticationGuard;
10 | let credentialsService: MockCredentialsService;
11 | let mockRouter: any;
12 | let mockSnapshot: any;
13 |
14 | beforeEach(() => {
15 | mockRouter = {
16 | navigate: jest.fn()
17 | };
18 | mockSnapshot = jest.fn(() => ({
19 | toString: jest.fn()
20 | }));
21 |
22 | TestBed.configureTestingModule({
23 | providers: [
24 | AuthenticationGuard,
25 | { provide: CredentialsService, useClass: MockCredentialsService },
26 | { provide: Router, useValue: mockRouter }
27 | ]
28 | });
29 |
30 | authenticationGuard = TestBed.inject(AuthenticationGuard);
31 | credentialsService = TestBed.inject(CredentialsService);
32 | });
33 |
34 | it('should have a canActivate method', () => {
35 | expect(typeof authenticationGuard.canActivate).toBe('function');
36 | });
37 |
38 | it('should return true if user is authenticated', () => {
39 | expect(authenticationGuard.canActivate(new ActivatedRouteSnapshot(), mockSnapshot)).toBe(true);
40 | });
41 |
42 | it('should return false and redirect to login if user is not authenticated', () => {
43 | // Arrange
44 | credentialsService.credentials = null;
45 |
46 | // Act
47 | const result = authenticationGuard.canActivate(new ActivatedRouteSnapshot(), mockSnapshot);
48 |
49 | // Assert
50 | expect(mockRouter.navigate).toHaveBeenCalledWith(['/login'], {
51 | queryParams: { redirect: undefined },
52 | replaceUrl: true
53 | });
54 | expect(result).toBe(false);
55 | });
56 |
57 | it('should save url as queryParam if user is not authenticated', () => {
58 | credentialsService.credentials = null;
59 | mockRouter.url = '/about';
60 | mockSnapshot.url = '/about';
61 |
62 | authenticationGuard.canActivate(new ActivatedRouteSnapshot(), mockSnapshot);
63 | expect(mockRouter.navigate).toHaveBeenCalledWith(['/login'], {
64 | queryParams: { redirect: mockRouter.url },
65 | replaceUrl: true
66 | });
67 | });
68 | });
69 |
--------------------------------------------------------------------------------
/src/app/auth/login.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
APP_NAME
4 |
10 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/src/polyfills.ts:
--------------------------------------------------------------------------------
1 | /***************************************************************************************************
2 | * Load `$localize` onto the global scope - used if i18n tags appear in Angular templates.
3 | */
4 | import '@angular/localize/init';
5 |
6 | /**
7 | * This file includes polyfills needed by Angular and is loaded before the app.
8 | * You can add your own extra polyfills to this file.
9 | *
10 | * This file is divided into 2 sections:
11 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
12 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
13 | * file.
14 | *
15 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
16 | * automatically update themselves. This includes recent versions of Safari, Chrome (including
17 | * Opera), Edge on the desktop, and iOS and Chrome on mobile.
18 | *
19 | * Learn more in https://angular.io/guide/browser-support
20 | */
21 |
22 | /***************************************************************************************************
23 | * BROWSER POLYFILLS
24 | */
25 |
26 | /**
27 | * By default, zone.js will patch all possible macroTask and DomEvents
28 | * user can disable parts of macroTask/DomEvents patch by setting following flags
29 | * because those flags need to be set before `zone.js` being loaded, and webpack
30 | * will put import in the top of bundle, so user need to create a separate file
31 | * in this directory (for example: zone-flags.ts), and put the following flags
32 | * into that file, and then add the following code before importing zone.js.
33 | * import './zone-flags';
34 | *
35 | * The flags allowed in zone-flags.ts are listed here.
36 | *
37 | * The following flags will work for all browsers.
38 | *
39 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
40 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
41 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
42 | *
43 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
44 | * with the following flag, it will bypass `zone.js` patch for IE/Edge
45 | *
46 | * (window as any).__Zone_enable_cross_context_check = true;
47 | *
48 | */
49 |
50 | /***************************************************************************************************
51 | * Zone JS is required by default for Angular itself.
52 | */
53 | import 'zone.js'; // Included with Angular CLI.
54 |
55 |
56 | /***************************************************************************************************
57 | * APPLICATION IMPORTS
58 | */
59 |
--------------------------------------------------------------------------------
/docs/coding-guides/unit-tests.md:
--------------------------------------------------------------------------------
1 | # Unit tests coding guide
2 |
3 | The main objective of unit tests is to detect regressions and to help you design software components. A suite of
4 | *good* unit tests can be *immensely* valuable for your project and makes it easier to refactor and expand your code.
5 | But keep in mind that a suite of *bad* unit tests can also be *immensely* painful, and hurt your development by
6 | inhibiting your ability to refactor or alter your code in any way.
7 |
8 | ## What to test?
9 |
10 | Everything! But if you need priorities, at least all business logic code must be tested: services, helpers, models...
11 | Shared directives/components should also be covered by unit tests, if you do not have the time to test every single
12 | component.
13 |
14 | Keep in mind that component unit tests should not overlap with [end-to-end tests](e2e-tests.md): while unit the tests
15 | cover the isolated behavior of the component bindings and methods, the end-to-end tests in opposition should cover the
16 | integration and interactions with other app components based on real use cases scenarios.
17 |
18 | ## Good practices
19 |
20 | - Name your tests cleanly and consistently
21 | - Do not only test nominal cases, the most important tests are the one covering the edge cases
22 | - Each test should be independent to all the others
23 | - Avoid unnecessary assertions: it's counter-productive to assert anything covered by another test, it just increase
24 | pointless failures and maintenance workload
25 | - Test only one code unit at a time: if you cannot do this, it means you have an architecture problem in your app
26 | - Mock out all external dependencies and state: if there is too much to mock, it is often a sign that maybe you
27 | should split your tested module into several more independent modules
28 | - Clearly separate or identify these 3 stages of each unit test (the *3A*): *arrange*, *act* and *assert*
29 | - When you fix a bug, add a test case for it to prevent regression
30 |
31 | ## Pitfalls
32 |
33 | - Sometimes your architecture might mean your code modify static variables during unit tests. Avoid this if you can,
34 | but if you can't, at least make sure each test resets the relevant statics before and after your tests.
35 | - Don’t unit-test configuration settings
36 | - Improving test coverage is good, but having meaningful tests is better: start with the latter first, and **only after
37 | essential features of your code unit are tested**, your can think of improving the coverage.
38 |
39 | ## Unit testing with Angular
40 |
41 | A good starting point for learning is the official
42 | [testing guide](https://angular.io/docs/ts/latest/guide/testing.html).
43 |
44 | But as you will most likely want to go bit further in real world apps, these
45 | [example test snippets](https://gist.github.com/wkwiatek/e8a4a9d92abc4739f04f5abddd3de8a7) are also very helpful to
46 | learn how to cover most common testing use cases.
47 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "tslint:recommended",
4 | "tslint-config-prettier"
5 | ],
6 | "rulesDirectory": [
7 | "codelyzer"
8 | ],
9 | "rules": {
10 | "array-type": false,
11 | "arrow-parens": false,
12 | "deprecation": {
13 | "severity": "warning"
14 | },
15 | "import-blacklist": [
16 | true,
17 | "rxjs/Rx"
18 | ],
19 | "max-line-length": [
20 | true,
21 | 120
22 | ],
23 | "member-access": false,
24 | "member-ordering": [
25 | true,
26 | {
27 | "order": [
28 | "public-static-field",
29 | "protected-static-field",
30 | "private-static-field",
31 | "public-instance-field",
32 | "protected-instance-field",
33 | "private-instance-field",
34 | "public-static-method",
35 | "protected-static-method",
36 | "private-static-method",
37 | "constructor",
38 | "public-instance-method",
39 | "protected-instance-method",
40 | "private-instance-method"
41 | ]
42 | }
43 | ],
44 | "interface-name": false,
45 | "max-classes-per-file": false,
46 | "no-consecutive-blank-lines": false,
47 | "no-console": [
48 | true,
49 | "debug",
50 | "time",
51 | "timeEnd",
52 | "trace"
53 | ],
54 | "no-duplicate-variable": [
55 | true,
56 | "check-parameters"
57 | ],
58 | "no-empty": false,
59 | "no-inferrable-types": [
60 | true,
61 | "ignore-params"
62 | ],
63 | "no-non-null-assertion": true,
64 | "no-redundant-jsdoc": true,
65 | "no-switch-case-fall-through": true,
66 | "no-unnecessary-initializer": true,
67 | "object-literal-sort-keys": false,
68 | "quotemark": [
69 | true,
70 | "single"
71 | ],
72 | "typedef": [
73 | true,
74 | "parameter",
75 | "property-declaration"
76 | ],
77 | "variable-name": false,
78 | "no-var-requires": false,
79 | "object-literal-key-quotes": false,
80 | "ordered-imports": false,
81 | "trailing-comma": false,
82 | "no-conflicting-lifecycle": true,
83 | "no-output-native": true,
84 | "directive-selector": [
85 | true,
86 | "attribute",
87 | "app",
88 | "camelCase"
89 | ],
90 | "component-selector": [
91 | true,
92 | "element",
93 | "page",
94 | "app",
95 | "kebab-case"
96 | ],
97 | "template-banana-in-box": true,
98 | "template-no-negated-async": true,
99 | "no-output-on-prefix": true,
100 | "no-inputs-metadata-property": true,
101 | "no-outputs-metadata-property": true,
102 | "no-host-metadata-property": true,
103 | "no-input-rename": true,
104 | "no-output-rename": true,
105 | "use-lifecycle-interface": true,
106 | "use-pipe-transform-interface": true,
107 | "component-class-suffix": true,
108 | "contextual-lifecycle": true,
109 | "directive-class-suffix": true
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/docs/coding-guides/typescript.md:
--------------------------------------------------------------------------------
1 | # TypeScript coding guide
2 |
3 | [TypeScript](http://www.typescriptlang.org) is a superset of JavaScript that greatly helps building large web
4 | applications.
5 |
6 | Coding conventions and best practices comes from the
7 | [TypeScript guidelines](https://github.com/Microsoft/TypeScript/wiki/Coding-guidelines), and are also detailed in the
8 | [TypeScript Deep Dive Style Guide](https://basarat.gitbooks.io/typescript/content/docs/styleguide/styleguide.html).
9 | In addition, this project also follows the general [Angular style guide](https://angular.io/guide/styleguide).
10 |
11 | ## Naming conventions
12 |
13 | - Use `PascalCase` for types, classes, interfaces, constants and enum values.
14 | - Use `camelCase` for variables, properties and functions
15 | - Avoid prefixing interfaces with a capital `I`, see [Angular style guide](https://angular.io/guide/styleguide#!#03-03)
16 | - Do not use `_` as a prefix for private properties. An exception can be made for backing fields like this:
17 | ```typescript
18 | private _foo: string;
19 | get foo() { return this._foo; } // foo is read-only to consumers
20 | ```
21 |
22 | ## Ordering
23 |
24 | - Within a file, type definitions should come first
25 | - Within a class, these priorities should be respected:
26 | * Properties comes before functions
27 | * Static symbols comes before instance symbols
28 | * Public symbols comes before private symbols
29 |
30 | ## Coding rules
31 |
32 | - Use single quotes `'` for strings
33 | - Always use strict equality checks: `===` and `!==` instead of `==` or `!=` to avoid comparison pitfalls (see
34 | [JavaScript equality table](https://dorey.github.io/JavaScript-Equality-Table/)).
35 | The only accepted usage for `==` is when you want to check a value against `null` or `undefined`.
36 | - Use `[]` instead of `Array` constructor
37 | - Use `{}` instead of `Object` constructor
38 | - Always specify types for function parameters and returns (if applicable)
39 | - Do not export types/functions unless you need to share it across multiple components
40 | - Do not introduce new types/values to the global namespace
41 | - Use arrow functions over anonymous function expressions
42 | - Only surround arrow function parameters when necessary.
43 | For example, `(x) => x + x` is wrong but the following are correct:
44 | * `x => x + x`
45 | * `(x, y) => x + y`
46 | * `
(x: T, y: T) => x === y`
47 |
48 | ## Definitions
49 |
50 | In order to infer types from JavaScript modules, TypeScript language supports external type definitions. They are
51 | located in the `node_modules/@types` folder.
52 |
53 | To manage type definitions, use standard `npm install|update|remove` commands.
54 |
55 | ## Enforcement
56 |
57 | Coding rules are enforced in this project via [TSLint](https://github.com/palantir/tslint).
58 | Angular-specific rules are also enforced via the [Codelyzer](https://github.com/mgechev/codelyzer) rule extensions.
59 |
60 | ## Learn more
61 |
62 | The read of [TypeScript Deep Dive](https://basarat.gitbooks.io/typescript) is recommended, this is a very good
63 | reference book for TypeScript (and also open-source).
64 |
--------------------------------------------------------------------------------
/src/app/i18n/i18n.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { TranslateService, LangChangeEvent } from '@ngx-translate/core';
3 | import { Subscription } from 'rxjs';
4 |
5 | import { Logger } from '@shared';
6 | import enUS from '../../translations/en-US.json';
7 | import frFR from '../../translations/fr-FR.json';
8 |
9 | const log = new Logger('I18nService');
10 | const languageKey = 'language';
11 |
12 | @Injectable({
13 | providedIn: 'root'
14 | })
15 | export class I18nService {
16 |
17 | defaultLanguage!: string;
18 | supportedLanguages!: string[];
19 |
20 | private langChangeSubscription!: Subscription;
21 |
22 | constructor(private translateService: TranslateService) {
23 | // Embed languages to avoid extra HTTP requests
24 | translateService.setTranslation('en-US', enUS);
25 | translateService.setTranslation('fr-FR', frFR);
26 | }
27 |
28 | /**
29 | * Initializes i18n for the application.
30 | * Loads language from local storage if present, or sets default language.
31 | * @param defaultLanguage The default language to use.
32 | * @param supportedLanguages The list of supported languages.
33 | */
34 | init(defaultLanguage: string, supportedLanguages: string[]) {
35 | this.defaultLanguage = defaultLanguage;
36 | this.supportedLanguages = supportedLanguages;
37 | this.language = '';
38 |
39 | // Warning: this subscription will always be alive for the app's lifetime
40 | this.langChangeSubscription = this.translateService.onLangChange
41 | .subscribe((event: LangChangeEvent) => { localStorage.setItem(languageKey, event.lang); });
42 | }
43 |
44 | /**
45 | * Cleans up language change subscription.
46 | */
47 | destroy() {
48 | if (this.langChangeSubscription) {
49 | this.langChangeSubscription.unsubscribe();
50 | }
51 | }
52 |
53 | /**
54 | * Sets the current language.
55 | * Note: The current language is saved to the local storage.
56 | * If no parameter is specified, the language is loaded from local storage (if present).
57 | * @param language The IETF language code to set.
58 | */
59 | set language(language: string) {
60 | let newLanguage = language || localStorage.getItem(languageKey) || this.translateService.getBrowserCultureLang() || '';
61 | let isSupportedLanguage = this.supportedLanguages.includes(newLanguage);
62 |
63 | // If no exact match is found, search without the region
64 | if (newLanguage && !isSupportedLanguage) {
65 | newLanguage = newLanguage.split('-')[0];
66 | newLanguage = this.supportedLanguages.find(supportedLanguage => supportedLanguage.startsWith(newLanguage)) || '';
67 | isSupportedLanguage = Boolean(newLanguage);
68 | }
69 |
70 | // Fallback if language is not supported
71 | if (!newLanguage || !isSupportedLanguage) {
72 | newLanguage = this.defaultLanguage;
73 | }
74 |
75 | language = newLanguage;
76 |
77 | log.debug(`Language set to ${language}`);
78 | this.translateService.use(language);
79 | }
80 |
81 | /**
82 | * Gets the current language.
83 | * @return The current language code.
84 | */
85 | get language(): string {
86 | return this.translateService.currentLang;
87 | }
88 |
89 | }
90 |
--------------------------------------------------------------------------------
/src/app/@shared/logger.service.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Simple logger system with the possibility of registering custom outputs.
3 | *
4 | * 4 different log levels are provided, with corresponding methods:
5 | * - debug : for debug information
6 | * - info : for informative status of the application (success, ...)
7 | * - warning : for non-critical errors that do not prevent normal application behavior
8 | * - error : for critical errors that prevent normal application behavior
9 | *
10 | * Example usage:
11 | * ```
12 | * import { Logger } from 'app/core/logger.service';
13 | *
14 | * const log = new Logger('myFile');
15 | * ...
16 | * log.debug('something happened');
17 | * ```
18 | *
19 | * To disable debug and info logs in production, add this snippet to your root component:
20 | * ```
21 | * export class AppComponent implements OnInit {
22 | * ngOnInit() {
23 | * if (environment.production) {
24 | * Logger.enableProductionMode();
25 | * }
26 | * ...
27 | * }
28 | * }
29 | *
30 | * If you want to process logs through other outputs than console, you can add LogOutput functions to Logger.outputs.
31 | */
32 |
33 | /**
34 | * The possible log levels.
35 | * LogLevel.Off is never emitted and only used with Logger.level property to disable logs.
36 | */
37 | export enum LogLevel {
38 | Off = 0,
39 | Error,
40 | Warning,
41 | Info,
42 | Debug
43 | }
44 |
45 | /**
46 | * Log output handler function.
47 | */
48 | export type LogOutput = (source: string | undefined, level: LogLevel, ...objects: any[]) => void;
49 |
50 | export class Logger {
51 |
52 | /**
53 | * Current logging level.
54 | * Set it to LogLevel.Off to disable logs completely.
55 | */
56 | static level = LogLevel.Debug;
57 |
58 | /**
59 | * Additional log outputs.
60 | */
61 | static outputs: LogOutput[] = [];
62 |
63 | /**
64 | * Enables production mode.
65 | * Sets logging level to LogLevel.Warning.
66 | */
67 | static enableProductionMode() {
68 | Logger.level = LogLevel.Warning;
69 | }
70 |
71 | constructor(private source?: string) { }
72 |
73 | /**
74 | * Logs messages or objects with the debug level.
75 | * Works the same as console.log().
76 | */
77 | debug(...objects: any[]) {
78 | this.log(console.log, LogLevel.Debug, objects);
79 | }
80 |
81 | /**
82 | * Logs messages or objects with the info level.
83 | * Works the same as console.log().
84 | */
85 | info(...objects: any[]) {
86 | this.log(console.info, LogLevel.Info, objects);
87 | }
88 |
89 | /**
90 | * Logs messages or objects with the warning level.
91 | * Works the same as console.log().
92 | */
93 | warn(...objects: any[]) {
94 | this.log(console.warn, LogLevel.Warning, objects);
95 | }
96 |
97 | /**
98 | * Logs messages or objects with the error level.
99 | * Works the same as console.log().
100 | */
101 | error(...objects: any[]) {
102 | this.log(console.error, LogLevel.Error, objects);
103 | }
104 |
105 | private log(func: (...args: any[]) => void, level: LogLevel, objects: any[]) {
106 | if (level <= Logger.level) {
107 | const log = this.source ? ['[' + this.source + ']'].concat(objects) : objects;
108 | func.apply(console, log);
109 | Logger.outputs.forEach(output => output.apply(output, [this.source, level, ...objects]));
110 | }
111 | }
112 |
113 | }
114 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ng-x-rocket",
3 | "version": "1.0.0",
4 | "private": true,
5 | "scripts": {
6 | "ng": "ng",
7 | "build": "npm run write:env -s && ng build",
8 | "start": "npm run write:env -s && ng serve --proxy-config proxy.conf.js",
9 | "serve:sw": "npm run build -s && npx http-server ./dist -p 4200",
10 | "lint": "ng lint && stylelint \"src/**/*.scss\"",
11 | "test": "npm run write:env -s && ng test",
12 | "test:ci": "npm run write:env -s && npm run lint -s && ng run ng-x-rocket:test:ci",
13 | "translations:extract": "ngx-translate-extract --input ./src --output ./src/translations/template.json --format=json --clean --sort",
14 | "docs": "hads ./docs -o",
15 | "write:env": "ngx-scripts env npm_package_version",
16 | "prettier": "prettier --write \"./src/**/*.{ts,js,html,scss}\"",
17 | "prettier:check": "prettier --list-different \"./src/**/*.{ts,js,html,scss}\"",
18 | "postinstall": "npm run prettier -s && husky install",
19 | "generate": "ng generate"
20 | },
21 | "dependencies": {
22 | "@angular/animations": "~14.1.3",
23 | "@angular/common": "~14.1.3",
24 | "@angular/compiler": "~14.1.3",
25 | "@angular/core": "~14.1.3",
26 | "@angular/forms": "~14.1.3",
27 | "@angular/localize": "~14.1.3",
28 | "@angular/platform-browser": "~14.1.3",
29 | "@angular/platform-browser-dynamic": "~14.1.3",
30 | "@angular/router": "~14.1.3",
31 | "@ngx-translate/core": "^14.0.0",
32 | "@angular/service-worker": "~14.1.3",
33 | "@ng-bootstrap/ng-bootstrap": "^13.0.0",
34 | "@popperjs/core": "^2.11.0",
35 | "bootstrap": "^5.0.2",
36 | "@fortawesome/fontawesome-free": "^6.1.2",
37 | "rxjs": "^7.5.0",
38 | "tslib": "^2.3.0",
39 | "zone.js": "~0.11.4"
40 | },
41 | "devDependencies": {
42 | "@angular-builders/jest": "^14.0.1",
43 | "@angular-devkit/build-angular": "~14.1.3",
44 | "@angular-eslint/builder": "~14.0.3",
45 | "@angular-eslint/eslint-plugin": "~14.0.3",
46 | "@angular-eslint/eslint-plugin-template": "~14.0.3",
47 | "@angular-eslint/schematics": "~14.0.3",
48 | "@angular-eslint/template-parser": "~14.0.3",
49 | "@angular/cli": "~14.1.3",
50 | "@angular/compiler-cli": "~14.1.3",
51 | "@angular/language-service": "~14.1.3",
52 | "@biesbjerg/ngx-translate-extract": "^7.0.3",
53 | "@biesbjerg/ngx-translate-extract-marker": "^1.0.0",
54 | "@ngx-rocket/scripts": "^5.2.2",
55 | "@ngneat/until-destroy": "^9.0.0",
56 | "@typescript-eslint/eslint-plugin": "~5.34.0",
57 | "@typescript-eslint/parser": "~5.34.0",
58 | "@types/jest": "^28.1.8",
59 | "@types/node": "^14.18.26",
60 | "eslint": "^8.3.0",
61 | "eslint-plugin-import": "latest",
62 | "eslint-plugin-jsdoc": "latest",
63 | "eslint-plugin-prefer-arrow": "latest",
64 | "hads": "^3.0.0",
65 | "https-proxy-agent": "^5.0.0",
66 | "jest": "^28.1.3",
67 | "ts-jest": "^28.0.8",
68 | "prettier": "^2.2.1",
69 | "stylelint-config-prettier": "^9.0.3",
70 | "pretty-quick": "^3.1.0",
71 | "husky": "^8.0.1",
72 | "stylelint": "~14.11.0",
73 | "stylelint-config-recommended-scss": "~7.0.0",
74 | "stylelint-config-standard": "~28.0.0",
75 | "postcss": "^8.4.5",
76 | "ts-node": "^10.1.0",
77 | "typescript": "~4.7.4"
78 | },
79 | "prettier": {
80 | "singleQuote": true,
81 | "overrides": [
82 | {
83 | "files": "*.scss",
84 | "options": {
85 | "singleQuote": false
86 | }
87 | }
88 | ]
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/app/auth/authentication.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed, fakeAsync, tick } from '@angular/core/testing';
2 |
3 | import { AuthenticationService } from './authentication.service';
4 | import { CredentialsService, Credentials } from './credentials.service';
5 | import { MockCredentialsService } from './credentials.service.mock';
6 |
7 | describe('AuthenticationService', () => {
8 | let authenticationService: AuthenticationService;
9 | let credentialsService: MockCredentialsService;
10 |
11 | beforeEach(() => {
12 | TestBed.configureTestingModule({
13 | providers: [{ provide: CredentialsService, useClass: MockCredentialsService }, AuthenticationService]
14 | });
15 |
16 | authenticationService = TestBed.inject(AuthenticationService);
17 | credentialsService = TestBed.inject(CredentialsService);
18 | credentialsService.credentials = null;
19 | jest.spyOn(credentialsService, 'setCredentials');
20 | });
21 |
22 | describe('login', () => {
23 | it('should return credentials', fakeAsync(() => {
24 | // Act
25 | const request = authenticationService.login({
26 | username: 'toto',
27 | password: '123'
28 | });
29 | tick();
30 |
31 | // Assert
32 | request.subscribe(credentials => {
33 | expect(credentials).toBeDefined();
34 | expect(credentials.token).toBeDefined();
35 | });
36 | }));
37 |
38 | it('should authenticate user', fakeAsync(() => {
39 | expect(credentialsService.isAuthenticated()).toBe(false);
40 |
41 | // Act
42 | const request = authenticationService.login({
43 | username: 'toto',
44 | password: '123'
45 | });
46 | tick();
47 |
48 | // Assert
49 | request.subscribe(() => {
50 | expect(credentialsService.isAuthenticated()).toBe(true);
51 | expect(credentialsService.credentials).not.toBeNull();
52 | expect((credentialsService.credentials as Credentials).token).toBeDefined();
53 | expect((credentialsService.credentials as Credentials).token).not.toBeNull();
54 | });
55 | }));
56 |
57 | it('should persist credentials for the session', fakeAsync(() => {
58 | // Act
59 | const request = authenticationService.login({
60 | username: 'toto',
61 | password: '123'
62 | });
63 | tick();
64 |
65 | // Assert
66 | request.subscribe(() => {
67 | expect(credentialsService.setCredentials).toHaveBeenCalled();
68 | expect((credentialsService.setCredentials as jest.Mock).mock.calls[0][1]).toBe(undefined);
69 | });
70 | }));
71 |
72 | it('should persist credentials across sessions', fakeAsync(() => {
73 | // Act
74 | const request = authenticationService.login({
75 | username: 'toto',
76 | password: '123',
77 | remember: true
78 | });
79 | tick();
80 |
81 | // Assert
82 | request.subscribe(() => {
83 | expect(credentialsService.setCredentials).toHaveBeenCalled();
84 | expect((credentialsService.setCredentials as jest.Mock).mock.calls[0][1]).toBe(true);
85 | });
86 | }));
87 | });
88 |
89 | describe('logout', () => {
90 | it('should clear user authentication', fakeAsync(() => {
91 | // Arrange
92 | const loginRequest = authenticationService.login({
93 | username: 'toto',
94 | password: '123'
95 | });
96 | tick();
97 |
98 | // Assert
99 | loginRequest.subscribe(() => {
100 | expect(credentialsService.isAuthenticated()).toBe(true);
101 |
102 | const request = authenticationService.logout();
103 | tick();
104 |
105 | request.subscribe(() => {
106 | expect(credentialsService.isAuthenticated()).toBe(false);
107 | expect(credentialsService.credentials).toBeNull();
108 | });
109 | });
110 | }));
111 | });
112 | });
113 |
--------------------------------------------------------------------------------
/angular.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "version": 1,
4 | "newProjectRoot": "projects",
5 | "projects": {
6 | "ng-x-rocket": {
7 | "projectType": "application",
8 | "schematics": {
9 | "@schematics/angular:component": {
10 | "style": "scss"
11 | },
12 | "@schematics/angular:application": {
13 | "strict": true
14 | }
15 | },
16 | "root": "",
17 | "sourceRoot": "src",
18 | "prefix": "app",
19 | "architect": {
20 | "build": {
21 | "builder": "@angular-devkit/build-angular:browser",
22 | "options": {
23 | "outputPath": "dist",
24 | "index": "src/index.html",
25 | "main": "src/main.ts",
26 | "polyfills": "src/polyfills.ts",
27 | "tsConfig": "tsconfig.app.json",
28 | "inlineStyleLanguage": "scss",
29 | "assets": [
30 | "src/favicon.ico",
31 | "src/apple-touch-icon.png",
32 | "src/robots.txt",
33 | "src/manifest.webmanifest",
34 | "src/assets"
35 | ],
36 | "styles": [
37 | "src/main.scss"
38 | ],
39 | "scripts": []
40 | },
41 | "configurations": {
42 | "production": {
43 | "budgets": [
44 | {
45 | "type": "initial",
46 | "maximumWarning": "2mb",
47 | "maximumError": "5mb"
48 | },
49 | {
50 | "type": "anyComponentStyle",
51 | "maximumWarning": "6kb",
52 | "maximumError": "10kb"
53 | }
54 | ],
55 | "serviceWorker": true,
56 | "fileReplacements": [
57 | {
58 | "replace": "src/environments/environment.ts",
59 | "with": "src/environments/environment.prod.ts"
60 | }
61 | ],
62 | "outputHashing": "all"
63 | },
64 | "development": {
65 | "buildOptimizer": false,
66 | "optimization": false,
67 | "vendorChunk": true,
68 | "extractLicenses": false,
69 | "sourceMap": true,
70 | "namedChunks": true
71 | },
72 | "ci": {
73 | "progress": false
74 | }
75 | },
76 | "defaultConfiguration": "production"
77 | },
78 | "serve": {
79 | "builder": "@angular-devkit/build-angular:dev-server",
80 | "configurations": {
81 | "production": {
82 | "browserTarget": "ng-x-rocket:build:production"
83 | },
84 | "development": {
85 | "browserTarget": "ng-x-rocket:build:development"
86 | },
87 | "ci": {
88 | "progress": false
89 | }
90 | },
91 | "defaultConfiguration": "development"
92 | },
93 | "extract-i18n": {
94 | "builder": "@angular-devkit/build-angular:extract-i18n",
95 | "options": {
96 | "browserTarget": "ng-x-rocket:build"
97 | }
98 | },
99 | "test": {
100 | "builder": "@angular-builders/jest:run",
101 | "options": {
102 | "watch": true
103 | },
104 | "configurations": {
105 | "ci": {
106 | "watch": false,
107 | "ci": true,
108 | "coverage": true,
109 | "silent": true
110 | }
111 | }
112 | },
113 | "lint": {
114 | "builder": "@angular-eslint/builder:lint",
115 | "options": {
116 | "lintFilePatterns": [
117 | "src/**/*.ts",
118 | "src/**/*.html"
119 | ]
120 | }
121 | }
122 | }
123 | }
124 | },
125 | "defaultProject": "ng-x-rocket"
126 | }
127 |
--------------------------------------------------------------------------------
/src/app/i18n/i18n.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed } from '@angular/core/testing';
2 | import { TranslateService, LangChangeEvent } from '@ngx-translate/core';
3 | import { Subject } from 'rxjs';
4 |
5 | import { I18nService } from './i18n.service';
6 |
7 | const defaultLanguage = 'en-US';
8 | const supportedLanguages = ['eo', 'en-US', 'fr-FR'];
9 |
10 | class MockTranslateService {
11 |
12 | currentLang = '';
13 | onLangChange = new Subject();
14 |
15 | use(language: string) {
16 | this.currentLang = language;
17 | this.onLangChange.next({
18 | lang: this.currentLang,
19 | translations: {}
20 | });
21 | }
22 |
23 | getBrowserCultureLang() {
24 | return 'en-US';
25 | }
26 |
27 | setTranslation(lang: string, translations: object, shouldMerge?: boolean) { }
28 |
29 | }
30 |
31 | describe('I18nService', () => {
32 | let i18nService: I18nService;
33 | let translateService: TranslateService;
34 | let onLangChangeSpy: jest.Mock;
35 |
36 | beforeEach(() => {
37 | TestBed.configureTestingModule({
38 | providers: [
39 | I18nService,
40 | { provide: TranslateService, useClass: MockTranslateService },
41 | ]
42 | });
43 |
44 | i18nService = TestBed.inject(I18nService);
45 | translateService = TestBed.inject(TranslateService);
46 |
47 | // Create spies
48 | onLangChangeSpy = jest.fn();
49 | translateService.onLangChange
50 | .subscribe((event: LangChangeEvent) => {
51 | onLangChangeSpy(event.lang);
52 | });
53 | jest.spyOn(translateService, 'use');
54 | });
55 |
56 | afterEach(() => {
57 | // Cleanup
58 | localStorage.removeItem('language');
59 | });
60 |
61 | describe('init', () => {
62 | it('should init with default language', () => {
63 | // Act
64 | i18nService.init(defaultLanguage, supportedLanguages);
65 |
66 | // Assert
67 | expect(translateService.use).toHaveBeenCalledWith(defaultLanguage);
68 | expect(onLangChangeSpy).toHaveBeenCalledWith(defaultLanguage);
69 | });
70 |
71 | it('should init with save language', () => {
72 | // Arrange
73 | const savedLanguage = 'eo';
74 | localStorage.setItem('language', savedLanguage);
75 |
76 | // Act
77 | i18nService.init(defaultLanguage, supportedLanguages);
78 |
79 | // Assert
80 | expect(translateService.use).toHaveBeenCalledWith(savedLanguage);
81 | expect(onLangChangeSpy).toHaveBeenCalledWith(savedLanguage);
82 | });
83 | });
84 |
85 | describe('set language', () => {
86 | it('should change current language', () => {
87 | // Arrange
88 | const newLanguage = 'eo';
89 | i18nService.init(defaultLanguage, supportedLanguages);
90 |
91 | // Act
92 | i18nService.language = newLanguage;
93 |
94 | // Assert
95 | expect(translateService.use).toHaveBeenCalledWith(newLanguage);
96 | expect(onLangChangeSpy).toHaveBeenCalledWith(newLanguage);
97 | });
98 |
99 | it('should change current language without a region match', () => {
100 | // Arrange
101 | const newLanguage = 'fr-CA';
102 | i18nService.init(defaultLanguage, supportedLanguages);
103 |
104 | // Act
105 | i18nService.language = newLanguage;
106 |
107 | // Assert
108 | expect(translateService.use).toHaveBeenCalledWith('fr-FR');
109 | expect(onLangChangeSpy).toHaveBeenCalledWith('fr-FR');
110 | });
111 |
112 | it('should change current language to default if unsupported', () => {
113 | // Arrange
114 | const newLanguage = 'es';
115 | i18nService.init(defaultLanguage, supportedLanguages);
116 |
117 | // Act
118 | i18nService.language = newLanguage;
119 |
120 | // Assert
121 | expect(translateService.use).toHaveBeenCalledWith(defaultLanguage);
122 | expect(onLangChangeSpy).toHaveBeenCalledWith(defaultLanguage);
123 | });
124 | });
125 |
126 | describe('get language', () => {
127 | it('should return current language', () => {
128 | // Arrange
129 | i18nService.init(defaultLanguage, supportedLanguages);
130 |
131 | // Act
132 | const currentLanguage = i18nService.language;
133 |
134 | // Assert
135 | expect(currentLanguage).toEqual(defaultLanguage);
136 | });
137 | });
138 |
139 | });
140 |
--------------------------------------------------------------------------------
/docs/coding-guides/sass.md:
--------------------------------------------------------------------------------
1 | # Sass coding guide
2 |
3 | [Sass](http://sass-lang.com) is a superset of CSS, which brings a lot of developer candy to help scaling CSS in large
4 | projects and keeping it maintainable.
5 |
6 | The main benefits of using Sass over plain CSS are *variables*, *nesting* and *mixins*, see the
7 | [basics guide](http://sass-lang.com/guide) for more details.
8 |
9 | > Note that this project use the newer, CSS-compatible **SCSS** syntax over the old
10 | [indented syntax](http://sass-lang.com/documentation/file.INDENTED_SYNTAX.html).
11 |
12 | ## Naming conventions
13 |
14 | - In the CSS world, everything should be named in `kebab-case` (lowercase words separated with a `-`).
15 | - File names should always be in `kebab-case`
16 |
17 | ## Coding rules
18 |
19 | - Use single quotes `'` for strings
20 | - Use this general nesting hierarchy when constructing your styles:
21 | ```scss
22 | // The base component class acts as the namespace, to avoid naming and style collisions
23 | .my-component {
24 | // Put here all component elements (flat)
25 | .my-element {
26 | // Use a third-level only for modifiers and state variations
27 | &.active { ... }
28 | }
29 | }
30 | ```
31 | Note that with
32 | [Angular view encapsulation](https://angular.io/docs/ts/latest/guide/component-styles.html#!#view-encapsulation),
33 | the first "namespace" level of nesting is not necessary as Angular takes care of the scoping for avoid collisions.
34 |
35 | > As a side note, we are aware of the [BEM naming approach](https://en.bem.info/tools/bem/bem-naming/), but we found
36 | it impractical for large projects. The nesting approach has drawbacks such as increased specificity, but it helps
37 | keeping everything nicely organized, and more importantly, *scoped*.
38 |
39 |
40 | Also keep in mind this general rules:
41 | - Always use **class selectors**, never use ID selectors and avoid element selectors whenever possible
42 | - No more than **3 levels** of nesting
43 | - No more than **3 qualifiers**
44 |
45 | ## Best practices
46 |
47 | - Use object-oriented CSS (OOCSS):
48 | * Factorize common code in base class, and extend it, for example:
49 | ```scss
50 | // Base button class
51 | .btn { ... }
52 |
53 | // Color variation
54 | .btn-warning { ... }
55 |
56 | // Size variation
57 | .btn-small { ... }
58 | ```
59 | * Try to name class by semantic, not style nor function for better reusability:
60 | Use `.btn-warning`, not `btn-orange` nor `btn-cancel`
61 | * Avoid undoing style, refactor using common base classes and extensions
62 |
63 | - Keep your style scoped
64 | * Clearly separate **global** (think *framework*) and **components** style
65 | * Global style should only go in `src/theme/`, never in components
66 | * Avoid style interactions between components, if some style may need to be shared, refactor it as a framework
67 | component in put it in your global theme.
68 | * Avoid using wider selectors than needed: always use classes if you can!
69 |
70 | - Avoid rules multiplication
71 | * The less CSS the better, factorize rules whenever it's possible
72 | * CSS is code, and like any code frequent refactoring is healthy
73 |
74 | - When ugly hacks cannot be avoided, create an explicit `src/hacks.scss` file and put it in:
75 | * These ugly hacks should only be **temporary**
76 | * Each hack should be documented with the author name, the problem and hack reason
77 | * Limit this file to a reasonable length (~100 lines) and refactor hacks with proper solutions when the limit is
78 | reached.
79 |
80 | ## Pitfalls
81 |
82 | - Never use the `!important` keyword. Ever.
83 | - Never use **inline** style in html, even *just for debugging* (because we **KNOW** it will end up in your commit)
84 |
85 | ## Browser compatibility
86 |
87 | You should never use browser-specific prefixes in your code, as [autoprefixer](https://github.com/postcss/autoprefixer)
88 | takes care of that part for you during the build process.
89 | You just need to declare which browsers you target in the [`browserslist`](https://github.com/ai/browserslist) file.
90 |
91 | ## Enforcement
92 |
93 | Coding rules are enforced in this project with [stylelint](https://stylelint.io).
94 | This tool also checks the compatibility of the rules used against the browsers you are targeting (specified in the
95 | [`browserslist`](https://github.com/ai/browserslist) file), via [doiuse](https://github.com/anandthakker/doiuse).
96 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ngX Starter Kit
2 |
3 | Web project starter kit including modern tools and workflow based on
4 | [angular-cli](https://github.com/angular/angular-cli), best practices from the community, a scalable base template and
5 | a good learning base.
6 |
7 | Generated using [ngX-Rocket](https://github.com/ngx-rocket/generator-ngx-rocket).
8 |
9 | ### Benefits
10 |
11 | - Quickstart a project in seconds and focus on features, not on frameworks or tools
12 |
13 | - Industrial-grade tools, ready for usage in a continuous integration environment and DevOps
14 |
15 | - Scalable architecture with base app template including example components, services and tests
16 |
17 | # Getting started
18 |
19 | 1. Go to project folder and install dependencies:
20 | ```bash
21 | npm install
22 | ```
23 |
24 | 2. Launch development server, and open `localhost:4200` in your browser:
25 | ```bash
26 | npm start
27 | ```
28 |
29 | # Project structure
30 |
31 | ```
32 | dist/ compiled version
33 | docs/ project docs and coding guides
34 | e2e/ end-to-end tests
35 | src/ project source code
36 | |- app/ app components
37 | | |- @shared/ shared module (common services, components, directives and pipes)
38 | | |- app.component.* app root component (shell)
39 | | |- app.module.ts app root module definition
40 | | |- app-routing.module.ts app routes
41 | | +- ... additional modules and components
42 | |- assets/ app assets (images, fonts, sounds...)
43 | |- environments/ values for various build environments
44 | |- theme/ app global scss variables and theme
45 | |- translations/ translations files
46 | |- index.html html entry point
47 | |- main.scss global style entry point
48 | |- main.ts app entry point
49 | |- polyfills.ts polyfills needed by Angular
50 | +- test.ts unit tests entry point
51 | reports/ test and coverage reports
52 | proxy.conf.js backend proxy configuration
53 | ```
54 |
55 | # Main tasks
56 |
57 | Task automation is based on [NPM scripts](https://docs.npmjs.com/misc/scripts).
58 |
59 | Tasks | Description
60 | ------------------------------|---------------------------------------------------------------------------------------
61 | npm start | Run development server on `http://localhost:4200/`
62 | npm run build [-- --env=prod] | Lint code and build app for production in `dist/` folder
63 | npm test | Run unit tests via [Karma](https://karma-runner.github.io) in watch mode
64 | npm run test:ci | Lint code and run unit tests once for continuous integration
65 | npm run e2e | Run e2e tests using [Protractor](http://www.protractortest.org)
66 | npm run lint | Lint code
67 | npm run translations:extract | Extract strings from code and templates to `src/app/translations/template.json`
68 | npm run docs | Display project documentation
69 |
70 | When building the application, you can specify the target environment using the additional flag `--env ` (do not
71 | forget to prepend `--` to pass arguments to npm scripts).
72 |
73 | The default build environment is `prod`.
74 |
75 | ## Development server
76 |
77 | Run `npm start` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change
78 | any of the source files.
79 | You should not use `ng serve` directly, as it does not use the backend proxy configuration by default.
80 |
81 | ## Code scaffolding
82 |
83 | Run `npm run generate -- component ` to generate a new component. You can also use
84 | `npm run generate -- directive|pipe|service|class|module`.
85 |
86 | If you have installed [angular-cli](https://github.com/angular/angular-cli) globally with `npm install -g @angular/cli`,
87 | you can also use the command `ng generate` directly.
88 |
89 | ## Additional tools
90 |
91 | Tasks are mostly based on the `angular-cli` tool. Use `ng help` to get more help or go check out the
92 | [Angular-CLI README](https://github.com/angular/angular-cli).
93 |
94 | # What's in the box
95 |
96 | The app template is based on [HTML5](http://whatwg.org/html), [TypeScript](http://www.typescriptlang.org) and
97 | [Sass](http://sass-lang.com). The translation files use the common [JSON](http://www.json.org) format.
98 |
99 | #### Tools
100 |
101 | Development, build and quality processes are based on [angular-cli](https://github.com/angular/angular-cli) and
102 | [NPM scripts](https://docs.npmjs.com/misc/scripts), which includes:
103 |
104 | - Optimized build and bundling process with [Webpack](https://webpack.github.io)
105 | - [Development server](https://webpack.github.io/docs/webpack-dev-server.html) with backend proxy and live reload
106 | - Cross-browser CSS with [autoprefixer](https://github.com/postcss/autoprefixer) and
107 | [browserslist](https://github.com/ai/browserslist)
108 | - Asset revisioning for [better cache management](https://webpack.github.io/docs/long-term-caching.html)
109 | - Unit tests using [Jasmine](http://jasmine.github.io) and [Karma](https://karma-runner.github.io)
110 | - End-to-end tests using [Protractor](https://github.com/angular/protractor)
111 | - Static code analysis: [TSLint](https://github.com/palantir/tslint), [Codelyzer](https://github.com/mgechev/codelyzer),
112 | [Stylelint](http://stylelint.io) and [HTMLHint](http://htmlhint.com/)
113 | - Local knowledgebase server using [Hads](https://github.com/sinedied/hads)
114 | - Automatic code formatting with [Prettier](https://prettier.io)
115 |
116 | #### Libraries
117 |
118 | - [Angular](https://angular.io)
119 | - [Bootstrap](https://getbootstrap.com)
120 | - [Font Awesome](http://fontawesome.io)
121 | - [RxJS](http://reactivex.io/rxjs)
122 | - [ng-bootstrap](https://ng-bootstrap.github.io)
123 | - [ngx-translate](https://github.com/ngx-translate/core)
124 | - [Lodash](https://lodash.com)
125 |
126 | #### Coding guides
127 |
128 | - [Angular](docs/coding-guides/angular.md)
129 | - [TypeScript](docs/coding-guides/typescript.md)
130 | - [Sass](docs/coding-guides/sass.md)
131 | - [HTML](docs/coding-guides/html.md)
132 | - [Unit tests](docs/coding-guides/unit-tests.md)
133 | - [End-to-end tests](docs/coding-guides/e2e-tests.md)
134 |
135 | #### Other documentation
136 |
137 | - [I18n guide](docs/i18n.md)
138 | - [Working behind a corporate proxy](docs/corporate-proxy.md)
139 | - [Updating dependencies and tools](docs/updating.md)
140 | - [Using a backend proxy for development](docs/backend-proxy.md)
141 | - [Browser routing](docs/routing.md)
142 |
143 | # License
144 |
145 | [MIT](https://github.com/ngx-rocket/generator-ngx-rocket/blob/main/LICENSE)
146 |
--------------------------------------------------------------------------------
/docs/coding-guides/angular.md:
--------------------------------------------------------------------------------
1 | # Introduction to Angular and modern design patterns
2 |
3 | [Angular](https://angular.io) (aka Angular 2, 4, 5, 6...) is a new framework completely rewritten from the ground up,
4 | replacing the now well-known [AngularJS](https://angularjs.org) framework (aka Angular 1.x).
5 |
6 | More that just a framework, Angular should now be considered as a whole *platform* which comes with a complete set of
7 | tools, like its own [CLI](https://github.com/angular/angular-cli), [debug utilities](https://augury.angular.io) or
8 | [performance tools](https://github.com/angular/angular/tree/master/packages/benchpress).
9 |
10 | Angular has been around for some time now, but I still get the feeling that it’s not getting the love it deserved,
11 | probably because of other players in the field like React or VueJS. While the simplicity behind these frameworks can
12 | definitely be attractive, they lack in my opinion what is essential when making big, enterprise-grade apps: a solid
13 | frame to lead both experienced developers and beginners in the same direction and a rational convergence of tools,
14 | patterns and documentation. Yes, the Angular learning curve may seems a little steep, but it’s definitely worth it.
15 |
16 | ## Getting started
17 |
18 | #### Newcomer
19 |
20 | If you're new to Angular you may feel overwhelmed by the quantity of new concepts to apprehend, so before digging
21 | into this project you may want to start with [this progressive tutorial](https://angular.io/tutorial) that will guide
22 | you step by step into building a complete Angular application.
23 |
24 | #### AngularJS veteran
25 |
26 | If you come from AngularJS and want to dig straight in the new version, you may want to take a look at the
27 | [AngularJS vs 2 quick reference](https://angular.io/guide/ajs-quick-reference).
28 |
29 | #### Cheatsheet
30 |
31 | Until you know the full Angular API by heart, you may want to keep this
32 | [cheatsheet](https://angular.io/guide/cheatsheet) that resumes the syntax and features on a single page at hand.
33 |
34 | ## Style guide
35 |
36 | This project follows the standard [Angular style guide](https://angular.io/guide/styleguide).
37 |
38 | More that just coding rules, this style guide also gives advices and best practices for a good application architecture
39 | and is an **essential reading** for starters. Reading deeper, you can even find many explanations for some design
40 | choices of the framework.
41 |
42 | ## FAQ
43 |
44 | There is a lot to dig in Angular and some questions frequently bother people. In fact, most of unclear stuff seems to be
45 | related to modules, for example the dreaded
46 | [**"Core vs Shared modules"**](https://angular.io/guide/ngmodule-faq#what-kinds-of-modules-should-i-have-and-how-should-i-use-them)
47 | question.
48 |
49 | The guys at Angular may have noticed that since you can now find
50 | [a nice FAQ on their website](https://angular.io/guide/ngmodule-faq#ngmodule-faqs) answering all the common questions
51 | regarding modules. Don't hesitate to take a look at it, even if you think you are experienced enough with Angular :wink:.
52 |
53 | ## Going deeper
54 |
55 | Even though they are not mandatory, Angular was designed for the use of design patterns you may not be accustomed to,
56 | like [reactive programming](#reactive-programming), [unidirectional data flow](#unidirectional-data-flow) and
57 | [centralized state management](#centralized-state-management).
58 |
59 | These concepts are difficult to resume in a few words, and despite being tightly related to each other they concern
60 | specific parts of an application flow, each being quite deep to learn on its own.
61 |
62 | You will essentially find here a list of good starting points to learn more on these subjects.
63 |
64 | #### Reactive programming
65 |
66 | You may not be aware of it, but Angular is now a *reactive system* by design.
67 | Although you are not forced to use reactive programming patterns, they make the core of the framework and it is
68 | definitely recommended to learn them if you want to leverage the best of Angular.
69 |
70 | Angular uses [RxJS](http://reactivex.io/rxjs/) to implement the *Observable* pattern.
71 |
72 | > An *Observable* is a stream of asynchronous events that can be processed with array-like operators.
73 |
74 | ##### From promises to observables
75 |
76 | While AngularJS used to rely heavily on [*Promises*](https://docs.angularjs.org/api/ng/service/$q) to handle
77 | asynchronous events, *Observables* are now used instead in Angular. Even though in specific cases like for HTTP
78 | requests, an *Observable* can be converted into a *Promise*, it is recommended to embrace the new paradigm as it can a
79 | lot more than *Promises*, with way less code. This transition is also explained in the
80 | [Angular tutorial](https://angular.io/tutorial/toh-pt6#!%23observables).
81 | Once you have made the switch, you will never look back again.
82 |
83 | ##### Learning references
84 |
85 | - [What is reactive programming?](http://paulstovell.com/blog/reactive-programming), explained nicely through a simple
86 | imaged story *(5 min)*
87 |
88 | - [The introduction to reactive programming you've been missing](https://gist.github.com/staltz/868e7e9bc2a7b8c1f754),
89 | the title says it all *(30 min)*
90 |
91 | - [Functional reactive programming for Angular 2 developers](http://blog.angular-university.io/functional-reactive-programming-for-angular-2-developers-rxjs-and-observables/),
92 | see the functional reactive programming principles in practice with Angular *(15 min)*
93 |
94 | - [RxMarbles](http://rxmarbles.com), a graphical representation of Rx operators that greatly help to understand their
95 | usage
96 |
97 | #### Unidirectional data flow
98 |
99 | In opposition with AngularJS where one of its selling points was two-way data binding which ended up causing a lot of
100 | major headaches for complex applications, Angular now enforces unidirectional data flow.
101 |
102 | What does it means? Said with other words, it means that change detection cannot cause cycles, which was one of
103 | AngularJS problematic points. It also helps to maintain simpler and more predictable data flows in applications, along
104 | with substantial performance improvements.
105 |
106 | **Wait, then why the Angular documentation have mention of a
107 | [two-way binding syntax](https://angular.io/guide/template-syntax#binding-syntax-an-overview)?**
108 |
109 | If you look closely, the new two-way binding syntax is just syntactic sugar to combine two *one-way* bindings (a
110 | *property* and *event* binding), keeping the data flow unidirectional.
111 |
112 | This change is really important, as it was often the cause of performance issues with AngularJS, and it one of the
113 | pillars enabling better performance in new Angular apps.
114 |
115 | While Angular tries to stay *pattern-agnostic* and can be used with conventional MV* patterns, it was designed with
116 | reactive programming in mind and really shines when used with reactive data flow patterns like
117 | [redux](http://redux.js.org/docs/basics/DataFlow.html),
118 | [Flux](https://facebook.github.io/flux/docs/in-depth-overview.html#content) or
119 | [MVI](http://futurice.com/blog/reactive-mvc-and-the-virtual-dom).
120 |
121 | #### Centralized state management
122 |
123 | As applications grow in size, keeping track of the all its individual components state and data flows can become
124 | tedious, and tend to be difficult to manage and debug.
125 |
126 | The main goal of using a centralized state management is to make state changes *predictable* by imposing certain
127 | restrictions on how and when updates can happen, using *unidirectional data flow*.
128 |
129 | This approach was first made popular with React with introduction of the
130 | [Flux](https://facebook.github.io/flux/docs/in-depth-overview.html#content) architecture. Many libraries emerged then
131 | trying to adapt and refine the original concept, and one of these gained massive popularity by providing a simpler,
132 | elegant alternative: [Redux](http://redux.js.org/docs/basics/DataFlow.html).
133 |
134 | Redux is at the same time a library (with the big *R*) and a design pattern (with the little *r*), the latter being
135 | framework-agnostic and working very well with Angular.
136 |
137 | The *redux* design pattern is based on these [3 principles](http://redux.js.org/docs/introduction/ThreePrinciples.html):
138 |
139 | - The application state is a *single immutable* data structure
140 | - A state change is triggered by an *action*, an object describing what happened
141 | - Pure functions called *reducers* take the previous state and the next action to compute the new state
142 |
143 | The core concepts behind these principles are nicely explained in
144 | [this example](http://redux.js.org/docs/introduction/CoreConcepts.html) *(3 min)*.
145 |
146 | For those interested, the redux pattern was notably inspired by
147 | [The Elm Architecture](https://guide.elm-lang.org/architecture/) and the [CQRS](https://martinfowler.com/bliki/CQRS.html)
148 | pattern.
149 |
150 | ##### Which library to use?
151 |
152 | You can make Angular work with any state management library you like, but your best bet would be to use
153 | [NGXS](http://ngxs.io) or [@ngrx](https://github.com/ngrx/platform). Both works the same as the popular
154 | [Redux](http://redux.js.org) library, but with a tight integration with Angular and [RxJS](http://reactivex.io/rxjs/),
155 | with some nice additional developer utilities.
156 |
157 | NGXS is based on the same concepts as @ngrx, but with less boilerplate and a nicer syntax, making it less intimidating.
158 |
159 | Here are some resources to get started:
160 |
161 | - [Angular NGXS tutorial with example from scratch](https://appdividend.com/2018/07/03/angular-ngxs-tutorial-with-example-from-scratch/),
162 | a guided tutorial for NGXS *(10 min)*
163 |
164 | - [Build a better Angular 2 application with redux and ngrx](http://onehungrymind.com/build-better-angular-2-application-redux-ngrx/),
165 | a nice tutorial for @ngrx *(30 min)*
166 |
167 | - [Comprehensive introduction to @ngrx/store](https://gist.github.com/btroncone/a6e4347326749f938510), an in-depth
168 | walkthrough to this library usage in Angular *(60 min)*
169 |
170 | ##### When to use it?
171 |
172 | You may have noticed that the starter template does not include a centralized state management system out of the box.
173 | Why is that? Well, while there is many benefits from using this pattern, the choice is ultimately up to your team and
174 | what you want to achieve with your app.
175 |
176 | Keep in mind that using a single centralized state for your app introduces a new layer a complexity
177 | [that might not be needed](https://medium.com/@dan_abramov/you-might-not-need-redux-be46360cf367), depending of your
178 | goal.
179 |
180 | ## Optimizing performance
181 |
182 | While the new Angular version resolves by design most of the performance issues that could be experienced with
183 | AngularJS, there is always room for improvements. Just keep in mind that delivering an app with good performance is
184 | often a matter of common sense and sane development practices.
185 |
186 | Here is [a list of key points](https://github.com/mgechev/angular-performance-checklist) to check for in your app to
187 | make sure you deliver the best experience to your customers.
188 |
189 | After going through the checklist, make sure to also run an audit of your page through
190 | [**Lighthouse**](https://developers.google.com/web/tools/lighthouse/), the latest Google tool that gives you meaningful
191 | insight about your app performance, accessibility, mobile compatibility and more.
192 |
193 | ## Keeping Angular up-to-date
194 |
195 | Angular development is moving fast, and updates to the core libs and tools are pushed regularly.
196 |
197 | Fortunately, the Angular team provides tools to help you follow through the updates:
198 |
199 | - `npm run ng update` allows you to update your app and its dependencies
200 |
201 | - The [Angular update website](https://update.angular.io) guides you through Angular changes and migrations, providing
202 | step by step guides from one version to another.
203 |
--------------------------------------------------------------------------------
/docs/coding-guides/build-specific-configurations.md:
--------------------------------------------------------------------------------
1 | # Build-Specific Configuration
2 |
3 | ## tl;dr's
4 |
5 | ngx-rocket comes with a very helpful `env` script that will save environment-variables set at build time to constants
6 | that can be used as configuration for your code. When combined with the `dotenv-cli` package, it enables maximum
7 | configurability while maintaining lots of simplicity for local development and testing.
8 |
9 | ### Cookbook for maximum independence of deployment-specific configuration
10 |
11 | Disclaimer: If you have a full-stack app in a monorepo, keep separate `.env` files for server-side and client-side
12 | configs, and make sure `.env` files are .gitignore'd and that secrets never make it into client-side `.env` file.
13 |
14 | For each configurable variable (e.g. BROWSER_URL, API_URL):
15 |
16 | - Add it to package.json's env script so that the build-time variables will be saved for runtime:
17 |
18 | ```javascript
19 | {
20 | "scripts": {
21 | "env": "ngx-scripts env npm_package_version BROWSER_URL API_URL",
22 | }
23 | }
24 | ```
25 |
26 | - Add it to or edit it in src/environments/environment.ts to expose it to your app as e.g. environment.API_URL:
27 | ```typescript
28 | export const environment = {
29 | // ...
30 | API_URL: env.API_URL,
31 | BROWSER_URL: env.BROWSER_URL,
32 | // ...
33 | }
34 | ```
35 | - Configure your CI's deployment to set the variables and export them to the build script before building - if your CI
36 | gives you a shell script to run, make it something like this:
37 | ```shell
38 | # bourne-like shells...
39 | export API_URL='https://api.staging.example.com'
40 | export BROWSER_URL='https://staging.example.com'
41 | # ...
42 | npm run build:ssr-and-client
43 | ```
44 | - Finally, to have your cake and eat it too and avoid having to do all that for local development and testing (or clutter
45 | your package.json up), install the `dotenv-cli` package and update your development-related npm scripts to take advantage
46 | of it:
47 | ```shell
48 | # environment.development.env.sh
49 | BROWSER_URL='http://localhost:4200'
50 | API_URL='http://localhost:4200'
51 | ```
52 | ```javascript
53 | {
54 | "scripts": {
55 | "start": "dotenv -e environment.development.env.sh -- npm run env && ng serve --aot",
56 | }
57 | }
58 | ```
59 |
60 | This way, app configurations will always come from deploy-specific environment variables, and your development environments
61 | are still easy to work with.
62 |
63 | For configuring the build itself (for example, if you want your QA build to be similar to your production build, but with
64 | source maps enabled), consider avoiding adding a build configuration to angular.json, and instead adding the respective
65 | overriding flag to the `ng` command in package.json:
66 | ```javascript
67 | {
68 | "scripts": {
69 | "build:client-and-server-bundles:qa": "NG_BUILD_OVERRIDES='--sourceMap=true' npm run build:client-and-server-bundles",
70 | "build:client-and-server-bundles": "npm run build:client-bundles && npm run build:server-bundles",
71 | "build:client-bundles": "npm run env && ng build --prod $NG_BUILD_OVERRIDES",
72 | }
73 | }
74 | ```
75 |
76 | The development server API proxy config can read runtime environment variables, so you can avoid having a superficial
77 | dev-server configuration by taking advantage of them:
78 | ```javascript
79 | {
80 | "scripts": {
81 | "start": "dotenv -e environment.development.env.sh -- npm run env && API_PROXY_HOST='http://localhost:9000' ng serve --aot",
82 | }
83 | }
84 | ```
85 | ```javascript
86 | const proxyConfig = [
87 | {
88 | context: '/api',
89 | pathRewrite: { '^/api': '' },
90 | target: `${process.env.API_PROXY_HOST}/api`,
91 | changeOrigin: true,
92 | secure: false,
93 | },
94 | {
95 | context: '/auth',
96 | pathRewrite: { '^/auth': '' },
97 | target: `${process.env.API_PROXY_HOST}/auth`,
98 | changeOrigin: true,
99 | secure: false,
100 | },
101 | ];
102 | ```
103 |
104 | Quick SSR note: SSR works by building all the client bundles like normal, but then rendering them in real-time. So,
105 | - the rest of your app from `main.server.ts` down has access to your build-time environment only, like your normal
106 | client bundles
107 | - but `server.ts` (the file configuring and running express) has access to your serve-time environment variables
108 |
109 | ### Less optimal alternatives
110 |
111 | - On the opposite extreme of the spectrum, you can keep all build-specific configuration in a separate environment
112 | file for each environment using Angular's built-in `fileReplacements`, but then you'll need a separate environment
113 | file even for deployment-specific configuration (like hostnames), which can get out of hand fast.
114 | - For a middle-of-the-road approach, you can divide configuration into two groups:
115 | * Configuration shared by each environment-type:
116 | - Environment-type examples include local development, staging/qa, test, production...
117 | - Examples of configuration like this include:
118 | * In test, animations are always disabled, but for all other environments, they're enabled
119 | * In production, the payment gateway's publishable key is the live key, but all other environments use the
120 | test key
121 | * Configuration that sometimes needs to be specific to an individual deployment of a given environment:
122 | - Examples of configuration like this include:
123 | * This particular staging/qa server's base for constructing URLs is qa-stable.example.com, but qa/staging
124 | environments could also be deployed to preprod.example.com or localhost:8081 or anywhereelse:7000.
125 | * This particular deployment uses a specific bucket for Amazon S3 uploads
126 | * In this approach, you can use Angular's `fileReplacements` for anything environment-specific and ngx-rocket's
127 | `env` for anything deployment-specific. You can even have certain deployment-specific configuration fall back
128 | to environment-specific defaults for certain environments like so:
129 | ```javascript
130 | export const environment = {
131 | // ...
132 | BROWSER_URL: env.BROWSER_URL || 'https://qa.example.com',
133 | // ...
134 | }
135 | ```
136 | - If you don't have lots of environment variables, you can avoid dotenv-cli and use your particular shell's method
137 | to expose the variables before running the ngx-rocket env tool.
138 |
139 | ## Introduction
140 |
141 | When building any maintainable application, a separation of configuration and code is always desired. In the case
142 | of configuration, some of it will need to vary from environment to environment, build to build, or deployment to
143 | deployment.
144 |
145 | This guide focuses on this type of build-specific configuration in a very broad sense of an Angular app, describing
146 | the specific Angular ways of controlling these configurations, detailing some angular-specific challenges, and
147 | highlighting some ngx-rocket tooling that help with them in mind.
148 |
149 | For an even broader non-Angular introduction of these concepts, see the
150 | [The Twelve-Factor App](https://12factor.net/config) methodology's opinions on how this type of configuration
151 | should be managed.
152 |
153 | ## Types of configuration
154 |
155 | At the highest level, build-specific configuration can be divided into two categories:
156 |
157 | 1. Configuration for how your app is built and served
158 | 2. Configuration used by your codebase
159 |
160 | ### Configuration for how your app is built and served
161 |
162 | This type of build-specific configuration is not used by your code, but is used to control the build system itself.
163 | Configuration like this goes into Angular's
164 | [workspace configuration](https://angular.io/guide/workspace-config#alternate-build-configurations). Instead of
165 | rehashing existing documentation on this, this document will highlight how it relates to this subject. Namely, the
166 | fact that in addition to specifying *HOW* the app is built for each build configuration, the workspace configuration
167 | allows mapping each build configuration to a separate environment configuration file for your codebase as well. It
168 | also allows for making separate dev-server configurations in case you need to run it differently.
169 |
170 | Therefore, each build configuration in the workspace configuration file is a tuple of
171 | (how-to-build, environment-file-for-codebase), and you'll need a separate configuration for each combination.
172 |
173 | ## Angular's out-of-the-box environment configuration
174 |
175 | ### When it works well
176 |
177 | This setup works quite well for configuration that's shared among all instances of an environment, like the following
178 | examples:
179 |
180 | - **test** environment always builds without source maps, disables animations, uses a recaptcha test key, and disables
181 | analytics
182 | - **dev** environment always builds with source maps, enables animations, uses a recaptcha live key, and disables
183 | analytics
184 | - **qa** environment always builds with source maps, enables animations, uses a recaptcha live key, and disables
185 | analytics
186 | - **prod** environment always builds without source maps, enables animations, uses a recaptcha live key, and enables
187 | analytics
188 |
189 | ### Limitations of Angular's `fileReplacements`
190 |
191 | But for certain deployment-specific configuration, things start to get really hairy, like in these examples:
192 |
193 | - QA build configuration needs to be built for local deployment, deployment to a server on the internet for QA
194 | purposes, and also deployment to another server on the internet for staging purposes
195 | - Production build needs multiple different deployments of the same app to different servers
196 |
197 | These cases can cause problems when:
198 |
199 | - Each deployment needs a separate API URL
200 | - Each deployment needs a separate URL for building its own URLs to where it's deployed
201 | - Each deployment needs separate API keys, bucket names, etc
202 |
203 | You *COULD* start creating separate configurations for each deployment, each with its own `fileReplacements`, but that
204 | would be really messy.
205 |
206 | ### Workarounds that don't work well
207 |
208 | One workaround would be to keep such configurations as globals in a separate deployment-specific script file. But
209 | that's pretty messy too. More importantly, there are limitations to where they can be used. For example, because
210 | of AOT, such configuration variables cannot be used in Angular's decorators, because they're not statically
211 | analyzable (i.e. their values knowable at build-time). So it would be better if we can keep everything in the same place.
212 |
213 | ### ngx-rocket to the rescue
214 |
215 | The ngx-rocket `env` task solves this problem really well, and avoids the need for separate `environment.ts` files for
216 | deployment-specific configuration.
217 |
218 | To add a deployment-specific configuration:
219 |
220 | 1. edit the existing `environment.ts` files for whichever environments you'd like to make that variable
221 | deployment-specific for by having it come from the imported "env" object - pro tip: you can even make it fall
222 | back to an environment-based default and still be statically analyzable!
223 | 2. add that variable name to the npm script's `env` task
224 |
225 | Now, as long as you have that environment variable set in the shell running the build, the `env` task will save it into
226 | the `.env.ts` file before building.
227 |
228 | If you really want, you can take things even further to the twelve-factor extreme, and you can even eliminate the
229 | need for `fileReplacements` entirely, and make all configuration come from environment variables. Whether this will be
230 | the right approach for your project will be up to you.
231 |
232 | This makes separate deployments awesome and flexible, but unfortunately makes things a little bit of a hassle for your
233 | local development, test, etc. environments because you have the burden of providing all those keys, settings, etc. as
234 | environment variables.
235 |
236 | To avoid having to do that, you'll can create a .gitignore'd `.env` file with all the variables set, and source it
237 | with your shell (e.g. `source .env.sh && npm env` in bourne-like shells or `env.bat; npm env` in windows).
238 | ```shell
239 | # bourne-like .env.sh
240 | export BROWSER_URL=localhost:4200
241 | ```
242 | ```shell
243 | REM windows env.bat
244 | SET BROWSER_URL=localhost:4200
245 | ```
246 |
247 | Luckily for us, there's a package called `dotenv-cli` that uses the `dotenv` package and does this in a cleaner and
248 | cross-platform way and comes with even more bells and whistles. You should use that instead, and make your env file
249 | like this instead:
250 | ```shell
251 | BROWSER_URL=localhost:4200
252 | ```
253 |
254 | ## When you can use environment variables directly without ngx-rocket `env`
255 |
256 | As a sidenote, ngx-rocket `env` isn't used for the proxy config file, because it isn't built and ran separately.
257 | Fortunately, for that same reason, you can directly use `process.env` within the proxy config file to avoid having
258 | separate proxy configs in most cases.
259 |
260 | On that same note, the `server.ts` for SSR builds can also access `process.env` as it's set at runtime. But keep in mind
261 | that it stops there - the app itself is built, so even in SSR the client app can't access process environment variables.
262 |
263 | ## Security Considerations
264 |
265 | Never forget that your entire Angular app goes to the client, including its configuration, including the environment
266 | variables you pass to the env task! As usual, you should **never add sensitive keys or secrets to the env task**.
267 |
268 | Finally, if your Angular project is the client-side of a full-stack monorepo, make sure to keep the client-side `.env`
269 | file separate from the server-side `.env` file, since your server-side is bound to have secrets.
270 |
--------------------------------------------------------------------------------