Konstantinos Kamaropoulos
5 years ago
commit
e30571af21
111 changed files with 19850 additions and 0 deletions
@ -0,0 +1,15 @@ |
|||
# Editor configuration, see http://editorconfig.org |
|||
root = true |
|||
|
|||
[*] |
|||
charset = utf-8 |
|||
indent_style = space |
|||
indent_size = 2 |
|||
insert_final_newline = true |
|||
trim_trailing_whitespace = true |
|||
end_of_line = lf |
|||
max_line_length = 120 |
|||
|
|||
[*.md] |
|||
max_line_length = off |
|||
trim_trailing_whitespace = false |
@ -0,0 +1,60 @@ |
|||
# See http://help.github.com/ignore-files/ for more about ignoring files. |
|||
|
|||
# Compiled output |
|||
/dist |
|||
/tmp |
|||
/out-tsc |
|||
# Only exists if Bazel was run |
|||
/bazel-out |
|||
|
|||
# Dependencies |
|||
/node_modules |
|||
|
|||
# Cordova |
|||
/www |
|||
/plugins |
|||
/platforms |
|||
|
|||
# Electron |
|||
/dist.electron |
|||
/electron.main.js |
|||
|
|||
# IDEs and editors |
|||
.idea/* |
|||
!.idea/runConfigurations/ |
|||
!.idea/codeStyleSettings.xml |
|||
.project |
|||
.classpath |
|||
.c9/ |
|||
*.launch |
|||
.settings/ |
|||
xcuserdata/ |
|||
*.sublime-workspace |
|||
|
|||
# IDE - VSCode |
|||
.vscode/* |
|||
!.vscode/settings.json |
|||
!.vscode/tasks.json |
|||
!.vscode/launch.json |
|||
!.vscode/extensions.json |
|||
|
|||
# Maven |
|||
/target |
|||
/log |
|||
|
|||
# Misc |
|||
/.sass-cache |
|||
/connect.lock |
|||
/coverage |
|||
/libpeerconnection.log |
|||
npm-debug.log |
|||
yarn-error.log |
|||
testem.log |
|||
/typings |
|||
/reports |
|||
/src/translations/template.* |
|||
/src/environments/.env.* |
|||
|
|||
# System Files |
|||
.DS_Store |
|||
Thumbs.db |
@ -0,0 +1,21 @@ |
|||
{ |
|||
"tagname-lowercase": false, |
|||
"attr-lowercase": false, |
|||
"attr-value-double-quotes": true, |
|||
"tag-pair": true, |
|||
"spec-char-escape": true, |
|||
"id-unique": true, |
|||
"src-not-empty": true, |
|||
"attr-no-duplication": true, |
|||
"title-require": true, |
|||
"tag-self-close": true, |
|||
"head-script-disabled": true, |
|||
"doctype-html5": true, |
|||
"id-class-value": "dash", |
|||
"style-disabled": true, |
|||
"inline-style-disabled": true, |
|||
"inline-script-disabled": true, |
|||
"space-tab-mixed-disabled": "true", |
|||
"id-class-ad-disabled": true, |
|||
"attr-unsafe-chars": true |
|||
} |
@ -0,0 +1,48 @@ |
|||
{ |
|||
"extends": [ |
|||
"stylelint-config-standard", |
|||
"stylelint-config-recommended-scss", |
|||
"stylelint-config-prettier" |
|||
], |
|||
"rules": { |
|||
"font-family-name-quotes": "always-where-recommended", |
|||
"function-url-quotes": [ |
|||
"always", |
|||
{ |
|||
"except": ["empty"] |
|||
} |
|||
], |
|||
"selector-attribute-quotes": "always", |
|||
"string-quotes": "double", |
|||
"max-nesting-depth": 3, |
|||
"selector-max-compound-selectors": 3, |
|||
"selector-max-specificity": "0,3,2", |
|||
"declaration-no-important": true, |
|||
"at-rule-no-vendor-prefix": true, |
|||
"media-feature-name-no-vendor-prefix": true, |
|||
"property-no-vendor-prefix": true, |
|||
"selector-no-vendor-prefix": true, |
|||
"value-no-vendor-prefix": true, |
|||
"no-empty-source": null, |
|||
"selector-class-pattern": "[a-z-]+", |
|||
"selector-id-pattern": "[a-z-]+", |
|||
"selector-max-id": 0, |
|||
"selector-no-qualifying-type": true, |
|||
"selector-max-universal": 0, |
|||
"selector-type-no-unknown": [ |
|||
true, |
|||
{ |
|||
"ignore": ["custom-elements", "default-namespace"] |
|||
} |
|||
], |
|||
"selector-pseudo-element-no-unknown": [ |
|||
true, |
|||
{ |
|||
"ignorePseudoElements": ["ng-deep"] |
|||
} |
|||
], |
|||
"unit-whitelist": ["px", "%", "em", "rem", "vw", "vh", "deg", "s"], |
|||
"max-empty-lines": 2, |
|||
"max-line-length": 120 |
|||
} |
|||
} |
@ -0,0 +1,25 @@ |
|||
{ |
|||
"generator-ngx-rocket": { |
|||
"version": "7.1.0", |
|||
"props": { |
|||
"location": "path", |
|||
"strict": false, |
|||
"skipInstall": false, |
|||
"skipQuickstart": false, |
|||
"initGit": true, |
|||
"appName": "Aiolos", |
|||
"target": ["web"], |
|||
"pwa": true, |
|||
"ui": "bootstrap", |
|||
"auth": false, |
|||
"lazy": true, |
|||
"angulartics": false, |
|||
"tools": ["prettier", "hads"], |
|||
"utility": [], |
|||
"projectName": "aiolos", |
|||
"packageManager": "npm", |
|||
"mobile": [], |
|||
"desktop": [] |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,144 @@ |
|||
# Aiolos |
|||
|
|||
This project was generated with [ngX-Rocket](https://github.com/ngx-rocket/generator-ngx-rocket/) |
|||
version 7.1.0 |
|||
|
|||
# Getting started |
|||
|
|||
1. Go to project folder and install dependencies: |
|||
|
|||
```sh |
|||
npm install |
|||
``` |
|||
|
|||
2. Launch development server, and open `localhost:4200` in your browser: |
|||
|
|||
```sh |
|||
npm start |
|||
``` |
|||
|
|||
# Project structure |
|||
|
|||
``` |
|||
dist/ web app production build |
|||
docs/ project docs and coding guides |
|||
e2e/ end-to-end tests |
|||
src/ project source code |
|||
|- app/ app components |
|||
| |- core/ core module (singleton services and single-use components) |
|||
| |- shared/ shared module (common components, directives and pipes) |
|||
| |- app.component.* app root component (shell) |
|||
| |- app.module.ts app root module definition |
|||
| |- app-routing.module.ts app routes |
|||
| +- ... additional modules and components |
|||
|- assets/ app assets (images, fonts, sounds...) |
|||
|- environments/ values for various build environments |
|||
|- theme/ app global scss variables and theme |
|||
|- translations/ translations files |
|||
|- index.html html entry point |
|||
|- main.scss global style entry point |
|||
|- main.ts app entry point |
|||
|- polyfills.ts polyfills needed by Angular |
|||
+- test.ts unit tests entry point |
|||
reports/ test and coverage reports |
|||
proxy.conf.js backend proxy configuration |
|||
``` |
|||
|
|||
# Main tasks |
|||
|
|||
Task automation is based on [NPM scripts](https://docs.npmjs.com/misc/scripts). |
|||
|
|||
| Task | Description | |
|||
| ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | |
|||
| `npm start` | Run development server on `http://localhost:4200/` | |
|||
| `npm run serve:sw` | Run test server on `http://localhost:4200/` with service worker enabled | |
|||
| `npm run build [-- --configuration=production]` | Lint code and build web app for production (with [AOT](https://angular.io/guide/aot-compiler)) in `dist/` folder | |
|||
| `npm test` | Run unit tests via [Karma](https://karma-runner.github.io) in watch mode | |
|||
| `npm run test:ci` | Lint code and run unit tests once for continuous integration | |
|||
| `npm run e2e` | Run e2e tests using [Protractor](http://www.protractortest.org) | |
|||
| `npm run lint` | Lint code | |
|||
| `npm run translations:extract` | Extract strings from code and templates to `src/app/translations/template.json` | |
|||
| `npm run docs` | Display project documentation and coding guides | |
|||
| `npm run prettier` | Automatically format all `.ts`, `.js` & `.scss` files | |
|||
|
|||
When building the application, you can specify the target configuration using the additional flag |
|||
`--configuration <name>` (do not forget to prepend `--` to pass arguments to npm scripts). |
|||
|
|||
The default build configuration is `prod`. |
|||
|
|||
## Development server |
|||
|
|||
Run `npm start` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change |
|||
any of the source files. |
|||
You should not use `ng serve` directly, as it does not use the backend proxy configuration by default. |
|||
|
|||
## Code scaffolding |
|||
|
|||
Run `npm run generate -- component <name>` to generate a new component. You can also use |
|||
`npm run generate -- directive|pipe|service|class|module`. |
|||
|
|||
If you have installed [angular-cli](https://github.com/angular/angular-cli) globally with `npm install -g @angular/cli`, |
|||
you can also use the command `ng generate` directly. |
|||
|
|||
## Additional tools |
|||
|
|||
Tasks are mostly based on the `angular-cli` tool. Use `ng help` to get more help or go check out the |
|||
[Angular-CLI README](https://github.com/angular/angular-cli). |
|||
|
|||
## Code formatting |
|||
|
|||
All `.ts`, `.js` & `.scss` files in this project are formatted automatically using [Prettier](https://prettier.io), |
|||
and enforced via the `test:ci` script. |
|||
|
|||
A pre-commit git hook has been configured on this project to automatically format staged files, using |
|||
(pretty-quick)[https://github.com/azz/pretty-quick], so you don't have to care for it. |
|||
|
|||
You can also force code formatting by running the command `npm run prettier`. |
|||
|
|||
# What's in the box |
|||
|
|||
The app template is based on [HTML5](http://whatwg.org/html), [TypeScript](http://www.typescriptlang.org) and |
|||
[Sass](http://sass-lang.com). The translation files use the common [JSON](http://www.json.org) format. |
|||
|
|||
#### Tools |
|||
|
|||
Development, build and quality processes are based on [angular-cli](https://github.com/angular/angular-cli) and |
|||
[NPM scripts](https://docs.npmjs.com/misc/scripts), which includes: |
|||
|
|||
- Optimized build and bundling process with [Webpack](https://webpack.github.io) |
|||
- [Development server](https://webpack.github.io/docs/webpack-dev-server.html) with backend proxy and live reload |
|||
- Cross-browser CSS with [autoprefixer](https://github.com/postcss/autoprefixer) and |
|||
[browserslist](https://github.com/ai/browserslist) |
|||
- Asset revisioning for [better cache management](https://webpack.github.io/docs/long-term-caching.html) |
|||
- Unit tests using [Jasmine](http://jasmine.github.io) and [Karma](https://karma-runner.github.io) |
|||
- End-to-end tests using [Protractor](https://github.com/angular/protractor) |
|||
- Static code analysis: [TSLint](https://github.com/palantir/tslint), [Codelyzer](https://github.com/mgechev/codelyzer), |
|||
[Stylelint](http://stylelint.io) and [HTMLHint](http://htmlhint.com/) |
|||
- Local knowledgebase server using [Hads](https://github.com/sinedied/hads) |
|||
- Automatic code formatting with [Prettier](https://prettier.io) |
|||
|
|||
#### Libraries |
|||
|
|||
- [Angular](https://angular.io) |
|||
- [Bootstrap 4](https://getbootstrap.com) |
|||
- [ng-bootsrap](https://ng-bootstrap.github.io/) |
|||
- [Font Awesome](http://fontawesome.io) |
|||
- [RxJS](http://reactivex.io/rxjs) |
|||
- [ngx-translate](https://github.com/ngx-translate/core) |
|||
|
|||
#### Coding guides |
|||
|
|||
- [Angular](docs/coding-guides/angular.md) |
|||
- [TypeScript](docs/coding-guides/typescript.md) |
|||
- [Sass](docs/coding-guides/sass.md) |
|||
- [HTML](docs/coding-guides/html.md) |
|||
- [Unit tests](docs/coding-guides/unit-tests.md) |
|||
- [End-to-end tests](docs/coding-guides/e2e-tests.md) |
|||
|
|||
#### Other documentation |
|||
|
|||
- [I18n guide](docs/i18n.md) |
|||
- [Working behind a corporate proxy](docs/corporate-proxy.md) |
|||
- [Updating dependencies and tools](docs/updating.md) |
|||
- [Using a backend proxy for development](docs/backend-proxy.md) |
|||
- [Browser routing](docs/routing.md) |
@ -0,0 +1,144 @@ |
|||
{ |
|||
"$schema": "./node_modules/@angular-devkit/core/src/workspace/workspace-schema.json", |
|||
"version": 1, |
|||
"newProjectRoot": "projects", |
|||
"projects": { |
|||
"aiolos": { |
|||
"root": "", |
|||
"sourceRoot": "src", |
|||
"projectType": "application", |
|||
"prefix": "app", |
|||
"schematics": { |
|||
"@schematics/angular:component": { |
|||
"styleext": "scss" |
|||
} |
|||
}, |
|||
"architect": { |
|||
"build": { |
|||
"builder": "@angular-devkit/build-angular:browser", |
|||
"options": { |
|||
"outputPath": "dist", |
|||
"index": "src/index.html", |
|||
"main": "src/main.ts", |
|||
"tsConfig": "tsconfig.app.json", |
|||
"polyfills": "src/polyfills.ts", |
|||
"assets": [ |
|||
"src/favicon.ico", |
|||
"src/apple-touch-icon.png", |
|||
"src/robots.txt", |
|||
"src/manifest.json", |
|||
"src/assets" |
|||
], |
|||
"styles": ["src/main.scss"], |
|||
"scripts": [] |
|||
}, |
|||
"configurations": { |
|||
"production": { |
|||
"optimization": true, |
|||
"outputHashing": "all", |
|||
"sourceMap": false, |
|||
"extractCss": true, |
|||
"namedChunks": false, |
|||
"aot": true, |
|||
"extractLicenses": true, |
|||
"vendorChunk": false, |
|||
"buildOptimizer": true, |
|||
"budgets": [ |
|||
{ |
|||
"type": "initial", |
|||
"maximumWarning": "2mb", |
|||
"maximumError": "5mb" |
|||
} |
|||
], |
|||
"serviceWorker": true, |
|||
"fileReplacements": [ |
|||
{ |
|||
"replace": "src/environments/environment.ts", |
|||
"with": "src/environments/environment.prod.ts" |
|||
} |
|||
] |
|||
}, |
|||
"ci": { |
|||
"progress": false |
|||
} |
|||
} |
|||
}, |
|||
"serve": { |
|||
"builder": "@angular-devkit/build-angular:dev-server", |
|||
"options": { |
|||
"hmr": true, |
|||
"hmrWarning": false, |
|||
"browserTarget": "aiolos:build" |
|||
}, |
|||
"configurations": { |
|||
"production": { |
|||
"hmr": false, |
|||
"browserTarget": "aiolos:build:production" |
|||
}, |
|||
"ci": { |
|||
"progress": false |
|||
} |
|||
} |
|||
}, |
|||
"extract-i18n": { |
|||
"builder": "@angular-devkit/build-angular:extract-i18n", |
|||
"options": { |
|||
"browserTarget": "aiolos:build" |
|||
} |
|||
}, |
|||
"test": { |
|||
"builder": "@angular-devkit/build-angular:karma", |
|||
"options": { |
|||
"main": "src/test.ts", |
|||
"karmaConfig": "karma.conf.js", |
|||
"polyfills": "src/polyfills.ts", |
|||
"tsConfig": "tsconfig.spec.json", |
|||
"scripts": [], |
|||
"styles": ["src/main.scss"], |
|||
"assets": [ |
|||
"src/favicon.ico", |
|||
"src/apple-touch-icon.png", |
|||
"src/robots.txt", |
|||
"src/manifest.json", |
|||
"src/assets" |
|||
] |
|||
}, |
|||
"configurations": { |
|||
"ci": { |
|||
"progress": false, |
|||
"watch": false |
|||
} |
|||
} |
|||
}, |
|||
"lint": { |
|||
"builder": "@angular-devkit/build-angular:tslint", |
|||
"options": { |
|||
"tsConfig": ["tsconfig.app.json", "tsconfig.spec.json"], |
|||
"exclude": ["**/node_modules/**"] |
|||
} |
|||
} |
|||
} |
|||
}, |
|||
"aiolos-e2e": { |
|||
"root": "e2e", |
|||
"projectType": "application", |
|||
"architect": { |
|||
"e2e": { |
|||
"builder": "@angular-devkit/build-angular:protractor", |
|||
"options": { |
|||
"protractorConfig": "e2e/protractor.conf.js", |
|||
"devServerTarget": "aiolos:serve" |
|||
} |
|||
}, |
|||
"lint": { |
|||
"builder": "@angular-devkit/build-angular:tslint", |
|||
"options": { |
|||
"tsConfig": ["e2e/tsconfig.e2e.json"], |
|||
"exclude": ["**/node_modules/**"] |
|||
} |
|||
} |
|||
} |
|||
} |
|||
}, |
|||
"defaultProject": "aiolos" |
|||
} |
@ -0,0 +1,4 @@ |
|||
# Analytics |
|||
|
|||
This project does not come with any analytics library. |
|||
Should you decide to use one, you may want to consider [Angulartics2](https://github.com/angulartics/angulartics2). |
@ -0,0 +1,43 @@ |
|||
# Backend proxy |
|||
|
|||
Usually when working on a web application you consume data from custom-made APIs. |
|||
|
|||
To ease development with our development server integrating live reload while keeping your backend API calls working, |
|||
we also have setup a backend proxy to redirect API calls to whatever URL and port you want. This allows you: |
|||
|
|||
- To develop frontend features without the need to run an API backend locally |
|||
- To use a local development server without [CORS](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing) issues |
|||
- To debug frontend code with data from a remote testing platform directly |
|||
|
|||
## How to configure |
|||
|
|||
In the root folder you will find a `proxy.conf.js`, containing the backend proxy configuration. |
|||
|
|||
The interesting part is there: |
|||
|
|||
```js |
|||
const proxyConfig = [ |
|||
{ |
|||
context: '/api', |
|||
pathRewrite: { '^/api': '' }, |
|||
target: 'http://api.icndb.com', |
|||
changeOrigin: true |
|||
} |
|||
]; |
|||
``` |
|||
|
|||
This is where you can setup one or more proxy rules. |
|||
|
|||
For the complete set of options, see the `http-proxy-middleware` |
|||
[documentation](https://github.com/chimurai/http-proxy-middleware#options). |
|||
|
|||
### Corporate proxy support |
|||
|
|||
To allow external API calls redirection through a corporate proxy, you will also find a `setupForCorporateProxy()` |
|||
function in the proxy configuration file. By default, this method configures a corporate proxy agent based on the |
|||
`HTTP_PROXY` environment variable, see the [corporate proxy documentation](corporate-proxy.md) for more details. |
|||
|
|||
If you need to, you can further customize this function to fit the network of your working environment. |
|||
|
|||
If your corporate proxy use a custom SSL certificate, your may need to add the `secure: false` option to your |
|||
backend proxy configuration. |
@ -0,0 +1,202 @@ |
|||
# Introduction to Angular and modern design patterns |
|||
|
|||
[Angular](https://angular.io) (aka Angular 2, 4, 5, 6...) is a new framework completely rewritten from the ground up, |
|||
replacing the now well-known [AngularJS](https://angularjs.org) framework (aka Angular 1.x). |
|||
|
|||
More that just a framework, Angular should now be considered as a whole _platform_ which comes with a complete set of |
|||
tools, like its own [CLI](https://github.com/angular/angular-cli), [debug utilities](https://augury.angular.io) or |
|||
[performance tools](https://github.com/angular/angular/tree/master/packages/benchpress). |
|||
|
|||
Angular has been around for some time now, but I still get the feeling that it’s not getting the love it deserved, |
|||
probably because of other players in the field like React or VueJS. While the simplicity behind these frameworks can |
|||
definitely be attractive, they lack in my opinion what is essential when making big, enterprise-grade apps: a solid |
|||
frame to lead both experienced developers and beginners in the same direction and a rational convergence of tools, |
|||
patterns and documentation. Yes, the Angular learning curve may seems a little steep, but it’s definitely worth it. |
|||
|
|||
## Getting started |
|||
|
|||
#### Newcomer |
|||
|
|||
If you're new to Angular you may feel overwhelmed by the quantity of new concepts to apprehend, so before digging |
|||
into this project you may want to start with [this progressive tutorial](https://angular.io/tutorial) that will guide |
|||
you step by step into building a complete Angular application. |
|||
|
|||
#### AngularJS veteran |
|||
|
|||
If you come from AngularJS and want to dig straight in the new version, you may want to take a look at the |
|||
[AngularJS vs 2 quick reference](https://angular.io/guide/ajs-quick-reference). |
|||
|
|||
#### Cheatsheet |
|||
|
|||
Until you know the full Angular API by heart, you may want to keep this |
|||
[cheatsheet](https://angular.io/guide/cheatsheet) that resumes the syntax and features on a single page at hand. |
|||
|
|||
## Style guide |
|||
|
|||
This project follows the standard [Angular style guide](https://angular.io/guide/styleguide). |
|||
|
|||
More that just coding rules, this style guide also gives advices and best practices for a good application architecture |
|||
and is an **essential reading** for starters. Reading deeper, you can even find many explanations for some design |
|||
choices of the framework. |
|||
|
|||
## FAQ |
|||
|
|||
There is a lot to dig in Angular and some questions frequently bother people. In fact, most of unclear stuff seems to be |
|||
related to modules, for example the dreaded |
|||
[**"Core vs Shared modules"**](https://angular.io/guide/ngmodule-faq#what-kinds-of-modules-should-i-have-and-how-should-i-use-them) |
|||
question. |
|||
|
|||
The guys at Angular may have noticed that since you can now find |
|||
[a nice FAQ on their website](https://angular.io/guide/ngmodule-faq#ngmodule-faqs) answering all the common questions |
|||
regarding modules. Don't hesitate to take a look at it, even if you think you are experienced enough with Angular :wink:. |
|||
|
|||
## Going deeper |
|||
|
|||
Even though they are not mandatory, Angular was designed for the use of design patterns you may not be accustomed to, |
|||
like [reactive programming](#reactive-programming), [unidirectional data flow](#unidirectional-data-flow) and |
|||
[centralized state management](#centralized-state-management). |
|||
|
|||
These concepts are difficult to resume in a few words, and despite being tightly related to each other they concern |
|||
specific parts of an application flow, each being quite deep to learn on its own. |
|||
|
|||
You will essentially find here a list of good starting points to learn more on these subjects. |
|||
|
|||
#### Reactive programming |
|||
|
|||
You may not be aware of it, but Angular is now a _reactive system_ by design. |
|||
Although you are not forced to use reactive programming patterns, they make the core of the framework and it is |
|||
definitely recommended to learn them if you want to leverage the best of Angular. |
|||
|
|||
Angular uses [RxJS](http://reactivex.io/rxjs/) to implement the _Observable_ pattern. |
|||
|
|||
> An _Observable_ is a stream of asynchronous events that can be processed with array-like operators. |
|||
|
|||
##### From promises to observables |
|||
|
|||
While AngularJS used to rely heavily on [_Promises_](https://docs.angularjs.org/api/ng/service/$q) to handle |
|||
asynchronous events, _Observables_ are now used instead in Angular. Even though in specific cases like for HTTP |
|||
requests, an _Observable_ can be converted into a _Promise_, it is recommended to embrace the new paradigm as it can a |
|||
lot more than _Promises_, with way less code. This transition is also explained in the |
|||
[Angular tutorial](https://angular.io/tutorial/toh-pt6#!%23observables). |
|||
Once you have made the switch, you will never look back again. |
|||
|
|||
##### Learning references |
|||
|
|||
- [What is reactive programming?](http://paulstovell.com/blog/reactive-programming), explained nicely through a simple |
|||
imaged story _(5 min)_ |
|||
|
|||
- [The introduction to reactive programming you've been missing](https://gist.github.com/staltz/868e7e9bc2a7b8c1f754), |
|||
the title says it all _(30 min)_ |
|||
|
|||
- [Functional reactive programming for Angular 2 developers](http://blog.angular-university.io/functional-reactive-programming-for-angular-2-developers-rxjs-and-observables/), |
|||
see the functional reactive programming principles in practice with Angular _(15 min)_ |
|||
|
|||
- [RxMarbles](http://rxmarbles.com), a graphical representation of Rx operators that greatly help to understand their |
|||
usage |
|||
|
|||
#### Unidirectional data flow |
|||
|
|||
In opposition with AngularJS where one of its selling points was two-way data binding which ended up causing a lot of |
|||
major headaches for complex applications, Angular now enforces unidirectional data flow. |
|||
|
|||
What does it means? Said with other words, it means that change detection cannot cause cycles, which was one of |
|||
AngularJS problematic points. It also helps to maintain simpler and more predictable data flows in applications, along |
|||
with substantial performance improvements. |
|||
|
|||
**Wait, then why the Angular documentation have mention of a |
|||
[two-way binding syntax](https://angular.io/guide/template-syntax#binding-syntax-an-overview)?** |
|||
|
|||
If you look closely, the new two-way binding syntax is just syntactic sugar to combine two _one-way_ bindings (a |
|||
_property_ and _event_ binding), keeping the data flow unidirectional. |
|||
|
|||
This change is really important, as it was often the cause of performance issues with AngularJS, and it one of the |
|||
pillars enabling better performance in new Angular apps. |
|||
|
|||
While Angular tries to stay _pattern-agnostic_ and can be used with conventional MV\* patterns, it was designed with |
|||
reactive programming in mind and really shines when used with reactive data flow patterns like |
|||
[redux](http://redux.js.org/docs/basics/DataFlow.html), |
|||
[Flux](https://facebook.github.io/flux/docs/in-depth-overview.html#content) or |
|||
[MVI](http://futurice.com/blog/reactive-mvc-and-the-virtual-dom). |
|||
|
|||
#### Centralized state management |
|||
|
|||
As applications grow in size, keeping track of the all its individual components state and data flows can become |
|||
tedious, and tend to be difficult to manage and debug. |
|||
|
|||
The main goal of using a centralized state management is to make state changes _predictable_ by imposing certain |
|||
restrictions on how and when updates can happen, using _unidirectional data flow_. |
|||
|
|||
This approach was first made popular with React with introduction of the |
|||
[Flux](https://facebook.github.io/flux/docs/in-depth-overview.html#content) architecture. Many libraries emerged then |
|||
trying to adapt and refine the original concept, and one of these gained massive popularity by providing a simpler, |
|||
elegant alternative: [Redux](http://redux.js.org/docs/basics/DataFlow.html). |
|||
|
|||
Redux is at the same time a library (with the big _R_) and a design pattern (with the little _r_), the latter being |
|||
framework-agnostic and working very well with Angular. |
|||
|
|||
The _redux_ design pattern is based on these [3 principles](http://redux.js.org/docs/introduction/ThreePrinciples.html): |
|||
|
|||
- The application state is a _single immutable_ data structure |
|||
- A state change is triggered by an _action_, an object describing what happened |
|||
- Pure functions called _reducers_ take the previous state and the next action to compute the new state |
|||
|
|||
The core concepts behind these principles are nicely explained in |
|||
[this example](http://redux.js.org/docs/introduction/CoreConcepts.html) _(3 min)_. |
|||
|
|||
For those interested, the redux pattern was notably inspired by |
|||
[The Elm Architecture](https://guide.elm-lang.org/architecture/) and the [CQRS](https://martinfowler.com/bliki/CQRS.html) |
|||
pattern. |
|||
|
|||
##### Which library to use? |
|||
|
|||
You can make Angular work with any state management library you like, but your best bet would be to use |
|||
[NGXS](http://ngxs.io) or [@ngrx](https://github.com/ngrx/platform). Both works the same as the popular |
|||
[Redux](http://redux.js.org) library, but with a tight integration with Angular and [RxJS](http://reactivex.io/rxjs/), |
|||
with some nice additional developer utilities. |
|||
|
|||
NGXS is based on the same concepts as @ngrx, but with less boilerplate and a nicer syntax, making it less intimidating. |
|||
|
|||
Here are some resources to get started: |
|||
|
|||
- [Angular NGXS tutorial with example from scratch](https://appdividend.com/2018/07/03/angular-ngxs-tutorial-with-example-from-scratch/), |
|||
a guided tutorial for NGXS _(10 min)_ |
|||
|
|||
- [Build a better Angular 2 application with redux and ngrx](http://onehungrymind.com/build-better-angular-2-application-redux-ngrx/), |
|||
a nice tutorial for @ngrx _(30 min)_ |
|||
|
|||
- [Comprehensive introduction to @ngrx/store](https://gist.github.com/btroncone/a6e4347326749f938510), an in-depth |
|||
walkthrough to this library usage in Angular _(60 min)_ |
|||
|
|||
##### When to use it? |
|||
|
|||
You may have noticed that the starter template does not include a centralized state management system out of the box. |
|||
Why is that? Well, while there is many benefits from using this pattern, the choice is ultimately up to your team and |
|||
what you want to achieve with your app. |
|||
|
|||
Keep in mind that using a single centralized state for your app introduces a new layer a complexity |
|||
[that might not be needed](https://medium.com/@dan_abramov/you-might-not-need-redux-be46360cf367), depending of your |
|||
goal. |
|||
|
|||
## Optimizing performance |
|||
|
|||
While the new Angular version resolves by design most of the performance issues that could be experienced with |
|||
AngularJS, there is always room for improvements. Just keep in mind that delivering an app with good performance is |
|||
often a matter of common sense and sane development practices. |
|||
|
|||
Here is [a list of key points](https://github.com/mgechev/angular-performance-checklist) to check for in your app to |
|||
make sure you deliver the best experience to your customers. |
|||
|
|||
After going through the checklist, make sure to also run an audit of your page through |
|||
[**Lighthouse**](https://developers.google.com/web/tools/lighthouse/), the latest Google tool that gives you meaningful |
|||
insight about your app performance, accessibility, mobile compatibility and more. |
|||
|
|||
## Keeping Angular up-to-date |
|||
|
|||
Angular development is moving fast, and updates to the core libs and tools are pushed regularly. |
|||
|
|||
Fortunately, the Angular team provides tools to help you follow through the updates: |
|||
|
|||
- `npm run ng update` allows you to update your app and its dependencies |
|||
|
|||
- The [Angular update website](https://update.angular.io) guides you through Angular changes and migrations, providing |
|||
step by step guides from one version to another. |
@ -0,0 +1,276 @@ |
|||
# Build-Specific Configuration |
|||
|
|||
## tl;dr's |
|||
|
|||
ngx-rocket comes with a very helpful `env` script that will save environment-variables set at build time to constants that can be used as configuration for your code. When combined with the `dotenv-cli` package, it enables maximum configurability |
|||
while maintaining lots of simplicity for local development and testing. |
|||
|
|||
### Cookbook for maximum independence of deployment-specific configuration |
|||
|
|||
Disclaimer: If you have a full-stack app in a monorepo, keep separate `.env` files for server-side and client-side |
|||
configs, and make sure `.env` files are .gitignore'd and that secrets never make it into client-side `.env` file. |
|||
|
|||
For each configurable variable (e.g. BROWSER_URL, API_URL): |
|||
|
|||
- Add it to package.json's env script so that the build-time variables will be saved for runtime: |
|||
|
|||
```javascript |
|||
{ |
|||
"scripts": { |
|||
"env": "ngx-scripts env npm_package_version BROWSER_URL API_URL", |
|||
} |
|||
} |
|||
``` |
|||
|
|||
- Add it to or edit it in src/environments/environment.ts to expose it to your app as e.g. environment.API_URL: |
|||
```typescript |
|||
export const environment = { |
|||
// ... |
|||
API_URL: env.API_URL, |
|||
BROWSER_URL: env.BROWSER_URL |
|||
// ... |
|||
}; |
|||
``` |
|||
- Configure your CI's deployment to set the variables and export them to the build script before building - if your CI |
|||
gives you a shell script to run, make it something like this: |
|||
```shell |
|||
# bourne-like shells... |
|||
export API_URL='https://api.staging.example.com' |
|||
export BROWSER_URL='https://staging.example.com' |
|||
# ... |
|||
npm run build:ssr-and-client |
|||
``` |
|||
- Finally, to have your cake and eat it too and avoid having to do all that for local development and testing (or clutter |
|||
your package.json up), install the `dotenv-cli` package and update your development-related npm scripts to take advantage |
|||
of it: |
|||
```shell |
|||
# environment.development.env.sh |
|||
BROWSER_URL='http://localhost:4200' |
|||
API_URL='http://localhost:4200' |
|||
``` |
|||
```javascript |
|||
{ |
|||
"scripts": { |
|||
"start": "dotenv -e environment.development.env.sh -- npm run env && ng serve --aot", |
|||
} |
|||
} |
|||
``` |
|||
|
|||
This way, app configurations will always come from deploy-specific environment variables, and your development environments |
|||
are still easy to work with. |
|||
|
|||
For configuring the build itself (for example, if you want your QA build to be similar to your production build, but with |
|||
source maps enabled), consider avoiding adding a build configuration to angular.json, and instead adding the respective |
|||
overriding flag to the `ng` command in package.json: |
|||
|
|||
```javascript |
|||
{ |
|||
"scripts": { |
|||
"build:client-and-server-bundles:qa": "NG_BUILD_OVERRIDES='--sourceMap=true' npm run build:client-and-server-bundles", |
|||
"build:client-and-server-bundles": "npm run build:client-bundles && npm run build:server-bundles", |
|||
"build:client-bundles": "npm run env && ng build --prod $NG_BUILD_OVERRIDES", |
|||
} |
|||
} |
|||
``` |
|||
|
|||
The development server API proxy config can read runtime environment variables, so you can avoid having a superficial |
|||
dev-server configuration by taking advantage of them: |
|||
|
|||
```javascript |
|||
{ |
|||
"scripts": { |
|||
"start": "dotenv -e environment.development.env.sh -- npm run env && API_PROXY_HOST='http://localhost:9000' ng serve --aot", |
|||
"e2e": "ngtw build && npm run env && API_PROXY_HOST='http://localhost:7357' ng e2e --webdriverUpdate=false", |
|||
} |
|||
} |
|||
``` |
|||
|
|||
```javascript |
|||
const proxyConfig = [ |
|||
{ |
|||
context: '/api', |
|||
pathRewrite: { '^/api': '' }, |
|||
target: `${process.env.API_PROXY_HOST}/api`, |
|||
changeOrigin: true, |
|||
secure: false |
|||
}, |
|||
{ |
|||
context: '/auth', |
|||
pathRewrite: { '^/auth': '' }, |
|||
target: `${process.env.API_PROXY_HOST}/auth`, |
|||
changeOrigin: true, |
|||
secure: false |
|||
} |
|||
]; |
|||
``` |
|||
|
|||
Quick SSR note: SSR works by building all the client bundles like normal, but then rendering them in real-time. So, |
|||
|
|||
- the rest of your app from `main.server.ts` down has access to your build-time environment only, like your normal |
|||
client bundles |
|||
- but `server.ts` (the file configuring and running express) has access to your serve-time environment variables |
|||
|
|||
### Less optimal alternatives |
|||
|
|||
- On the opposite extreme of the spectrum, you can keep all build-specific configuration in a separate environment |
|||
file for each environment using Angular's built-in `fileReplacements`, but then you'll need a separate environment |
|||
file even for deployment-specific configuration (like hostnames), which can get out of hand fast. |
|||
- For a middle-of-the-road approach, you can divide configuration into two groups: |
|||
- Configuration shared by each environment-type: |
|||
- Environment-type examples include local development, staging/qa, test, production... |
|||
- Examples of configuration like this include: |
|||
- In test, animations are always disabled, but for all other environments, they're enabled |
|||
- In production, the payment gateway's publishable key is the live key, but all other environments use the |
|||
test key |
|||
- Configuration that sometimes needs to be specific to an individual deployment of a given environment: |
|||
- Examples of configuration like this include: |
|||
- This particular staging/qa server's base for constructing URLs is qa-stable.example.com, but qa/staging |
|||
environments could also be deployed to preprod.example.com or localhost:8081 or anywhereelse:7000. |
|||
- This particular deployment uses a specific bucket for Amazon S3 uploads |
|||
- In this approach, you can use Angular's `fileReplacements` for anything environment-specific and ngx-rocket's |
|||
`env` for anything deployment-specific. You can even have certain deployment-specific configuration fall back |
|||
to environment-specific defaults for certain environments like so: |
|||
```javascript |
|||
export const environment = { |
|||
// ... |
|||
BROWSER_URL: env.BROWSER_URL || 'https://qa.example.com' |
|||
// ... |
|||
}; |
|||
``` |
|||
- If you don't have lots of environment variables, you can avoid dotenv-cli and use your particular shell's method |
|||
to expose the variables before running the ngx-rocket env tool. |
|||
|
|||
## Introduction |
|||
|
|||
When building any maintainable application, a separation of configuration and code is always desired. In the case |
|||
of configuration, some of it will need to vary from environment to environment, build to build, or deployment to |
|||
deployment. |
|||
|
|||
This guide focuses on this type of build-specific configuration in a very broad sense of an Angular app, describing |
|||
the specific Angular ways of controlling these configurations, detailing some angular-specific challenges, and |
|||
highlighting some ngx-rocket tooling that help with them in mind. |
|||
|
|||
For an even broader non-Angular introduction of these concepts, see the |
|||
[The Twelve-Factor App](https://12factor.net/config) methodology's opinions on how this type of configuration |
|||
should be managed. |
|||
|
|||
## Types of configuration |
|||
|
|||
At the highest level, build-specific configuration can be divided into two categories: |
|||
|
|||
1. Configuration for how your app is built and served |
|||
2. Configuration used by your codebase |
|||
|
|||
### Configuration for how your app is built and served |
|||
|
|||
This type of build-specific configuration is not used by your code, but is used to control the build system itself. |
|||
Configuration like this goes into Angular's |
|||
[workspace configuration](https://angular.io/guide/workspace-config#alternate-build-configurations). Instead of |
|||
rehashing existing documentation on this, this document will highlight how it relates to this subject. Namely, the |
|||
fact that in addition to specifying _HOW_ the app is built for each build configuration, the workspace configuration |
|||
allows mapping each build configuration to a separate environment configuration file for your codebase as well. It |
|||
also allows for making separate dev-server configurations in case you need to run it differently. |
|||
|
|||
Therefore, each build configuration in the workspace configuration file is a tuple of |
|||
(how-to-build, environment-file-for-codebase), and you'll need a separate configuration for each combination. |
|||
|
|||
## Angular's out-of-the-box environment configuration |
|||
|
|||
### When it works well |
|||
|
|||
This setup works quite well for configuration that's shared among all instances of an environment, like the following |
|||
examples: |
|||
|
|||
- **test** environment always builds without source maps, disables animations, uses a recaptcha test key, and disables |
|||
analytics |
|||
- **dev** environment always builds with source maps, enables animations, uses a recaptcha live key, and disables |
|||
analytics |
|||
- **qa** environment always builds with source maps, enables animations, uses a recaptcha live key, and disables |
|||
analytics |
|||
- **prod** environment always builds without source maps, enables animations, uses a recaptcha live key, and enables |
|||
analytics |
|||
|
|||
### Limitations of Angular's `fileReplacements` |
|||
|
|||
But for certain deployment-specific configuration, things start to get really hairy, like in these examples: |
|||
|
|||
- QA build configuration needs to be built for local deployment, deployment to a server on the internet for QA |
|||
purposes, and also deployment to another server on the internet for staging purposes |
|||
- Production build needs multiple different deployments of the same app to different servers |
|||
|
|||
These cases can cause problems when: |
|||
|
|||
- Each deployment needs a separate API URL |
|||
- Each deployment needs a separate URL for building its own URLs to where it's deployed |
|||
- Each deployment needs separate API keys, bucket names, etc |
|||
|
|||
You _COULD_ start creating separate configurations for each deployment, each with its own `fileReplacements`, but that |
|||
would be really messy. |
|||
|
|||
### Workarounds that don't work well |
|||
|
|||
One workaround would be to keep such configurations as globals in a separate deployment-specific script file. But |
|||
that's pretty messy too. More importantly, there are limitations to where they can be used. For example, because |
|||
of AOT, such configuration variables cannot be used in Angular's decorators, because they're not statically |
|||
analyzable (i.e. their values knowable at build-time). So it would be better if we can keep everything in the same place. |
|||
|
|||
### ngx-rocket to the rescue |
|||
|
|||
The ngx-rocket `env` task solves this problem really well, and avoids the need for separate `environment.ts` files for |
|||
deployment-specific configuration. |
|||
|
|||
To add a deployment-specific configuration: |
|||
|
|||
1. edit the existing `environment.ts` files for whichever environments you'd like to make that variable |
|||
deployment-specific for by having it come from the imported "env" object - pro tip: you can even make it fall |
|||
back to an environment-based default and still be statically analyzable! |
|||
2. add that variable name to the npm script's `env` task |
|||
|
|||
Now, as long as you have that environment variable set in the shell running the build, the `env` task will save it into |
|||
the `.env.ts` file before building. |
|||
|
|||
If you really want, you can take things even further to the twelve-factor extreme, and you can even eliminate the |
|||
need for `fileReplacements` entirely, and make all configuration come from environment variables. Whether this will be |
|||
the right approach for your project will be up to you. |
|||
|
|||
This makes separate deployments awesome and flexible, but unfortunately makes things a little bit of a hassle for your |
|||
local development, test, etc. environments because you have the burden of providing all those keys, settings, etc. as |
|||
environment variables. |
|||
|
|||
To avoid having to do that, you'll can create a .gitignore'd `.env` file with all the variables set, and source it |
|||
with your shell (e.g. `source .env.sh && npm env` in bourne-like shells or `env.bat; npm env` in windows). |
|||
|
|||
```shell |
|||
# bourne-like .env.sh |
|||
export BROWSER_URL=localhost:4200 |
|||
``` |
|||
|
|||
```shell |
|||
REM windows env.bat |
|||
SET BROWSER_URL=localhost:4200 |
|||
``` |
|||
|
|||
Luckily for us, there's a package called `dotenv-cli` that uses the `dotenv` package and does this in a cleaner and |
|||
cross-platform way and comes with even more bells and whistles. You should use that instead, and make your env file |
|||
like this instead: |
|||
|
|||
```shell |
|||
BROWSER_URL=localhost:4200 |
|||
``` |
|||
|
|||
## When you can use environment variables directly without ngx-rocket `env` |
|||
|
|||
As a sidenote, ngx-rocket `env` isn't used for the proxy config file, because it isn't built and ran separately. |
|||
Fortunately, for that same reason, you can directly use `process.env` within the proxy config file to avoid having |
|||
separate proxy configs in most cases. |
|||
|
|||
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 |
|||
that it stops there - the app itself is built, so even in SSR the client app can't access process environment variables. |
|||
|
|||
## Security Considerations |
|||
|
|||
Never forget that your entire Angular app goes to the client, including its configuration, including the environment |
|||
variables you pass to the env task! As usual, you should **never add sensitive keys or secrets to the env task**. |
|||
|
|||
Finally, if your Angular project is the client-side of a full-stack monorepo, make sure to keep the client-side `.env` |
|||
file separate from the server-side `.env` file, since your server-side is bound to have secrets. |
@ -0,0 +1,101 @@ |
|||
# End-to-end tests coding guide |
|||
|
|||
End-to-end (E2E for short) tests are meant to test the behavior of your application, from start to finish. |
|||
|
|||
While unit tests are the first choice for catching bugs and regression on individual components, it is a good idea to |
|||
complement them with test cases covering the integration between the individual components, hence the need for E2E |
|||
tests. |
|||
|
|||
These tests use [Protractor](https://github.com/angular/protractor), which is a framework built for Angular on top of |
|||
[Selenium](https://github.com/SeleniumHQ/selenium) to control browsers and simulate user inputs. |
|||
[Jasmine](http://jasmine.github.io) is used as the base test framework. |
|||
|
|||
Many of protractor's actions and assertions are asynchronous and return promises. To ensure that test steps are |
|||
performed in the intended order, generated projects are set up to use async/await as the flow control mechanism |
|||
because of its good readability. See the [Protractor async/await](https://www.protractortest.org/#/async-await) page |
|||
for more information and examples on using async/await in tests, and the |
|||
[Protractor API guide](https://www.protractortest.org/#/api) to determine which API calls are asynchronous. |
|||
|
|||
Beware that some examples of protractor tests you'll find on the internet might not be using async/await. Tests like |
|||
these that you encounter were using the now-deprecated "selenium promise manager" flow control mechanism, so they |
|||
should not be used verbatim. See the [Protractor control flow](https://www.protractortest.org/#/control-flow) page |
|||
for more details. |
|||
|
|||
## Good practices |
|||
|
|||
- Avoid whenever possible inter-dependencies between your E2E tests |
|||
- Run E2E tests on your continuous integration server against different browsers |
|||
- If you use an Agile methodology, cover each user story acceptance factors with an E2E test |
|||
|
|||
## Page objects |
|||
|
|||
E2E tests should follow the _[Page Object](https://github.com/SeleniumHQ/selenium/wiki/PageObjects)_ pattern. |
|||
|
|||
#### What is a page object? |
|||
|
|||
A page object: |
|||
|
|||
- Models the objects on a page under test: |
|||
- _Properties_ wrap page elements |
|||
- _Methods_ wrap code that interacts with the page elements |
|||
- Simplifies the test scripts |
|||
- Reduces the amount of duplicated code |
|||
|
|||
If the UI changes, the fix only needs to be applied in one place. |
|||
|
|||
#### How to define a page object |
|||
|
|||
```typescript |
|||
// login.po.ts |
|||
import { browser, element, by } from 'protractor'; |
|||
|
|||
export class LoginPage { |
|||
emailInput = element(by.css('input[name=^"email"]')); |
|||
passwordInput = element(by.css('input[name=^"password"]')); |
|||
loginButton = element(by.css('button[(click)^="login"]')); |
|||
registerButton = element(by.css('button[(click)^="register"]')); |
|||
|
|||
async navigateTo() { |
|||
await browser.get('/'); |
|||
} |
|||
|
|||
async getGreetingText() { |
|||
return await element(by.css('.greeting')).getText(); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
#### How to use a page object |
|||
|
|||
```typescript |
|||
// login.e2e-spec.ts |
|||
import { LoginPage } from './login.po'; |
|||
|
|||
describe('Login', () => { |
|||
let page: LoginPage; |
|||
|
|||
beforeEach(async () => { |
|||
page = new LoginPage(); |
|||
await page.navigateTo(); |
|||
}); |
|||
|
|||
it('should navigate to the register page when the register button is clicked', async () => { |
|||
await page.registerButton.click(); |
|||
|
|||
expect(await browser.getCurrentUrl()).toContain('/register'); |
|||
}); |
|||
|
|||
it('should allow a user to log in', async () => { |
|||
await page.emailInput.sendKeys('test@mail.com'); |
|||
await page.passwordInput.sendKeys('abc123'); |
|||
await page.loginButton.click(); |
|||
|
|||
expect(await page.getGreetingText()).toContain('Welcome, Test User'); |
|||
}); |
|||
}); |
|||
``` |
|||
|
|||
## Credits |
|||
|
|||
Parts of this guide were freely inspired by this |
|||
[presentation](https://docs.google.com/presentation/d/1B6manhG0zEXkC-H-tPo2vwU06JhL8w9-XCF9oehXzAQ). |
@ -0,0 +1,39 @@ |
|||
# HTML coding guide |
|||
|
|||
## Naming conventions |
|||
|
|||
- Everything should be named in `kebab-case` (lowercase words separated with a `-`): tags, attributes, IDs, etc, |
|||
**except for everything bound to Angular** such variables, directives or events which should be in `camelCase` |
|||
- File names should always be in `kebab-case` |
|||
|
|||
## Coding rules |
|||
|
|||
- Use HTML5 doctype: `<!doctype html>` |
|||
- Use HTML [semantic elements](https://developer.mozilla.org/docs/Web/HTML/Sections_and_Outlines_of_an_HTML5_document) |
|||
- Use double quotes `"` around attribute values in tags |
|||
- Use a new line for every block, list, or table element, and indent every such child element |
|||
- Clearly Separate structure (HTML) from presentation (CSS) from behavior (JavaScript): |
|||
- Never use inline CSS or JavaScript |
|||
- Keep any logic out of the HTML |
|||
- `type` attribute for stylesheets and script tags should be omitted |
|||
|
|||
## Common pitfalls |
|||
|
|||
- **Block**-type tags cannot be nested inside **inline**-type tags: a `<div>` tag cannot be nested in a `<span>`. |
|||
This rule also applies regarding the `display` value of an element. |
|||
- HTML is **not** XML: empty tags cannot be self-closing and will result in improper results |
|||
- `<div/>` will be interpreted as a simple `<div>` without closing tag! |
|||
- The only tags that allows self-closing are the one that does not require a closing tag in first place: |
|||
these are the void elements that do not not accept content `<br>`, `<hr>`, `<img>`, `<input>`, `<meta>`, `<link>` |
|||
(and others). |
|||
|
|||
## Templates |
|||
|
|||
In accordance with the [Angular style guide](https://angular.io/guide/styleguide), HTML templates should be extracted in |
|||
separate files, when more than 3 lines. |
|||
|
|||
Only use inline templates sparingly in very simple components with less than 3 lines of HTML. |
|||
|
|||
## Enforcement |
|||
|
|||
Coding rules enforcement and basic sanity checks are done in this project by [HTMLHint](http://htmlhint.com). |
@ -0,0 +1,102 @@ |
|||
# Sass coding guide |
|||
|
|||
[Sass](http://sass-lang.com) is a superset of CSS, which brings a lot of developer candy to help scaling CSS in large |
|||
projects and keeping it maintainable. |
|||
|
|||
The main benefits of using Sass over plain CSS are _variables_, _nesting_ and _mixins_, see the |
|||
[basics guide](http://sass-lang.com/guide) for more details. |
|||
|
|||
> Note that this project use the newer, CSS-compatible **SCSS** syntax over the old |
|||
> [indented syntax](http://sass-lang.com/documentation/file.INDENTED_SYNTAX.html). |
|||
|
|||
## Naming conventions |
|||
|
|||
- In the CSS world, everything should be named in `kebab-case` (lowercase words separated with a `-`). |
|||
- File names should always be in `kebab-case` |
|||
|
|||
## Coding rules |
|||
|
|||
- Use single quotes `'` for strings |
|||
- Use this general nesting hierarchy when constructing your styles: |
|||
|
|||
```scss |
|||
// The base component class acts as the namespace, to avoid naming and style collisions |
|||
.my-component { |
|||
// Put here all component elements (flat) |
|||
.my-element { |
|||
// Use a third-level only for modifiers and state variations |
|||
&.active { ... } |
|||
} |
|||
} |
|||
``` |
|||
|
|||
Note that with |
|||
[Angular view encapsulation](https://angular.io/docs/ts/latest/guide/component-styles.html#!#view-encapsulation), |
|||
the first "namespace" level of nesting is not necessary as Angular takes care of the scoping for avoid collisions. |
|||
|
|||
> As a side note, we are aware of the [BEM naming approach](https://en.bem.info/tools/bem/bem-naming/), but we found |
|||
> it impractical for large projects. The nesting approach has drawbacks such as increased specificity, but it helps |
|||
> keeping everything nicely organized, and more importantly, _scoped_. |
|||
|
|||
Also keep in mind this general rules: |
|||
|
|||
- Always use **class selectors**, never use ID selectors and avoid element selectors whenever possible |
|||
- No more than **3 levels** of nesting |
|||
- No more than **3 qualifiers** |
|||
|
|||
## Best practices |
|||
|
|||
- Use object-oriented CSS (OOCSS): |
|||
|
|||
- Factorize common code in base class, and extend it, for example: |
|||
|
|||
```scss |
|||
// Base button class |
|||
.btn { ... } |
|||
|
|||
// Color variation |
|||
.btn-warning { ... } |
|||
|
|||
// Size variation |
|||
.btn-small { ... } |
|||
``` |
|||
|
|||
- Try to name class by semantic, not style nor function for better reusability: |
|||
Use `.btn-warning`, not `btn-orange` nor `btn-cancel` |
|||
- Avoid undoing style, refactor using common base classes and extensions |
|||
|
|||
- Keep your style scoped |
|||
|
|||
- Clearly separate **global** (think _framework_) and **components** style |
|||
- Global style should only go in `src/theme/`, never in components |
|||
- Avoid style interactions between components, if some style may need to be shared, refactor it as a framework |
|||
component in put it in your global theme. |
|||
- Avoid using wider selectors than needed: always use classes if you can! |
|||
|
|||
- Avoid rules multiplication |
|||
|
|||
- The less CSS the better, factorize rules whenever it's possible |
|||
- CSS is code, and like any code frequent refactoring is healthy |
|||
|
|||
- When ugly hacks cannot be avoided, create an explicit `src/hacks.scss` file and put it in: |
|||
- These ugly hacks should only be **temporary** |
|||
- Each hack should be documented with the author name, the problem and hack reason |
|||
- Limit this file to a reasonable length (~100 lines) and refactor hacks with proper solutions when the limit is |
|||
reached. |
|||
|
|||
## Pitfalls |
|||
|
|||
- Never use the `!important` keyword. Ever. |
|||
- Never use **inline** style in html, even _just for debugging_ (because we **KNOW** it will end up in your commit) |
|||
|
|||
## Browser compatibility |
|||
|
|||
You should never use browser-specific prefixes in your code, as [autoprefixer](https://github.com/postcss/autoprefixer) |
|||
takes care of that part for you during the build process. |
|||
You just need to declare which browsers you target in the [`browserslist`](https://github.com/ai/browserslist) file. |
|||
|
|||
## Enforcement |
|||
|
|||
Coding rules are enforced in this project with [stylelint](https://stylelint.io). |
|||
This tool also checks the compatibility of the rules used against the browsers you are targeting (specified in the |
|||
[`browserslist`](https://github.com/ai/browserslist) file), via [doiuse](https://github.com/anandthakker/doiuse). |
@ -0,0 +1,63 @@ |
|||
# TypeScript coding guide |
|||
|
|||
[TypeScript](http://www.typescriptlang.org) is a superset of JavaScript that greatly helps building large web |
|||
applications. |
|||
|
|||
Coding conventions and best practices comes from the |
|||
[TypeScript guidelines](https://github.com/Microsoft/TypeScript/wiki/Coding-guidelines), and are also detailed in the |
|||
[TypeScript Deep Dive Style Guide](https://basarat.gitbooks.io/typescript/content/docs/styleguide/styleguide.html). |
|||
In addition, this project also follows the general [Angular style guide](https://angular.io/guide/styleguide). |
|||
|
|||
## Naming conventions |
|||
|
|||
- Use `PascalCase` for types, classes, interfaces, constants and enum values. |
|||
- Use `camelCase` for variables, properties and functions |
|||
- Avoid prefixing interfaces with a capital `I`, see [Angular style guide](https://angular.io/guide/styleguide#!#03-03) |
|||
- Do not use `_` as a prefix for private properties. An exception can be made for backing fields like this: |
|||
```typescript |
|||
private _foo: string; |
|||
get foo() { return this._foo; } // foo is read-only to consumers |
|||
``` |
|||
|
|||
## Ordering |
|||
|
|||
- Within a file, type definitions should come first |
|||
- Within a class, these priorities should be respected: |
|||
- Properties comes before functions |
|||
- Static symbols comes before instance symbols |
|||
- Public symbols comes before private symbols |
|||
|
|||
## Coding rules |
|||
|
|||
- Use single quotes `'` for strings |
|||
- Always use strict equality checks: `===` and `!==` instead of `==` or `!=` to avoid comparison pitfalls (see |
|||
[JavaScript equality table](https://dorey.github.io/JavaScript-Equality-Table/)). |
|||
The only accepted usage for `==` is when you want to check a value against `null` or `undefined`. |
|||
- Use `[]` instead of `Array` constructor |
|||
- Use `{}` instead of `Object` constructor |
|||
- Always specify types for function parameters and returns (if applicable) |
|||
- Do not export types/functions unless you need to share it across multiple components |
|||
- Do not introduce new types/values to the global namespace |
|||
- Use arrow functions over anonymous function expressions |
|||
- Only surround arrow function parameters when necessary. |
|||
For example, `(x) => x + x` is wrong but the following are correct: |
|||
- `x => x + x` |
|||
- `(x, y) => x + y` |
|||
- `<T>(x: T, y: T) => x === y` |
|||
|
|||
## Definitions |
|||
|
|||
In order to infer types from JavaScript modules, TypeScript language supports external type definitions. They are |
|||
located in the `node_modules/@types` folder. |
|||
|
|||
To manage type definitions, use standard `npm install|update|remove` commands. |
|||
|
|||
## Enforcement |
|||
|
|||
Coding rules are enforced in this project via [TSLint](https://github.com/palantir/tslint). |
|||
Angular-specific rules are also enforced via the [Codelyzer](https://github.com/mgechev/codelyzer) rule extensions. |
|||
|
|||
## Learn more |
|||
|
|||
The read of [TypeScript Deep Dive](https://basarat.gitbooks.io/typescript) is recommended, this is a very good |
|||
reference book for TypeScript (and also open-source). |
@ -0,0 +1,46 @@ |
|||
# Unit tests coding guide |
|||
|
|||
The main objective of unit tests is to detect regressions and to help you design software components. A suite of |
|||
_good_ unit tests can be _immensely_ valuable for your project and makes it easier to refactor and expand your code. |
|||
But keep in mind that a suite of _bad_ unit tests can also be _immensely_ painful, and hurt your development by |
|||
inhibiting your ability to refactor or alter your code in any way. |
|||
|
|||
## What to test? |
|||
|
|||
Everything! But if you need priorities, at least all business logic code must be tested: services, helpers, models... |
|||
Shared directives/components should also be covered by unit tests, if you do not have the time to test every single |
|||
component. |
|||
|
|||
Keep in mind that component unit tests should not overlap with [end-to-end tests](e2e-tests.md): while unit the tests |
|||
cover the isolated behavior of the component bindings and methods, the end-to-end tests in opposition should cover the |
|||
integration and interactions with other app components based on real use cases scenarios. |
|||
|
|||
## Good practices |
|||
|
|||
- Name your tests cleanly and consistently |
|||
- Do not only test nominal cases, the most important tests are the one covering the edge cases |
|||
- Each test should be independent to all the others |
|||
- Avoid unnecessary assertions: it's counter-productive to assert anything covered by another test, it just increase |
|||
pointless failures and maintenance workload |
|||
- Test only one code unit at a time: if you cannot do this, it means you have an architecture problem in your app |
|||
- Mock out all external dependencies and state: if there is too much to mock, it is often a sign that maybe you |
|||
should split your tested module into several more independent modules |
|||
- Clearly separate or identify these 3 stages of each unit test (the _3A_): _arrange_, _act_ and _assert_ |
|||
- When you fix a bug, add a test case for it to prevent regression |
|||
|
|||
## Pitfalls |
|||
|
|||
- Sometimes your architecture might mean your code modify static variables during unit tests. Avoid this if you can, |
|||
but if you can't, at least make sure each test resets the relevant statics before and after your tests. |
|||
- Don’t unit-test configuration settings |
|||
- Improving test coverage is good, but having meaningful tests is better: start with the latter first, and **only after |
|||
essential features of your code unit are tested**, your can think of improving the coverage. |
|||
|
|||
## Unit testing with Angular |
|||
|
|||
A good starting point for learning is the official |
|||
[testing guide](https://angular.io/docs/ts/latest/guide/testing.html). |
|||
|
|||
But as you will most likely want to go bit further in real world apps, these |
|||
[example test snippets](https://gist.github.com/wkwiatek/e8a4a9d92abc4739f04f5abddd3de8a7) are also very helpful to |
|||
learn how to cover most common testing use cases. |
@ -0,0 +1,51 @@ |
|||
# Working behind a corporate proxy |
|||
|
|||
## Environment |
|||
|
|||
Most tools (including npm and git) use the `HTTP_PROXY` and `HTTPS_PROXY` environment variables to work with a |
|||
corporate proxy. |
|||
|
|||
### Windows |
|||
|
|||
In Windows environments, add the `HTTP_PROXY` and `HTTPS_PROXY` system environment variable, with these values: |
|||
|
|||
- HTTP_PROXY: `http://<username>:<password>@<proxy_server>:<proxy_port>` |
|||
- HTTPS_PROXY: `%HTTP_PROXY%` |
|||
|
|||
### Unix |
|||
|
|||
Add these lines to your `~/.bash_profile` or `~/.profile`: |
|||
|
|||
```sh |
|||
export HTTP_PROXY="http://<username>:<password>@<proxy_server>:<proxy_port>" |
|||
export HTTPS_PROXY="$HTTP_PROXY" |
|||
``` |
|||
|
|||
## Proxy with SSL custom certificate |
|||
|
|||
Some proxy like **zscaler** use a custom SSL certificate to inspect request, which may cause npm commands to fail. |
|||
|
|||
To solve this problem, you can disable the `strict-ssl` option in npm. |
|||
|
|||
## Proxy exceptions |
|||
|
|||
If you need to access repositories on your local network that should bypass proxy, set the `NO_PROXY` environment |
|||
variable, in the same way as `HTTP_PROXY`: |
|||
|
|||
### Windows |
|||
|
|||
- NO_PROXY: `127.0.0.1, localhost, <your_local_server_ip_or_hostname>` |
|||
|
|||
### Unix |
|||
|
|||
```sh |
|||
export NO_PROXY="127.0.0.1, localhost, <your_local_server_ip_or_hostname>" |
|||
``` |
|||
|
|||
### Npm |
|||
|
|||
Run this command in your project directory: |
|||
|
|||
```sh |
|||
npm set strict-ssl false |
|||
``` |
@ -0,0 +1,58 @@ |
|||
# I18n |
|||
|
|||
The internationalization of the application is managed by [ngx-translate](https://github.com/ngx-translate/core). |
|||
|
|||
## Adding translatable strings |
|||
|
|||
### In HTML templates |
|||
|
|||
Use the `translate` directive on an HTML element to automatically translate its content: |
|||
|
|||
```html |
|||
<span translate>This text will be translated.</span> |
|||
``` |
|||
|
|||
You can also use the `translate` pipe if needed: |
|||
|
|||
```html |
|||
<button title="{{ 'Add item' | translate }}">+</button> |
|||
``` |
|||
|
|||
### In TypeScript code |
|||
|
|||
If you need to translate strings in JavaScript code, import the `TranslateService` dependency and use the asynchronous |
|||
`get()` method: |
|||
|
|||
```typescript |
|||
let title; |
|||
translateService.get('My page title').subscribe((res: string) => { |
|||
title = res; |
|||
}); |
|||
``` |
|||
|
|||
## Extracting strings to translate |
|||
|
|||
Once you are ready to translate your app, just run `npm run translations:extract`. |
|||
It will create a `template.json` file in the `src/translations` folder. |
|||
|
|||
You can then use any text or code editor to generate the `.json` files for each of your supported languages, and put |
|||
them in the `src/translations` folder. |
|||
|
|||
Do no forget to edit the files in `src/environment` to add the supported languages of your application. |
|||
|
|||
### Marking strings for extraction |
|||
|
|||
If strings are not directly passed to `translateService` or put in HTML templates, they may be missing from the |
|||
extraction process. |
|||
|
|||
For these cases, you have to use the dummy `extract()` function: |
|||
|
|||
```typescript |
|||
import { extract } from './core/i18n.service'; |
|||
|
|||
function toBeTranslatedLater() { |
|||
return extract('A string to be translated'); |
|||
} |
|||
``` |
|||
|
|||
Strings marked like this will then be properly extracted. |
@ -0,0 +1,9 @@ |
|||
# Aiolos |
|||
|
|||
Welcome to the project documentation! |
|||
|
|||
Use `npm run docs` for easier navigation. |
|||
|
|||
## Available documentation |
|||
|
|||
[[index]] |
@ -0,0 +1,43 @@ |
|||
# Browser routing |
|||
|
|||
To allow navigation without triggering a server request, Angular now use by default the |
|||
[HTML5 pushState](https://developer.mozilla.org/en-US/docs/Web/API/History_API#Adding_and_modifying_history_entries) |
|||
API enabling natural URL style (like `localhost:4200/home/`), in opposition to Angular 1 which used the _hashbang_ hack |
|||
routing style (like `localhost:4200/#/home/`). |
|||
|
|||
This change has several consequences you should know of, be sure to read the |
|||
[browser URL styles](https://angular.io/docs/ts/latest/guide/router.html#!#browser-url-styles) notice to fully |
|||
understand the differences between the two approaches. |
|||
|
|||
In short: |
|||
|
|||
- 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) |
|||
is required for older browsers. |
|||
|
|||
- You have the option to perform _server-side rendering_ later if you need to increase your app perceived performance. |
|||
|
|||
- You need to [configure URL rewriting](#server-configuration) on your server so that all routes serve your index file. |
|||
|
|||
It is still possible to revert to the hash strategy, but unless you have specific needs, you should stick with the |
|||
default HTML5 routing mode. |
|||
|
|||
## Server configuration |
|||
|
|||
To allow your angular application working properly as a _Single Page Application_ (SPA) and allow bookmarking or |
|||
refreshing any page, you need some configuration on your server, otherwise you will be running into troubles. |
|||
|
|||
> Note that during development, the live reload server already supports SPA mode. |
|||
|
|||
The basic idea is simply to serve the `index.html` file for every request aimed at your application. |
|||
|
|||
Here is an example on how to perform this on an [Express](http://expressjs.com) NodeJS server: |
|||
|
|||
```js |
|||
// Put this in your `server.js` file, after your other rules (APIs, static files...) |
|||
app.get('/*', function(req, res) { |
|||
res.sendFile(__dirname + '/index.html'); |
|||
}); |
|||
``` |
|||
|
|||
For other servers like [Nginx](https://www.nginx.com/blog/creating-nginx-rewrite-rules/) or |
|||
[Apache](http://httpd.apache.org/docs/2.0/misc/rewriteguide.html), you may look for how to perform _URL rewriting_. |
@ -0,0 +1,46 @@ |
|||
# Updating npm dependencies |
|||
|
|||
- Check outdated packages |
|||
|
|||
```sh |
|||
npm outdated |
|||
``` |
|||
|
|||
- Update local packages according to `package.json` |
|||
|
|||
```sh |
|||
npm update |
|||
``` |
|||
|
|||
- Upgrade packages manually |
|||
|
|||
```sh |
|||
npm install --save[-dev] <package_name>@latest |
|||
``` |
|||
|
|||
Alternatively, you can use [npm-check](https://github.com/dylang/npm-check) to perform an interactive upgrade: |
|||
|
|||
```sh |
|||
npm-check -u --skip-unused |
|||
``` |
|||
|
|||
## Locking package versions |
|||
|
|||
Starting from `npm@5` a new `package-lock.json` file is |
|||
[automatically generated](https://docs.npmjs.com/files/package-locks) when using `npm install` commands, to ensure a |
|||
reproducible dependency tree and avoid unwanted package updates. |
|||
|
|||
If you use a previous npm version, it is recommended to use [npm shrinkwrap](https://docs.npmjs.com/cli/shrinkwrap) to |
|||
lock down all your dependencies version: |
|||
|
|||
```sh |
|||
npm shrinkwrap --dev |
|||
``` |
|||
|
|||
This will create a file `npm-shrinkwrap.json` alongside your `package.json` files. |
|||
|
|||
> Do not forget to run shrinkwrap each time you manually update your dependencies! |
|||
|
|||
# Updating angular-related dependencies |
|||
|
|||
See the [Angular update website](https://update.angular.io) to guide you through the updating/upgrading steps. |
@ -0,0 +1,37 @@ |
|||
// Protractor configuration file, see link for more information
|
|||
// https://github.com/angular/protractor/blob/master/lib/config.ts
|
|||
|
|||
const { SpecReporter } = require('jasmine-spec-reporter'); |
|||
|
|||
exports.config = { |
|||
SELENIUM_PROMISE_MANAGER: false, |
|||
allScriptsTimeout: 11000, |
|||
specs: ['./src/**/*.e2e-spec.ts'], |
|||
capabilities: { |
|||
browserName: process.env.PROTRACTOR_BROWSER || 'chrome', |
|||
chromeOptions: { |
|||
binary: process.env.PROTRACTOR_CHROME_BIN || undefined, |
|||
args: process.env.PROTRACTOR_CHROME_ARGS ? JSON.parse(process.env.PROTRACTOR_CHROME_ARGS) : ['lang=en-US'], |
|||
prefs: { |
|||
intl: { accept_languages: 'en-US' } |
|||
} |
|||
} |
|||
}, |
|||
// Only works with Chrome and Firefox
|
|||
directConnect: true, |
|||
baseUrl: 'http://localhost:4200/', |
|||
framework: 'jasmine2', |
|||
jasmineNodeOpts: { |
|||
showColors: true, |
|||
defaultTimeoutInterval: 30000, |
|||
print: function() {} |
|||
}, |
|||
onPrepare() { |
|||
require('ts-node').register({ |
|||
project: require('path').join(__dirname, './tsconfig.e2e.json') |
|||
}); |
|||
|
|||
// Better console spec reporter
|
|||
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); |
|||
} |
|||
}; |
@ -0,0 +1,23 @@ |
|||
import { browser, ExpectedConditions as until } from 'protractor'; |
|||
import { AppSharedPage } from './page-objects/app-shared.po'; |
|||
import { ShellPage } from './page-objects/shell.po'; |
|||
|
|||
describe('when the app loads', () => { |
|||
const app = new AppSharedPage(); |
|||
const shell = new ShellPage(); |
|||
|
|||
beforeAll(async () => { |
|||
await app.navigateAndSetLanguage(); |
|||
}); |
|||
|
|||
it('should display the shell page', async () => { |
|||
expect(await browser.getCurrentUrl()).toContain('/'); |
|||
}); |
|||
|
|||
describe('and the page loads', () => { |
|||
it('should display the hello message', async () => { |
|||
await browser.wait(until.visibilityOf(shell.welcomeText), 5000, 'Element taking too long to appear'); |
|||
expect(await shell.getParagraphText()).toEqual('Hello world !'); |
|||
}); |
|||
}); |
|||
}); |
@ -0,0 +1,18 @@ |
|||
/* |
|||
* Use the Page Object pattern to define the page under test. |
|||
* See docs/coding-guide/e2e-tests.md for more info. |
|||
*/ |
|||
|
|||
import { browser, element, by } from 'protractor'; |
|||
|
|||
export class AppSharedPage { |
|||
async navigateAndSetLanguage() { |
|||
// Forces default language
|
|||
await this.navigateTo(); |
|||
await browser.executeScript(() => localStorage.setItem('language', 'en-US')); |
|||
} |
|||
|
|||
async navigateTo() { |
|||
await browser.get('/'); |
|||
} |
|||
} |
@ -0,0 +1,14 @@ |
|||
/* |
|||
* Use the Page Object pattern to define the page under test. |
|||
* See docs/coding-guide/e2e-tests.md for more info. |
|||
*/ |
|||
|
|||
import { browser, element, by } from 'protractor'; |
|||
|
|||
export class ShellPage { |
|||
welcomeText = element(by.css('app-root h1')); |
|||
|
|||
getParagraphText() { |
|||
return this.welcomeText.getText(); |
|||
} |
|||
} |
@ -0,0 +1,9 @@ |
|||
{ |
|||
"extends": "../tsconfig.json", |
|||
"compilerOptions": { |
|||
"outDir": "../out-tsc/e2e", |
|||
"module": "commonjs", |
|||
"target": "es5", |
|||
"types": ["jasmine", "jasminewd2", "node"] |
|||
} |
|||
} |
@ -0,0 +1,43 @@ |
|||
// Karma configuration file, see link for more information
|
|||
// https://karma-runner.github.io/1.0/config/configuration-file.html
|
|||
|
|||
process.env.CHROME_BIN = require('puppeteer').executablePath(); |
|||
const path = require('path'); |
|||
|
|||
module.exports = function(config) { |
|||
config.set({ |
|||
basePath: '.', |
|||
frameworks: ['jasmine', '@angular-devkit/build-angular'], |
|||
plugins: [ |
|||
require('karma-jasmine'), |
|||
require('karma-chrome-launcher'), |
|||
require('karma-junit-reporter'), |
|||
require('karma-coverage-istanbul-reporter'), |
|||
require('@angular-devkit/build-angular/plugins/karma') |
|||
], |
|||
client: { |
|||
clearContext: false, // leave Jasmine Spec Runner output visible in browser
|
|||
captureConsole: Boolean(process.env.KARMA_ENABLE_CONSOLE) |
|||
}, |
|||
junitReporter: { |
|||
outputDir: path.join(__dirname, './reports/junit/'), |
|||
outputFile: 'TESTS-xunit.xml', |
|||
useBrowserName: false, |
|||
suite: '' // Will become the package name attribute in xml testsuite element
|
|||
}, |
|||
coverageIstanbulReporter: { |
|||
reports: ['html', 'lcovonly', 'text-summary'], |
|||
dir: path.join(__dirname, './reports/coverage'), |
|||
fixWebpackSourcePaths: true |
|||
}, |
|||
reporters: ['progress', 'junit'], |
|||
port: 9876, |
|||
colors: true, |
|||
// Level of logging, can be: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG
|
|||
logLevel: config.LOG_INFO, |
|||
autoWatch: true, |
|||
browsers: ['ChromeHeadless'], |
|||
singleRun: false, |
|||
restartOnFileChange: true |
|||
}); |
|||
}; |
@ -0,0 +1,20 @@ |
|||
{ |
|||
"index": "/index.html", |
|||
"assetGroups": [ |
|||
{ |
|||
"name": "app", |
|||
"installMode": "prefetch", |
|||
"resources": { |
|||
"files": ["/favicon.ico", "/index.html", "/manifest.json", "/*.css", "/*.js"] |
|||
} |
|||
}, |
|||
{ |
|||
"name": "assets", |
|||
"installMode": "lazy", |
|||
"updateMode": "prefetch", |
|||
"resources": { |
|||
"files": ["/assets/**", "/*.woff", "/*.woff2", "/*.ttf", "/*.eot"] |
|||
} |
|||
} |
|||
] |
|||
} |
File diff suppressed because it is too large
@ -0,0 +1,94 @@ |
|||
{ |
|||
"name": "aiolos", |
|||
"version": "1.0.0", |
|||
"private": true, |
|||
"scripts": { |
|||
"ng": "ng", |
|||
"build": "npm run env -s && ng build --prod", |
|||
"start": "npm run env -s && ng serve --proxy-config proxy.conf.js", |
|||
"serve:sw": "npm run build -s && npx http-server ./dist -p 4200", |
|||
"lint": "ng lint && stylelint \"src/**/*.scss\" --syntax scss && htmlhint \"src\" --config .htmlhintrc", |
|||
"test": "npm run env -s && ng test", |
|||
"test:ci": "npm run env -s && npm run lint -s && ng test --configuration=ci", |
|||
"e2e": "npm run env -s && ng e2e", |
|||
"translations:extract": "ngx-translate-extract --input ./src --output ./src/translations/template.json --format=json --clean --sort --marker extract", |
|||
"docs": "hads ./docs -o", |
|||
"env": "ngx-scripts env npm_package_version", |
|||
"prettier": "prettier --write \"./{src,e2e}/**/*.{ts,js,html,scss}\"", |
|||
"prettier:check": "prettier --list-different \"./{src,e2e}/**/*.{ts,js,html,scss}\"", |
|||
"postinstall": "npm run prettier -s", |
|||
"generate": "ng generate" |
|||
}, |
|||
"dependencies": { |
|||
"@angular/animations": "^8.1.0", |
|||
"@angular/common": "^8.1.0", |
|||
"@angular/compiler": "^8.1.0", |
|||
"@angular/core": "^8.1.0", |
|||
"@angular/forms": "^8.1.0", |
|||
"@angular/platform-browser": "^8.1.0", |
|||
"@angular/platform-browser-dynamic": "^8.1.0", |
|||
"@angular/router": "^8.1.0", |
|||
"@ngx-translate/core": "^11.0.1", |
|||
"@angular/service-worker": "^8.1.0", |
|||
"@ng-bootstrap/ng-bootstrap": "^5.0.0-rc.1", |
|||
"bootstrap": "^4.1.1", |
|||
"@fortawesome/fontawesome-free": "^5.1.0", |
|||
"rxjs": "^6.5.2", |
|||
"zone.js": "^0.9.1" |
|||
}, |
|||
"devDependencies": { |
|||
"@angular/cli": "~8.1.0", |
|||
"@angular/compiler-cli": "^8.1.0", |
|||
"@angular/language-service": "^8.1.0", |
|||
"@angular-devkit/build-angular": "^0.801.0", |
|||
"@angularclass/hmr": "^2.1.3", |
|||
"@biesbjerg/ngx-translate-extract": "^2.3.4", |
|||
"@ngx-rocket/scripts": "^4.0.0", |
|||
"@types/jasmine": "^3.3.13", |
|||
"@types/jasminewd2": "^2.0.3", |
|||
"@types/node": "^10.9.0", |
|||
"codelyzer": "^5.1.0", |
|||
"hads": "^1.7.0", |
|||
"htmlhint": "^0.11.0", |
|||
"https-proxy-agent": "^2.0.0", |
|||
"jasmine-core": "~3.4.0", |
|||
"jasmine-spec-reporter": "~4.2.1", |
|||
"karma": "~4.2.0", |
|||
"karma-chrome-launcher": "^3.0.0", |
|||
"karma-cli": "~2.0.0", |
|||
"karma-coverage-istanbul-reporter": "^2.0.2", |
|||
"karma-jasmine": "^2.0.1", |
|||
"karma-jasmine-html-reporter": "^1.4.0", |
|||
"karma-junit-reporter": "^1.2.0", |
|||
"prettier": "^1.16.3", |
|||
"tslint-config-prettier": "^1.14.0", |
|||
"stylelint-config-prettier": "^5.1.0", |
|||
"pretty-quick": "^1.10.0", |
|||
"husky": "^3.0.0", |
|||
"protractor": "~5.4.0", |
|||
"puppeteer": "^1.17.0", |
|||
"stylelint": "~10.1.0", |
|||
"stylelint-config-recommended-scss": "~3.3.0", |
|||
"stylelint-config-standard": "~18.3.0", |
|||
"stylelint-scss": "~3.9.0", |
|||
"ts-node": "^8.3.0", |
|||
"tslint": "~5.18.0", |
|||
"typescript": "~3.4.0" |
|||
}, |
|||
"prettier": { |
|||
"singleQuote": true, |
|||
"overrides": [ |
|||
{ |
|||
"files": "*.scss", |
|||
"options": { |
|||
"singleQuote": false |
|||
} |
|||
} |
|||
] |
|||
}, |
|||
"husky": { |
|||
"hooks": { |
|||
"pre-commit": "pretty-quick --staged" |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,41 @@ |
|||
const HttpsProxyAgent = require('https-proxy-agent'); |
|||
|
|||
/* |
|||
* API proxy configuration. |
|||
* This allows you to proxy HTTP request like `http.get('/api/stuff')` to another server/port. |
|||
* This is especially useful during app development to avoid CORS issues while running a local server. |
|||
* For more details and options, see https://angular.io/guide/build#using-corporate-proxy
|
|||
*/ |
|||
const proxyConfig = [ |
|||
{ |
|||
context: '/api', |
|||
pathRewrite: { '^/api': '' }, |
|||
target: 'https://api.chucknorris.io', |
|||
changeOrigin: true, |
|||
secure: false |
|||
} |
|||
]; |
|||
|
|||
/* |
|||
* Configures a corporate proxy agent for the API proxy if needed. |
|||
*/ |
|||
function setupForCorporateProxy(proxyConfig) { |
|||
if (!Array.isArray(proxyConfig)) { |
|||
proxyConfig = [proxyConfig]; |
|||
} |
|||
|
|||
const proxyServer = process.env.http_proxy || process.env.HTTP_PROXY; |
|||
let agent = null; |
|||
|
|||
if (proxyServer) { |
|||
console.log(`Using corporate proxy server: ${proxyServer}`); |
|||
agent = new HttpsProxyAgent(proxyServer); |
|||
proxyConfig.forEach(entry => { |
|||
entry.agent = agent; |
|||
}); |
|||
} |
|||
|
|||
return proxyConfig; |
|||
} |
|||
|
|||
module.exports = setupForCorporateProxy(proxyConfig); |
@ -0,0 +1,17 @@ |
|||
import { NgModule } from '@angular/core'; |
|||
import { Routes, RouterModule } from '@angular/router'; |
|||
|
|||
import { extract } from '@app/core'; |
|||
import { AboutComponent } from './about.component'; |
|||
|
|||
const routes: Routes = [ |
|||
// Module is lazy loaded, see app-routing.module.ts
|
|||
{ path: '', component: AboutComponent, data: { title: extract('About') } } |
|||
]; |
|||
|
|||
@NgModule({ |
|||
imports: [RouterModule.forChild(routes)], |
|||
exports: [RouterModule], |
|||
providers: [] |
|||
}) |
|||
export class AboutRoutingModule {} |
@ -0,0 +1,8 @@ |
|||
<div class="container-fluid"> |
|||
<div class="jumbotron text-center"> |
|||
<h1> |
|||
<span translate>APP_NAME</span> |
|||
</h1> |
|||
<p><i class="far fa-bookmark"></i> <span translate>Version</span> {{ version }}</p> |
|||
</div> |
|||
</div> |
@ -0,0 +1,24 @@ |
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; |
|||
|
|||
import { AboutComponent } from './about.component'; |
|||
|
|||
describe('AboutComponent', () => { |
|||
let component: AboutComponent; |
|||
let fixture: ComponentFixture<AboutComponent>; |
|||
|
|||
beforeEach(async(() => { |
|||
TestBed.configureTestingModule({ |
|||
declarations: [AboutComponent] |
|||
}).compileComponents(); |
|||
})); |
|||
|
|||
beforeEach(() => { |
|||
fixture = TestBed.createComponent(AboutComponent); |
|||
component = fixture.componentInstance; |
|||
fixture.detectChanges(); |
|||
}); |
|||
|
|||
it('should create', () => { |
|||
expect(component).toBeTruthy(); |
|||
}); |
|||
}); |
@ -0,0 +1,16 @@ |
|||
import { Component, OnInit } from '@angular/core'; |
|||
|
|||
import { environment } from '@env/environment'; |
|||
|
|||
@Component({ |
|||
selector: 'app-about', |
|||
templateUrl: './about.component.html', |
|||
styleUrls: ['./about.component.scss'] |
|||
}) |
|||
export class AboutComponent implements OnInit { |
|||
version: string | null = environment.version; |
|||
|
|||
constructor() {} |
|||
|
|||
ngOnInit() {} |
|||
} |
@ -0,0 +1,12 @@ |
|||
import { NgModule } from '@angular/core'; |
|||
import { CommonModule } from '@angular/common'; |
|||
import { TranslateModule } from '@ngx-translate/core'; |
|||
|
|||
import { AboutRoutingModule } from './about-routing.module'; |
|||
import { AboutComponent } from './about.component'; |
|||
|
|||
@NgModule({ |
|||
imports: [CommonModule, TranslateModule, AboutRoutingModule], |
|||
declarations: [AboutComponent] |
|||
}) |
|||
export class AboutModule {} |
@ -0,0 +1,16 @@ |
|||
import { NgModule } from '@angular/core'; |
|||
import { Routes, RouterModule, PreloadAllModules } from '@angular/router'; |
|||
import { Shell } from '@app/shell/shell.service'; |
|||
|
|||
const routes: Routes = [ |
|||
Shell.childRoutes([{ path: 'about', loadChildren: './about/about.module#AboutModule' }]), |
|||
// Fallback when no prior route is matched
|
|||
{ path: '**', redirectTo: '', pathMatch: 'full' } |
|||
]; |
|||
|
|||
@NgModule({ |
|||
imports: [RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })], |
|||
exports: [RouterModule], |
|||
providers: [] |
|||
}) |
|||
export class AppRoutingModule {} |
@ -0,0 +1 @@ |
|||
<router-outlet></router-outlet> |
@ -0,0 +1,22 @@ |
|||
import { TestBed, async } from '@angular/core/testing'; |
|||
import { RouterTestingModule } from '@angular/router/testing'; |
|||
import { TranslateModule } from '@ngx-translate/core'; |
|||
|
|||
import { CoreModule } from '@app/core'; |
|||
import { AppComponent } from './app.component'; |
|||
|
|||
describe('AppComponent', () => { |
|||
beforeEach(async(() => { |
|||
TestBed.configureTestingModule({ |
|||
imports: [RouterTestingModule, TranslateModule.forRoot(), CoreModule], |
|||
declarations: [AppComponent], |
|||
providers: [] |
|||
}).compileComponents(); |
|||
})); |
|||
|
|||
it('should create the app', async(() => { |
|||
const fixture = TestBed.createComponent(AppComponent); |
|||
const app = fixture.debugElement.componentInstance; |
|||
expect(app).toBeTruthy(); |
|||
}), 30000); |
|||
}); |
@ -0,0 +1,65 @@ |
|||
import { Component, OnInit, OnDestroy } from '@angular/core'; |
|||
import { Router, NavigationEnd, ActivatedRoute } from '@angular/router'; |
|||
import { Title } from '@angular/platform-browser'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { merge } from 'rxjs'; |
|||
import { filter, map, switchMap } from 'rxjs/operators'; |
|||
|
|||
import { environment } from '@env/environment'; |
|||
import { Logger, I18nService, untilDestroyed } from '@app/core'; |
|||
|
|||
const log = new Logger('App'); |
|||
|
|||
@Component({ |
|||
selector: 'app-root', |
|||
templateUrl: './app.component.html', |
|||
styleUrls: ['./app.component.scss'] |
|||
}) |
|||
export class AppComponent implements OnInit, OnDestroy { |
|||
constructor( |
|||
private router: Router, |
|||
private activatedRoute: ActivatedRoute, |
|||
private titleService: Title, |
|||
private translateService: TranslateService, |
|||
private i18nService: I18nService |
|||
) {} |
|||
|
|||
ngOnInit() { |
|||
// Setup logger
|
|||
if (environment.production) { |
|||
Logger.enableProductionMode(); |
|||
} |
|||
|
|||
log.debug('init'); |
|||
|
|||
// Setup translations
|
|||
this.i18nService.init(environment.defaultLanguage, environment.supportedLanguages); |
|||
|
|||
const onNavigationEnd = this.router.events.pipe(filter(event => event instanceof NavigationEnd)); |
|||
|
|||
// Change page title on navigation or language change, based on route data
|
|||
merge(this.translateService.onLangChange, onNavigationEnd) |
|||
.pipe( |
|||
map(() => { |
|||
let route = this.activatedRoute; |
|||
while (route.firstChild) { |
|||
route = route.firstChild; |
|||
} |
|||
return route; |
|||
}), |
|||
filter(route => route.outlet === 'primary'), |
|||
switchMap(route => route.data), |
|||
untilDestroyed(this) |
|||
) |
|||
.subscribe(event => { |
|||
const title = event.title; |
|||
if (title) { |
|||
this.titleService.setTitle(this.translateService.instant(title)); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
ngOnDestroy() { |
|||
this.i18nService.destroy(); |
|||
} |
|||
} |
@ -0,0 +1,35 @@ |
|||
import { BrowserModule } from '@angular/platform-browser'; |
|||
import { NgModule } from '@angular/core'; |
|||
import { FormsModule } from '@angular/forms'; |
|||
import { HttpClientModule } from '@angular/common/http'; |
|||
import { ServiceWorkerModule } from '@angular/service-worker'; |
|||
import { TranslateModule } from '@ngx-translate/core'; |
|||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; |
|||
|
|||
import { environment } from '@env/environment'; |
|||
import { CoreModule } from '@app/core'; |
|||
import { SharedModule } from '@app/shared'; |
|||
import { HomeModule } from './home/home.module'; |
|||
import { ShellModule } from './shell/shell.module'; |
|||
import { AppComponent } from './app.component'; |
|||
import { AppRoutingModule } from './app-routing.module'; |
|||
|
|||
@NgModule({ |
|||
imports: [ |
|||
BrowserModule, |
|||
ServiceWorkerModule.register('./ngsw-worker.js', { enabled: environment.production }), |
|||
FormsModule, |
|||
HttpClientModule, |
|||
TranslateModule.forRoot(), |
|||
NgbModule, |
|||
CoreModule, |
|||
SharedModule, |
|||
ShellModule, |
|||
HomeModule, |
|||
AppRoutingModule // must be imported as the last module as it contains the fallback route
|
|||
], |
|||
declarations: [AppComponent], |
|||
providers: [], |
|||
bootstrap: [AppComponent] |
|||
}) |
|||
export class AppModule {} |
@ -0,0 +1,30 @@ |
|||
import { NgModule, Optional, SkipSelf } from '@angular/core'; |
|||
import { CommonModule } from '@angular/common'; |
|||
import { HTTP_INTERCEPTORS, HttpClient, HttpClientModule } from '@angular/common/http'; |
|||
import { RouteReuseStrategy, RouterModule } from '@angular/router'; |
|||
import { TranslateModule } from '@ngx-translate/core'; |
|||
|
|||
import { RouteReusableStrategy } from './route-reusable-strategy'; |
|||
import { HttpService } from './http/http.service'; |
|||
|
|||
@NgModule({ |
|||
imports: [CommonModule, HttpClientModule, TranslateModule, RouterModule], |
|||
providers: [ |
|||
{ |
|||
provide: HttpClient, |
|||
useClass: HttpService |
|||
}, |
|||
{ |
|||
provide: RouteReuseStrategy, |
|||
useClass: RouteReusableStrategy |
|||
} |
|||
] |
|||
}) |
|||
export class CoreModule { |
|||
constructor(@Optional() @SkipSelf() parentModule: CoreModule) { |
|||
// Import guard
|
|||
if (parentModule) { |
|||
throw new Error(`${parentModule} has already been loaded. Import Core module in the AppModule only.`); |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,48 @@ |
|||
import { Type } from '@angular/core'; |
|||
import { TestBed } from '@angular/core/testing'; |
|||
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; |
|||
import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http'; |
|||
|
|||
import { environment } from '@env/environment'; |
|||
import { ApiPrefixInterceptor } from './api-prefix.interceptor'; |
|||
|
|||
describe('ApiPrefixInterceptor', () => { |
|||
let http: HttpClient; |
|||
let httpMock: HttpTestingController; |
|||
|
|||
beforeEach(() => { |
|||
TestBed.configureTestingModule({ |
|||
imports: [HttpClientTestingModule], |
|||
providers: [ |
|||
{ |
|||
provide: HTTP_INTERCEPTORS, |
|||
useClass: ApiPrefixInterceptor, |
|||
multi: true |
|||
} |
|||
] |
|||
}); |
|||
|
|||
http = TestBed.get(HttpClient); |
|||
httpMock = TestBed.get(HttpTestingController as Type<HttpTestingController>); |
|||
}); |
|||
|
|||
afterEach(() => { |
|||
httpMock.verify(); |
|||
}); |
|||
|
|||
it('should prepend environment.serverUrl to the request url', () => { |
|||
// Act
|
|||
http.get('/toto').subscribe(); |
|||
|
|||
// Assert
|
|||
httpMock.expectOne({ url: environment.serverUrl + '/toto' }); |
|||
}); |
|||
|
|||
it('should not prepend environment.serverUrl to request url', () => { |
|||
// Act
|
|||
http.get('hTtPs://domain.com/toto').subscribe(); |
|||
|
|||
// Assert
|
|||
httpMock.expectOne({ url: 'hTtPs://domain.com/toto' }); |
|||
}); |
|||
}); |
@ -0,0 +1,20 @@ |
|||
import { Injectable } from '@angular/core'; |
|||
import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http'; |
|||
import { Observable } from 'rxjs'; |
|||
|
|||
import { environment } from '@env/environment'; |
|||
|
|||
/** |
|||
* Prefixes all requests not starting with `http[s]` with `environment.serverUrl`. |
|||
*/ |
|||
@Injectable({ |
|||
providedIn: 'root' |
|||
}) |
|||
export class ApiPrefixInterceptor implements HttpInterceptor { |
|||
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { |
|||
if (!/^(http|https):/i.test(request.url)) { |
|||
request = request.clone({ url: environment.serverUrl + request.url }); |
|||
} |
|||
return next.handle(request); |
|||
} |
|||
} |
@ -0,0 +1,115 @@ |
|||
import { Type } from '@angular/core'; |
|||
import { TestBed } from '@angular/core/testing'; |
|||
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; |
|||
import { HTTP_INTERCEPTORS, HttpClient, HttpResponse } from '@angular/common/http'; |
|||
|
|||
import { CacheInterceptor } from './cache.interceptor'; |
|||
import { HttpCacheService } from './http-cache.service'; |
|||
|
|||
describe('CacheInterceptor', () => { |
|||
let interceptorOptions: object | null = {}; |
|||
let httpCacheService: HttpCacheService; |
|||
let http: HttpClient; |
|||
let httpMock: HttpTestingController; |
|||
|
|||
function createInterceptor(_httpCacheService: HttpCacheService) { |
|||
return new CacheInterceptor(_httpCacheService).configure(interceptorOptions); |
|||
} |
|||
|
|||
beforeEach(() => { |
|||
TestBed.configureTestingModule({ |
|||
imports: [HttpClientTestingModule], |
|||
providers: [ |
|||
HttpCacheService, |
|||
{ |
|||
provide: HTTP_INTERCEPTORS, |
|||
useFactory: createInterceptor, |
|||
deps: [HttpCacheService], |
|||
multi: true |
|||
} |
|||
] |
|||
}); |
|||
}); |
|||
|
|||
afterEach(() => { |
|||
httpCacheService.cleanCache(); |
|||
httpMock.verify(); |
|||
}); |
|||
|
|||
describe('with default configuration', () => { |
|||
beforeEach(() => { |
|||
interceptorOptions = null; |
|||
http = TestBed.get(HttpClient); |
|||
httpMock = TestBed.get(HttpTestingController as Type<HttpTestingController>); |
|||
httpCacheService = TestBed.get(HttpCacheService); |
|||
}); |
|||
|
|||
it('should cache the request', () => { |
|||
// Act
|
|||
http.get('/toto').subscribe(() => { |
|||
// Assert
|
|||
const cachedData = httpCacheService.getCacheData('/toto'); |
|||
expect(cachedData).toBeDefined(); |
|||
expect(cachedData ? cachedData.body : null).toEqual('someData'); |
|||
}); |
|||
|
|||
httpMock.expectOne({ url: '/toto' }).flush('someData'); |
|||
}); |
|||
|
|||
it('should respond from the cache', () => { |
|||
// Arrange
|
|||
httpCacheService.setCacheData('/toto', new HttpResponse({ body: 'cachedData' })); |
|||
|
|||
// Act
|
|||
http.get('/toto').subscribe(response => { |
|||
// Assert
|
|||
expect(response).toEqual('cachedData'); |
|||
}); |
|||
|
|||
httpMock.expectNone({ url: '/toto' }); |
|||
}); |
|||
|
|||
it('should not cache the request in case of error', () => { |
|||
// Act
|
|||
http.get('/toto').subscribe( |
|||
() => {}, |
|||
() => { |
|||
// Assert
|
|||
expect(httpCacheService.getCacheData('/toto')).toBeNull(); |
|||
} |
|||
); |
|||
|
|||
httpMock.expectOne({}).flush(null, { |
|||
status: 404, |
|||
statusText: 'error' |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
describe('with update forced configuration', () => { |
|||
beforeEach(() => { |
|||
interceptorOptions = { update: true }; |
|||
http = TestBed.get(HttpClient); |
|||
httpMock = TestBed.get(HttpTestingController as Type<HttpTestingController>); |
|||
httpCacheService = TestBed.get(HttpCacheService); |
|||
}); |
|||
|
|||
afterEach(() => { |
|||
httpCacheService.cleanCache(); |
|||
httpMock.verify(); |
|||
}); |
|||
|
|||
it('should force cache update', () => { |
|||
// Arrange
|
|||
httpCacheService.setCacheData('/toto', new HttpResponse({ body: 'oldCachedData' })); |
|||
|
|||
// Act
|
|||
http.get('/toto').subscribe(response => { |
|||
// Assert
|
|||
expect(response).toEqual('newData'); |
|||
}); |
|||
|
|||
httpMock.expectOne({ url: '/toto' }).flush('newData'); |
|||
}); |
|||
}); |
|||
}); |
@ -0,0 +1,57 @@ |
|||
import { Injectable } from '@angular/core'; |
|||
import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpResponse } from '@angular/common/http'; |
|||
import { Observable, Subscriber } from 'rxjs'; |
|||
|
|||
import { HttpCacheService } from './http-cache.service'; |
|||
|
|||
/** |
|||
* Caches HTTP requests. |
|||
* Use ExtendedHttpClient fluent API to configure caching for each request. |
|||
*/ |
|||
@Injectable({ |
|||
providedIn: 'root' |
|||
}) |
|||
export class CacheInterceptor implements HttpInterceptor { |
|||
private forceUpdate = false; |
|||
|
|||
constructor(private httpCacheService: HttpCacheService) {} |
|||
|
|||
/** |
|||
* Configures interceptor options |
|||
* @param options If update option is enabled, forces request to be made and updates cache entry. |
|||
* @return The configured instance. |
|||
*/ |
|||
configure(options?: { update?: boolean } | null): CacheInterceptor { |
|||
const instance = new CacheInterceptor(this.httpCacheService); |
|||
if (options && options.update) { |
|||
instance.forceUpdate = true; |
|||
} |
|||
return instance; |
|||
} |
|||
|
|||
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { |
|||
if (request.method !== 'GET') { |
|||
return next.handle(request); |
|||
} |
|||
|
|||
return new Observable((subscriber: Subscriber<HttpEvent<any>>) => { |
|||
const cachedData = this.forceUpdate ? null : this.httpCacheService.getCacheData(request.urlWithParams); |
|||
if (cachedData !== null) { |
|||
// Create new response to avoid side-effects
|
|||
subscriber.next(new HttpResponse(cachedData as object)); |
|||
subscriber.complete(); |
|||
} else { |
|||
next.handle(request).subscribe( |
|||
event => { |
|||
if (event instanceof HttpResponse) { |
|||
this.httpCacheService.setCacheData(request.urlWithParams, event); |
|||
} |
|||
subscriber.next(event); |
|||
}, |
|||
error => subscriber.error(error), |
|||
() => subscriber.complete() |
|||
); |
|||
} |
|||
}); |
|||
} |
|||
} |
@ -0,0 +1,58 @@ |
|||
import { Type } from '@angular/core'; |
|||
import { TestBed } from '@angular/core/testing'; |
|||
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; |
|||
import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http'; |
|||
|
|||
import { ErrorHandlerInterceptor } from './error-handler.interceptor'; |
|||
|
|||
describe('ErrorHandlerInterceptor', () => { |
|||
let errorHandlerInterceptor: ErrorHandlerInterceptor; |
|||
let http: HttpClient; |
|||
let httpMock: HttpTestingController; |
|||
|
|||
function createInterceptor() { |
|||
errorHandlerInterceptor = new ErrorHandlerInterceptor(); |
|||
return errorHandlerInterceptor; |
|||
} |
|||
|
|||
beforeEach(() => { |
|||
TestBed.configureTestingModule({ |
|||
imports: [HttpClientTestingModule], |
|||
providers: [ |
|||
{ |
|||
provide: HTTP_INTERCEPTORS, |
|||
useFactory: createInterceptor, |
|||
multi: true |
|||
} |
|||
] |
|||
}); |
|||
|
|||
http = TestBed.get(HttpClient); |
|||
httpMock = TestBed.get(HttpTestingController as Type<HttpTestingController>); |
|||
}); |
|||
|
|||
afterEach(() => { |
|||
httpMock.verify(); |
|||
}); |
|||
|
|||
it('should catch error and call error handler', () => { |
|||
// Arrange
|
|||
// Note: here we spy on private method since target is customization here,
|
|||
// but you should replace it by actual behavior in your app
|
|||
spyOn(ErrorHandlerInterceptor.prototype as any, 'errorHandler').and.callThrough(); |
|||
|
|||
// Act
|
|||
http.get('/toto').subscribe( |
|||
() => fail('should error'), |
|||
() => { |
|||
// Assert
|
|||
expect((ErrorHandlerInterceptor.prototype as any).errorHandler).toHaveBeenCalled(); |
|||
} |
|||
); |
|||
|
|||
httpMock.expectOne({}).flush(null, { |
|||
status: 404, |
|||
statusText: 'error' |
|||
}); |
|||
}); |
|||
}); |
@ -0,0 +1,30 @@ |
|||
import { Injectable } from '@angular/core'; |
|||
import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http'; |
|||
import { Observable } from 'rxjs'; |
|||
import { catchError } from 'rxjs/operators'; |
|||
|
|||
import { environment } from '@env/environment'; |
|||
import { Logger } from '../logger.service'; |
|||
|
|||
const log = new Logger('ErrorHandlerInterceptor'); |
|||
|
|||
/** |
|||
* Adds a default error handler to all requests. |
|||
*/ |
|||
@Injectable({ |
|||
providedIn: 'root' |
|||
}) |
|||
export class ErrorHandlerInterceptor implements HttpInterceptor { |
|||
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { |
|||
return next.handle(request).pipe(catchError(error => this.errorHandler(error))); |
|||
} |
|||
|
|||
// Customize the default error handler here if needed
|
|||
private errorHandler(response: HttpEvent<any>): Observable<HttpEvent<any>> { |
|||
if (!environment.production) { |
|||
// Do something with the error
|
|||
log.error('Request error', response); |
|||
} |
|||
throw response; |
|||
} |
|||
} |
@ -0,0 +1,192 @@ |
|||
import { TestBed } from '@angular/core/testing'; |
|||
import { HttpResponse } from '@angular/common/http'; |
|||
|
|||
import { HttpCacheService, HttpCacheEntry } from './http-cache.service'; |
|||
|
|||
const cachePersistenceKey = 'httpCache'; |
|||
|
|||
describe('HttpCacheService', () => { |
|||
let httpCacheService: HttpCacheService; |
|||
let response: HttpResponse<any>; |
|||
|
|||
beforeEach(() => { |
|||
TestBed.configureTestingModule({ |
|||
providers: [HttpCacheService] |
|||
}); |
|||
|
|||
// Start fresh
|
|||
window.sessionStorage.removeItem(cachePersistenceKey); |
|||
window.localStorage.removeItem(cachePersistenceKey); |
|||
|
|||
httpCacheService = TestBed.get(HttpCacheService); |
|||
|
|||
response = new HttpResponse({ body: 'data' }); |
|||
}); |
|||
|
|||
afterEach(() => { |
|||
httpCacheService.cleanCache(); |
|||
}); |
|||
|
|||
describe('setCacheData', () => { |
|||
it('should set cache data', () => { |
|||
// Act
|
|||
httpCacheService.setCacheData('/popo', response); |
|||
|
|||
// Assert
|
|||
expect(httpCacheService.getCacheData('/popo')).toEqual(response); |
|||
}); |
|||
|
|||
it('should replace existing data', () => { |
|||
// Arrange
|
|||
const newResponse = new HttpResponse({ body: 'new data' }); |
|||
|
|||
// Act
|
|||
httpCacheService.setCacheData('/popo', response); |
|||
httpCacheService.setCacheData('/popo', newResponse); |
|||
|
|||
// Assert
|
|||
expect(httpCacheService.getCacheData('/popo')).toEqual(newResponse); |
|||
}); |
|||
|
|||
it('should set cache date correctly', () => { |
|||
// Act
|
|||
const date = new Date(123); |
|||
httpCacheService.setCacheData('/popo', response, date); |
|||
httpCacheService.setCacheData('/hoho', response); |
|||
|
|||
// Assert
|
|||
expect((httpCacheService.getHttpCacheEntry('/popo') as HttpCacheEntry).lastUpdated).toBe(date); |
|||
expect((httpCacheService.getHttpCacheEntry('/hoho') as HttpCacheEntry).lastUpdated).not.toBe(date); |
|||
}); |
|||
}); |
|||
|
|||
describe('getCacheData', () => { |
|||
it('should return null if no cache', () => { |
|||
expect(httpCacheService.getCacheData('/hoho')).toBe(null); |
|||
}); |
|||
|
|||
it('should return cached data if exists', () => { |
|||
// Act
|
|||
httpCacheService.setCacheData('/hoho', response); |
|||
|
|||
// Assert
|
|||
expect(httpCacheService.getCacheData('/hoho')).toEqual(response); |
|||
}); |
|||
|
|||
it('should return cached data with url parameters if exists', () => { |
|||
// Act
|
|||
httpCacheService.setCacheData('/hoho?pif=paf', response); |
|||
|
|||
// Assert
|
|||
expect(httpCacheService.getCacheData('/hoho?pif=paf')).toEqual(response); |
|||
}); |
|||
}); |
|||
|
|||
describe('getHttpCacheEntry', () => { |
|||
it('should return null if no cache', () => { |
|||
expect(httpCacheService.getHttpCacheEntry('/hoho')).toBe(null); |
|||
}); |
|||
|
|||
it('should return cached data date if exists', () => { |
|||
// Arrange
|
|||
const date = new Date(123); |
|||
|
|||
// Act
|
|||
httpCacheService.setCacheData('/hoho', response, date); |
|||
const entry = httpCacheService.getHttpCacheEntry('/hoho') as HttpCacheEntry; |
|||
|
|||
// Assert
|
|||
expect(entry).not.toBeNull(); |
|||
expect(entry.lastUpdated).toEqual(date); |
|||
expect(entry.data).toEqual(response); |
|||
}); |
|||
}); |
|||
|
|||
describe('clearCacheData', () => { |
|||
it('should clear existing cache data', () => { |
|||
// Set cache
|
|||
httpCacheService.setCacheData('/hoho', response); |
|||
expect(httpCacheService.getCacheData('/hoho')).toEqual(response); |
|||
|
|||
// Clear cache
|
|||
httpCacheService.clearCache('/hoho'); |
|||
expect(httpCacheService.getCacheData('/hoho')).toBe(null); |
|||
}); |
|||
|
|||
it('should do nothing if no cache exists', () => { |
|||
expect(httpCacheService.getCacheData('/lolo')).toBe(null); |
|||
httpCacheService.clearCache('/hoho'); |
|||
expect(httpCacheService.getCacheData('/lolo')).toBe(null); |
|||
}); |
|||
}); |
|||
|
|||
describe('cleanCache', () => { |
|||
it('should clear all cache if no date is specified', () => { |
|||
// Set cache
|
|||
httpCacheService.setCacheData('/hoho', response); |
|||
httpCacheService.setCacheData('/popo', response); |
|||
expect(httpCacheService.getCacheData('/hoho')).toBe(response); |
|||
expect(httpCacheService.getCacheData('/popo')).toBe(response); |
|||
|
|||
// Clean cache
|
|||
httpCacheService.cleanCache(); |
|||
expect(httpCacheService.getCacheData('/hoho')).toBe(null); |
|||
expect(httpCacheService.getCacheData('/popo')).toBe(null); |
|||
}); |
|||
|
|||
it('should clear existing since specified date', () => { |
|||
// Set cache
|
|||
httpCacheService.setCacheData('/hoho', response); |
|||
expect(httpCacheService.getCacheData('/hoho')).toBe(response); |
|||
|
|||
// Clean cache
|
|||
httpCacheService.cleanCache(new Date()); |
|||
expect(httpCacheService.getCacheData('/hoho')).toBe(null); |
|||
}); |
|||
|
|||
it('should not affect cache entries newer than specified date', () => { |
|||
// Set cache
|
|||
httpCacheService.setCacheData('/hoho', response); |
|||
expect(httpCacheService.getCacheData('/hoho')).toBe(response); |
|||
|
|||
// Clean cache
|
|||
const date = new Date(); |
|||
httpCacheService.setCacheData('/lolo', response, new Date(date.getTime() + 10)); |
|||
httpCacheService.cleanCache(date); |
|||
|
|||
// Assert
|
|||
expect(httpCacheService.getCacheData('/hoho')).toBe(null); |
|||
expect(httpCacheService.getCacheData('/lolo')).toBe(response); |
|||
}); |
|||
}); |
|||
|
|||
describe('setPersistence', () => { |
|||
beforeEach(() => { |
|||
httpCacheService.setPersistence(); |
|||
httpCacheService.cleanCache = jasmine.createSpy('cleanCache'); |
|||
}); |
|||
|
|||
it('should clear previous cache data when persistence value change', () => { |
|||
httpCacheService.setPersistence('local'); |
|||
expect(httpCacheService.cleanCache).toHaveBeenCalledWith(); |
|||
}); |
|||
|
|||
it('should persist cache to local storage', () => { |
|||
expect(localStorage.getItem(cachePersistenceKey)).toBeNull(); |
|||
|
|||
httpCacheService.setPersistence('local'); |
|||
httpCacheService.setCacheData('/hoho', response); |
|||
|
|||
expect(localStorage.getItem(cachePersistenceKey)).not.toBeNull(); |
|||
}); |
|||
|
|||
it('should persist cache to session storage', () => { |
|||
expect(sessionStorage.getItem(cachePersistenceKey)).toBeNull(); |
|||
|
|||
httpCacheService.setPersistence('session'); |
|||
httpCacheService.setCacheData('/hoho', response); |
|||
|
|||
expect(sessionStorage.getItem(cachePersistenceKey)).not.toBeNull(); |
|||
}); |
|||
}); |
|||
}); |
@ -0,0 +1,117 @@ |
|||
import { Injectable } from '@angular/core'; |
|||
import { HttpResponse } from '@angular/common/http'; |
|||
|
|||
import { Logger } from '../logger.service'; |
|||
|
|||
const log = new Logger('HttpCacheService'); |
|||
const cachePersistenceKey = 'httpCache'; |
|||
|
|||
export interface HttpCacheEntry { |
|||
lastUpdated: Date; |
|||
data: HttpResponse<any>; |
|||
} |
|||
|
|||
/** |
|||
* Provides a cache facility for HTTP requests with configurable persistence policy. |
|||
*/ |
|||
@Injectable({ |
|||
providedIn: 'root' |
|||
}) |
|||
export class HttpCacheService { |
|||
private cachedData: { [key: string]: HttpCacheEntry } = {}; |
|||
private storage: Storage | null = null; |
|||
|
|||
constructor() { |
|||
this.loadCacheData(); |
|||
} |
|||
|
|||
/** |
|||
* Sets the cache data for the specified request. |
|||
* @param url The request URL. |
|||
* @param data The received data. |
|||
* @param lastUpdated The cache last update, current date is used if not specified. |
|||
*/ |
|||
setCacheData(url: string, data: HttpResponse<any>, lastUpdated?: Date) { |
|||
this.cachedData[url] = { |
|||
lastUpdated: lastUpdated || new Date(), |
|||
data |
|||
}; |
|||
log.debug(`Cache set for key: "${url}"`); |
|||
this.saveCacheData(); |
|||
} |
|||
|
|||
/** |
|||
* Gets the cached data for the specified request. |
|||
* @param url The request URL. |
|||
* @return The cached data or null if no cached data exists for this request. |
|||
*/ |
|||
getCacheData(url: string): HttpResponse<any> | null { |
|||
const cacheEntry = this.cachedData[url]; |
|||
|
|||
if (cacheEntry) { |
|||
log.debug(`Cache hit for key: "${url}"`); |
|||
return cacheEntry.data; |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
|
|||
/** |
|||
* Gets the cached entry for the specified request. |
|||
* @param url The request URL. |
|||
* @return The cache entry or null if no cache entry exists for this request. |
|||
*/ |
|||
getHttpCacheEntry(url: string): HttpCacheEntry | null { |
|||
return this.cachedData[url] || null; |
|||
} |
|||
|
|||
/** |
|||
* Clears the cached entry (if exists) for the specified request. |
|||
* @param url The request URL. |
|||
*/ |
|||
clearCache(url: string): void { |
|||
delete this.cachedData[url]; |
|||
log.debug(`Cache cleared for key: "${url}"`); |
|||
this.saveCacheData(); |
|||
} |
|||
|
|||
/** |
|||
* Cleans cache entries older than the specified date. |
|||
* @param expirationDate The cache expiration date. If no date is specified, all cache is cleared. |
|||
*/ |
|||
cleanCache(expirationDate?: Date) { |
|||
if (expirationDate) { |
|||
Object.entries(this.cachedData).forEach(([key, value]) => { |
|||
if (expirationDate >= value.lastUpdated) { |
|||
delete this.cachedData[key]; |
|||
} |
|||
}); |
|||
} else { |
|||
this.cachedData = {}; |
|||
} |
|||
this.saveCacheData(); |
|||
} |
|||
|
|||
/** |
|||
* Sets the cache persistence policy. |
|||
* Note that changing the cache persistence will also clear the cache from its previous storage. |
|||
* @param persistence How the cache should be persisted, it can be either local or session storage, or if no value is |
|||
* provided it will be only in-memory (default). |
|||
*/ |
|||
setPersistence(persistence?: 'local' | 'session') { |
|||
this.cleanCache(); |
|||
this.storage = persistence === 'local' || persistence === 'session' ? window[persistence + 'Storage'] : null; |
|||
this.loadCacheData(); |
|||
} |
|||
|
|||
private saveCacheData() { |
|||
if (this.storage) { |
|||
this.storage.setItem(cachePersistenceKey, JSON.stringify(this.cachedData)); |
|||
} |
|||
} |
|||
|
|||
private loadCacheData() { |
|||
const data = this.storage ? this.storage.getItem(cachePersistenceKey) : null; |
|||
this.cachedData = data ? JSON.parse(data) : {}; |
|||
} |
|||
} |
@ -0,0 +1,106 @@ |
|||
import { Type } from '@angular/core'; |
|||
import { TestBed } from '@angular/core/testing'; |
|||
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; |
|||
import { HttpClient, HttpInterceptor } from '@angular/common/http'; |
|||
|
|||
import { HttpService } from './http.service'; |
|||
import { HttpCacheService } from './http-cache.service'; |
|||
import { ErrorHandlerInterceptor } from './error-handler.interceptor'; |
|||
import { CacheInterceptor } from './cache.interceptor'; |
|||
import { ApiPrefixInterceptor } from './api-prefix.interceptor'; |
|||
|
|||
describe('HttpService', () => { |
|||
let httpCacheService: HttpCacheService; |
|||
let http: HttpClient; |
|||
let httpMock: HttpTestingController; |
|||
let interceptors: HttpInterceptor[]; |
|||
|
|||
beforeEach(() => { |
|||
TestBed.configureTestingModule({ |
|||
imports: [HttpClientTestingModule], |
|||
providers: [ |
|||
ErrorHandlerInterceptor, |
|||
CacheInterceptor, |
|||
ApiPrefixInterceptor, |
|||
HttpCacheService, |
|||
{ |
|||
provide: HttpClient, |
|||
useClass: HttpService |
|||
} |
|||
] |
|||
}); |
|||
|
|||
http = TestBed.get(HttpClient); |
|||
httpMock = TestBed.get(HttpTestingController as Type<HttpTestingController>); |
|||
httpCacheService = TestBed.get(HttpCacheService); |
|||
|
|||
const realRequest = http.request; |
|||
spyOn(HttpService.prototype, 'request').and.callFake(function( |
|||
this: any, |
|||
method: string, |
|||
url: string, |
|||
options?: any |
|||
) { |
|||
interceptors = this.interceptors; |
|||
return realRequest.call(this, method, url, options); |
|||
}); |
|||
}); |
|||
|
|||
afterEach(() => { |
|||
httpCacheService.cleanCache(); |
|||
httpMock.verify(); |
|||
}); |
|||
|
|||
it('should use error handler, API prefix and no cache by default', () => { |
|||
// Act
|
|||
const request = http.get('/toto'); |
|||
|
|||
// Assert
|
|||
request.subscribe(() => { |
|||
expect(http.request).toHaveBeenCalled(); |
|||
expect(interceptors.some(i => i instanceof ApiPrefixInterceptor)).toBeTruthy(); |
|||
expect(interceptors.some(i => i instanceof ErrorHandlerInterceptor)).toBeTruthy(); |
|||
expect(interceptors.some(i => i instanceof CacheInterceptor)).toBeFalsy(); |
|||
}); |
|||
httpMock.expectOne({}).flush({}); |
|||
}); |
|||
|
|||
it('should use cache', () => { |
|||
// Act
|
|||
const request = http.cache().get('/toto'); |
|||
|
|||
// Assert
|
|||
request.subscribe(() => { |
|||
expect(interceptors.some(i => i instanceof ApiPrefixInterceptor)).toBeTruthy(); |
|||
expect(interceptors.some(i => i instanceof ErrorHandlerInterceptor)).toBeTruthy(); |
|||
expect(interceptors.some(i => i instanceof CacheInterceptor)).toBeTruthy(); |
|||
}); |
|||
httpMock.expectOne({}).flush({}); |
|||
}); |
|||
|
|||
it('should skip error handler', () => { |
|||
// Act
|
|||
const request = http.skipErrorHandler().get('/toto'); |
|||
|
|||
// Assert
|
|||
request.subscribe(() => { |
|||
expect(interceptors.some(i => i instanceof ApiPrefixInterceptor)).toBeTruthy(); |
|||
expect(interceptors.some(i => i instanceof ErrorHandlerInterceptor)).toBeFalsy(); |
|||
expect(interceptors.some(i => i instanceof CacheInterceptor)).toBeFalsy(); |
|||
}); |
|||
httpMock.expectOne({}).flush({}); |
|||
}); |
|||
|
|||
it('should not use API prefix', () => { |
|||
// Act
|
|||
const request = http.disableApiPrefix().get('/toto'); |
|||
|
|||
// Assert
|
|||
request.subscribe(() => { |
|||
expect(interceptors.some(i => i instanceof ApiPrefixInterceptor)).toBeFalsy(); |
|||
expect(interceptors.some(i => i instanceof ErrorHandlerInterceptor)).toBeTruthy(); |
|||
expect(interceptors.some(i => i instanceof CacheInterceptor)).toBeFalsy(); |
|||
}); |
|||
httpMock.expectOne({}).flush({}); |
|||
}); |
|||
}); |
@ -0,0 +1,110 @@ |
|||
import { Inject, Injectable, InjectionToken, Injector, Optional, Type } from '@angular/core'; |
|||
import { HttpClient, HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http'; |
|||
import { Observable } from 'rxjs'; |
|||
|
|||
import { ErrorHandlerInterceptor } from './error-handler.interceptor'; |
|||
import { CacheInterceptor } from './cache.interceptor'; |
|||
import { ApiPrefixInterceptor } from './api-prefix.interceptor'; |
|||
|
|||
// HttpClient is declared in a re-exported module, so we have to extend the original module to make it work properly
|
|||
// (see https://github.com/Microsoft/TypeScript/issues/13897)
|
|||
declare module '@angular/common/http/http' { |
|||
// Augment HttpClient with the added configuration methods from HttpService, to allow in-place replacement of
|
|||
// HttpClient with HttpService using dependency injection
|
|||
export interface HttpClient { |
|||
/** |
|||
* Enables caching for this request. |
|||
* @param forceUpdate Forces request to be made and updates cache entry. |
|||
* @return The new instance. |
|||
*/ |
|||
cache(forceUpdate?: boolean): HttpClient; |
|||
|
|||
/** |
|||
* Skips default error handler for this request. |
|||
* @return The new instance. |
|||
*/ |
|||
skipErrorHandler(): HttpClient; |
|||
|
|||
/** |
|||
* Do not use API prefix for this request. |
|||
* @return The new instance. |
|||
*/ |
|||
disableApiPrefix(): HttpClient; |
|||
} |
|||
} |
|||
|
|||
// From @angular/common/http/src/interceptor: allows to chain interceptors
|
|||
class HttpInterceptorHandler implements HttpHandler { |
|||
constructor(private next: HttpHandler, private interceptor: HttpInterceptor) {} |
|||
|
|||
handle(request: HttpRequest<any>): Observable<HttpEvent<any>> { |
|||
return this.interceptor.intercept(request, this.next); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Allows to override default dynamic interceptors that can be disabled with the HttpService extension. |
|||
* Except for very specific needs, you should better configure these interceptors directly in the constructor below |
|||
* for better readability. |
|||
* |
|||
* For static interceptors that should always be enabled (like ApiPrefixInterceptor), use the standard |
|||
* HTTP_INTERCEPTORS token. |
|||
*/ |
|||
export const HTTP_DYNAMIC_INTERCEPTORS = new InjectionToken<HttpInterceptor>('HTTP_DYNAMIC_INTERCEPTORS'); |
|||
|
|||
/** |
|||
* Extends HttpClient with per request configuration using dynamic interceptors. |
|||
*/ |
|||
@Injectable({ |
|||
providedIn: 'root' |
|||
}) |
|||
export class HttpService extends HttpClient { |
|||
constructor( |
|||
private httpHandler: HttpHandler, |
|||
private injector: Injector, |
|||
@Optional() @Inject(HTTP_DYNAMIC_INTERCEPTORS) private interceptors: HttpInterceptor[] = [] |
|||
) { |
|||
super(httpHandler); |
|||
|
|||
if (!this.interceptors) { |
|||
// Configure default interceptors that can be disabled here
|
|||
this.interceptors = [this.injector.get(ApiPrefixInterceptor), this.injector.get(ErrorHandlerInterceptor)]; |
|||
} |
|||
} |
|||
|
|||
cache(forceUpdate?: boolean): HttpClient { |
|||
const cacheInterceptor = this.injector |
|||
.get(CacheInterceptor as Type<CacheInterceptor>) |
|||
.configure({ update: forceUpdate }); |
|||
return this.addInterceptor(cacheInterceptor); |
|||
} |
|||
|
|||
skipErrorHandler(): HttpClient { |
|||
return this.removeInterceptor(ErrorHandlerInterceptor); |
|||
} |
|||
|
|||
disableApiPrefix(): HttpClient { |
|||
return this.removeInterceptor(ApiPrefixInterceptor); |
|||
} |
|||
|
|||
// Override the original method to wire interceptors when triggering the request.
|
|||
request(method?: any, url?: any, options?: any): any { |
|||
const handler = this.interceptors.reduceRight( |
|||
(next, interceptor) => new HttpInterceptorHandler(next, interceptor), |
|||
this.httpHandler |
|||
); |
|||
return new HttpClient(handler).request(method, url, options); |
|||
} |
|||
|
|||
private removeInterceptor(interceptorType: Type<HttpInterceptor>): HttpService { |
|||
return new HttpService( |
|||
this.httpHandler, |
|||
this.injector, |
|||
this.interceptors.filter(i => !(i instanceof interceptorType)) |
|||
); |
|||
} |
|||
|
|||
private addInterceptor(interceptor: HttpInterceptor): HttpService { |
|||
return new HttpService(this.httpHandler, this.injector, this.interceptors.concat([interceptor])); |
|||
} |
|||
} |
@ -0,0 +1,138 @@ |
|||
import { TestBed } from '@angular/core/testing'; |
|||
import { TranslateService, LangChangeEvent } from '@ngx-translate/core'; |
|||
import { Subject } from 'rxjs'; |
|||
|
|||
import { extract, I18nService } from './i18n.service'; |
|||
|
|||
const defaultLanguage = 'en-US'; |
|||
const supportedLanguages = ['eo', 'en-US', 'fr-FR']; |
|||
|
|||
class MockTranslateService { |
|||
currentLang = ''; |
|||
onLangChange = new Subject(); |
|||
|
|||
use(language: string) { |
|||
this.currentLang = language; |
|||
this.onLangChange.next({ |
|||
lang: this.currentLang, |
|||
translations: {} |
|||
}); |
|||
} |
|||
|
|||
getBrowserCultureLang() { |
|||
return 'en-US'; |
|||
} |
|||
|
|||
setTranslation(lang: string, translations: object, shouldMerge?: boolean) {} |
|||
} |
|||
|
|||
describe('I18nService', () => { |
|||
let i18nService: I18nService; |
|||
let translateService: TranslateService; |
|||
let onLangChangeSpy: jasmine.Spy; |
|||
|
|||
beforeEach(() => { |
|||
TestBed.configureTestingModule({ |
|||
providers: [I18nService, { provide: TranslateService, useClass: MockTranslateService }] |
|||
}); |
|||
|
|||
i18nService = TestBed.get(I18nService); |
|||
translateService = TestBed.get(TranslateService); |
|||
|
|||
// Create spies
|
|||
onLangChangeSpy = jasmine.createSpy('onLangChangeSpy'); |
|||
translateService.onLangChange.subscribe((event: LangChangeEvent) => { |
|||
onLangChangeSpy(event.lang); |
|||
}); |
|||
spyOn(translateService, 'use').and.callThrough(); |
|||
}); |
|||
|
|||
afterEach(() => { |
|||
// Cleanup
|
|||
localStorage.removeItem('language'); |
|||
}); |
|||
|
|||
describe('extract', () => { |
|||
it('should not modify string', () => { |
|||
expect(extract('Hello world !')).toEqual('Hello world !'); |
|||
}); |
|||
}); |
|||
|
|||
describe('init', () => { |
|||
it('should init with default language', () => { |
|||
// Act
|
|||
i18nService.init(defaultLanguage, supportedLanguages); |
|||
|
|||
// Assert
|
|||
expect(translateService.use).toHaveBeenCalledWith(defaultLanguage); |
|||
expect(onLangChangeSpy).toHaveBeenCalledWith(defaultLanguage); |
|||
}); |
|||
|
|||
it('should init with save language', () => { |
|||
// Arrange
|
|||
const savedLanguage = 'eo'; |
|||
localStorage.setItem('language', savedLanguage); |
|||
|
|||
// Act
|
|||
i18nService.init(defaultLanguage, supportedLanguages); |
|||
|
|||
// Assert
|
|||
expect(translateService.use).toHaveBeenCalledWith(savedLanguage); |
|||
expect(onLangChangeSpy).toHaveBeenCalledWith(savedLanguage); |
|||
}); |
|||
}); |
|||
|
|||
describe('set language', () => { |
|||
it('should change current language', () => { |
|||
// Arrange
|
|||
const newLanguage = 'eo'; |
|||
i18nService.init(defaultLanguage, supportedLanguages); |
|||
|
|||
// Act
|
|||
i18nService.language = newLanguage; |
|||
|
|||
// Assert
|
|||
expect(translateService.use).toHaveBeenCalledWith(newLanguage); |
|||
expect(onLangChangeSpy).toHaveBeenCalledWith(newLanguage); |
|||
}); |
|||
|
|||
it('should change current language without a region match', () => { |
|||
// Arrange
|
|||
const newLanguage = 'fr-CA'; |
|||
i18nService.init(defaultLanguage, supportedLanguages); |
|||
|
|||
// Act
|
|||
i18nService.language = newLanguage; |
|||
|
|||
// Assert
|
|||
expect(translateService.use).toHaveBeenCalledWith('fr-FR'); |
|||
expect(onLangChangeSpy).toHaveBeenCalledWith('fr-FR'); |
|||
}); |
|||
|
|||
it('should change current language to default if unsupported', () => { |
|||
// Arrange
|
|||
const newLanguage = 'es'; |
|||
i18nService.init(defaultLanguage, supportedLanguages); |
|||
|
|||
// Act
|
|||
i18nService.language = newLanguage; |
|||
|
|||
// Assert
|
|||
expect(translateService.use).toHaveBeenCalledWith(defaultLanguage); |
|||
expect(onLangChangeSpy).toHaveBeenCalledWith(defaultLanguage); |
|||
}); |
|||
}); |
|||
|
|||
describe('get language', () => { |
|||
it('should return current language', () => { |
|||
// Arrange
|
|||
i18nService.init(defaultLanguage, supportedLanguages); |
|||
|
|||
// Act
|
|||
const currentLanguage = i18nService.language; |
|||
|
|||
// Assert
|
|||
expect(currentLanguage).toEqual(defaultLanguage); |
|||
}); |
|||
}); |
|||
}); |
@ -0,0 +1,96 @@ |
|||
import { Injectable } from '@angular/core'; |
|||
import { TranslateService, LangChangeEvent } from '@ngx-translate/core'; |
|||
import { Subscription } from 'rxjs'; |
|||
|
|||
import { Logger } from './logger.service'; |
|||
import enUS from '../../translations/en-US.json'; |
|||
import frFR from '../../translations/fr-FR.json'; |
|||
|
|||
const log = new Logger('I18nService'); |
|||
const languageKey = 'language'; |
|||
|
|||
/** |
|||
* Pass-through function to mark a string for translation extraction. |
|||
* Running `npm translations:extract` will include the given string by using this. |
|||
* @param s The string to extract for translation. |
|||
* @return The same string. |
|||
*/ |
|||
export function extract(s: string) { |
|||
return s; |
|||
} |
|||
|
|||
@Injectable({ |
|||
providedIn: 'root' |
|||
}) |
|||
export class I18nService { |
|||
defaultLanguage!: string; |
|||
supportedLanguages!: string[]; |
|||
|
|||
private langChangeSubscription!: Subscription; |
|||
|
|||
constructor(private translateService: TranslateService) { |
|||
// Embed languages to avoid extra HTTP requests
|
|||
translateService.setTranslation('en-US', enUS); |
|||
translateService.setTranslation('fr-FR', frFR); |
|||
} |
|||
|
|||
/** |
|||
* Initializes i18n for the application. |
|||
* Loads language from local storage if present, or sets default language. |
|||
* @param defaultLanguage The default language to use. |
|||
* @param supportedLanguages The list of supported languages. |
|||
*/ |
|||
init(defaultLanguage: string, supportedLanguages: string[]) { |
|||
this.defaultLanguage = defaultLanguage; |
|||
this.supportedLanguages = supportedLanguages; |
|||
this.language = ''; |
|||
|
|||
// Warning: this subscription will always be alive for the app's lifetime
|
|||
this.langChangeSubscription = this.translateService.onLangChange.subscribe((event: LangChangeEvent) => { |
|||
localStorage.setItem(languageKey, event.lang); |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* Cleans up language change subscription. |
|||
*/ |
|||
destroy() { |
|||
if (this.langChangeSubscription) { |
|||
this.langChangeSubscription.unsubscribe(); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Sets the current language. |
|||
* Note: The current language is saved to the local storage. |
|||
* If no parameter is specified, the language is loaded from local storage (if present). |
|||
* @param language The IETF language code to set. |
|||
*/ |
|||
set language(language: string) { |
|||
language = language || localStorage.getItem(languageKey) || this.translateService.getBrowserCultureLang(); |
|||
let isSupportedLanguage = this.supportedLanguages.includes(language); |
|||
|
|||
// If no exact match is found, search without the region
|
|||
if (language && !isSupportedLanguage) { |
|||
language = language.split('-')[0]; |
|||
language = this.supportedLanguages.find(supportedLanguage => supportedLanguage.startsWith(language)) || ''; |
|||
isSupportedLanguage = Boolean(language); |
|||
} |
|||
|
|||
// Fallback if language is not supported
|
|||
if (!isSupportedLanguage) { |
|||
language = this.defaultLanguage; |
|||
} |
|||
|
|||
log.debug(`Language set to ${language}`); |
|||
this.translateService.use(language); |
|||
} |
|||
|
|||
/** |
|||
* Gets the current language. |
|||
* @return The current language code. |
|||
*/ |
|||
get language(): string { |
|||
return this.translateService.currentLang; |
|||
} |
|||
} |
@ -0,0 +1,10 @@ |
|||
export * from './core.module'; |
|||
export * from './i18n.service'; |
|||
export * from './http/http.service'; |
|||
export * from './http/http-cache.service'; |
|||
export * from './http/api-prefix.interceptor'; |
|||
export * from './http/cache.interceptor'; |
|||
export * from './http/error-handler.interceptor'; |
|||
export * from './route-reusable-strategy'; |
|||
export * from './logger.service'; |
|||
export * from './until-destroyed'; |
@ -0,0 +1,78 @@ |
|||
import { Logger, LogLevel, LogOutput } from './logger.service'; |
|||
|
|||
const logMethods = ['log', 'info', 'warn', 'error']; |
|||
|
|||
describe('Logger', () => { |
|||
let savedConsole: any[]; |
|||
let savedLevel: LogLevel; |
|||
let savedOutputs: LogOutput[]; |
|||
|
|||
beforeAll(() => { |
|||
savedConsole = []; |
|||
logMethods.forEach(m => { |
|||
savedConsole[m] = console[m]; |
|||
console[m] = () => {}; |
|||
}); |
|||
savedLevel = Logger.level; |
|||
savedOutputs = Logger.outputs; |
|||
}); |
|||
|
|||
beforeEach(() => { |
|||
Logger.level = LogLevel.Debug; |
|||
}); |
|||
|
|||
afterAll(() => { |
|||
logMethods.forEach(m => { |
|||
console[m] = savedConsole[m]; |
|||
}); |
|||
Logger.level = savedLevel; |
|||
Logger.outputs = savedOutputs; |
|||
}); |
|||
|
|||
it('should create an instance', () => { |
|||
expect(new Logger()).toBeTruthy(); |
|||
}); |
|||
|
|||
it('should add a new LogOutput and receives log entries', () => { |
|||
// Arrange
|
|||
const outputSpy = jasmine.createSpy('outputSpy'); |
|||
const log = new Logger('test'); |
|||
|
|||
// Act
|
|||
Logger.outputs.push(outputSpy); |
|||
|
|||
log.debug('d'); |
|||
log.info('i'); |
|||
log.warn('w'); |
|||
log.error('e', { error: true }); |
|||
|
|||
// Assert
|
|||
expect(outputSpy).toHaveBeenCalled(); |
|||
expect(outputSpy.calls.count()).toBe(4); |
|||
expect(outputSpy).toHaveBeenCalledWith('test', LogLevel.Debug, 'd'); |
|||
expect(outputSpy).toHaveBeenCalledWith('test', LogLevel.Info, 'i'); |
|||
expect(outputSpy).toHaveBeenCalledWith('test', LogLevel.Warning, 'w'); |
|||
expect(outputSpy).toHaveBeenCalledWith('test', LogLevel.Error, 'e', { error: true }); |
|||
}); |
|||
|
|||
it('should add a new LogOutput and receives only production log entries', () => { |
|||
// Arrange
|
|||
const outputSpy = jasmine.createSpy('outputSpy'); |
|||
const log = new Logger('test'); |
|||
|
|||
// Act
|
|||
Logger.outputs.push(outputSpy); |
|||
Logger.enableProductionMode(); |
|||
|
|||
log.debug('d'); |
|||
log.info('i'); |
|||
log.warn('w'); |
|||
log.error('e', { error: true }); |
|||
|
|||
// Assert
|
|||
expect(outputSpy).toHaveBeenCalled(); |
|||
expect(outputSpy.calls.count()).toBe(2); |
|||
expect(outputSpy).toHaveBeenCalledWith('test', LogLevel.Warning, 'w'); |
|||
expect(outputSpy).toHaveBeenCalledWith('test', LogLevel.Error, 'e', { error: true }); |
|||
}); |
|||
}); |
@ -0,0 +1,111 @@ |
|||
/** |
|||
* Simple logger system with the possibility of registering custom outputs. |
|||
* |
|||
* 4 different log levels are provided, with corresponding methods: |
|||
* - debug : for debug information |
|||
* - info : for informative status of the application (success, ...) |
|||
* - warning : for non-critical errors that do not prevent normal application behavior |
|||
* - error : for critical errors that prevent normal application behavior |
|||
* |
|||
* Example usage: |
|||
* ``` |
|||
* import { Logger } from 'app/core/logger.service'; |
|||
* |
|||
* const log = new Logger('myFile'); |
|||
* ... |
|||
* log.debug('something happened'); |
|||
* ``` |
|||
* |
|||
* To disable debug and info logs in production, add this snippet to your root component: |
|||
* ``` |
|||
* export class AppComponent implements OnInit { |
|||
* ngOnInit() { |
|||
* if (environment.production) { |
|||
* Logger.enableProductionMode(); |
|||
* } |
|||
* ... |
|||
* } |
|||
* } |
|||
* |
|||
* If you want to process logs through other outputs than console, you can add LogOutput functions to Logger.outputs. |
|||
*/ |
|||
|
|||
/** |
|||
* The possible log levels. |
|||
* LogLevel.Off is never emitted and only used with Logger.level property to disable logs. |
|||
*/ |
|||
export enum LogLevel { |
|||
Off = 0, |
|||
Error, |
|||
Warning, |
|||
Info, |
|||
Debug |
|||
} |
|||
|
|||
/** |
|||
* Log output handler function. |
|||
*/ |
|||
export type LogOutput = (source: string | undefined, level: LogLevel, ...objects: any[]) => void; |
|||
|
|||
export class Logger { |
|||
/** |
|||
* Current logging level. |
|||
* Set it to LogLevel.Off to disable logs completely. |
|||
*/ |
|||
static level = LogLevel.Debug; |
|||
|
|||
/** |
|||
* Additional log outputs. |
|||
*/ |
|||
static outputs: LogOutput[] = []; |
|||
|
|||
/** |
|||
* Enables production mode. |
|||
* Sets logging level to LogLevel.Warning. |
|||
*/ |
|||
static enableProductionMode() { |
|||
Logger.level = LogLevel.Warning; |
|||
} |
|||
|
|||
constructor(private source?: string) {} |
|||
|
|||
/** |
|||
* Logs messages or objects with the debug level. |
|||
* Works the same as console.log(). |
|||
*/ |
|||
debug(...objects: any[]) { |
|||
this.log(console.log, LogLevel.Debug, objects); |
|||
} |
|||
|
|||
/** |
|||
* Logs messages or objects with the info level. |
|||
* Works the same as console.log(). |
|||
*/ |
|||
info(...objects: any[]) { |
|||
this.log(console.info, LogLevel.Info, objects); |
|||
} |
|||
|
|||
/** |
|||
* Logs messages or objects with the warning level. |
|||
* Works the same as console.log(). |
|||
*/ |
|||
warn(...objects: any[]) { |
|||
this.log(console.warn, LogLevel.Warning, objects); |
|||
} |
|||
|
|||
/** |
|||
* Logs messages or objects with the error level. |
|||
* Works the same as console.log(). |
|||
*/ |
|||
error(...objects: any[]) { |
|||
this.log(console.error, LogLevel.Error, objects); |
|||
} |
|||
|
|||
private log(func: (...args: any[]) => void, level: LogLevel, objects: any[]) { |
|||
if (level <= Logger.level) { |
|||
const log = this.source ? ['[' + this.source + ']'].concat(objects) : objects; |
|||
func.apply(console, log); |
|||
Logger.outputs.forEach(output => output.apply(output, [this.source, level, ...objects])); |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,26 @@ |
|||
import { ActivatedRouteSnapshot, DetachedRouteHandle, RouteReuseStrategy } from '@angular/router'; |
|||
|
|||
/** |
|||
* A route strategy allowing for explicit route reuse. |
|||
* Used as a workaround for https://github.com/angular/angular/issues/18374
|
|||
* To reuse a given route, add `data: { reuse: true }` to the route definition. |
|||
*/ |
|||
export class RouteReusableStrategy extends RouteReuseStrategy { |
|||
public shouldDetach(route: ActivatedRouteSnapshot): boolean { |
|||
return false; |
|||
} |
|||
|
|||
public store(route: ActivatedRouteSnapshot, detachedTree: DetachedRouteHandle | null): void {} |
|||
|
|||
public shouldAttach(route: ActivatedRouteSnapshot): boolean { |
|||
return false; |
|||
} |
|||
|
|||
public retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null { |
|||
return null; |
|||
} |
|||
|
|||
public shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean { |
|||
return future.routeConfig === curr.routeConfig || future.data.reuse; |
|||
} |
|||
} |
@ -0,0 +1,165 @@ |
|||
import { OnInit, OnDestroy } from '@angular/core'; |
|||
import { Subject, Subscription } from 'rxjs'; |
|||
|
|||
import { untilDestroyed } from './until-destroyed'; |
|||
|
|||
function createObserver() { |
|||
return { |
|||
next: jasmine.createSpy(), |
|||
error: jasmine.createSpy(), |
|||
complete: jasmine.createSpy() |
|||
}; |
|||
} |
|||
|
|||
describe('untilDestroyed', () => { |
|||
it('should not destroy other instances', () => { |
|||
// Arrange
|
|||
const spy = createObserver(); |
|||
const spy2 = createObserver(); |
|||
|
|||
class Test implements OnDestroy { |
|||
obs!: Subscription; |
|||
|
|||
ngOnDestroy() {} |
|||
|
|||
subscribe(cb: any) { |
|||
this.obs = new Subject().pipe(untilDestroyed(this)).subscribe(cb); |
|||
} |
|||
} |
|||
|
|||
// Act
|
|||
const component1 = new Test(); |
|||
const component2 = new Test(); |
|||
component1.subscribe(spy); |
|||
component2.subscribe(spy2); |
|||
component1.ngOnDestroy(); |
|||
|
|||
// Assert
|
|||
expect(spy.complete).toHaveBeenCalledTimes(1); |
|||
expect(spy2.complete).not.toHaveBeenCalled(); |
|||
component2.ngOnDestroy(); |
|||
expect(spy2.complete).toHaveBeenCalledTimes(1); |
|||
}); |
|||
|
|||
it('should work with multiple observables', () => { |
|||
// Arrange
|
|||
const spy = createObserver(); |
|||
const spy2 = createObserver(); |
|||
const spy3 = createObserver(); |
|||
|
|||
class Test implements OnDestroy { |
|||
obs = new Subject().pipe(untilDestroyed(this)).subscribe(spy); |
|||
obs2 = new Subject().pipe(untilDestroyed(this)).subscribe(spy2); |
|||
obs3 = new Subject().pipe(untilDestroyed(this)).subscribe(spy3); |
|||
|
|||
ngOnDestroy() {} |
|||
} |
|||
|
|||
// Act
|
|||
const instance = new Test(); |
|||
instance.ngOnDestroy(); |
|||
|
|||
// Assert
|
|||
expect(spy.complete).toHaveBeenCalledTimes(1); |
|||
expect(spy2.complete).toHaveBeenCalledTimes(1); |
|||
expect(spy3.complete).toHaveBeenCalledTimes(1); |
|||
}); |
|||
|
|||
it('should work with classes that are not components', () => { |
|||
// Arrange
|
|||
const spy = createObserver(); |
|||
|
|||
// Act
|
|||
class Test { |
|||
obs = new Subject().pipe(untilDestroyed(this, 'destroy')).subscribe(spy); |
|||
|
|||
destroy() {} |
|||
} |
|||
|
|||
// Assert
|
|||
const instance = new Test(); |
|||
instance.destroy(); |
|||
expect(spy.complete).toHaveBeenCalledTimes(1); |
|||
}); |
|||
|
|||
it('should unsubscribe from anywhere', () => { |
|||
// Arrange
|
|||
const spy = createObserver(); |
|||
const spy2 = createObserver(); |
|||
const spy3 = createObserver(); |
|||
|
|||
class LoginComponent implements OnInit, OnDestroy { |
|||
dummy = new Subject().pipe(untilDestroyed(this)).subscribe(spy); |
|||
|
|||
constructor() { |
|||
new Subject().pipe(untilDestroyed(this)).subscribe(spy2); |
|||
} |
|||
|
|||
ngOnInit() { |
|||
new Subject().pipe(untilDestroyed(this)).subscribe(spy3); |
|||
} |
|||
|
|||
ngOnDestroy() {} |
|||
} |
|||
|
|||
// Act
|
|||
const instance = new LoginComponent(); |
|||
instance.ngOnInit(); |
|||
instance.ngOnDestroy(); |
|||
|
|||
// Assert
|
|||
expect(spy.complete).toHaveBeenCalledTimes(1); |
|||
expect(spy2.complete).toHaveBeenCalledTimes(1); |
|||
expect(spy3.complete).toHaveBeenCalledTimes(1); |
|||
}); |
|||
|
|||
it('should throw when destroy method doesnt exist', () => { |
|||
// Arrange
|
|||
const spy = createObserver(); |
|||
|
|||
class LoginComponent { |
|||
dummy = new Subject().pipe(untilDestroyed(this)).subscribe(spy); |
|||
} |
|||
|
|||
// Assert
|
|||
expect(() => new LoginComponent()).toThrow(); |
|||
}); |
|||
|
|||
it('should not throw when destroy method is implemented on super class', () => { |
|||
// Arrange
|
|||
const spy = createObserver(); |
|||
|
|||
class A implements OnDestroy { |
|||
ngOnDestroy() {} |
|||
} |
|||
|
|||
class B extends A { |
|||
dummy = new Subject().pipe(untilDestroyed(this)).subscribe(spy); |
|||
} |
|||
|
|||
// Assert
|
|||
expect(() => new B()).not.toThrow(); |
|||
}); |
|||
|
|||
it('should work with subclass', () => { |
|||
// Arrange
|
|||
const spy = createObserver(); |
|||
|
|||
class Parent implements OnDestroy { |
|||
ngOnDestroy() {} |
|||
} |
|||
|
|||
class Child extends Parent { |
|||
obs = new Subject().pipe(untilDestroyed(this)).subscribe(spy); |
|||
|
|||
constructor() { |
|||
super(); |
|||
} |
|||
} |
|||
|
|||
// Assert
|
|||
const instance = new Child(); |
|||
instance.ngOnDestroy(); |
|||
expect(spy.complete).toHaveBeenCalledTimes(1); |
|||
}); |
|||
}); |
@ -0,0 +1,62 @@ |
|||
import { Observable, Subject } from 'rxjs'; |
|||
import { takeUntil } from 'rxjs/operators'; |
|||
|
|||
const untilDestroyedSymbol = Symbol('untilDestroyed'); |
|||
|
|||
/** |
|||
* RxJS operator that unsubscribe from observables on destory. |
|||
* Code forked from https://github.com/NetanelBasal/ngx-take-until-destroy
|
|||
* |
|||
* IMPORTANT: Add the `untilDestroyed` operator as the last one to prevent leaks with intermediate observables in the |
|||
* operator chain. |
|||
* |
|||
* @param instance The parent Angular component or object instance. |
|||
* @param destroyMethodName The method to hook on (default: 'ngOnDestroy'). |
|||
* @example |
|||
* ``` |
|||
* import { untilDestroyed } from '@app/core'; |
|||
* |
|||
* @Component({ |
|||
* selector: 'app-example', |
|||
* templateUrl: './example.component.html' |
|||
* }) |
|||
* export class ExampleComponent implements OnInit, OnDestroy { |
|||
* ngOnInit() { |
|||
* interval(1000) |
|||
* .pipe(untilDestroyed(this)) |
|||
* .subscribe(val => console.log(val)); |
|||
* } |
|||
* |
|||
* // This method must be present, even if empty.
|
|||
* ngOnDestroy() { |
|||
* // To protect you, an error will be thrown if it doesn't exist.
|
|||
* } |
|||
* } |
|||
* ``` |
|||
*/ |
|||
export function untilDestroyed(instance: object, destroyMethodName: string = 'ngOnDestroy') { |
|||
return <T>(source: Observable<T>) => { |
|||
const originalDestroy = instance[destroyMethodName]; |
|||
const hasDestroyFunction = typeof originalDestroy === 'function'; |
|||
|
|||
if (!hasDestroyFunction) { |
|||
throw new Error( |
|||
`${instance.constructor.name} is using untilDestroyed but doesn't implement ${destroyMethodName}` |
|||
); |
|||
} |
|||
|
|||
if (!instance[untilDestroyedSymbol]) { |
|||
instance[untilDestroyedSymbol] = new Subject(); |
|||
|
|||
instance[destroyMethodName] = function() { |
|||
if (hasDestroyFunction) { |
|||
originalDestroy.apply(this, arguments); |
|||
} |
|||
instance[untilDestroyedSymbol].next(); |
|||
instance[untilDestroyedSymbol].complete(); |
|||
}; |
|||
} |
|||
|
|||
return source.pipe(takeUntil<T>(instance[untilDestroyedSymbol])); |
|||
}; |
|||
} |
@ -0,0 +1,20 @@ |
|||
import { NgModule } from '@angular/core'; |
|||
import { Routes, RouterModule } from '@angular/router'; |
|||
|
|||
import { extract } from '@app/core'; |
|||
import { HomeComponent } from './home.component'; |
|||
import { Shell } from '@app/shell/shell.service'; |
|||
|
|||
const routes: Routes = [ |
|||
Shell.childRoutes([ |
|||
{ path: '', redirectTo: '/home', pathMatch: 'full' }, |
|||
{ path: 'home', component: HomeComponent, data: { title: extract('Home') } } |
|||
]) |
|||
]; |
|||
|
|||
@NgModule({ |
|||
imports: [RouterModule.forChild(routes)], |
|||
exports: [RouterModule], |
|||
providers: [] |
|||
}) |
|||
export class HomeRoutingModule {} |
@ -0,0 +1,10 @@ |
|||
<div class="container-fluid"> |
|||
<div class="jumbotron text-center"> |
|||
<h1> |
|||
<img class="logo" src="assets/ngx-rocket-logo.png" alt="angular logo" /> |
|||
<span translate>Hello world !</span> |
|||
</h1> |
|||
<app-loader [isLoading]="isLoading"></app-loader> |
|||
<q [hidden]="isLoading">{{ quote }}</q> |
|||
</div> |
|||
</div> |
@ -0,0 +1,9 @@ |
|||
.logo { |
|||
width: 100px; |
|||
} |
|||
|
|||
q { |
|||
font-style: italic; |
|||
font-size: 1.2rem; |
|||
quotes: "« " " »"; |
|||
} |
@ -0,0 +1,30 @@ |
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; |
|||
import { HttpClientTestingModule } from '@angular/common/http/testing'; |
|||
|
|||
import { CoreModule } from '@app/core'; |
|||
import { SharedModule } from '@app/shared'; |
|||
import { HomeComponent } from './home.component'; |
|||
import { QuoteService } from './quote.service'; |
|||
|
|||
describe('HomeComponent', () => { |
|||
let component: HomeComponent; |
|||
let fixture: ComponentFixture<HomeComponent>; |
|||
|
|||
beforeEach(async(() => { |
|||
TestBed.configureTestingModule({ |
|||
imports: [CoreModule, SharedModule, HttpClientTestingModule], |
|||
declarations: [HomeComponent], |
|||
providers: [QuoteService] |
|||
}).compileComponents(); |
|||
})); |
|||
|
|||
beforeEach(() => { |
|||
fixture = TestBed.createComponent(HomeComponent); |
|||
component = fixture.componentInstance; |
|||
fixture.detectChanges(); |
|||
}); |
|||
|
|||
it('should create', () => { |
|||
expect(component).toBeTruthy(); |
|||
}); |
|||
}); |
@ -0,0 +1,30 @@ |
|||
import { Component, OnInit } from '@angular/core'; |
|||
import { finalize } from 'rxjs/operators'; |
|||
|
|||
import { QuoteService } from './quote.service'; |
|||
|
|||
@Component({ |
|||
selector: 'app-home', |
|||
templateUrl: './home.component.html', |
|||
styleUrls: ['./home.component.scss'] |
|||
}) |
|||
export class HomeComponent implements OnInit { |
|||
quote: string | undefined; |
|||
isLoading = false; |
|||
|
|||
constructor(private quoteService: QuoteService) {} |
|||
|
|||
ngOnInit() { |
|||
this.isLoading = true; |
|||
this.quoteService |
|||
.getRandomQuote({ category: 'dev' }) |
|||
.pipe( |
|||
finalize(() => { |
|||
this.isLoading = false; |
|||
}) |
|||
) |
|||
.subscribe((quote: string) => { |
|||
this.quote = quote; |
|||
}); |
|||
} |
|||
} |
@ -0,0 +1,15 @@ |
|||
import { NgModule } from '@angular/core'; |
|||
import { CommonModule } from '@angular/common'; |
|||
import { TranslateModule } from '@ngx-translate/core'; |
|||
|
|||
import { CoreModule } from '@app/core'; |
|||
import { SharedModule } from '@app/shared'; |
|||
import { HomeRoutingModule } from './home-routing.module'; |
|||
import { HomeComponent } from './home.component'; |
|||
import { QuoteService } from './quote.service'; |
|||
|
|||
@NgModule({ |
|||
imports: [CommonModule, TranslateModule, CoreModule, SharedModule, HomeRoutingModule], |
|||
declarations: [HomeComponent] |
|||
}) |
|||
export class HomeModule {} |
@ -0,0 +1,59 @@ |
|||
import { Type } from '@angular/core'; |
|||
import { TestBed, async } from '@angular/core/testing'; |
|||
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; |
|||
|
|||
import { CoreModule, HttpCacheService } from '@app/core'; |
|||
import { QuoteService } from './quote.service'; |
|||
|
|||
describe('QuoteService', () => { |
|||
let quoteService: QuoteService; |
|||
let httpMock: HttpTestingController; |
|||
|
|||
beforeEach(() => { |
|||
TestBed.configureTestingModule({ |
|||
imports: [CoreModule, HttpClientTestingModule], |
|||
providers: [HttpCacheService, QuoteService] |
|||
}); |
|||
|
|||
quoteService = TestBed.get(QuoteService); |
|||
httpMock = TestBed.get(HttpTestingController as Type<HttpTestingController>); |
|||
|
|||
const htttpCacheService = TestBed.get(HttpCacheService); |
|||
htttpCacheService.cleanCache(); |
|||
}); |
|||
|
|||
afterEach(() => { |
|||
httpMock.verify(); |
|||
}); |
|||
|
|||
describe('getRandomQuote', () => { |
|||
it('should return a random Chuck Norris quote', () => { |
|||
// Arrange
|
|||
const mockQuote = { value: 'a random quote' }; |
|||
|
|||
// Act
|
|||
const randomQuoteSubscription = quoteService.getRandomQuote({ category: 'toto' }); |
|||
|
|||
// Assert
|
|||
randomQuoteSubscription.subscribe((quote: string) => { |
|||
expect(quote).toEqual(mockQuote.value); |
|||
}); |
|||
httpMock.expectOne({}).flush(mockQuote); |
|||
}); |
|||
|
|||
it('should return a string in case of error', () => { |
|||
// Act
|
|||
const randomQuoteSubscription = quoteService.getRandomQuote({ category: 'toto' }); |
|||
|
|||
// Assert
|
|||
randomQuoteSubscription.subscribe((quote: string) => { |
|||
expect(typeof quote).toEqual('string'); |
|||
expect(quote).toContain('Error'); |
|||
}); |
|||
httpMock.expectOne({}).flush(null, { |
|||
status: 500, |
|||
statusText: 'error' |
|||
}); |
|||
}); |
|||
}); |
|||
}); |
@ -0,0 +1,30 @@ |
|||
import { Injectable } from '@angular/core'; |
|||
import { HttpClient } from '@angular/common/http'; |
|||
import { Observable, of } from 'rxjs'; |
|||
import { map, catchError } from 'rxjs/operators'; |
|||
|
|||
const routes = { |
|||
quote: (c: RandomQuoteContext) => `/jokes/random?category=${c.category}` |
|||
}; |
|||
|
|||
export interface RandomQuoteContext { |
|||
// The quote's category: 'dev', 'explicit'...
|
|||
category: string; |
|||
} |
|||
|
|||
@Injectable({ |
|||
providedIn: 'root' |
|||
}) |
|||
export class QuoteService { |
|||
constructor(private httpClient: HttpClient) {} |
|||
|
|||
getRandomQuote(context: RandomQuoteContext): Observable<string> { |
|||
return this.httpClient |
|||
.cache() |
|||
.get(routes.quote(context)) |
|||
.pipe( |
|||
map((body: any) => body.value), |
|||
catchError(() => of('Error, could not load joke :-(')) |
|||
); |
|||
} |
|||
} |
@ -0,0 +1,2 @@ |
|||
export * from './shared.module'; |
|||
export * from './loader/loader.component'; |
@ -0,0 +1,3 @@ |
|||
<div [hidden]="!isLoading" class="text-xs-center"> |
|||
<i class="fas fa-cog fa-spin fa-3x"></i> <span>{{ message }}</span> |
|||
</div> |
@ -0,0 +1,3 @@ |
|||
.fa { |
|||
vertical-align: middle; |
|||
} |
@ -0,0 +1,64 @@ |
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; |
|||
|
|||
import { LoaderComponent } from './loader.component'; |
|||
|
|||
describe('LoaderComponent', () => { |
|||
let component: LoaderComponent; |
|||
let fixture: ComponentFixture<LoaderComponent>; |
|||
|
|||
beforeEach(async(() => { |
|||
TestBed.configureTestingModule({ |
|||
declarations: [LoaderComponent] |
|||
}).compileComponents(); |
|||
})); |
|||
|
|||
beforeEach(() => { |
|||
fixture = TestBed.createComponent(LoaderComponent); |
|||
component = fixture.componentInstance; |
|||
fixture.detectChanges(); |
|||
}); |
|||
|
|||
it('should not be visible by default', () => { |
|||
// Arrange
|
|||
const element = fixture.nativeElement; |
|||
const div = element.querySelectorAll('div')[0]; |
|||
|
|||
// Assert
|
|||
expect(div.getAttribute('hidden')).not.toBeNull(); |
|||
}); |
|||
|
|||
it('should be visible when app is loading', () => { |
|||
// Arrange
|
|||
const element = fixture.nativeElement; |
|||
const div = element.querySelectorAll('div')[0]; |
|||
|
|||
// Act
|
|||
fixture.componentInstance.isLoading = true; |
|||
fixture.detectChanges(); |
|||
|
|||
// Assert
|
|||
expect(div.getAttribute('hidden')).toBeNull(); |
|||
}); |
|||
|
|||
it('should not display a message by default', () => { |
|||
// Arrange
|
|||
const element = fixture.nativeElement; |
|||
const span = element.querySelectorAll('span')[0]; |
|||
|
|||
// Assert
|
|||
expect(span.textContent).toBe(''); |
|||
}); |
|||
|
|||
it('should display specified message', () => { |
|||
// Arrange
|
|||
const element = fixture.nativeElement; |
|||
const span = element.querySelectorAll('span')[0]; |
|||
|
|||
// Act
|
|||
fixture.componentInstance.message = 'testing'; |
|||
fixture.detectChanges(); |
|||
|
|||
// Assert
|
|||
expect(span.textContent).toBe('testing'); |
|||
}); |
|||
}); |
@ -0,0 +1,15 @@ |
|||
import { Component, OnInit, Input } from '@angular/core'; |
|||
|
|||
@Component({ |
|||
selector: 'app-loader', |
|||
templateUrl: './loader.component.html', |
|||
styleUrls: ['./loader.component.scss'] |
|||
}) |
|||
export class LoaderComponent implements OnInit { |
|||
@Input() isLoading = false; |
|||
@Input() message: string | undefined; |
|||
|
|||
constructor() {} |
|||
|
|||
ngOnInit() {} |
|||
} |
@ -0,0 +1,11 @@ |
|||
import { NgModule } from '@angular/core'; |
|||
import { CommonModule } from '@angular/common'; |
|||
|
|||
import { LoaderComponent } from './loader/loader.component'; |
|||
|
|||
@NgModule({ |
|||
imports: [CommonModule], |
|||
declarations: [LoaderComponent], |
|||
exports: [LoaderComponent] |
|||
}) |
|||
export class SharedModule {} |
@ -0,0 +1,37 @@ |
|||
<header> |
|||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark"> |
|||
<a class="navbar-brand" href="https://github.com/ngx-rocket" translate>APP_NAME</a> |
|||
<button |
|||
class="navbar-toggler" |
|||
type="button" |
|||
aria-controls="navbar-menu" |
|||
aria-label="Toggle navigation" |
|||
(click)="toggleMenu()" |
|||
[attr.aria-expanded]="!menuHidden" |
|||
> |
|||
<span class="navbar-toggler-icon"></span> |
|||
</button> |
|||
<div id="navbar-menu" class="collapse navbar-collapse float-xs-none" [ngbCollapse]="menuHidden"> |
|||
<div class="navbar-nav"> |
|||
<a class="nav-item nav-link text-uppercase" routerLink="/home" routerLinkActive="active"> |
|||
<i class="fas fa-home"></i> |
|||
<span translate>Home</span> |
|||
</a> |
|||
<a class="nav-item nav-link text-uppercase" routerLink="/about" routerLinkActive="active"> |
|||
<i class="fas fa-question-circle"></i> |
|||
<span translate>About</span> |
|||
</a> |
|||
</div> |
|||
<div class="navbar-nav ml-auto"> |
|||
<div class="nav-item" ngbDropdown display="dynamic" placement="bottom-right"> |
|||
<a id="language-dropdown" class="nav-link" ngbDropdownToggle>{{ currentLanguage }}</a> |
|||
<div ngbDropdownMenu aria-labelledby="language-dropdown"> |
|||
<button class="dropdown-item" *ngFor="let language of languages" (click)="setLanguage(language)"> |
|||
{{ language }} |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</nav> |
|||
</header> |
@ -0,0 +1,9 @@ |
|||
@import "src/theme/theme-variables"; |
|||
|
|||
.navbar { |
|||
margin-bottom: $spacer; |
|||
} |
|||
|
|||
.nav-link.dropdown-toggle { |
|||
cursor: pointer; |
|||
} |
@ -0,0 +1,30 @@ |
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; |
|||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; |
|||
import { TranslateModule } from '@ngx-translate/core'; |
|||
import { RouterTestingModule } from '@angular/router/testing'; |
|||
|
|||
import { I18nService } from '@app/core'; |
|||
import { HeaderComponent } from './header.component'; |
|||
|
|||
describe('HeaderComponent', () => { |
|||
let component: HeaderComponent; |
|||
let fixture: ComponentFixture<HeaderComponent>; |
|||
|
|||
beforeEach(async(() => { |
|||
TestBed.configureTestingModule({ |
|||
imports: [RouterTestingModule, NgbModule, TranslateModule.forRoot()], |
|||
declarations: [HeaderComponent], |
|||
providers: [I18nService] |
|||
}).compileComponents(); |
|||
})); |
|||
|
|||
beforeEach(() => { |
|||
fixture = TestBed.createComponent(HeaderComponent); |
|||
component = fixture.componentInstance; |
|||
fixture.detectChanges(); |
|||
}); |
|||
|
|||
it('should create', () => { |
|||
expect(component).toBeTruthy(); |
|||
}); |
|||
}); |
@ -0,0 +1,32 @@ |
|||
import { Component, OnInit } from '@angular/core'; |
|||
|
|||
import { I18nService } from '@app/core'; |
|||
|
|||
@Component({ |
|||
selector: 'app-header', |
|||
templateUrl: './header.component.html', |
|||
styleUrls: ['./header.component.scss'] |
|||
}) |
|||
export class HeaderComponent implements OnInit { |
|||
menuHidden = true; |
|||
|
|||
constructor(private i18nService: I18nService) {} |
|||
|
|||
ngOnInit() {} |
|||
|
|||
toggleMenu() { |
|||
this.menuHidden = !this.menuHidden; |
|||
} |
|||
|
|||
setLanguage(language: string) { |
|||
this.i18nService.language = language; |
|||
} |
|||
|
|||
get currentLanguage(): string { |
|||
return this.i18nService.language; |
|||
} |
|||
|
|||
get languages(): string[] { |
|||
return this.i18nService.supportedLanguages; |
|||
} |
|||
} |
@ -0,0 +1,2 @@ |
|||
<app-header></app-header> |
|||
<router-outlet></router-outlet> |
@ -0,0 +1,31 @@ |
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; |
|||
import { RouterTestingModule } from '@angular/router/testing'; |
|||
import { TranslateModule } from '@ngx-translate/core'; |
|||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; |
|||
|
|||
import { CoreModule } from '@app/core'; |
|||
|
|||
import { ShellComponent } from './shell.component'; |
|||
import { HeaderComponent } from './header/header.component'; |
|||
|
|||
describe('ShellComponent', () => { |
|||
let component: ShellComponent; |
|||
let fixture: ComponentFixture<ShellComponent>; |
|||
|
|||
beforeEach(async(() => { |
|||
TestBed.configureTestingModule({ |
|||
imports: [RouterTestingModule, TranslateModule.forRoot(), NgbModule, CoreModule], |
|||
declarations: [HeaderComponent, ShellComponent] |
|||
}).compileComponents(); |
|||
})); |
|||
|
|||
beforeEach(() => { |
|||
fixture = TestBed.createComponent(ShellComponent); |
|||
component = fixture.componentInstance; |
|||
fixture.detectChanges(); |
|||
}); |
|||
|
|||
it('should create', () => { |
|||
expect(component).toBeTruthy(); |
|||
}); |
|||
}); |
@ -0,0 +1,12 @@ |
|||
import { Component, OnInit } from '@angular/core'; |
|||
|
|||
@Component({ |
|||
selector: 'app-shell', |
|||
templateUrl: './shell.component.html', |
|||
styleUrls: ['./shell.component.scss'] |
|||
}) |
|||
export class ShellComponent implements OnInit { |
|||
constructor() {} |
|||
|
|||
ngOnInit() {} |
|||
} |
@ -0,0 +1,14 @@ |
|||
import { NgModule } from '@angular/core'; |
|||
import { CommonModule } from '@angular/common'; |
|||
import { TranslateModule } from '@ngx-translate/core'; |
|||
import { RouterModule } from '@angular/router'; |
|||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; |
|||
|
|||
import { ShellComponent } from './shell.component'; |
|||
import { HeaderComponent } from './header/header.component'; |
|||
|
|||
@NgModule({ |
|||
imports: [CommonModule, TranslateModule, NgbModule, RouterModule], |
|||
declarations: [HeaderComponent, ShellComponent] |
|||
}) |
|||
export class ShellModule {} |
@ -0,0 +1,27 @@ |
|||
import { TestBed, inject } from '@angular/core/testing'; |
|||
|
|||
import { ShellComponent } from './shell.component'; |
|||
import { Shell } from './shell.service'; |
|||
|
|||
describe('Shell', () => { |
|||
beforeEach(() => { |
|||
TestBed.configureTestingModule({ |
|||
declarations: [ShellComponent] |
|||
}); |
|||
}); |
|||
|
|||
describe('childRoutes', () => { |
|||
it('should create routes as children of shell', () => { |
|||
// Prepare
|
|||
const testRoutes = [{ path: 'test' }]; |
|||
|
|||
// Act
|
|||
const result = Shell.childRoutes(testRoutes); |
|||
|
|||
// Assert
|
|||
expect(result.path).toBe(''); |
|||
expect(result.children).toBe(testRoutes); |
|||
expect(result.component).toBe(ShellComponent); |
|||
}); |
|||
}); |
|||
}); |
@ -0,0 +1,23 @@ |
|||
import { Routes, Route } from '@angular/router'; |
|||
|
|||
import { ShellComponent } from './shell.component'; |
|||
|
|||
/** |
|||
* Provides helper methods to create routes. |
|||
*/ |
|||
export class Shell { |
|||
/** |
|||
* Creates routes using the shell component and authentication. |
|||
* @param routes The routes to add. |
|||
* @return The new route using shell as the base. |
|||
*/ |
|||
static childRoutes(routes: Routes): Route { |
|||
return { |
|||
path: '', |
|||
component: ShellComponent, |
|||
children: routes, |
|||
// Reuse ShellComponent instance when navigating between child views
|
|||
data: { reuse: true } |
|||
}; |
|||
} |
|||
} |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 12 KiB |
@ -0,0 +1,10 @@ |
|||
# This file is currently used by autoprefixer to adjust CSS to support the below specified browsers |
|||
# For additional information regarding the format and rule options, please see: |
|||
# https://github.com/browserslist/browserslist#queries |
|||
|
|||
last 2 versions |
|||
> 0.5% |
|||
Firefox ESR |
|||
not dead |
|||
# For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed |
|||
not IE 9-11 |
@ -0,0 +1,16 @@ |
|||
// `.env.ts` is generated by the `npm run env` command
|
|||
// `npm run env` exposes environment variables as JSON for any usage you might
|
|||
// want, like displaying the version or getting extra config from your CI bot, etc.
|
|||
// This is useful for granularity you might need beyond just the environment.
|
|||
// Note that as usual, any environment variables you expose through it will end up in your
|
|||
// bundle, and you should not use it for any sensitive information like passwords or keys.
|
|||
import { env } from './.env'; |
|||
|
|||
export const environment = { |
|||
production: true, |
|||
hmr: false, |
|||
version: env.npm_package_version, |
|||
serverUrl: 'https://api.chucknorris.io', |
|||
defaultLanguage: 'en-US', |
|||
supportedLanguages: ['en-US', 'fr-FR'] |
|||
}; |
@ -0,0 +1,29 @@ |
|||
// This file can be replaced during build by using the `fileReplacements` array.
|
|||
// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
|
|||
// The list of file replacements can be found in `angular.json`.
|
|||
|
|||
// `.env.ts` is generated by the `npm run env` command
|
|||
// `npm run env` exposes environment variables as JSON for any usage you might
|
|||
// want, like displaying the version or getting extra config from your CI bot, etc.
|
|||
// This is useful for granularity you might need beyond just the environment.
|
|||
// Note that as usual, any environment variables you expose through it will end up in your
|
|||
// bundle, and you should not use it for any sensitive information like passwords or keys.
|
|||
import { env } from './.env'; |
|||
|
|||
export const environment = { |
|||
production: false, |
|||
hmr: true, |
|||
version: env.npm_package_version + '-dev', |
|||
serverUrl: '/api', |
|||
defaultLanguage: 'en-US', |
|||
supportedLanguages: ['en-US', 'fr-FR'] |
|||
}; |
|||
|
|||
/* |
|||
* For easier debugging in development mode, you can import the following file |
|||
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. |
|||
* |
|||
* This import should be commented out in production mode because it will have a negative impact |
|||
* on performance if an error is thrown. |
|||
*/ |
|||
// import 'zone.js/dist/zone-error'; // Included with Angular CLI.
|
After Width: | Height: | Size: 15 KiB |
@ -0,0 +1,19 @@ |
|||
import { enableProdMode, NgModuleRef, ApplicationRef } from '@angular/core'; |
|||
import { createNewHosts } from '@angularclass/hmr'; |
|||
|
|||
export function hmrBootstrap(module: any, bootstrap: () => Promise<NgModuleRef<any>>) { |
|||
let ngModule: NgModuleRef<any>; |
|||
module.hot.accept(); |
|||
|
|||
bootstrap() |
|||
.then(mod => (ngModule = mod)) |
|||
.catch(err => console.error(err)); |
|||
|
|||
module.hot.dispose(() => { |
|||
const appRef: ApplicationRef = ngModule.injector.get(ApplicationRef); |
|||
const elements = appRef.components.map(c => c.location.nativeElement); |
|||
const makeVisible = createNewHosts(elements); |
|||
ngModule.destroy(); |
|||
makeVisible(); |
|||
}); |
|||
} |
@ -0,0 +1,32 @@ |
|||
<!DOCTYPE html> |
|||
<html lang="en"> |
|||
<head> |
|||
<meta charset="utf-8" /> |
|||
<!-- needs to be right at the top to prevent Chrome from reloading favicon on every route change --> |
|||
<link rel="icon" type="image/x-icon" href="favicon.ico" /> |
|||
<title>Aiolos</title> |
|||
<base href="/" /> |
|||
<meta name="viewport" content="width=device-width, initial-scale=1" /> |
|||
<meta name="theme-color" content="#4e8ef7" /> |
|||
<link rel="manifest" href="manifest.json" /> |
|||
|
|||
<!-- add to homescreen for ios --> |
|||
<meta name="apple-mobile-web-app-capable" content="yes" /> |
|||
<meta name="apple-mobile-web-app-status-bar-style" content="default" /> |
|||
<link rel="apple-touch-icon" sizes="180x180" href="apple-touch-icon.png" /> |
|||
</head> |
|||
<body> |
|||
<!--[if lt IE 10]> |
|||
<p> |
|||
You are using an <strong>outdated</strong> browser. Please |
|||
<a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience. |
|||
</p> |
|||
<![endif]--> |
|||
<noscript> |
|||
<p> |
|||
This page requires JavaScript to work properly. Please enable JavaScript in your browser. |
|||
</p> |
|||
</noscript> |
|||
<app-root></app-root> |
|||
</body> |
|||
</html> |
@ -0,0 +1,17 @@ |
|||
/* |
|||
* Entry point of global application style. |
|||
* Component-specific style should not go here and be included directly as part of the components. |
|||
*/ |
|||
|
|||
// Theme variables, must be included before the libraries to allow overriding defaults |
|||
@import "theme/theme-variables"; |
|||
|
|||
// 3rd party libraries |
|||
@import "~bootstrap/scss/bootstrap"; |
|||
@import "~@fortawesome/fontawesome-free/scss/fontawesome.scss"; |
|||
@import "~@fortawesome/fontawesome-free/scss/brands.scss"; |
|||
@import "~@fortawesome/fontawesome-free/scss/regular.scss"; |
|||
@import "~@fortawesome/fontawesome-free/scss/solid.scss"; |
|||
|
|||
// Theme customization |
|||
@import "theme/theme"; |
@ -0,0 +1,24 @@ |
|||
/* |
|||
* Entry point of the application. |
|||
* Only platform bootstrapping code should be here. |
|||
* For app-specific initialization, use `app/app.component.ts`. |
|||
*/ |
|||
|
|||
import { enableProdMode } from '@angular/core'; |
|||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; |
|||
|
|||
import { AppModule } from '@app/app.module'; |
|||
import { environment } from '@env/environment'; |
|||
import { hmrBootstrap } from './hmr'; |
|||
|
|||
if (environment.production) { |
|||
enableProdMode(); |
|||
} |
|||
|
|||
const bootstrap = () => platformBrowserDynamic().bootstrapModule(AppModule); |
|||
|
|||
if (environment.hmr) { |
|||
hmrBootstrap(module, bootstrap); |
|||
} else { |
|||
bootstrap().catch(err => console.error(err)); |
|||
} |
@ -0,0 +1,21 @@ |
|||
{ |
|||
"name": "Aiolos", |
|||
"short_name": "Aiolos", |
|||
"theme_color": "#488aff", |
|||
"background_color": "#488aff", |
|||
"scope": "/", |
|||
"start_url": "/", |
|||
"display": "standalone", |
|||
"icons": [ |
|||
{ |
|||
"src": "assets/ngx-rocket-logo.png", |
|||
"sizes": "512x512", |
|||
"type": "image/png" |
|||
}, |
|||
{ |
|||
"src": "assets/ngx-rocket-logo@192.png", |
|||
"sizes": "192x192", |
|||
"type": "image/png" |
|||
} |
|||
] |
|||
} |
@ -0,0 +1,62 @@ |
|||
/** |
|||
* This file includes polyfills needed by Angular and is loaded before the app. |
|||
* You can add your own extra polyfills to this file. |
|||
* |
|||
* This file is divided into 2 sections: |
|||
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. |
|||
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main |
|||
* file. |
|||
* |
|||
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that |
|||
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), |
|||
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. |
|||
* |
|||
* Learn more in https://angular.io/guide/browser-support
|
|||
*/ |
|||
|
|||
/*************************************************************************************************** |
|||
* BROWSER POLYFILLS |
|||
*/ |
|||
|
|||
/** IE10 and IE11 requires the following for NgClass support on SVG elements */ |
|||
// import 'classlist.js'; // Run `npm install --save classlist.js`.
|
|||
|
|||
/** |
|||
* Web Animations `@angular/platform-browser/animations` |
|||
* Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. |
|||
* Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). |
|||
*/ |
|||
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
|
|||
|
|||
/** |
|||
* By default, zone.js will patch all possible macroTask and DomEvents |
|||
* user can disable parts of macroTask/DomEvents patch by setting following flags |
|||
* because those flags need to be set before `zone.js` being loaded, and webpack |
|||
* will put import in the top of bundle, so user need to create a separate file |
|||
* in this directory (for example: zone-flags.ts), and put the following flags |
|||
* into that file, and then add the following code before importing zone.js. |
|||
* import './zone-flags.ts'; |
|||
* |
|||
* The flags allowed in zone-flags.ts are listed here. |
|||
* |
|||
* The following flags will work for all browsers. |
|||
* |
|||
* (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
|
|||
* (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
|
|||
* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
|
|||
* |
|||
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js |
|||
* with the following flag, it will bypass `zone.js` patch for IE/Edge |
|||
* |
|||
* (window as any).__Zone_enable_cross_context_check = true; |
|||
* |
|||
*/ |
|||
|
|||
/*************************************************************************************************** |
|||
* Zone JS is required by default for Angular itself. |
|||
*/ |
|||
import 'zone.js/dist/zone'; // Included with Angular CLI.
|
|||
|
|||
/*************************************************************************************************** |
|||
* APPLICATION IMPORTS |
|||
*/ |
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue