├── .editorconfig
├── .gitignore
├── README.md
├── angular.json
├── browserslist
├── e2e
├── app.e2e-spec.ts
├── app.po.ts
└── tsconfig.e2e.json
├── karma.conf.js
├── package-lock.json
├── package.json
├── protractor.conf.js
├── server.js
├── server
├── data
│ ├── books.json
│ └── readers.json
└── routes
│ ├── books.js
│ ├── index.js
│ └── readers.js
├── src
├── app
│ ├── add-book
│ │ ├── add-book.component.html
│ │ └── add-book.component.ts
│ ├── add-reader
│ │ ├── add-reader.component.html
│ │ └── add-reader.component.ts
│ ├── app-routing.module.ts
│ ├── app.component.css
│ ├── app.component.html
│ ├── app.component.ts
│ ├── app.module.ts
│ ├── core
│ │ ├── badge.service.ts
│ │ ├── book-tracker-error-handler.service.ts
│ │ ├── data.service.ts
│ │ ├── http-cache.service.ts
│ │ ├── logger.service.ts
│ │ └── plain-logger.service.ts
│ ├── dashboard
│ │ ├── dashboard.component.html
│ │ └── dashboard.component.ts
│ ├── data.ts
│ ├── edit-book
│ │ ├── edit-book.component.html
│ │ └── edit-book.component.ts
│ ├── edit-reader
│ │ ├── edit-reader.component.html
│ │ └── edit-reader.component.ts
│ └── models
│ │ ├── book.ts
│ │ ├── bookTrackerError.ts
│ │ ├── oldBook.ts
│ │ └── reader.ts
├── assets
│ └── .gitkeep
├── environments
│ ├── environment.prod.ts
│ └── environment.ts
├── favicon.ico
├── index.html
├── main.ts
├── polyfills.ts
├── styles.css
└── test.ts
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.spec.json
└── tslint.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.md]
12 | max_line_length = off
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | node_modules/
4 | dist/
5 | .vscode/
6 |
7 | # compiled output
8 | /tmp
9 | /out-tsc
10 |
11 | # dependencies
12 |
13 | # IDEs and editors
14 | /.idea
15 | .project
16 | .classpath
17 | .c9/
18 | *.launch
19 | .settings/
20 | *.sublime-workspace
21 |
22 | # IDE - VSCode
23 |
24 | # misc
25 | /.sass-cache
26 | /connect.lock
27 | /coverage
28 | /libpeerconnection.log
29 | npm-debug.log
30 | testem.log
31 | /typings
32 |
33 | # e2e
34 | /e2e/*.js
35 | /e2e/*.map
36 |
37 | # System Files
38 | .DS_Store
39 | Thumbs.db
40 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Angular Next Steps: Patterns for Clean Code
2 |
3 | ## Demo App and Exercises
4 |
5 | This repo contains the demo app used in my *Angular Next Steps: Patterns for Clean Code* training from [O'Reilly](https://learning.oreilly.com/live-training/).
6 |
7 | After cloning or downloading the code, install all of the required npm packages by running the following command in a terminal windows inside the **angular-clean-code** folder:
8 |
9 | **npm install**
10 |
11 | Build and run the BookTracker app with the following commands:
12 |
13 | **ng build**
14 |
15 | **npm start**
--------------------------------------------------------------------------------
/angular.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "version": 1,
4 | "newProjectRoot": "projects",
5 | "projects": {
6 | "booktracker": {
7 | "projectType": "application",
8 | "schematics": {},
9 | "root": "",
10 | "sourceRoot": "src",
11 | "prefix": "app",
12 | "architect": {
13 | "build": {
14 | "builder": "@angular-devkit/build-angular:browser",
15 | "options": {
16 | "outputPath": "dist/booktracker",
17 | "index": "src/index.html",
18 | "main": "src/main.ts",
19 | "polyfills": "src/polyfills.ts",
20 | "tsConfig": "tsconfig.app.json",
21 | "aot": true,
22 | "assets": [
23 | "src/favicon.ico",
24 | "src/assets"
25 | ],
26 | "styles": [
27 | "node_modules/bootstrap/dist/css/bootstrap.min.css",
28 | "src/styles.css"
29 | ],
30 | "scripts": []
31 | },
32 | "configurations": {
33 | "production": {
34 | "fileReplacements": [
35 | {
36 | "replace": "src/environments/environment.ts",
37 | "with": "src/environments/environment.prod.ts"
38 | }
39 | ],
40 | "optimization": true,
41 | "outputHashing": "all",
42 | "sourceMap": false,
43 | "extractCss": true,
44 | "namedChunks": false,
45 | "extractLicenses": true,
46 | "vendorChunk": false,
47 | "buildOptimizer": true,
48 | "budgets": [
49 | {
50 | "type": "initial",
51 | "maximumWarning": "2mb",
52 | "maximumError": "5mb"
53 | },
54 | {
55 | "type": "anyComponentStyle",
56 | "maximumWarning": "6kb"
57 | }
58 | ]
59 | }
60 | }
61 | },
62 | "serve": {
63 | "builder": "@angular-devkit/build-angular:dev-server",
64 | "options": {
65 | "browserTarget": "booktracker:build"
66 | },
67 | "configurations": {
68 | "production": {
69 | "browserTarget": "booktracker:build:production"
70 | }
71 | }
72 | },
73 | "extract-i18n": {
74 | "builder": "@angular-devkit/build-angular:extract-i18n",
75 | "options": {
76 | "browserTarget": "booktracker:build"
77 | }
78 | },
79 | "test": {
80 | "builder": "@angular-devkit/build-angular:karma",
81 | "options": {
82 | "main": "src/test.ts",
83 | "polyfills": "src/polyfills.ts",
84 | "tsConfig": "tsconfig.spec.json",
85 | "karmaConfig": "karma.conf.js",
86 | "assets": [
87 | "src/favicon.ico",
88 | "src/assets"
89 | ],
90 | "styles": [
91 | "src/styles.css"
92 | ],
93 | "scripts": []
94 | }
95 | },
96 | "lint": {
97 | "builder": "@angular-devkit/build-angular:tslint",
98 | "options": {
99 | "tsConfig": [
100 | "tsconfig.app.json",
101 | "tsconfig.spec.json",
102 | "e2e/tsconfig.json"
103 | ],
104 | "exclude": [
105 | "**/node_modules/**"
106 | ]
107 | }
108 | },
109 | "e2e": {
110 | "builder": "@angular-devkit/build-angular:protractor",
111 | "options": {
112 | "protractorConfig": "e2e/protractor.conf.js",
113 | "devServerTarget": "booktracker:serve"
114 | },
115 | "configurations": {
116 | "production": {
117 | "devServerTarget": "booktracker:serve:production"
118 | }
119 | }
120 | }
121 | }
122 | }},
123 | "defaultProject": "booktracker"
124 | }
--------------------------------------------------------------------------------
/browserslist:
--------------------------------------------------------------------------------
1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
2 | # For additional information regarding the format and rule options, please see:
3 | # https://github.com/browserslist/browserslist#queries
4 |
5 | # You can see what browsers were selected by your queries by running:
6 | # npx browserslist
7 |
8 | > 0.5%
9 | last 2 versions
10 | Firefox ESR
11 | not dead
12 | not IE 9-11 # For IE 9-11 support, remove 'not'.
--------------------------------------------------------------------------------
/e2e/app.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import { BooktrackerPage } from './app.po';
2 |
3 | describe('booktracker App', () => {
4 | let page: BooktrackerPage;
5 |
6 | beforeEach(() => {
7 | page = new BooktrackerPage();
8 | });
9 |
10 | it('should display welcome message', () => {
11 | page.navigateTo();
12 | expect(page.getParagraphText()).toEqual('Welcome to app!!');
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/e2e/app.po.ts:
--------------------------------------------------------------------------------
1 | import { browser, by, element } from 'protractor';
2 |
3 | export class BooktrackerPage {
4 | navigateTo() {
5 | return browser.get('/');
6 | }
7 |
8 | getParagraphText() {
9 | return element(by.css('app-root h1')).getText();
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/e2e/tsconfig.e2e.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/e2e",
5 | "module": "commonjs",
6 | "target": "es5",
7 | "types": [
8 | "jasmine",
9 | "jasminewd2",
10 | "node"
11 | ]
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration file, see link for more information
2 | // https://karma-runner.github.io/1.0/config/configuration-file.html
3 |
4 | module.exports = function (config) {
5 | config.set({
6 | basePath: '',
7 | frameworks: ['jasmine', '@angular-devkit/build-angular'],
8 | plugins: [
9 | require('karma-jasmine'),
10 | require('karma-chrome-launcher'),
11 | require('karma-jasmine-html-reporter'),
12 | require('karma-coverage-istanbul-reporter'),
13 | require('@angular-devkit/build-angular/plugins/karma')
14 | ],
15 | client: {
16 | clearContext: false // leave Jasmine Spec Runner output visible in browser
17 | },
18 | coverageIstanbulReporter: {
19 | dir: require('path').join(__dirname, './coverage/ng8test1'),
20 | reports: ['html', 'lcovonly', 'text-summary'],
21 | fixWebpackSourcePaths: true
22 | },
23 | reporters: ['progress', 'kjhtml'],
24 | port: 9876,
25 | colors: true,
26 | logLevel: config.LOG_INFO,
27 | autoWatch: true,
28 | browsers: ['Chrome'],
29 | singleRun: false,
30 | restartOnFileChange: true
31 | });
32 | };
33 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "booktracker",
3 | "version": "0.0.0",
4 | "license": "MIT",
5 | "scripts": {
6 | "ng": "ng",
7 | "start": "concurrently --kill-others \"ng build --watch --no-delete-output-path\" \"node server.js\"",
8 | "build": "ng build && node server.js",
9 | "test": "ng test",
10 | "lint": "ng lint",
11 | "e2e": "ng e2e"
12 | },
13 | "private": true,
14 | "dependencies": {
15 | "@angular/animations": "9.1.11",
16 | "@angular/common": "9.1.11",
17 | "@angular/compiler": "9.1.11",
18 | "@angular/core": "9.1.11",
19 | "@angular/forms": "9.1.11",
20 | "@angular/platform-browser": "9.1.11",
21 | "@angular/platform-browser-dynamic": "9.1.11",
22 | "@angular/router": "9.1.11",
23 | "rxjs": "6.5.4",
24 | "tslib": "1.10.0",
25 | "zone.js": "0.10.2",
26 | "body-parser": "1.19.0",
27 | "bootstrap": "3.4.1",
28 | "express": "4.17.1",
29 | "morgan": "1.10.0",
30 | "serve-favicon": "2.5.0"
31 | },
32 | "devDependencies": {
33 | "@angular-devkit/build-angular": "~0.901.8",
34 | "@angular/cli": "9.1.8",
35 | "@angular/compiler-cli": "9.1.11",
36 | "@angular/language-service": "9.1.11",
37 | "@types/node": "12.11.1",
38 | "@types/jasmine": "3.5.0",
39 | "@types/jasminewd2": "2.0.3",
40 | "codelyzer": "5.1.2",
41 | "jasmine-core": "3.5.0",
42 | "jasmine-spec-reporter": "4.2.1",
43 | "karma": "5.0.0",
44 | "karma-chrome-launcher": "3.1.0",
45 | "karma-coverage-istanbul-reporter": "2.1.0",
46 | "karma-jasmine": "3.0.1",
47 | "karma-jasmine-html-reporter": "1.4.2",
48 | "protractor": "7.0.0",
49 | "ts-node": "8.3.0",
50 | "tslint": "6.1.0",
51 | "typescript": "3.8.3",
52 | "concurrently": "5.2.0"
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/protractor.conf.js:
--------------------------------------------------------------------------------
1 | // Protractor configuration file, see link for more information
2 | // https://github.com/angular/protractor/blob/master/lib/config.ts
3 |
4 | const { SpecReporter } = require('jasmine-spec-reporter');
5 |
6 | exports.config = {
7 | allScriptsTimeout: 11000,
8 | specs: [
9 | './e2e/**/*.e2e-spec.ts'
10 | ],
11 | capabilities: {
12 | 'browserName': 'chrome'
13 | },
14 | directConnect: true,
15 | baseUrl: 'http://localhost:4200/',
16 | framework: 'jasmine',
17 | jasmineNodeOpts: {
18 | showColors: true,
19 | defaultTimeoutInterval: 30000,
20 | print: function() {}
21 | },
22 | onPrepare() {
23 | require('ts-node').register({
24 | project: 'e2e/tsconfig.e2e.json'
25 | });
26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
27 | }
28 | };
29 |
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | var express = require('express');
2 | var path = require('path');
3 | var favicon = require('serve-favicon');
4 | var logger = require('morgan');
5 | var bodyParser = require('body-parser');
6 |
7 | var routes = require('./server/routes/index');
8 | var readers = require('./server/routes/readers');
9 | var books = require('./server/routes/books');
10 |
11 | var app = express();
12 |
13 | app.use(favicon(path.join(__dirname, 'dist/booktracker/favicon.ico')));
14 | app.use(logger('dev'));
15 | app.use(bodyParser.json());
16 | app.use(bodyParser.urlencoded({ extended: false }));
17 | app.use(express.static(path.join(__dirname, 'dist/booktracker')));
18 |
19 | // app.use('/', routes);
20 | app.use('/api/readers', readers);
21 | app.use('/api/books', books);
22 | app.get('*', function(req, res) {
23 | res.sendFile(path.join(__dirname, 'dist/booktracker/index.html'));
24 | });
25 |
26 | app.set('port', process.env.PORT || 3000);
27 |
28 | app.listen(app.get('port'));
29 |
30 | console.log('Listening on port: ' + app.get('port'));
31 |
32 | module.exports = app;
33 |
--------------------------------------------------------------------------------
/server/data/books.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "bookID": 1,
4 | "title": "Goodnight Moon",
5 | "author": "Margaret Wise Brown",
6 | "publicationYear": "1953"
7 | },
8 | {
9 | "bookID": 2,
10 | "title": "Winnie-the-Pooh",
11 | "author": "A. A. Milne",
12 | "publicationYear": "1926"
13 | },
14 | {
15 | "bookID": 3,
16 | "title": "Where the Wild Things Are",
17 | "author": "Maurice Sendak",
18 | "publicationYear": "1963"
19 | },
20 | {
21 | "bookID": 4,
22 | "title": "The Hobbit",
23 | "author": "J. R. R. Tolkien",
24 | "publicationYear": "1937"
25 | },
26 | {
27 | "bookID": 5,
28 | "title": "Curious George",
29 | "author": "H. A. Rey",
30 | "publicationYear": "1941"
31 | },
32 | {
33 | "bookID": 6,
34 | "title": "Alice's Adventures in Wonderland",
35 | "author": "Lewis Carroll",
36 | "publicationYear": "1865"
37 | },
38 | {
39 | "bookID": 7,
40 | "title": "Corduroy",
41 | "author": "Don Freeman",
42 | "publicationYear": "1968"
43 | }
44 | ]
--------------------------------------------------------------------------------
/server/data/readers.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "readerID": 1,
4 | "name": "Marie",
5 | "weeklyReadingGoal": 350,
6 | "totalMinutesRead": 5600
7 | },
8 | {
9 | "readerID": 2,
10 | "name": "Daniel",
11 | "weeklyReadingGoal": 210,
12 | "totalMinutesRead": 3000
13 | },
14 | {
15 | "readerID": 3,
16 | "name": "Lanier",
17 | "weeklyReadingGoal": 140,
18 | "totalMinutesRead": 600
19 | }
20 | ]
--------------------------------------------------------------------------------
/server/routes/books.js:
--------------------------------------------------------------------------------
1 | var express = require('express');
2 | var fs = require('fs');
3 | var datafile = 'server/data/books.json';
4 | var router = express.Router();
5 |
6 | /* GET all books and POST new books */
7 | router.route('/')
8 | .get(function(req, res) {
9 | var data = getBookData();
10 | res.send(data);
11 | })
12 |
13 | .post(function(req, res) {
14 |
15 | var data = getBookData();
16 | var nextID = getNextAvailableID(data);
17 |
18 | var newBook = {
19 | bookID: nextID,
20 | title: req.body.title,
21 | author: req.body.author,
22 | publicationYear: req.body.publicationYear
23 | };
24 |
25 | data.push(newBook);
26 |
27 | saveBookData(data);
28 |
29 | // res.set('Content-Type', 'application/json');
30 | res.status(201).send(newBook);
31 | });
32 |
33 |
34 | /* GET, PUT and DELETE individual books */
35 | router.route('/:id')
36 |
37 | .get(function(req, res) {
38 |
39 | //console.log('Retrieving book id: ' + req.params.id);
40 |
41 | var data = getBookData();
42 |
43 | var matchingBooks = data.filter(function(item) {
44 | return item.bookID == req.params.id;
45 | });
46 |
47 | if(matchingBooks.length === 0) {
48 | res.sendStatus(404);
49 | } else {
50 | res.send(matchingBooks[0]);
51 | }
52 | })
53 |
54 | .delete(function(req, res) {
55 |
56 | var data = getBookData();
57 |
58 | var pos = data.map(function(e) {
59 | return e.bookID;
60 | }).indexOf(parseInt(req.params.id, 10));
61 |
62 | if (pos > -1) {
63 | data.splice(pos, 1);
64 | } else {
65 | res.sendStatus(404);
66 | }
67 |
68 | saveBookData(data);
69 | res.sendStatus(204);
70 |
71 | })
72 |
73 | .put(function(req, res) {
74 |
75 | var data = getBookData();
76 |
77 | var matchingBooks = data.filter(function(item) {
78 | return item.bookID == req.params.id;
79 | });
80 |
81 | if(matchingBooks.length === 0) {
82 | res.sendStatus(404);
83 | } else {
84 |
85 | var bookToUpdate = matchingBooks[0];
86 | bookToUpdate.title = req.body.title;
87 | bookToUpdate.author = req.body.author;
88 | bookToUpdate.publicationYear = req.body.publicationYear;
89 |
90 | saveBookData(data);
91 | res.sendStatus(204);
92 |
93 | }
94 | });
95 |
96 | function getNextAvailableID(allBooks) {
97 |
98 | var maxID = 0;
99 |
100 | allBooks.forEach(function(element, index, array) {
101 |
102 | if(element.bookID > maxID) {
103 | maxID = element.bookID;
104 | }
105 |
106 | });
107 |
108 | return ++maxID;
109 |
110 | }
111 |
112 | function getBookData() {
113 | var data = fs.readFileSync(datafile, 'utf8');
114 | return JSON.parse(data);
115 | }
116 |
117 | function saveBookData(data) {
118 | fs.writeFile(datafile, JSON.stringify(data, null, 4), function (err) {
119 | if (err) {
120 | console.log(err);
121 | }
122 | });
123 | }
124 |
125 | module.exports = router;
126 |
--------------------------------------------------------------------------------
/server/routes/index.js:
--------------------------------------------------------------------------------
1 | var express = require('express');
2 | var router = express.Router();
3 |
4 | /* GET home page. */
5 | router.get('/', function(req, res) {
6 | res.render('index', { title: 'Express' });
7 | });
8 |
9 | module.exports = router;
10 |
--------------------------------------------------------------------------------
/server/routes/readers.js:
--------------------------------------------------------------------------------
1 | var express = require('express');
2 | var fs = require('fs');
3 | var datafile = 'server/data/readers.json';
4 | var router = express.Router();
5 |
6 | /* GET all books and POST new readers */
7 | router.route('/')
8 | .get(function(req, res) {
9 | var data = getReaderData();
10 | res.send(data);
11 | })
12 |
13 | .post(function(req, res) {
14 |
15 | var data = getReaderData();
16 | var nextID = getNextAvailableID(data);
17 |
18 | var newReader = {
19 | readerID: nextID,
20 | name: req.body.name,
21 | weeklyReadingGoal: req.body.weeklyReadingGoal,
22 | totalMinutesRead: req.body.totalMinutesRead
23 | };
24 |
25 | data.push(newReader);
26 |
27 | saveReaderData(data);
28 |
29 | // res.set('Content-Type', 'application/json');
30 | res.status(201).send(newReader);
31 | });
32 |
33 |
34 | /* GET, PUT and DELETE individual readers */
35 | router.route('/:id')
36 |
37 | .get(function(req, res) {
38 |
39 | //console.log('Retrieving reader id: ' + req.params.id);
40 |
41 | var data = getReaderData();
42 |
43 | var matchingReaders = data.filter(function(item) {
44 | return item.readerID == req.params.id;
45 | });
46 |
47 | if(matchingReaders.length === 0) {
48 | res.sendStatus(404);
49 | } else {
50 | res.send(matchingReaders[0]);
51 | }
52 | })
53 |
54 | .delete(function(req, res) {
55 |
56 | var data = getReaderData();
57 |
58 | var pos = data.map(function(e) {
59 | return e.readerID;
60 | }).indexOf(parseInt(req.params.id, 10));
61 |
62 | if (pos > -1) {
63 | data.splice(pos, 1);
64 | } else {
65 | res.sendStatus(404);
66 | }
67 |
68 | saveReaderData(data);
69 | res.sendStatus(204);
70 |
71 | })
72 |
73 | .put(function(req, res) {
74 |
75 | var data = getReaderData();
76 |
77 | var matchingReaders = data.filter(function(item) {
78 | return item.readerID == req.params.id;
79 | });
80 |
81 | if(matchingReaders.length === 0) {
82 | res.sendStatus(404);
83 | } else {
84 |
85 | var readerToUpdate = matchingReaders[0];
86 | readerToUpdate.name = req.body.name;
87 | readerToUpdate.weeklyReadingGoal = req.body.weeklyReadingGoal;
88 | readerToUpdate.totalMinutesRead = req.body.totalMinutesRead;
89 |
90 | saveReaderData(data);
91 | res.sendStatus(204);
92 |
93 | }
94 | });
95 |
96 | function getNextAvailableID(allReaders) {
97 |
98 | var maxID = 0;
99 |
100 | allReaders.forEach(function(element, index, array) {
101 |
102 | if(element.readerID > maxID) {
103 | maxID = element.readerID;
104 | }
105 |
106 | });
107 |
108 | return ++maxID;
109 |
110 | }
111 |
112 | function getReaderData() {
113 | var data = fs.readFileSync(datafile, 'utf8');
114 | return JSON.parse(data);
115 | }
116 |
117 | function saveReaderData(data) {
118 | fs.writeFile(datafile, JSON.stringify(data, null, 4), function (err) {
119 | if (err) {
120 | console.log(err);
121 | }
122 | });
123 | }
124 |
125 | module.exports = router;
126 |
--------------------------------------------------------------------------------
/src/app/add-book/add-book.component.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/add-book/add-book.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 |
3 | import { Book } from "src/app/models/book";
4 | import { DataService } from 'src/app/core/data.service';
5 |
6 | @Component({
7 | selector: 'app-add-book',
8 | templateUrl: './add-book.component.html',
9 | styles: []
10 | })
11 | export class AddBookComponent implements OnInit {
12 |
13 | constructor(private dataService: DataService) { }
14 |
15 | ngOnInit() { }
16 |
17 | saveBook(formValues: any): void {
18 | let newBook: Book = formValues;
19 | newBook.bookID = 0;
20 | console.log(newBook);
21 |
22 | this.dataService.addBook(newBook)
23 | .subscribe(
24 | (data: Book) => console.log(data),
25 | (err: any) => console.log(err)
26 | );
27 | }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/src/app/add-reader/add-reader.component.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/add-reader/add-reader.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 |
3 | import { Reader } from "src/app/models/reader";
4 |
5 | @Component({
6 | selector: 'app-add-reader',
7 | templateUrl: './add-reader.component.html',
8 | styles: []
9 | })
10 | export class AddReaderComponent implements OnInit {
11 |
12 | constructor() { }
13 |
14 | ngOnInit() { }
15 |
16 | saveReader(formValues: any): void {
17 | let newReader: Reader = formValues;
18 | newReader.readerID = 0;
19 | console.log(newReader);
20 | console.warn('Save new reader not yet implemented.');
21 | }
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/src/app/app-routing.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { RouterModule, Routes } from '@angular/router';
3 |
4 | import { AddBookComponent } from "src/app/add-book/add-book.component";
5 | import { AddReaderComponent } from "src/app/add-reader/add-reader.component";
6 | import { DashboardComponent } from "src/app/dashboard/dashboard.component";
7 | import { EditBookComponent } from "src/app/edit-book/edit-book.component";
8 | import { EditReaderComponent } from "src/app/edit-reader/edit-reader.component";
9 |
10 | const routes: Routes = [
11 | { path: 'dashboard', component: DashboardComponent },
12 | { path: 'addbook', component: AddBookComponent },
13 | { path: 'addreader', component: AddReaderComponent },
14 | { path: 'editreader/:id', component: EditReaderComponent },
15 | { path: 'editbook/:id', component: EditBookComponent },
16 | { path: '', redirectTo: 'dashboard', pathMatch: 'full' }
17 | ];
18 |
19 | @NgModule({
20 | imports: [RouterModule.forRoot(routes)],
21 | exports: [RouterModule]
22 | })
23 | export class AppRoutingModule { }
24 |
--------------------------------------------------------------------------------
/src/app/app.component.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bricewilson/angular-clean-code/25ef32040dd63b6576d927d0b945bcf4f02d814d/src/app/app.component.css
--------------------------------------------------------------------------------
/src/app/app.component.html:
--------------------------------------------------------------------------------
1 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-root',
5 | templateUrl: './app.component.html',
6 | styleUrls: ['./app.component.css']
7 | })
8 | export class AppComponent {
9 | title = 'app';
10 | }
11 |
--------------------------------------------------------------------------------
/src/app/app.module.ts:
--------------------------------------------------------------------------------
1 | import { BrowserModule } from '@angular/platform-browser';
2 | import { NgModule } from '@angular/core';
3 | import { FormsModule } from '@angular/forms';
4 | import { HttpClientModule } from '@angular/common/http';
5 |
6 | import { AddBookComponent } from './add-book/add-book.component';
7 | import { AddReaderComponent } from './add-reader/add-reader.component';
8 | import { AppComponent } from './app.component';
9 | import { AppRoutingModule } from './app-routing.module';
10 | import { DashboardComponent } from './dashboard/dashboard.component';
11 | import { EditBookComponent } from './edit-book/edit-book.component';
12 | import { EditReaderComponent } from './edit-reader/edit-reader.component';
13 |
14 | @NgModule({
15 | declarations: [
16 | AppComponent,
17 | DashboardComponent,
18 | AddBookComponent,
19 | EditReaderComponent,
20 | EditBookComponent,
21 | AddReaderComponent
22 | ],
23 | imports: [
24 | BrowserModule,
25 | AppRoutingModule,
26 | FormsModule,
27 | HttpClientModule
28 | ],
29 | bootstrap: [AppComponent]
30 | })
31 | export class AppModule { }
32 |
--------------------------------------------------------------------------------
/src/app/core/badge.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 |
3 | @Injectable({
4 | providedIn: 'root'
5 | })
6 | export class BadgeService {
7 |
8 | constructor() { }
9 |
10 | getReaderBadge(minutesRead: number): string {
11 |
12 | if (minutesRead > 5000) {
13 | return 'Book Worm';
14 | }
15 | else if (minutesRead > 2500) {
16 | return 'Page Turner';
17 | }
18 | else {
19 | return 'Getting Started';
20 | }
21 |
22 | }
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/src/app/core/book-tracker-error-handler.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, ErrorHandler } from '@angular/core';
2 | import { BookTrackerError } from 'src/app/models/bookTrackerError';
3 |
4 | @Injectable({
5 | providedIn: 'root'
6 | })
7 | export class BookTrackerErrorHandlerService implements ErrorHandler {
8 |
9 | handleError(error: any): void {
10 | let customError: BookTrackerError = new BookTrackerError();
11 | customError.errorNumber = 200;
12 | customError.message = (error).message;
13 | customError.friendlyMessage = 'An error occurred. Please try again.';
14 |
15 | console.log(customError);
16 | }
17 |
18 | constructor() { }
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/src/app/core/data.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
3 | import { Observable, throwError } from 'rxjs';
4 | import { map, catchError } from 'rxjs/operators';
5 |
6 | import { allBooks, allReaders } from 'src/app/data';
7 | import { Reader } from "src/app/models/reader";
8 | import { Book } from "src/app/models/book";
9 | import { BookTrackerError } from 'src/app/models/bookTrackerError';
10 | import { OldBook } from 'src/app/models/oldBook';
11 |
12 | @Injectable({
13 | providedIn: 'root'
14 | })
15 | export class DataService {
16 |
17 | constructor(private http: HttpClient) { }
18 |
19 | mostPopularBook: Book = allBooks[0];
20 |
21 | setMostPopularBook(popularBook: Book): void {
22 | this.mostPopularBook = popularBook;
23 | }
24 |
25 | getAllReaders(): Observable {
26 | return this.http.get('/api/readers');
27 | }
28 |
29 | getReaderById(id: number): Observable {
30 | return this.http.get(`/api/readers/${id}`, {
31 | headers: new HttpHeaders({
32 | 'Accept': 'application/json'
33 | })
34 | });
35 | }
36 |
37 | getAllBooks(): Observable {
38 | return this.http.get('/api/books');
39 | }
40 |
41 | private handleHttpError(error: HttpErrorResponse): Observable {
42 | let dataError = new BookTrackerError();
43 | dataError.errorNumber = 100;
44 | dataError.message = error.statusText;
45 | dataError.friendlyMessage = 'An error occurred retrieving data.';
46 | return throwError(dataError);
47 | }
48 |
49 | getBookById(id: number): Observable {
50 | return this.http.get(`/api/books/${id}`, {
51 | headers: new HttpHeaders({
52 | 'Accept': 'application/json'
53 | })
54 | });
55 | }
56 |
57 | getOldBookById(id: number): Observable {
58 | return this.http.get(`/api/books/${id}`)
59 | .pipe(
60 | map(b => {
61 | bookTitle: b.title,
62 | year: b.publicationYear
63 | })
64 | )
65 | }
66 |
67 | addBook(newBook: Book): Observable {
68 | return this.http.post('/api/books', newBook, {
69 | headers: new HttpHeaders({
70 | 'Content-Type': 'application/json'
71 | })
72 | });
73 | }
74 |
75 | updateBook(updatedBook: Book): Observable {
76 | return this.http.put(`/api/books/${updatedBook.bookID}`, updatedBook, {
77 | headers: new HttpHeaders({
78 | 'Content-Type': 'application/json'
79 | })
80 | });
81 | }
82 |
83 | deleteBook(bookID: number): Observable {
84 | return this.http.delete(`/api/books/${bookID}`);
85 | }
86 |
87 | }
88 |
--------------------------------------------------------------------------------
/src/app/core/http-cache.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { HttpResponse } from '@angular/common/http';
3 |
4 | @Injectable({
5 | providedIn: 'root'
6 | })
7 | export class HttpCacheService {
8 |
9 | private requests: any = { };
10 |
11 | constructor() { }
12 |
13 | put(url: string, response: HttpResponse): void {
14 | this.requests[url] = response;
15 | }
16 |
17 | get(url: string): HttpResponse | undefined {
18 | return this.requests[url];
19 | }
20 |
21 | invalidateUrl(url: string): void {
22 | this.requests[url] = undefined;
23 | }
24 |
25 | invalidateCache(): void {
26 | this.requests = { };
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/app/core/logger.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 |
3 | @Injectable({
4 | providedIn: 'root'
5 | })
6 | export class LoggerService {
7 | log(message: string): void {
8 | const timeString: String = new Date().toLocaleTimeString();
9 | console.log(`${message} (${timeString})`);
10 | }
11 |
12 | error(message: string): void {
13 | console.error(`ERROR: ${message}`);
14 | }
15 | }
--------------------------------------------------------------------------------
/src/app/core/plain-logger.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 |
3 | import { LoggerService } from './logger.service';
4 |
5 | @Injectable({
6 | providedIn: 'root'
7 | })
8 | export class PlainLoggerService implements LoggerService {
9 |
10 | log(message: string): void {
11 | console.log(message);
12 | }
13 |
14 | error(message: string): void {
15 | console.log(message);
16 | }
17 |
18 | constructor() { }
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/src/app/dashboard/dashboard.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
All Books
7 |
8 |
9 |
10 | {{book.title}}
11 | Edit
12 | Delete
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
All Readers
23 |
24 |
25 |
26 | {{reader.name}}
27 | Edit
28 | Delete
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
Most Popular Book
39 |
40 |
41 | {{mostPopularBook.title}} by {{mostPopularBook.author}}
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/src/app/dashboard/dashboard.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 | import { Title } from '@angular/platform-browser';
3 | import { ActivatedRoute } from '@angular/router';
4 |
5 | import { Book } from "src/app/models/book";
6 | import { Reader } from "src/app/models/reader";
7 | import { DataService } from 'src/app/core/data.service';
8 | import { BookTrackerError } from 'src/app/models/bookTrackerError';
9 |
10 | @Component({
11 | selector: 'app-dashboard',
12 | templateUrl: './dashboard.component.html',
13 | styles: []
14 | })
15 | export class DashboardComponent implements OnInit {
16 |
17 | deleteBook(bookID: number): void {
18 | this.dataService.deleteBook(bookID)
19 | .subscribe(
20 | (data: void) => {
21 | let index: number = this.allBooks.findIndex(book => book.bookID === bookID);
22 | this.allBooks.splice(index, 1);
23 | },
24 | (err: any) => console.log(err)
25 | );
26 | }
27 |
28 | deleteReader(readerID: number): void {
29 | console.warn(`Delete reader not yet implemented (readerID: ${readerID}).`);
30 | }
31 |
32 | ngOnInit() {
33 |
34 | this.dataService.getAllBooks()
35 | .subscribe(
36 | books => this.allBooks = books
37 | );
38 |
39 | this.dataService.getAllReaders()
40 | .subscribe(
41 | readers => this.allReaders = readers
42 | );
43 |
44 | this.mostPopularBook = this.dataService.mostPopularBook;
45 |
46 | this.title.setTitle(`Book Tracker`);
47 | }
48 |
49 | allBooks: Book[];
50 | allReaders: Reader[];
51 | mostPopularBook: Book;
52 |
53 | constructor(private dataService: DataService,
54 | private title: Title) { }
55 |
56 | }
57 |
--------------------------------------------------------------------------------
/src/app/data.ts:
--------------------------------------------------------------------------------
1 | import { Book } from "src/app/models/book";
2 | import { Reader } from "src/app/models/reader";
3 |
4 | export const allReaders: Reader[] = [
5 | { readerID: 1, name: 'Marie', weeklyReadingGoal: 400, totalMinutesRead: 5600 },
6 | { readerID: 2, name: 'Daniel', weeklyReadingGoal: 210, totalMinutesRead: 3000 },
7 | { readerID: 3, name: 'Lanier', weeklyReadingGoal: 140, totalMinutesRead: 600 }
8 | ];
9 |
10 | export const allBooks: Book[] = [
11 | { bookID: 1, title: 'Goodnight Moon', author: 'Margaret Wise Brown', publicationYear: 1953 },
12 | { bookID: 2, title: 'Winnie-the-Pooh', author: 'A. A. Milne', publicationYear: 1926 },
13 | { bookID: 3, title: 'Where the Wild Things Are', author: 'Maurice Sendak', publicationYear: 1963 },
14 | { bookID: 4, title: 'The Hobbit', author: 'J. R. R. Tolkien', publicationYear: 1937 },
15 | { bookID: 5, title: 'Curious George', author: 'H. A. Rey', publicationYear: 1941 },
16 | { bookID: 6, title: 'Alice\'s Adventures in Wonderland', author: 'Lewis Carroll', publicationYear: 1865 },
17 | ];
--------------------------------------------------------------------------------
/src/app/edit-book/edit-book.component.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/edit-book/edit-book.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 | import { ActivatedRoute } from '@angular/router';
3 |
4 | import { Book } from 'src/app/models/book';
5 | import { DataService } from 'src/app/core/data.service';
6 |
7 | @Component({
8 | selector: 'app-edit-book',
9 | templateUrl: './edit-book.component.html',
10 | styles: []
11 | })
12 | export class EditBookComponent implements OnInit {
13 |
14 | selectedBook: Book;
15 |
16 | constructor(private route: ActivatedRoute,
17 | private dataService: DataService) { }
18 |
19 | ngOnInit() {
20 | let bookID: number = parseInt(this.route.snapshot.params['id']);
21 |
22 | this.dataService.getBookById(bookID)
23 | .subscribe(
24 | (data: Book) => this.selectedBook = data,
25 | (err: any) => console.log(err)
26 | );
27 | }
28 |
29 | setMostPopular(): void {
30 | this.dataService.setMostPopularBook(this.selectedBook);
31 | }
32 |
33 | saveChanges(): void {
34 | this.dataService.updateBook(this.selectedBook)
35 | .subscribe(
36 | (data: void) => console.log(`${this.selectedBook.title} updated successfully.`),
37 | (err: any) => console.log(err)
38 | );
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/app/edit-reader/edit-reader.component.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/edit-reader/edit-reader.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 | import { ActivatedRoute } from '@angular/router';
3 |
4 | import { Reader } from "src/app/models/reader";
5 | import { DataService } from 'src/app/core/data.service';
6 | import { BadgeService } from 'src/app/core/badge.service';
7 |
8 | @Component({
9 | selector: 'app-edit-reader',
10 | templateUrl: './edit-reader.component.html',
11 | styles: [],
12 | providers: [BadgeService]
13 | })
14 | export class EditReaderComponent implements OnInit {
15 |
16 | selectedReader: Reader;
17 | currentBadge: string;
18 |
19 | constructor(private route: ActivatedRoute,
20 | private dataService: DataService,
21 | private badgeService: BadgeService) { }
22 |
23 | ngOnInit() {
24 | let readerID: number = parseInt(this.route.snapshot.params['id']);
25 |
26 | this.dataService.getReaderById(readerID)
27 | .subscribe(
28 | (data: Reader) => this.selectedReader = data,
29 | (err: any) => console.log(err)
30 | );
31 |
32 | this.currentBadge = this.badgeService.getReaderBadge(this.selectedReader.totalMinutesRead);
33 | }
34 |
35 | saveChanges() {
36 | console.warn('Save reader not yet implemented.');
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/app/models/book.ts:
--------------------------------------------------------------------------------
1 | export class Book {
2 | bookID: number;
3 | title: string;
4 | author: string;
5 | publicationYear: number;
6 | }
7 |
--------------------------------------------------------------------------------
/src/app/models/bookTrackerError.ts:
--------------------------------------------------------------------------------
1 | export class BookTrackerError {
2 | errorNumber: number;
3 | message: string;
4 | friendlyMessage: string;
5 | }
--------------------------------------------------------------------------------
/src/app/models/oldBook.ts:
--------------------------------------------------------------------------------
1 | export class OldBook {
2 | bookTitle: string;
3 | year: number;
4 | }
--------------------------------------------------------------------------------
/src/app/models/reader.ts:
--------------------------------------------------------------------------------
1 | export class Reader {
2 | readerID: number;
3 | name: string;
4 | weeklyReadingGoal: number;
5 | totalMinutesRead: number;
6 | }
7 |
--------------------------------------------------------------------------------
/src/assets/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bricewilson/angular-clean-code/25ef32040dd63b6576d927d0b945bcf4f02d814d/src/assets/.gitkeep
--------------------------------------------------------------------------------
/src/environments/environment.prod.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | production: true
3 | };
4 |
--------------------------------------------------------------------------------
/src/environments/environment.ts:
--------------------------------------------------------------------------------
1 | // The file contents for the current environment will overwrite these during build.
2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do
3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead.
4 | // The list of which env maps to which file can be found in `.angular-cli.json`.
5 |
6 | export const environment = {
7 | production: false
8 | };
9 |
--------------------------------------------------------------------------------
/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bricewilson/angular-clean-code/25ef32040dd63b6576d927d0b945bcf4f02d814d/src/favicon.ico
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Book Tracker
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { enableProdMode } from '@angular/core';
2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
3 |
4 | import { AppModule } from './app/app.module';
5 | import { environment } from './environments/environment';
6 |
7 | if (environment.production) {
8 | enableProdMode();
9 | }
10 |
11 | platformBrowserDynamic().bootstrapModule(AppModule)
12 | .catch(err => console.error(err));
13 |
--------------------------------------------------------------------------------
/src/polyfills.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file includes polyfills needed by Angular and is loaded before the app.
3 | * You can add your own extra polyfills to this file.
4 | *
5 | * This file is divided into 2 sections:
6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
8 | * file.
9 | *
10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
13 | *
14 | * Learn more in https://angular.io/guide/browser-support
15 | */
16 |
17 | /***************************************************************************************************
18 | * BROWSER POLYFILLS
19 | */
20 |
21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */
22 | // import 'classlist.js'; // Run `npm install --save classlist.js`.
23 |
24 | /**
25 | * Web Animations `@angular/platform-browser/animations`
26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
28 | */
29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`.
30 |
31 | /**
32 | * By default, zone.js will patch all possible macroTask and DomEvents
33 | * user can disable parts of macroTask/DomEvents patch by setting following flags
34 | * because those flags need to be set before `zone.js` being loaded, and webpack
35 | * will put import in the top of bundle, so user need to create a separate file
36 | * in this directory (for example: zone-flags.ts), and put the following flags
37 | * into that file, and then add the following code before importing zone.js.
38 | * import './zone-flags.ts';
39 | *
40 | * The flags allowed in zone-flags.ts are listed here.
41 | *
42 | * The following flags will work for all browsers.
43 | *
44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
47 | *
48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge
50 | *
51 | * (window as any).__Zone_enable_cross_context_check = true;
52 | *
53 | */
54 |
55 | /***************************************************************************************************
56 | * Zone JS is required by default for Angular itself.
57 | */
58 | import 'zone.js/dist/zone'; // Included with Angular CLI.
59 |
60 |
61 | /***************************************************************************************************
62 | * APPLICATION IMPORTS
63 | */
64 |
--------------------------------------------------------------------------------
/src/styles.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bricewilson/angular-clean-code/25ef32040dd63b6576d927d0b945bcf4f02d814d/src/styles.css
--------------------------------------------------------------------------------
/src/test.ts:
--------------------------------------------------------------------------------
1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files
2 |
3 | import 'zone.js/dist/zone-testing';
4 | import { getTestBed } from '@angular/core/testing';
5 | import {
6 | BrowserDynamicTestingModule,
7 | platformBrowserDynamicTesting
8 | } from '@angular/platform-browser-dynamic/testing';
9 |
10 | declare const require: any;
11 |
12 | // First, initialize the Angular testing environment.
13 | getTestBed().initTestEnvironment(
14 | BrowserDynamicTestingModule,
15 | platformBrowserDynamicTesting()
16 | );
17 | // Then we find all the tests.
18 | const context = require.context('./', true, /\.spec\.ts$/);
19 | // And load the modules.
20 | context.keys().map(context);
21 |
--------------------------------------------------------------------------------
/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "./out-tsc/app",
5 | "types": []
6 | },
7 | "files": [
8 | "src/main.ts",
9 | "src/polyfills.ts"
10 | ],
11 | "include": [
12 | "src/**/*.d.ts"
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "baseUrl": "./",
5 | "outDir": "./dist/out-tsc",
6 | "sourceMap": true,
7 | "declaration": false,
8 | "downlevelIteration": true,
9 | "emitDecoratorMetadata": true,
10 | "experimentalDecorators": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "target": "es2015",
14 | "typeRoots": [
15 | "node_modules/@types"
16 | ],
17 | "lib": [
18 | "es2018",
19 | "dom"
20 | ]
21 | }
22 | }
--------------------------------------------------------------------------------
/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "./out-tsc/spec",
5 | "types": [
6 | "jasmine",
7 | "node"
8 | ]
9 | },
10 | "files": [
11 | "src/test.ts",
12 | "src/polyfills.ts"
13 | ],
14 | "include": [
15 | "src/**/*.spec.ts",
16 | "src/**/*.d.ts"
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "tslint:recommended",
3 | "rules": {
4 | "array-type": false,
5 | "arrow-parens": false,
6 | "deprecation": {
7 | "severity": "warn"
8 | },
9 | "component-class-suffix": true,
10 | "contextual-lifecycle": true,
11 | "directive-class-suffix": true,
12 | "directive-selector": [
13 | true,
14 | "attribute",
15 | "app",
16 | "camelCase"
17 | ],
18 | "component-selector": [
19 | true,
20 | "element",
21 | "app",
22 | "kebab-case"
23 | ],
24 | "import-blacklist": [
25 | true,
26 | "rxjs/Rx"
27 | ],
28 | "interface-name": false,
29 | "max-classes-per-file": false,
30 | "max-line-length": [
31 | true,
32 | 140
33 | ],
34 | "member-access": false,
35 | "member-ordering": [
36 | true,
37 | {
38 | "order": [
39 | "static-field",
40 | "instance-field",
41 | "static-method",
42 | "instance-method"
43 | ]
44 | }
45 | ],
46 | "no-consecutive-blank-lines": false,
47 | "no-console": [
48 | true,
49 | "debug",
50 | "info",
51 | "time",
52 | "timeEnd",
53 | "trace"
54 | ],
55 | "no-empty": false,
56 | "no-inferrable-types": [
57 | true,
58 | "ignore-params"
59 | ],
60 | "no-non-null-assertion": true,
61 | "no-redundant-jsdoc": true,
62 | "no-switch-case-fall-through": true,
63 | "no-use-before-declare": true,
64 | "no-var-requires": false,
65 | "object-literal-key-quotes": [
66 | true,
67 | "as-needed"
68 | ],
69 | "object-literal-sort-keys": false,
70 | "ordered-imports": false,
71 | "quotemark": [
72 | true,
73 | "single"
74 | ],
75 | "trailing-comma": false,
76 | "no-conflicting-lifecycle": true,
77 | "no-host-metadata-property": true,
78 | "no-input-rename": true,
79 | "no-inputs-metadata-property": true,
80 | "no-output-native": true,
81 | "no-output-on-prefix": true,
82 | "no-output-rename": true,
83 | "no-outputs-metadata-property": true,
84 | "template-banana-in-box": true,
85 | "template-no-negated-async": true,
86 | "use-lifecycle-interface": true,
87 | "use-pipe-transform-interface": true
88 | },
89 | "rulesDirectory": [
90 | "codelyzer"
91 | ]
92 | }
--------------------------------------------------------------------------------