How to start an Angular project?
Author: deejayy, 2020-03-11, updated: 2020-10-20
Easy, just "ng new", innit?
ng new angular10-project-init
? Would you like to add Angular routing? Yes
? Which stylesheet format would you like to use? SCSS [ https://sass-lang.com/documentation/syntax#scss ]
Official documentation of ng new.
Now you'll have an Angular project set up:
- node_modules prepared with necessary packages
- git set up for version tracking with an initial commit
- project files under "src" directory
This is sort of a checklist.
README.md
It is important to let the others (and by others you can mean yourself 2 years later) know what is this project about. Feel free to extend the already prepared README.md
file with specific information about the application.
Working environment
Editor config
Create or extend the .editorconfig
file with the following content:
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
[*.md]
max_line_length = off
trim_trailing_whitespace = false
[*.html]
quote_type = double
You might want to install the corresponding plugin in your IDE (eg. "Editorconfig for VSCode")
More on editorconfig on the official page.
Github setup (optional)
If you are using github, you can prepare templates for verious activities in the .github
directory:
- contributing.md: what the others should know about how to contribute
- pull_request_template.md: an initial comment template for every pull request (eg. a brief description about the merging process)
Check github's guidelines on the topic.
AWS (optional)
Are you going to deploy in an AWS CloudFront environment? The CodePipeline may need a specification in the buildspec.yml
file:
version: 0.2
phases:
install:
commands:
- echo "Installing packages."
- npm install
- echo "Installation of packages is done"
pre_build:
commands:
- echo "Placeholder for pre_build"
build:
commands:
- echo "Build started on /Project Name Here/"
- npm run build -- --configuration=${ENV}
- echo "Build completed on /Project Name Here/"
post_build:
commands:
- echo "Copying directory app to to s3 bucket ${BUCKET_NAME}"
- aws s3 sync dist/ "s3://${BUCKET_NAME}/" --delete --sse
- aws configure set preview.cloudfront true && aws cloudfront create-invalidation --distribution-id "${DISTRIBUTION_ID}" --paths /\*
- echo "Post build finihsed"
artifacts:
files:
- dist/**/*
- dist/*
How the buildspec file should look like according to AWS.
Project specific
Browser compatibility
Check browserslist
if you have to support older browsers as well, here are some hints about how it works.
Testing with Jest
But why Jest? Because it is faster!
npm install --save-dev @angular-builders/jest @types/jest jest jest-html-reporter jest-preset-angular
Create jest.config.js
with the following content:
module.exports = {
preset: "jest-preset-angular",
globals: {
"ts-jest": {
tsConfig: "<rootDir>/tsconfig.spec.json",
stringifyContentPathRegex: "\\.html$",
diagnostics: false,
},
},
setupFilesAfterEnv: ["<rootDir>/src/setupJest.ts"],
moduleNameMapper: {
"@app/(.*)": "<rootDir>/src/app/$1",
"@env/(.*)": "<rootDir>/src/environments/$1",
},
};
Change the angular.json
/ projects.<project name>.architect.test
block to the following:
"test": {
"builder": "@angular-builders/jest:run",
"options": {
"no-cache": true,
"reporters": []
}
},
Add new files for jest setup:
src/jestGlobalMocks.ts
export function storageMock() {
const storage = {};
return {
setItem: (key: string, value?: any) => {
storage[key] = value || '';
},
getItem: (key: string) => {
return key in storage ? storage[key] : null;
},
removeItem: (key: string) => {
delete storage[key];
},
get length() {
return Object.keys(storage).length;
},
key: (index: number) => {
const keys = Object.keys(storage);
return keys[index] || null;
},
};
}
Object.defineProperty(window, 'localStorage', { value: storageMock });
Object.defineProperty(window, 'sessionStorage', { value: storageMock });
Object.defineProperty(window, 'gtag', { value: () => {} });
Object.defineProperty(window, 'ga', { value: () => {} });
src/jestGlobalMocks.spec.ts
import { storageMock } from './jestGlobalMocks';
describe('JestMocks', () => {
it('localStorage must be empty', () => {
const localStorage = storageMock();
expect(localStorage.length).toEqual(0);
});
it('localStorage set item / get item', () => {
const localStorage = storageMock();
localStorage.setItem('key-empty');
localStorage.setItem('key-number', 1);
localStorage.setItem('key-string', 'value');
localStorage.setItem('key-array', [ 2 ]);
localStorage.setItem('key-object', { a: 'b' });
expect(localStorage.getItem('key-not-found')).toEqual(null);
expect(localStorage.getItem('key-empty')).toEqual('');
expect(localStorage.getItem('key-number')).toEqual(1);
expect(localStorage.getItem('key-string')).toEqual('value');
expect(localStorage.getItem('key-array')).toEqual([ 2 ]);
expect(localStorage.getItem('key-object')).toEqual({ a: 'b' });
});
it('localStorage set item / remove item', () => {
const localStorage = storageMock();
localStorage.setItem('key-number', 1);
expect(localStorage.getItem('key-number')).toEqual(1);
localStorage.removeItem('key-number');
expect(localStorage.getItem('key-number')).toEqual(null);
});
it('localStorage set item / key', () => {
const localStorage = storageMock();
localStorage.setItem('key-number', 1);
expect(localStorage.key(0)).toEqual('key-number');
expect(localStorage.key(1)).toEqual(null);
});
});
src/setupJest.ts
import 'jest-preset-angular';
import './jestGlobalMocks';
Now try out jest
jest jestGlobalMocks
PASS src/jestGlobalMocks.spec.ts
JestMocks
√ localStorage must be empty (3ms)
√ localStorage set item / get item (2ms)
√ localStorage set item / remove item (1ms)
√ localStorage set item / key
Test Suites: 1 passed, 1 total
Tests: 4 passed, 4 total
Snapshots: 0 total
Time: 2.771s, estimated 4s
Ran all test suites matching /jestGlobalMocks/i.
Module path aliases
If you see "../" in any TypeScript file, you're doomed. Relative paths will cause a lot of headaches later. The solution is: module path aliases! Check out this intro about it!
There are two places where you need to maintain the list of module path aliases.
- package.json (for Jest, see above)
- tsconfig.json (for build, see below)
Check tsconfig.json
/ compilerOptions
branch and add this:
"paths": {
"@app/*": ["src/app/*"],
"@env/*": ["src/environments/*"]
},
Version number
Open package.json
and set the version
value.
Trick: you can add this line to every environment.ts if you want to advertise the version number runtime:
import { version } from '../../package.json';
export const environment = {
production: false,
version: version,
};
Don't forget about letting the typescript compiler know that you want to import a json in a TS file, add these two lines to your tsconfig.json
/ compilerOptions
:
"resolveJsonModule": true,
"esModuleInterop": true
You can add this version number to eg. some online tracking tool, like Google Analytics (check angulartics2 module).
Static code checking tools
Lint-staged, stylelint and commit hook. Because static check can improve the code quality significantly.
npm install --save-dev lint-staged husky stylelint stylelint-config-standard-scss stylelint-scss tslint tslint-clean-code tslint-microsoft-contrib
Add the following items to the package.json
/ scripts
section:
"lint": "ng lint && npm run lint:styles",
"lint:styles": "stylelint --syntax=scss --config=./stylelint **/*.scss",
"lint-staged": "lint-staged -r",
"precommit": "npm run lint-staged"
... and this blocks to root:
"lint-staged": {
"*.ts": [
"tslint",
"jest --findRelatedTests"
],
"*.scss": "stylelint --syntax=scss --config=./stylelint"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
stylelint.json
{
"extends": "stylelint-config-standard-scss",
"rules": {
"at-rule-empty-line-before": [
"always",
{
"except": [
"blockless-after-same-name-blockless",
"blockless-after-blockless",
"first-nested"
],
"ignoreAtRules": [
"import",
"include"
]
}
],
"at-rule-no-vendor-prefix": true,
"at-rule-allowed-list": [
"mixin",
"if",
"function",
"return",
"import",
"include",
"media",
"keyframes",
"supports",
"font-face"
],
"block-no-empty": [
true,
{
"ignore": [
"comments"
]
}
],
"color-no-invalid-hex": true,
"comment-no-empty": true,
"declaration-block-no-duplicate-properties": true,
"declaration-block-no-shorthand-property-overrides": true,
"declaration-empty-line-before": "never",
"font-family-no-duplicate-names": true,
"font-family-no-missing-generic-family-keyword": true,
"function-calc-no-invalid": true,
"function-calc-no-unspaced-operator": true,
"function-linear-gradient-no-nonstandard-direction": true,
"keyframe-declaration-no-important": true,
"media-feature-name-no-unknown": true,
"no-descending-specificity": true,
"no-duplicate-at-import-rules": true,
"no-duplicate-selectors": true,
"no-extra-semicolons": true,
"no-invalid-double-slash-comments": true,
"property-no-unknown": [
true,
{
"ignoreProperties": [
"scroll",
"overflow-x",
"overflow-y"
]
}
],
"property-no-vendor-prefix": [
true,
{
"ignoreProperties": [
"appearance"
]
}
],
"rule-empty-line-before": [
"always",
{
"except": [
"after-single-line-comment",
"first-nested"
]
}
],
"selector-pseudo-class-no-unknown": true,
"selector-pseudo-element-no-unknown": true,
"selector-no-vendor-prefix": true,
"selector-type-no-unknown": true,
"string-no-newline": true,
"unit-no-unknown": true
}
}
Try it out with the command
npm run lint
tslint.json
{
"rulesDirectory": [
"tslint-microsoft-contrib",
"codelyzer",
"tslint-clean-code"
],
"rules": {
"align": [
false
],
"array-type": [
true,
"array"
],
"arrow-return-shorthand": [true, "multiline"],
"await-promise": false,
"adjacent-overload-signatures": false,
"arrow-parens": true,
"ban": false,
"callable-types": true,
"class-name": true,
"comment-format": [
false
],
"completed-docs": [
false
],
"curly": true,
"cyclomatic-complexity": [
true,
6
],
"deprecation": {
"severity": "warning"
},
"eofline": true,
"export-name": false,
"file-header": [
false
],
"forin": true,
"function-name": [
true,
{
"static-method-regex": "^[A-Za-z_\\d]+$"
}
],
"import-blacklist": [
true,
"rxjs/Rx"
],
"import-spacing": true,
"indent": [true, "spaces", 2],
"insecure-random": true,
"interface-name": false,
"interface-over-type-literal": false,
"jquery-deferred-must-complete": true,
"jsdoc-format": false,
"label-position": true,
"linebreak-style": [
true,
"LF"
],
"max-classes-per-file": false,
"max-file-line-count": [
false
],
"max-func-body-length": [
true,
100,
{
"ignore-parameters-to-function-regex": "describe"
}
],
"max-line-length": [
true,
140
],
"member-access": true,
"member-ordering": [
false
],
"missing-jsdoc": false,
"missing-optional-annotation": false,
"min-class-cohesion": false,
"new-parens": true,
"newspaper-order": false,
"no-angle-bracket-type-assertion": false,
"no-any": true,
"no-arg": true,
"no-backbone-get-set-outside-model": false,
"no-banned-terms": true,
"no-bitwise": true,
"no-boolean-literal-compare": false,
"no-conditional-assignment": true,
"no-consecutive-blank-lines": [
true
],
"no-console": [
true,
"debug",
"info",
"log",
"time",
"timeEnd",
"trace"
],
"no-constant-condition": true,
"no-construct": true,
"no-control-regex": true,
"no-debugger": true,
"no-default-export": true,
"no-delete-expression": true,
"no-disable-auto-sanitization": true,
"no-document-domain": true,
"no-document-write": true,
"no-duplicate-switch-case": true,
"no-duplicate-parameter-names": true,
"no-duplicate-super": true,
"no-duplicate-variable": true,
"no-empty": false,
"no-empty-interface": false,
"no-empty-line-after-opening-brace": true,
"no-eval": true,
"no-exec-script": true,
"no-floating-promises": false,
"no-for-in": true,
"no-for-in-array": true,
"no-function-expression": true,
"no-http-string": [
true,
"http://localhost"
],
"no-import-side-effect": false,
"no-inferrable-types": false,
"no-inferred-empty-object-type": false,
"no-inner-html": true,
"no-internal-module": true,
"no-invalid-regexp": true,
"no-invalid-template-strings": true,
"no-invalid-this": true,
"no-jquery-raw-elements": true,
"no-magic-numbers": true,
"no-mergeable-namespace": false,
"no-missing-visibility-modifiers": true,
"no-misused-new": true,
"no-multiline-string": false,
"no-multiple-var-decl": true,
"no-namespace": false,
"no-null-keyword": false,
"no-octal-literal": true,
"no-parameter-properties": true,
"no-reference": true,
"no-reference-import": true,
"no-regex-spaces": true,
"no-require-imports": true,
"no-reserved-keywords": false,
"no-shadowed-variable": true,
"no-single-line-block-comment": true,
"no-sparse-arrays": true,
"no-stateless-class": false,
"no-string-based-set-immediate": true,
"no-string-based-set-interval": true,
"no-string-based-set-timeout": true,
"no-string-literal": true,
"no-string-throw": true,
"no-suspicious-comment": true,
"no-switch-case-fall-through": true,
"no-trailing-whitespace": true,
"no-typeof-undefined": true,
"no-unbound-method": false,
"no-unnecessary-callback-wrapper": true,
"no-unnecessary-field-initialization": true,
"no-unnecessary-initializer": true,
"no-unnecessary-local-variable": true,
"no-unnecessary-override": true,
"no-unnecessary-qualifier": false,
"no-unnecessary-semicolons": true,
"no-unsafe-any": false,
"no-unsafe-finally": true,
"no-unsupported-browser-code": false,
"no-unused-expression": true,
"no-var-keyword": true,
"no-var-requires": true,
"no-this-assignment": true,
"no-void-expression": false,
"no-with-statement": true,
"non-literal-require": true,
"object-literal-key-quotes": [
true,
"as-needed"
],
"object-literal-shorthand": false,
"object-literal-sort-keys": false,
"one-line": [
true,
"check-open-brace",
"check-catch",
"check-else",
"check-whitespace"
],
"one-variable-per-declaration": true,
"only-arrow-functions": [
true,
"allow-declarations",
"allow-named-functions"
],
"ordered-imports": [
false
],
"possible-timing-attack": true,
"prefer-array-literal": true,
"prefer-const": true,
"prefer-for-of": false,
"prefer-function-over-method": false,
"prefer-method-signature": true,
"prefer-template": true,
"strict-type-predicates": false,
"promise-function-async": false,
"promise-must-complete": true,
"quotemark": [
true,
"single",
"avoid-escape"
],
"radix": false,
"restrict-plus-operands": true,
"semicolon": [
true,
"always"
],
"space-before-function-paren": false,
"strict-boolean-expressions": false,
"switch-default": false,
"trailing-comma": [
true,
{
"singleline": "never",
"multiline": {
"objects": "always",
"arrays": "always",
"functions": "always",
"imports": "always",
"exports": "always",
"typeLiterals": "always"
}
}
],
"triple-equals": [
true,
"allow-null-check"
],
"typedef": [
true,
"call-signature",
"parameter",
"property-declaration"
],
"typedef-whitespace": [
false
],
"variable-name": {
"options": [
"ban-keywords",
"check-format",
"allow-pascal-case"
]
},
"whitespace": {
"options": [
"check-branch",
"check-decl",
"check-operator",
"check-separator",
"check-type",
"check-typecast"
]
},
"underscore-consistent-invocation": true,
"unified-signatures": true,
"use-isnan": true,
"use-named-parameter": true,
"no-useless-files": true,
"component-class-suffix": true,
"contextual-lifecycle": true,
"directive-class-suffix": true,
"no-conflicting-lifecycle": true,
"no-host-metadata-property": true,
"no-input-rename": true,
"no-inputs-metadata-property": true,
"no-output-native": true,
"no-output-on-prefix": true,
"no-output-rename": true,
"no-outputs-metadata-property": true,
"template-banana-in-box": true,
"template-no-negated-async": true,
"use-lifecycle-interface": true,
"use-pipe-transform-interface": true,
"prefer-on-push-component-change-detection": true,
"directive-selector": [
true,
"attribute",
"app",
"camelCase"
],
"component-selector": [
true,
"element",
"app",
"kebab-case"
]
}
}
Try it out with the command
tslint --project .
Prepare the app
CSS reset
npm install modern-css-reset
Add reset css to the build process: angular.json
/ projects.<project name>.architect.build.options.styles
"styles": [
"node_modules/modern-css-reset/dist/reset.css",
"src/styles.scss"
],
Thanks to Andy for the modern css reset.
Application theme and utils (css)
Some useful mixins for responsive design
src/mixins.scss
$breakpoints: (
"phone": 400px,
"phone-wide": 480px,
"phablet": 560px,
"tablet-small": 640px,
"tablet": 768px,
"tablet-wide": 1024px,
"desktop": 1248px,
"desktop-wide": 1440px
);
@mixin mq($width, $type: max) {
@if map_has_key($breakpoints, $width) {
$width: map_get($breakpoints, $width);
@if $type == max {
$width: $width - 1px;
}
@media only screen and (#{$type}-width: $width) {
@content;
}
}
}
@function smart-scale($minwidth, $maxwwidth, $minscreen: 320, $maxscreen: 1440) {
@return calc(#{$minwidth + 'px'} + (#{$maxwwidth} - #{$minwidth}) * ((100vw - #{$minscreen + 'px'}) / (#{$maxscreen} - #{$minscreen})));
}
The mq
mixin will allow you to shorten the usual media query definitions in css, also adds some readable definitions for breakpoints which you can adjust later in a single place.
The smart-scale
magic is to make a specific size relative between the minscreen
and maxscreen
in proportion with the minwidth
and maxwidth
. It can be used to scale font sizes, box widths, anything what you want to define in pixels.
Application theme with the colors and fonts, and all you want to put in. Use hsl
color codes for better readability.
src/app-theme.scss
@import "mixins.scss";
$theme-color-1: hsl(200, 80, 75%);
$theme-color-2: hsl(200, 80, 70%);
$theme-color-3: hsl(200, 80, 65%);
$theme-color-4: hsl(200, 80, 60%);
$theme-color-5: hsl(200, 80, 55%);
$theme-color-6: hsl(200, 80, 50%);
$theme-color-7: hsl(200, 80, 45%);
$theme-color-8: hsl(200, 80, 40%);
$theme-color-9: hsl(200, 80, 35%);
$theme-color-10: hsl(200, 80, 30%);
$theme-color: $theme-color-5;
$theme-light: $theme-color-9;
$theme-default: $theme-color;
$theme-hover: $theme-color-4;
$theme-pressed: $theme-color-2;
$theme-background: hsl(200, 100, 100%);
$warn-color-1: hsl(0, 80, 75%);
$warn-color-2: hsl(0, 80, 70%);
$warn-color-3: hsl(0, 80, 65%);
$warn-color-4: hsl(0, 80, 60%);
$warn-color-5: hsl(0, 80, 55%);
$warn-color-6: hsl(0, 80, 50%);
$warn-color-7: hsl(0, 80, 45%);
$warn-color-8: hsl(0, 80, 40%);
$warn-color-9: hsl(0, 80, 35%);
$warn-color-10: hsl(0, 80, 30%);
$warn-color: $warn-color-5;
$warn-light: $warn-color-9;
$warn-default: $warn-color;
$warn-hover: $warn-color-4;
$warn-pressed: $warn-color-2;
$theme-font: Tahoma, sans-serif;
$black: rgba(0, 0, 0, 1);
$white: rgba(255, 255, 255, 1);
Add your theme to src/styles.scss
:
@import "app-theme.scss";
You'll unfortunately need this import in every component's stylesheet, for that, this snippet is needed in the angular.json in order to make it work (place it under projects.<project name>.architect.build.options
):
"stylePreprocessorOptions": {
"includePaths": [
"src"
]
},
Angular Material (optional)
npm install @angular/cdk @angular/material hammerjs
Add BrowserAnimationsModule
to app.module.ts
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
...
imports: [
...,
BrowserAnimationsModule,
],
Add basic fonts to index.html
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
Add hammerjs to main.ts
import 'hammerjs';
Create an scss file for material theme:
src/material-theme.scss
@import "app-theme.scss";
@import "~@angular/material/theming";
@include mat-core();
$mat-app: (
50: $theme-color-1,
100: $theme-color-2,
200: $theme-color-3,
300: $theme-color-4,
400: $theme-color-5,
500: $theme-color-6,
600: $theme-color-7,
700: $theme-color-8,
800: $theme-color-9,
900: $theme-color-10,
A100: $theme-light,
A200: $theme-default,
A400: $theme-hover,
A700: $theme-pressed,
contrast: (
50: $black,
100: $black,
200: $black,
300: $black,
400: $white,
500: $white,
600: $white,
700: $white,
800: $white,
900: $white,
A100: $black,
A200: $white,
A400: $white,
A700: $white
)
);
$mat-app-warn: (
50: $warn-color-1,
100: $warn-color-2,
200: $warn-color-3,
300: $warn-color-4,
400: $warn-color-5,
500: $warn-color-6,
600: $warn-color-7,
700: $warn-color-8,
800: $warn-color-9,
900: $warn-color-10,
A100: $warn-light,
A200: $warn-default,
A400: $warn-hover,
A700: $warn-pressed,
contrast: (
50: $black,
100: $black,
200: $black,
300: $black,
400: $black,
500: $white,
600: $white,
700: $white,
800: $white,
900: $white,
A100: $black,
A200: $white,
A400: $white,
A700: $white
)
);
$app-theme: mat-palette($mat-app);
$app-accent: mat-palette($mat-app, A200, A100, A400);
$app-warn: mat-palette($mat-app-warn, A200, A100, A400);
$app-theme: mat-light-theme($app-theme, $app-accent, $app-warn);
$custom-typography: mat-typography-config(
$font-family: $theme-font,
$headline: mat-typography-level(32px, 48px, 800),
$caption: mat-typography-level(16px, 24px, 400),
$body-1: mat-typography-level(16px, 24px, 400),
$body-2: mat-typography-level(16px, 24px, 600)
);
@include angular-material-theme($app-theme);
body {
color: $theme-color;
font-size: 16px;
font-family: $theme-font;
}
Add your material theme to styles.scss
(replace the import of app-theme.scss
):
@import "material-theme.scss";
Get rid of placeholders
Adjust app.component.html | ts | spec.ts
files, remove placeholders, invalid test cases.
Run the first static check
And also fix test errors, if any
npm run lint
jest
ng build --prod
Take care of multiple environments (optional)
If you have other environments apart from local (environment.ts) and production (environment.prod.ts), create them and add them to angular.json as well.
Example:
"dev": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.dev.ts"
}
]
},
"staging": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.staging.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true
},
Recommended packages
- angular-bem (github)
- angulartics2 (github)
- immer (homepage)
- @ngrx/store @ngrx/effects @ngrx/store-devtools
- @auth0/angular-jwt (github)
- NGRX Api Caller module (github / howto)
Directory structure
Create the directory structure as per the article:
https://itnext.io/choosing-a-highly-scalable-folder-structure-in-angular-d987de65ec7