- 1. Introduction
- 2. Setup Option 1: Just Angular
- 3. Setup Option 2: JHipster = Angular with Spring Boot Backend and Deployment in Pivotal Cloud Foundry
- 4. Package Management
- 5. Tooling
- 6. Debugging
- 7. Bootstrap
- 8. Writing Components
- 9. Databinding
- 10. Directives
- 11. Models
- 12. Components & Databinding
- 13. View Encapsulation
- 14. Local References
- 15. Accessing DOM Elements via ElementRef
- 16. Component Lifecycle
- 17. Services and Dependency Injection
- 18. Routing
- 19. Observables
- 20. Forms
- 21. Best Practices
- 22. Pipes
- 23. Http Requests
- 24. Authentification
- 25. Modules
- 26. Unit Tests
- 27. Deploying Angular Applications
- 28. Acknowledgements
- 29. Structuring Angular Applications
- 30. Cross-Cutting Concerns
These are notes and code snippets from the Udemy Workshop The complete guide to Angular. The "Course Project" is published here. Although this repository here uses code snippets from the Udemy Workshop, the original course is very much recommended.
This workshop is still in development.
-
Angular = framework for reactive single-page-applications
-
single-page = one HTML-file that gets changed by JS
-
reactive = not necessarily a server-call (and if, can be done in the background)
-
-
version madness
-
"Angular.js" = Angular 1 = first version of framework
-
"Angular 2" = first major rewrite, also known as simply "Angular"
-
"Angular 3" skipped for some reason
-
"Angular 4" and "Angular 5" = latest versions
-
however, only two major and potentially incompatible versions: Angular.js and "the rest" a.k.a. "Angular" because only Angular 2 had major changes
-
-
this option: only little Angular project for learning plain angular
-
following steps are for Windows machines; Linux can slightly diver
-
… because Angular CLI needs it …
-
manages packages and loads a local development server
-
builds new projects that can than be used
-
less setup, less writing boiler plate, more coding business logic
-
in Windows console:
npm install -g @angular/cli
-
npm = Node package manager, came with NodeJS
-
package.json in NPM comparable to pom.xml in Maven
-
-g = global install
-
@angular/cli = package
-
create new folder and navigate to it in console
-
creating new project (in Windows console):
ng new my-first-app
-
"ng" stands for Angular
-
my-first-app is the name of the app
-
overview of what will be created:
-
folder e2e contains end-to-end testing
-
folder src/app should be the only location changes should be applied to
-
src/index.html = HTML file that is changed by the Java Script of the framework. The dynamic content is inserted here:
<body> <app-root></app-root> </body>
-
folder node_modules is just for build tools and contains for example downloaded content like Bootstrap. Hence this is excluded in the .gitignore. These files are not necessary for deployment on server, just for development. For deployment, the content is bundled into one single file by the CLI.
-
README.md contains the most important commands
-
other single files = configuration; no need to touch … maybe later
-
-
navigate in created folder and run (in Windows console)
ng serve
-
will build all source code and run a dev server (see output for address)
-
should be kept running all the time because changes in files are automatically saved, compiled and the app refreshed in the browser (without the need to hit F5!)
3. Setup Option 2: JHipster = Angular with Spring Boot Backend and Deployment in Pivotal Cloud Foundry
-
Yarn = Dependency Manager
-
"yarn global add generator-jhipster" in Terminal will install Yarn
-
running "ng serve" (like in Setup Option 1) in this folder doesn’t work :(
-
instead:
-
"mvnw" to start Maven build and run application OR
-
"yarn start" to start webpack development server for monitoring and generating beans and so on. Also notices changes in files and deploys them automatically OR
-
via IDE: Maven Projects → Plugins → spring-boot → spring-boot:run or simply execute run config (gets created automatically). This is also what will be done after deployment, so this is most likely the best option.
-
Attention: The application tends to switch to the prod-profile after deployment! To prevent this, add the VM Option "-Dspring.profiles.active=dev" in the run config.
-
-
-
for example in free version of Pivotal Web Services
-
in terminal; explicit command to deploy to Cloud Foundry (see help)
jhipster cloudfoundry
-
this will execute "cf push", create a route to the app and bind services like the database
-
Attention:
-
When running the first time, this will ask to overwrite the pom.xml because during build, additional dependencies are inserted. Overwrite the file.
-
However, the new pom.xml doesn’t get loaded with the first deployment. Hence, it will fail.
-
"Solution": Deploy a second time.
-
After this first run, every deployment will work fine.
-
-
JDL Studio = Online Generator for JDL-files that can be imported into JHipster and entities are created
-
Node Package Manager
-
= package manager for JavaScript
-
(a lot of languages have package managers: PHP has Composer, Python has PyPi, Java has Gradle and Maven, …)
-
installing, sharing, distributing code
-
package.json contains external dependencies, however just the first layer of dependencies. The underlying layers will be resolved automatically.
-
package-lock.json is automatically created and contains the exact dependency tree and locks this tree to be used when resolving dependencies
-
= superset of NPM
-
= "Yet Another Resource Negotiator"
-
package manager that uses NPM registry as backend
-
yarn.lock file stores exact versions of dependencies
-
yarn updates yarn.lock automatically when dependencies are installed or updated (NPM needs the shrinkwrap command)
-
very fast compared to NPM because NPM installs sequentially, Yarn in parallel
-
installation for example:
yarn add --dev webpack
-
--dev means that dependencies are installed in devDependencies array in package.json (for development) whereas omitting --dev causes them to be installed in the dependencies-array (for production)
-
used to run commands like this to run all scripts in the "build" section of the package.json file:
yarn run build
-
ran into this problem with "rimraf":
C:\repositories\xyz>yarn run build yarn run v1.3.2 $ yarn run webpack:prod $ yarn run cleanup && yarn run webpack:prod:main && yarn run clean-www $ rimraf build/{aot,www} Der Befehl "rimraf" ist entweder falsch geschrieben oder konnte nicht gefunden werden.
-
solution: look at package.json: some dependencies have warnings that they are not installed. Alt+Enter and run "yarn install"
-
JavaScript has different versions
-
Babel converts new JavaScript code into older versions
-
enables development with newest JS version without worrying about browser support
-
IntelliJ IDEA supports Angular right from the start:
-
Reference search also working:
-
also, WebStorm is a lightweight IntelliJ IDEA and is suited for web development right away. However, IntelliJ IDEA can be upgraded via plugins to offer nearly the same functionality.
-
To avoid warnings, the behavior of adding quotation marks should be adjusted from double quotation marks to single quotation marks. Otherwise, IDEA will add double quotation marks in import-statements which will cause a lot of errors that have to be corrected manually.
-
= Plugin for working with HTML and CSS
-
already activated in IntelliJ IDEA
-
workflow: write abbreviation, press Tab
-
documentation for settings for HTML-support and CSS-support
-
in settings "enable abbreviation preview":
-
main problem: TypeScript getting translated into JavaScript
-
solution: open developer tools in browser (in this example Vivaldi) (F12) → "Sources"
-
TypeScript sources available in the left window under webpack
-
adding breakpoints like in IDE
-
= Chrome extension specifically for debugging Angular applications
-
new tab in developer tools
-
Bootstrap = toolkit for HTML, CSS and JS that provides a lot of ready-to-user CSS and components
-
CSS-styles for tables, buttons, images and more
-
Components like button groups, navigation bars and progress bars
-
in the course, Bootstrap 3 is used. Hence use
npm install --save bootstrap@3
instead of
npm install --save bootstrap
-
run this in IntelliJ IDEA via build-in Terminal will download Bootstrap
-
after downloading, it has to be imported:
-
open .angular-cli.json
-
add something to the array of styles:
"styles": [
"styles.css"
],
-
add newly downloaded Bootstrap-style from directory node_modules:
"styles": [ "../node_modules/bootstrap/dist/css/bootstrap.min.css", "styles.css" ],
-
components = key feature of Angular
-
reusable
-
separation of concerns because every component has its own controller and therefore business logic
-
what is a component and what not is often the question at hand
-
after creating project with CLI, following files in src/app:
-
app.component.css
-
CSS file for this specific component
-
-
app.component.html
-
template of this component
-
what is written in this file is being copied to wherever the component is being used
-
-
app.component.spec.ts
-
tests
-
-
app.component.ts
-
definition of the component
-
defines the name (="selector") of the component ("app-root") with which it can be used in other HTML-files
-
-
app.module.ts
-
declarations and imports for the whole application
-
-
-
naming convention in Angular: [name of component].component.[file type], for example "server.component.ts" is the type script file for the server component
-
another aspect in Angular: "Decorator" = feature to enhance components with functionality, for example "@Component". Decorator needs information to know what to do with the annotated class, so a JSON object is provided:
@Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] })
-
create new directory in src/app, for example "server"
-
create server.component.ts with a (unique!) selector and a reference to a template
-
create template server.component.html
-
register new component in app.module.ts in the declarations-array (there are other ways to make the new component known to the app, but that’s the right way)
-
use new component in app.component.html - NOT in the index.html because of best practice
-
open a new terminal window beside the one running ng serve
-
the following will create a new component named "servers"
ng generate component servers
-
will create a new folder in src/app and add an entry in app.module.ts, registering the new component
-
pro-tip: There’s a shortcut for this:
ng g c servers
-
for better structure, components should be encapsulated in a folder structure which can be defined by applying a path:
ng g c management/technical/servers
-
= Communication between TypeScript-Code (which is business logic) and the HTML-Template
-
Output Data from TypeScript to HTML-Template:
-
String Interpolation:
{{data}}
-
Property Binding:
[property]="data"
-
-
React to to events
-
Event Binding:
(event)="expression"
-
for example:
<input type="text" class="form-control" (input)="onUpdateServerName($event)">
-
"$event" is the object automatically created with every event
-
-
-
combination of both: Two-way-Binding:
[(ngModel)]="data"
-
Example: inserting images can be done two ways:
-
1. String Interpolation:
<img src="{{recipe.imagePath}}" alt="{{recipe.name}}" class="img-responsive" style="max-height: 50px;">
-
2. Property Binding:
<img [src]="recipe.imagePath" alt="{{recipe.name}}" class="img-responsive" style="max-height: 50px;">
-
-
= instructions in the DOM
-
"Angular, please add something to the DOM"
-
⇒ components are directives, but directives with a template (there are also directives without a template)
-
directives are inserted via attribute:
<p colorThisText>Receives a green background</p>
@Directive({ selector: 'colorThisText' }) export class ColorTextDirective { ... }
-
important build-in directive:
<p *ngIf="serverCreated">Server was created, server name is {{serverName}}</p>
-
star before "ngIf" indicates ngIf being a structural directive = changes the DOM
-
another example: ngFor loops through an array (example displays list of app-server-components that each print out status of a single server):
<app-server *ngFor="let server of servers"></app-server>
-
another example for *ngIf with its else-part: only show a div if an item has been selected. If it hasn’t been selected, show an infotext instead. This uses the local reference that is mentioned later in this tutorial.
<div class="col-md-3"> <app-detail *ngIf="selectedItem; else infotext" [selectedItem]="selectedItem"></app-detail> </div> <ng-template #infotext> <p>Select an item!</p> </ng-template>
-
attention: no more than one structural directive allowed on the same element
-
attribute-directives change elements they are placed on. Example for calling a method to get the color for a text:
<p [ngStyle]="{color: getColor()}">Server with ID .. </p>
-
example for marking all odd lines have a yellow background and all even ones a transparent background:
<li [ngStyle]="{backgroundColor: odd % 2 !== 0 ? 'yellow' : 'transparent'}" ></li>
-
another attribute-directive to apply CSS-classes:
<p [ngClass]="{ online: serverStatus === 'online', offline: serverStatus === 'offline' }"> Server with ID ...</p>
-
to write own directives, either create new folder "better-highlight" with file "better-highlight.directive.ts" …
-
… or create everything needed for the directive "betterHighlight" with:
ng g d better-highlight
-
in better-highlight.directive.ts:
@Directive({ selector: '[appBetterHighlight]' }) export class BetterHighlightDirective implements OnInit { constructor(private elRef: ElementRef, private renderer: Renderer2) {} ngOnInit() { this.renderer.setStyle(this.elRef.nativeElement, 'background-color', 'green'); } }
-
brackets in selector-name tell Angular that this is an attribute-directive
-
the parameters in the constructor are injected by Angular and even created if not existing
-
constructor parameter elementRef = element the directive has been placed on
-
Renderer2 is a better way of rendering elements - more methods see here
-
keyword private in constructor triggers creation of property
-
directive doesn’t have a view - hence only lifecycle hook onInit and onDestroy available
-
new directives have to be added to app.module.ts in declarations
-
usage in HTML:
<p appBetterHighlight>My green text</p>
@Directive({
selector: '[appBetterHighlight]'
})
export class BetterHighlightDirective implements OnInit {
constructor(private elRef: ElementRef, private renderer: Renderer2) {}
ngOnInit() {
}
@HostListener('mouseenter') mouseOver(eventData: Event) {
this.renderer.setStyle(this.elRef.nativeElement, 'background-color', 'green');
}
@HostListener('mouseleave') mouseLeave(eventData: Event) {
this.renderer.setStyle(this.elRef.nativeElement, 'background-color', 'transparent');
}
}
-
decorator HostListener is provided with the name of an event (in this case mouseenter) on which the specified method shall be executed
@Directive({
selector: '[appBetterHighlight]'
})
export class BetterHighlightDirective implements OnInit {
constructor(private elRef: ElementRef, private renderer: Renderer2) {}
ngOnInit() {
}
@HostBinding('style.backgroundColor') backgroundColor: string = 'transparent';
@HostListener('mouseenter') mouseOver(eventData: Event) {
this.backgroundColor = 'green';
}
@HostListener('mouseleave') mouseOver(eventData: Event) {
this.backgroundColor = 'transparent';
}
}
-
decorator HostBinding gets the property of the hosting element to which the created property should be bound
@Directive({
selector: '[appBetterHighlight]'
})
export class BetterHighlightDirective implements OnInit {
@Input() defaultColor: string = 'transparent';
@Input() highlightColor: string = 'blue';
@HostBinding('style.backgroundColor') backgroundColor: string;
constructor(private elRef: ElementRef, private renderer: Renderer2) {}
ngOnInit() {
this.backgroundColor = this.defaultColor;
}
@HostListener('mouseenter') mouseOver(eventData: Event) {
this.backgroundColor = this.highlightColor;
}
@HostListener('mouseleave') mouseOver(eventData: Event) {
this.backgroundColor = this.defaultColor;
}
}
-
used in HTML:
<p appBetterHighlight [defaultColor]="'transparent'" [highlightColor]="'green'">My colored text</p>
-
when strings are passed as parameters, shortcut: squared brackets and single quotation marks can be ommited
<p appBetterHighlight [defaultColor]="'transparent'" highlightColor="green">My colored text</p>
ng g d unless
-
= opposite of ng-if directive
@Directive({
selector: '[appUnless]'
})
export class UnlessDirective {
@Input() set appUnless(condition: boolean) {
if(!condition) {
this.vcRef.createEmbeddedView(this.templateRef);
} else {
this.vcRef.clear();
}
}
constructor(private templateRef: TemplateRef<any>, private vcRef: ViewContainerRef) {}
}
-
in HTML:
<div *appUnless="onlyOddNmbersOrSomeOtherBooleanProperty"> ... stuff ... </div>
-
for example recipe.model.ts
-
simple TypeScript file that contains the model of the data to display
-
no annotation like @Model because plain TypeScript class sufficient
-
best practice: if shared between multiple components, models should be put in a "shared"-folder directly below "app"
-
main question: How can data be passed between components?
-
Property- and Event Binding can be applied on:
-
HTML elements
-
directives
-
components
-
self-specified, custom properties in self-written components
-
-
properties per default only part of their own component and not accessible from outside
-
has to be explicitly exposed to the outside-world via a decorator @Input:
export class MyChildComponent { @Input() element: {type: string, name: string, content: string}; }
-
decorator has to be executed like a function, hence the parenthesis
-
"Input" because an event gets passed into the component
-
this makes the property accessible to parent-components of this component (!)
-
parent-component can now bind to this property (in template of parent component) (element in squared brackets is the property that gets bound):
<div> <app-child-item *ngFor="let element of elements" [element]="element"> </app-child-item> </div>
-
name of property viewed by the outside can be changed by using an alias. The following makes the property visible as "myUltracoolProperty":
export class MyWrapper { @Input('myUltracoolProperty') element: {type: string, name: string, content: string}; }
-
= the other direction in regard to previous section
-
used to inform parent-component about changes occurring in child-component
-
in html of parent-component:
<my-child-component (myEvent)="onEventThrown($event)"></my-child-component>
-
= In defining the child-component within the parent-component, the event myEvent is defined as something that can be expected to occur. If thrown, method onEventThrown with the parameter $event will be executed - see TypeScript file of parent-component:
export class MyParentComponent { ... onEventThrown(eventData: {x: string, y: string}) { ... } ... }
-
in child-TypeScript:
export class MyChildComponent { @Output() myEvent = new EventEmitter<{x: string, y: string}>(); ... someFunctionThatGetsCalledSometime() { this.eventThrown.emit({'my x-value', 'my y-value'}); } }
-
important: name of the event (in this case "myEvent") has to be the same in definition in child component TypeScript file as well as the parent component HTML template
-
parenthesis at end of definition of eventThrown instantiate EventEmitter
-
"Output" because event gets passed out of the component
-
like with @Input, also alias possible:
export class MyChildComponent { @Output('mySpecialEventThrown') eventThrown = new EventEmitter<{x: string, y: string}>(); ... someFunctionThatGetsCalledSometime() { this.eventThrown.emit({'my x-value', 'my y-value'}); } }
-
EventEmitter can also pass a void value by setting "void":
@Output() myEvent = new EventEmitter<void>();
-
important: EventEmitters are Subjects (see below) and should only be used for @Output, see stackoverflow and this post.
-
css-files defined per component, for example "app.component.css" for the app-component
-
these CSS-files only applied to HTML generated by this component despite having global definitions in CSS-files:
p { color: blue; }
-
… should be applied to all p-tags in the application, but is only applied to p-tags in component
-
= different behavior than standard CSS! Only Angular-behavior!
-
when inspecting code in browser, generated attributes visible:
<p _ngcontent-ejo-1>....</p>
-
for each component, one of those attributes will be generated with unique names
-
in TypeScript-file:
@Component({ ... encapsulation: ViewEncapsulation.None })
-
… will lead to all styles defined in this component to be applied globally
-
ViewEncapsulation.Native causes the Shadow-DOM function that isn’t supported by all browsers
-
ViewEncapsulation.Emulated = default = recommended
-
(only!) in HTML-templates, local references can be defined and used (only) within this template (not in the TypeScript-file):
<input type="text" #myInput> <button (click)="doStuff(myInput)">Click here</button>
-
in template:
<input type="text" #myInput>
-
in TypeScript:
export class ... { @ViewChild('myInput') myInput : ElementRef; }
-
argument of @ViewChild = name of local reference
-
ElementRef = type of all @ViewChild-annotated properties
-
getting underlying HTML-element:
myInput.nativeElement
-
ElementRef should only be used for accessing DOM-elements, not changing them!
-
also available: @ContentChild = access to content from another component
-
every lifecycle-step = hook that can be used to do things
-
Lifecycle of every component:
-
ngOnChanges - whenever bound input property changes
-
ngOnInit - initialization
-
ngDoCheck - every change detection run (often!)
-
ngAfterContentInit - content projected into view
-
ngAfterContentChecked - content checked
-
ngAfterViewInit - view has been initialized
-
ngAfterViewChecked - view checked
-
ngOnDestroy - called before destroying an object
-
-
ngOnChanges = only hook that recives an argument with some information:
ngOnChanges(changes: SimpleChanges) { ... }
-
Service
-
can be used throughout the application to avoid duplication of code
-
hold data
-
used to communicate between components
-
-
should be located near the other classes implementing the business feature of this service
-
service is just a normal TypeScript-class! No @Service-decorator!
export class LoggingService { logSomethingToConsole(message: string) { console.log('This got logged: ' + message); } }
-
instances of services should be created by Angular via dependency injection, not manually. Therefore, two things necessary:
-
provider with type of service
-
dependency injection in constructor
-
@Component({
selector: 'my-cool-component',
templateUrl: './my-cool.component.html',
styleUrls: ['./my-cool.component.css'],
providers: [LoggingService]
})
export class MyCoolComponent {
constructor(private loggingService: LoggingService) {}
...
}
export class MyDataService {
myData = [
{
id: 1,
name: 'data 1'
},
{
id: 2,
name: 'data 2'
},
{
id: 3,
name: 'data 3'
}
];
addData(id: number, name: string) {
this.myData.push({id: id, name: name});
}
}
-
every component using this data must hold a copy of it:
@Component({ selector: 'my-cool-component', templateUrl: './my-cool.component.html', styleUrls: ['./my-cool.component.css'], providers: [MyDataService] }) export class MyCoolComponent implements OnInit { data: {id: number, name: string}[] = []; constructor(private myDataService: MyDataService) {} ngOnInit() { this.data = this.myDataService.myData; } ... }
-
initialization of data array should not be done in constructor, but in onInit!
-
services injected in one component can be used in all its child-components
-
hence: if service provided in AppModule, this instance is available in all other components throughout the application
-
if a service is provided in two components of the same tree, different instances of this service will be created!
-
to have the same instance in two components, parent component needs entry in providers and injection in constructor; child component only needs injection in constructor
-
@Injectable() means, that there can be other services injected into the annotated service:
@Injectable() export class MyDataService { myData = [ { id: 1, name: 'data 1' }, { id: 2, name: 'data 2' }, { id: 3, name: 'data 3' } ]; constructor(private logginService: LoggingService) {} addData(id: number, name: string) { this.myData.push({id: id, name: name}); this.loggingService.logSomethingToConsole('new data added!'); } }
-
@Injectable() should only be added if services are injected
-
since Angular 6: ProvidedIn to automatically register services and guards:
@Injectable({ providedIn: 'root' })
-
no need to add manually as a provider in app.module.ts
-
recommendation of Angular team: use with new features, but no need to replace all old code
-
not possible with Interceptors because of multi-binding-syntax
-
allows to change URL, so it seems to be a multi-site-application, however it’s still a single-page-application
-
example: localhost:4200/users loading UsersComponent
-
routes registered in app.module.ts:
const appRoutes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'users', component: UsersComponent },
{ path: 'data', component: DataComponent }
];
@NgModule({
...
imports: [
...
RouterModule.forRoot(appRoutes)
...
],
...
in app.component.html: definition of where the router should load the currently selected route:
<router-outlet></router-outlet>
-
wrong way: using a href tag like this:
<a href="/users">Users</a>
-
this will reload the app every time the link is clicked, which will reset the state of the whole app
-
instead use routerLink directive:
<a routerLink="/users">Users</a>
-
difference between an absolute path like "/users" and a relative path like "users": relative path gets appended to the current URL, so when already on localhost:4200/users and clicking the relative path: localhost:4200/users/users
-
also possible to navigate to other paths with
<a routerLink="../../users">Users</a>
-
router links with routerLink-directive != normal links, hence no automatic CSS styling. Solution: routerLinkActive-directory will attach specified class active when route is active :
<li routerLinkActive="active"><a routerLink="/users">Users</a> <li routerLinkActive="active"><a routerLink="/data">Data</a>
-
Problem with this: if route "/" is configured this way, it will always be styled with active because "/" is included in "/users" and "/data". Solution:
<li routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}"><a routerLink="/">Home</a> <li routerLinkActive="active"><a routerLink="/users">Users</a> <li routerLinkActive="active"><a routerLink="/data">Data</a>
<button (click)="onLoadServers()">Load Route</button>
constructor(private router: Router) {}
onLoadServers() {
this.router.navigate(['/servers']);
}
-
With routerLink, relative paths such as "users" would result in visiting for example localhost:4200/users/users. With navigate() this is not the case:
constructor(private router: Router) {}
onLoadServers() {
this.router.navigate(['servers']);
}
-
Reason: by default, navigate() targets the root domain, hence it makes no difference if /servers or servers is configured. Changeable with
constructor(private router: Router,
private route ActivatedRoute) {}
onLoadServers() {
this.router.navigate(['servers'], {relativeTo: this.route});
}
-
example:
localhost:4200/users/10/Anna
-
to load users with specific ID via URL, in app.module.ts:
const appRoutes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'users', component: UsersComponent },
{ path: 'users/:id:name', component: UsersComponent },
{ path: 'data', component: DataComponent }
];
@NgModule({
...
imports: [
...
RouterModule.forRoot(appRoutes)
...
],
...
-
in component:
constructor(private route: ActivatedRoute) {}
ngOnInit() {
this.user = {
id: this.route.snapshot.params['id'],
name: this.route.snapshot.params['name']
};
}
-
'id' and 'name' in ngOnInit() is parsed from the URL, see above in app.modules.ts: path: 'users/:id:name'
-
Attention: order of routes important: In this example here, calls to /new will cause the first route to load with an error, because "new" will be interpreted as the id. Solution: define path with variables last:
{path: ':id/edit ', component: RecipeEditComponent}, {path: 'new', component: RecipeEditComponent}
<a [routerLink]="['/users', 10, 'Anna']">Link to Anna</a>
this will change the URL, but Angular won’t reload the data - has to be triggered:
constructor(private route: ActivatedRoute) {}
ngOnInit() {
this.user = {
id: this.route.snapshot.params['id'],
name: this.route.snapshot.params['name']
};
this.route.params.subscribe(
(params: Params) => {
this.user.id = params['id'];
this.user.name = params['name'];
}
);
}
-
Example:
localhost:4200/users/10/Anna/edit?role=admin&mode=test#loading
-
question mark = separation to URL
-
ampersands = separation between multiple parameters
-
hash-sign = jump to specific position in page
const appRoutes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'users/:id:name/edit', component: EditUsersComponent },
];
@NgModule({
...
imports: [
...
RouterModule.forRoot(appRoutes)
...
],
...
<a [routerLink]="['/users', 10, 'Anna', 'edit']" [queryParams]="{role: 'admin', mode: 'test'}" [fragment]="'loading'" >Link to Anna</a>
-
calling this programmatically:
constructor(private router: Router) {}
onLoadUser(id: number, name: string) {
this.router.navigate(
['/users', id, name, 'edit'],
{queryParams: {role: 'admin', mode: 'test'},
fragment: 'loading'}
);
}
-
retrieving data:
constructor(private route: ActivatedRoute) {}
ngOnInit() {
// as before, this will not react to changes:
console.log(this.route.snapshot.queryParams);
console.log(this.route.snapshot.fragment);
// ... this will:
this.route.queryParams.subscribe(...);
this.route.fragment.subscribe(...);
}
-
pitfall: If variables in component are of type number and should be read from the always-string-valued URL, cast necessary via "+":
const id = +this.route.snapshot.params['id'];
-
when visiting route, whole page is loaded
-
use-case: only load part of page
-
also useful for getting rid of duplication - see this code where many entries begin with "users":
const appRoutes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'users', component: UsersComponent },
{ path: 'users/:id', component: UsersComponent },
{ path: 'users/:name', component: UsersComponent },
];
-
solution:
const appRoutes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'users', component: UsersComponent, children: [
{ path: ':id', component: UsersComponent },
{ path: ':name', component: UsersComponent }
]
},
];
-
Child-Routes need a router-outlet in the users-component
-
one existing outlet in app.component.html only for root-routes, in this case users
-
new outlet in users-component will automatically used for all child-routes of users
-
problem: when calling router.navigate, all parameters are removed from URL
-
solution:
this.router.navigate(
['/users', id, name, 'edit'],
{relativeTo: this.route, queryParamsHandling: 'merge'}
);
-
queryParamsHandling:
-
merge = merge new and old parameters
-
preserve = overwrite new ones with old ones
-
-
if user visits non-existing page (by manually typing URL), error-page should be displayed
const appRoutes: Routes = [
...
{ path: 'not-found', component: NotFoundComponent },
{ path: '**', redirectTo: '/not-found' }
];
-
important: redirect has to be the last entry in routes-array!
-
another configuration:
const appRoutes: Routes = [ {path: '', redirectTo: '/recipes', pathMatch: 'full'}, {path: 'recipes', component: RecipesComponent}, {path: 'shopping-list', component: ShoppingListComponent} ];
-
first path with empty URL needs pathMatch because empty URL is part of every URL, hence this redirect would always apply. pathMath: 'full' forces the full path to be the empty URL to match this redirect, hence only empty URL will be redirected.
-
auth-guard.service.ts = normal service, but responsible for guarding
-
method canActivate either returns an Observable, a Promise or a boolean
-
AuthService = service that asks server for permissions
-
AuthService.isAuthenticated() returns a promise
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private authService: AuthSerice, private router: Router) {}
canActivate(route: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
return this.authService.isAuthenticated()
then(
(authenticated: boolean) => {
if(authenticated) {
return true;
} else {
this.router.navigate(['/']);
}
}
);
}
}
-
to use this guard, in app-routing.module.ts:
const appRoutes: Routes = [ { path: '', component: HomeComponent }, { path: 'users', canActivate: [AuthGuard], component: UsersComponent, children: [ { path: ':id', component: UsersComponent }, { path: ':name', component: UsersComponent } ] }, ];
-
also, AuthGuard will have to be added as a provider in app.module.ts
-
users and all child-routes will be guarded
-
to guard child-modules:
-
implement interface CanActivateChild
-
use canActivateChild in const appRoutes in app-routing.module.ts
-
-
other guard: canDeactivate to react on leaving a route (for example to enforce saving)
-
canDeactivate is typed with the component that should be left, for example to check for unsaved content (other guards not typed because component doesn’t exist yet)
-
-
if route is guarded by multiple guards: if just one guard has veto, access not granted
const appRoutes: Routes = [
...
{ path: 'not-found', component: NotFoundComponent, data: {message: 'Page not found'} },
{ path: '**', redirectTo: '/not-found' }
];
can be used in NotFoundComponent:
export class NotFoundComponent implements OnInit {
errorMessage: string;
constructor(private route: ActivatedRoute) {}
ngOnInit() {
this.errorMessage = this.route.snapshot.data['message'];
// if data in route changes, observe these changes:
this.route.data.subscribe(
(data: Data) => {
this.errorMessage = data['message'];
}
);
}
}
-
Resolver loads data before displaying the route. In contrast: loading a route and displaying it and after that load data in onInit() also works.
interface User {
id: number;
name: string
}
@Injectable()
export class UserResolver implements Resolve<User> {
constructor(private userService: UserService) {}
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<User> | Promise<User> | User {
return this.userService.getUser(+route.params['id']);
}
}
-
in app-routing.modules.ts:
const appRoutes: Routes = [ ... const appRoutes: Routes = [ { path: '', component: HomeComponent }, { path: 'users', canActivate: [AuthGuard], component: UsersComponent, children: [ { path: ':id', component: UsersComponent, resolve: {user: UserResolver} }, { path: ':name', component: UsersComponent } ] }, ]; ];
-
in user.component.ts:
... ngOnInit() { this.route.data .subscribe( (data: Data) => { this.user = data['user']; } ); } ...
-
in real deployment: paths like "myApp:4200/servers" may not be resolved because server may look for a server.html file (which doesn’t exist)
-
solution: route all requests to index.html (because that’s where Angular is)
-
best solution: configure server
-
alternative solution: in app-routing.module.ts:
@NgModule({
imports: [
RouterModule.forRoot(appRoutes, {useHash: true});
],
exports: [RouterModule]
})
export class AppRoutingModule {
}
-
leads to URLs including hash-tag: localhost:4200/#/users
-
= "hash-mode routing"
-
hash-tag separates part that’s interesting to server (before tag) and that’s interesting for Angular (after tag)
-
attention: slightly different syntax with Angular 6 which uses RXJS 6
-
observables over three callbacks:
this.route.params
.subscribe(
(params: Params) => {
// next- callback
},
() => {
// error- callback
},
() => {
// complete- callback
}
);
-
note: error- and complete-callback don’t make much sense in this case of router-parameters
-
many ways of creating observable - only most common ways shown here. Complete documentation see RxJS docs
// Emit a new number counting from 0 upwards every second const myNumbers = Observable.interval(1000); myNumbers.subscribe( (number: number) => { console.log(number); } );
// Building an observable from scratch
const myObervable = Observable.create((observer: Observer<string>) => {
setTimeout(() => {
// emit a normal data package that can be catched by the observer with the first parameter
observer.next('first package');
},2000);
setTimeout(() => {
observer.next('second package');
},4000);
setTimeout(() => {
observer.error('this does not work');
},5000);
});
myObservable.subscribe(
(data: string) => {
console.log(data);
},
(error: string) => {
console.log(error);
},
() => {
console.log('completed!');
}
);
-
subscriptions to observables still existing, even when component holding observable gets destroyed (by page-change)
-
hence: always unsubscribe!
-
first example with whole class and unsubscription:
export class HomeComponent implements OnInit, OnDestroy {
numbersObservablesSubscription: Subscription;
constructor() { }
ngOnInit() {
// Emit a new number counting from 0 upwards every second
const myNumbers = Observable.interval(1000);
this.numbersObservableSubscription = myNumbers.subscribe(
(number: number) => {
console.log(number);
}
);
}
ngOnDestroy() {
this.numbersObservablesSubscription.unsubscribe();
}
}
-
Angular’s observables clean up automatically - but best practice to unsubscribe nevertheless
-
subject = observable and observer at the same time!
export class UserService { userActivated = new Subject(); someMethod() { this.userActivated.subscribe( (id: number) => { // some business-logic with id } ); } }
// ... in the class that uses the UserService ...
onActivate() {
// acting as an observer but also pushing own user-id back
this.usersService.userActivated.next(this.id);
}
-
important: EventEmitters are Subjects and should only be used for @Output, see stackoverflow and this post.
-
a lot of operators available, see RxJS docs
-
one example:
const myNumbers = Observable.interval(1000)
.pipe(map(
(data: number) => {
return data * 2;
}
));
adding this to package.json …
"rxjs": "^6.0.0-rc.0",
will cause this error:
error TS2305: Module .... has no exported member 'Subject'.
Solution: in every (!) class, write
import { Subject } from 'rxjs';
instead of
import { Subject } from 'rxjs/Subject';
Also important for every other class:
import { Subject, Observable, Observer, Subscription } from 'rxjs';
-
two approaches:
-
template-driven (write form in HTML, Angular infers form object from it that ultimately is used in Java Script)
-
reactive (write form in Type Script and HTML, Angular doesn’t infer or create anything)
-
-
import FormsModule in app.module.ts
-
submit-functionality should not be in HTML in button with type="submit" because click here causes build-in functionality that collides with how Angular works - instead:
<form (ngSubmit)="onSubmit(f)" #f="ngForm">
-
local reference f is used as a parameter for onSubmit() and provides access to this form - however, strange syntax necessary
-
ngModel directive added in HTML = making Angular aware that HTML element should be a control:
<input type="text" id="username" class="form-control" ngModel name="username" >
-
name can be added to every HTML control (not Angular-specific) and serves as connector between template and TypeScript
-
in TypeScript:
onSubmit(form: NgForm) {
console.log(form.value.username);
}
-
object of type NgForm provides access to the form, including all controls and the data from the form
-
valid-field in NgForm dependent on validation
-
validation causes CSS classes to be added to components in form, for example ng-dirty, ng-valid - that can be added to the CSS file of the component
-
however, still possible to enter every input string - validation has to be handled programmatically!
-
ngModel added to tell Angular that input is a control (however, value of input field not bound!)
-
invalid if empty:
<input
type="text"
id="username"
class="form-control"
ngModel
name="username"
required>
-
invalid if empty and validation of email:
<input type="email" id="email" class="form-control" ngModel name="email" required email >
-
HTML 5 Validation enable by adding ngNativeValidate to a control
-
example: disabling submit-button:
<button class="btn btn-primary" type="submit" [disabled]="!f.valid">Submit</button>
-
example: showing help text:
<input type="email" id="email" class="form-control" ngModel name="email" required email #email="ngModel"> <span class="help-block" *ngIf="!email.valid && email.touched">Please enter valid email</span>
-
using regular expressions to only make positive numbers valid:
<label for="amount">Amount</label> <input type="number" id="amount" class="form-control" name="amount" ngModel required pattern="^[1-9]+[0-9]*$" >
<select
id="secret"
class="form-control"
[ngModel]="'default-value'"
name="secret">
-
This can also be bound (one-way!) to a property: [ngModel]="myProperty"
-
non-binding = simply adding ngModel in HTML = declaring input as control
-
one-way-binding see above
-
two-way-binding (property in Type Script file omitted):
<textarea name="questionAnswer" rows="3" [(ngModel)]></textarea> <p>Your reply: {{ answer }}</p>
export class AppComponent {
@ViewChild('f') myForm: NgForm;
patchValueIntoMyForm() {
this.myForm.form.patchValue({
username: suggestedName
});
}
}
-
also available: setValue which will set values in every element of the form
-
in app.module.ts, import ReactiveFormsModule
-
simple form:
export class AppComponent implements OnInit { genders = ['male', 'female']; signupForm: FormGroup; ngOnInit() { this.signupForm = new FormGroup({ 'username': new FormControl('Default User Name'), 'email': new FormControl(null), 'gender': new FormControl('male') }); } onSubmit() { console.log(this.signupForm); } }
<form [formGroup]="signupForm" (ngSubmit)="onSubmit()">
<input
type="text"
id="username"
formControlName="username"
class="form-control">
<input
type="text"
id="email"
formControlName="email"
class="form-control">
<input
type="radio"
formControlName="gender"
value="male"
</form>
export class AppComponent implements OnInit {
genders = ['male', 'female'];
signupForm: FormGroup;
ngOnInit() {
this.signupForm = new FormGroup({
'username': new FormControl('Default User Name', Validators.required),
'email': new FormControl(null, [Validators.required, Validators.email]),
'gender': new FormControl('male', Validators.required)
});
}
onSubmit() {
console.log(this.signupForm);
}
}
<form [formGroup]="signupForm" (ngSubmit)="onSubmit()">
<input
type="text"
id="username"
formControlName="username"
class="form-control">
<input
type="text"
id="email"
formControlName="email"
class="form-control">
<span class="help-block" *ngIf="!signupForm.get('email').valid && signupForm.get('email').touched">Please enter valid email</span>
<input
type="radio"
formControlName="gender"
value="male"
</form>
export class AppComponent implements OnInit {
genders = ['male', 'female'];
signupForm: FormGroup;
ngOnInit() {
this.signupForm = new FormGroup({
'userData': new FormGroup({
'username': new FormControl('Default User Name', Validators.required),
'email': new FormControl(null, [Validators.required, Validators.email])
}),
'gender': new FormControl('male', Validators.required)
});
}
onSubmit() {
console.log(this.signupForm);
}
}
<form [formGroup]="signupForm" (ngSubmit)="onSubmit()">
<div formGroupName="userData">
<input
type="text"
id="username"
formControlName="username"
class="form-control">
<input
type="text"
id="email"
formControlName="email"
class="form-control">
<span class="help-block" *ngIf="!signupForm.get('userData.email').valid && signupForm.get('userData.email').touched">Please enter valid email</span>
</div>
<input
type="radio"
formControlName="gender"
value="male"
</form>
export class AppComponent implements OnInit {
genders = ['male', 'female'];
signupForm: FormGroup;
ngOnInit() {
this.signupForm = new FormGroup({
'userData': new FormGroup({
'username': new FormControl('Default User Name', Validators.required),
'email': new FormControl(null, [Validators.required, Validators.email])
}),
'gender': new FormControl('male', Validators.required),
'hobbies': new FormArray([])
});
}
onSubmit() {
console.log(this.signupForm);
}
onAddHobby() {
// Cast to array necessary
(<FormArray>this.signupForm.get('hobbies')).push(new FormControl(null));
}
}
<div formArrayName="hobbies">
<div
class="form-group"
*ngFor="let hobbyControl of signupForm.get('hobbies').controls; let i = index>
<input type="text" class="form-control" [formControlName]="i">
</div>
</div>
-
Validator = function that gets called automatically
export class AppComponent implements OnInit {
signupForm: FormGroup;
ngOnInit() {
this.signupForm = new FormGroup({
'username': new FormControl('Default User Name', [Validators.required, this.forbiddenNames.bind(this)])
});
}
}
-
this.forbiddenNames.bind(this) necessary to make this work in the function here:
export class AppComponent {
forbiddenUsernames = ['X', 'Y'];
forbiddenNames(control: FormControl): {[s: string]: boolean} {
if(this.forbiddenUsernames.indexOf(control.value) !== -1) {
return {'nameIsForbidden': true};
}
// if validation successfull, null or nothing should be returned
return null;
}
}
-
Angular adds failed validations as error codes in the result object, which then can be used for special error messages for example
-
for example when calling server for validation
-
asynchronous validators passed as 3rd parameter in form creation:
export class AppComponent implements OnInit {
signupForm: FormGroup;
ngOnInit() {
this.signupForm = new FormGroup({
'email': new FormControl(null, [Validators.required, Validators.email], this.forbiddenEmails)
});
}
}
forbiddenEmails(control: FormControl): Promise<any> | Observable<any> {
const promise = new Promise<any>((resolve, reject) => {
setTimeout(() => {
if(control.value === 'my@mailadress.com') {
resolve({emailIsForbidden': true});
else {
resolve(null);
}
},1500);
});
return promise;
}
// fires whenever a value of a form changes, for example when user inputs data
this.signupForn.valueChanges.subscribe(
(value) => console.log(value);
);
// Status of the form, like invalid, valid or pending
this.signupForn.statusChanges.subscribe(
(status) => console.log(status);
);
-
Returning an array from a method this way will return a reference to this array which could be used to alter the array:
return this.data;
-
making it safer with returning a slice (=copy) of the array:
return this.data.slice();
-
however, changes on the array will not migrate to every component that uses the original data. Solution: informing components of new data with event-emitters
-
more complex route configuration shouldn’t be in app.modules.ts, but exported to another class like AppRoutingModule in app-routing.module.ts:
const appRoutes: Routes = [
...
];
@NgModule({
imports: [
RouterModule.forRoot(appRoutes);
],
exports: [RouterModule]
})
export class AppRoutingModule {
}
in app.module.ts:
...
imports: [
...
AppRoutingModule
],
...
-
transforms output in template without changing actual data
-
example: make certain string uppercase without changing saved data:
{{ myString | uppercase }}
-
format dates:
{{ server.started | date }}
-
parametrize pipes (multiple parameters via multiple colons):
{{ server.started | date:'fullDate' }} ... <li *ngFor="let i of collection | slice:1:3">{{i}}</li>
-
chaining of pipes:
{{ server.started | date:'fullDate' | uppercase}}
-
console:
ng g p shorten
-
shorten.pipe.ts:
@Pipe({ name: 'shorten' }) export class ShortenPipe implements PipeTransform { transform(value: any, limit: number) { return value.substr(0, limit); } }
-
for pipes without parameters just omit the second parameter
-
in app.module.ts:
... declarations: [ ... ShortenPipe ], ...
-
use:
{{ mystring | shorten:10 }}
-
Pipes are not re-run automatically if data changes! Enforcing re-running pipe when underlying / piped data changes by adding pure: false to declaration. However, may lead to performing issues:
@Pipe({
name: 'shorten',
pure: false
})
export class ShortenPipe implements PipeTransform {
...
-
new in Angular 6: HttpClient, see below. However, using Http as shown here also valid.
-
add HttpModule in app.module.ts (at imports)!
@Injectable() export class ServerService { constructor(private http: Http) {} storeServers(servers: any[]) { return this.http.post('https://my-url', servers); } }
-
post-method will only create an observable and not immediately send the post-request. Hence: subscribe to it so request is send.
-
in some component on button-click:
... onSave() { this.serverService.storeServers(this.servers) .subscribe( (response) => console.log(response), (error) => console.log(error) ); } ...
-
unsubscribing from subscription not necessary in this case because after request is done, Angular will do that automatically
@Injectable()
export class ServerService {
constructor(private http: Http) {}
storeServers(servers: any[]) {
return this.http.post('https://my-url', servers);
}
getServers() {
return this.http.get('https://my-url');
}
}
...
onGet() {
this.serverService.getServers()
.subscribe(
(response: Response) => {
const data = response.json();
console.log(data);
},
(error) => console.log(error)
);
}
...
-
transformation of response into objects should be done in ServerService because otherwise it would have to be copied in every component that causes the server call
-
map() will wrap data automatically in observable:
@Injectable() export class ServerService { constructor(private http: Http) {} storeServers(servers: any[]) { return this.http.put('https://my-url', servers); } getServers() { return this.http.get('https://my-url') .pipe(.map( (response: Response) => { const data = response.json(); return data; } )); } }
...
onGet() {
this.serverService.getServers()
.subscribe(
(servers: any[]) => {
console.log(servers);
},
(error) => console.log(error)
);
}
...
@Injectable()
export class ServerService {
constructor(private http: Http) {}
storeServers(servers: any[]) {
return this.http.put('https://my-url', servers);
}
getServers() {
return this.http.get('https://my-url')
.pipe(.map(
(response: Response) => {
const data = response.json();
return data;
}
))
.pipe(catchError(
(error: Response) => {
console.log(error);
// catch-operator will NOT create an observable
// automatically like the map-operator does, so
// it has to be create manually:
return Observable.throw(error);
}
));
}
}
-
new in Angular 6: HttpClient. Using Http as shown above also valid; however HttpClient brings new functionality
-
to use HttpClient, add HttpClientModule in app.module.ts (at imports) from @angular/common/http
-
same example as above, but with HttpClient:
@Injectable()
export class ServerService {
constructor(private httpClient: HttpClient) {}
storeServers(servers: any[]) {
// for put-methods, httpClient syntax equals http syntax:
return this.httpClient.put('https://my-url', servers);
}
getServers() {
// for get-methods, explicit typing of response possible because get() unwraps the body data:
return this.httpClient.get<Server[]>('https://my-url')
.pipe(map(
(servers) => {
return data;
}
));
}
}
}
-
options for put (as 3rd parameter) and get (as 2nd parameter): for example (as shown below) getting the whole response as text, instead of as JSON:
this.httpClient.get('https://my-url', { observe: 'response', responseType: 'text' }) .pipe(map( ... ));
-
another example for further options: requesting events:
@Injectable()
export class ServerService {
constructor(private httpClient: HttpClient) {}
storeServers(servers: any[]) {
return this.httpClient.put('https://my-url', servers, {
observe: 'events'
});
}
}
... in component:
...
onGet() {
this.serverService.getServers()
.subscribe(
(response: HttpEvent<Object>) => {
// with "observe: 'events', response will have additional
// information regarding the event type. These can be used
// to filter for certain events:
console.log(response.type === HttpEventType.Sent); // "true" for "sent"-Events, false for rest
}
);
}
...
-
OK to set it like this:
storeServers(servers: any[]) { return this.httpClient.put('https://my-url?x=' + x , servers); }
-
better way:
storeServers(servers: any[]) { return this.httpClient.put('https://my-url', servers, { params: new HttpParams().set('x', x) }); }
storeStuff() {
// creating a new request with "new HttpRequest()" basically the
// same as using "httpClient.put()", which creates pre-configrued
// request objects.
const req = new HttpRequest('PUT', 'https://my-url', this.myData, {reportProgress: true});
return this.httpClient.request(req);
}
-
will result in receiving several objects of type: 1 (upload progress) and type: 3 (download progress), which give information about the progress (loaded: 500, total: 500)
-
use-cases:
-
including headers, e.g. for authentification
-
central error handling
-
caching
-
sending multiple requests with same attribute, for example authorization token - automatic setting of this token would be nice
-
-
solution for last use case: sending requests without token and manipulate every outgoing request in another place
-
new file: auth.interceptor.ts in shared:
export class AuthInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// simply forward the current request to be handled without any changes:
return next.handle(req);
}
}
-
naming: "next" because interceptors can be chained, hence "next" as the next element in the chain
-
interceptor only used when it is provided, for example in app.module.ts. However, special syntax!
import { HTTP_INTERCEPTORS } from '@angular/common/http'; import { AuthInterceptor } from '../shared/auth.interceptor'; ... providers: [ ... {provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true}, ... ...
-
to register multiple interceptors, duplicate line {provide: …
-
modifying requests, for example send an auth token with every request:
@Injectable() export class AuthInterceptor implements HttpInterceptor { constructor(private authService: AuthService) {} intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { // requests are immutable, hence changes have to be made via a clone()-method // that provides possibility for changing the object: const copiedReq = req.clone({params: req.params.set('auth', this.authService.getToken())}); return next.handle(copiedReq); } }
-
also possible to intercept incoming responses (don’t forget to register this interceptor, see above):
export class LoggingInterceptor implements HttpInterceptor { intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { // handle-method returns an Observable which can be used to track every request. // However, "subscribe()" would consume the request so it would not get passed on. // Solution: "do"-method (which got renamed in Angular 6 from "do" to "tap"): return next.handle(req).pipe(tap( event => { console.log('Logging', event); } )); } }
-
if multiple interceptors are registered, the order in which they are registered decides the order of execution
-
with server-side rendering technologies, server stores session and client only gets session cookie
-
in single page applications however:
-
not that many requests to the backend because a lot of logic is handled by Angular
-
no session stored in server, server stateless.
-
but, to not have authentification at every single request: auth-token generated by server and send from server to client which is used for further requests
-
-
token used by Angular: JSON Web Tokens = JWO
-
hence, Angular client has to store token it got from server to use it for every request
-
"securely transmitting information between parties as a JSON object"
-
"trusted because digitally signed"
-
authorization = most common scenario for JWT
-
single sign-on often uses JWT
-
another use-case: secure information exchange
-
dot-separated structure of a JWT: header.payload.signature
-
debugger available at jwt.io
-
token = single authentification factor, hence should not be kept long time
-
token visible in browser dev tools → Application → Storage → Local Storage → http://localhost:4200
-
detailed information in this Github repo
-
until now, only one module: App Module
-
Feature modules: set of components and directives that define a feature should be outsourced in their own module
-
new file: myname.module.ts
-
decorate with @NgModule()
-
add CommonModule as import (contains common directives)
-
BrowserModule contains all features of CommonModule and some additional features that are needed at app startup - hence, BrowserModule should be added to app.module.ts, but not CommonModule
-
add custom declarations, imports, providers and the main module (bootstrap) - syntax see app.module.ts
-
add new module to import in app.module.ts
-
create new routing declaration: myname-routing.module.ts because every module manages its own routing - however, it has to be RouterModule.forChild() because forRoot is only valid for the root-router which is app-routing.module.ts. Don’t forget to add this routing module to the newly created module (in imports).
@NgModule({ imports: [RouterModule.forChild(appRoutes)], exports: [RouterModule] }) export class MynameRoutingModule {
Limitation: Components, Pipes and Directives must not be declared in more than one module
-
Directives that should be used in multiple modules should be in a SharedModule
-
typically, only one SharedModule that contains everything that gets shared
-
in shared-folder, create new shared.module.ts
-
add every directive that should be shared in declarations and exports:
@NgModule({
declarations: [
MyCoolDirective
],
exports: [
MyCoolDirective
]
})
export class SharedModule {}
-
every component has to be declared exactly once in an Angular app
-
component only usable and visible in module where it is declared
-
to make component visible to other modules: add it to exports
-
hence: shared components should be declared in SharedModule and only there
-
shared-module can be imported into other modules and exported component can be used there
-
attention: components from the SharedModule must not be declared in other modules (i.e. added to declarations), but imported (i.e. added to imports) (because components must be declared exactly once)
-
attention: never provide services in shared modules because that’s bad style and results in problems with lazy loading
-
user may not visit all modules
-
however, everything under imports in AppModule will be downloaded when visiting app
-
solution: load only necessary / probable modules, lazy-load the rest when needed
-
lazy-loading defined in routing-file:
const appRoutes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'lazy', loadChildren: './lazy/lazy.module#LazyModule' }
];
@NgModule({
imports: [RouterModule.forRoot(appRoutes)],
exports: [RouterModule]
})
export class AppRoutingModule {
}
-
beware that loadChildren uses a string whereas other definitions in the routing-file take a type, which automatically creates a dependency to these modules - a simple string doesn’t do that
-
string consists of path and class name, separated by #
-
beware: routing of lazy-loaded component has to be changed so that it doesn’t have an own root:
const lazyRoutes: Routes = [ { path: '', component: LazyComponent } ...
-
If a service is referenced in the providers-array of a lazy-loaded module, Angular will create a new instance of this service as soon as the module is loaded because the creation of the other services (referenced in providers-array of the eagerly loaded modules) are finished being created.
-
If the service is only provided in the eagerly loaded modules and not additionally in the lazy-loaded module, all services will use the same instance of this service.
-
= module that can be created to make AppModule leaner by collecting everything that is only used in the AppModule
-
possible contents:
-
header
-
HomeComponent
-
-
compiling = parsing of HTML-template files and compiling to Java Script
-
2 modes:
-
Just-in-Time = develop code, load it into production, download into browser, then compile
-
Ahead-of-Time = immediately compile to Java Script, load into browser
-
-
with AOT:
-
faster startup because parsing and compilation doesn’t happen in browser
-
templates checked during development (errors that are only visible in browser), hence errors immediately visible in terminal (instead of later in browser)
-
smaller file size because compiler (+unneeded features) doesn’t need to be shipped
-
"Tree Shaking" = not-needed libraries removed to make smaller download
-
-
enabling AOT:
ng build --prod
-
= short form of
ng build --prod --aot
-
however, introduced with Angular 6:
ng serve --aot
-
this will enable AOT - however this option not pushed and marketed by Angular team, maybe changed in future
-
with ATO: startup time cut roughly in half (linear behavior; valid for all applications)
import { TestBed, async } from '@angular/core/testing';
import { AppComponent } from '../app.component';
describe('MyApp', () => {
beforeEach(() => {
// Configures the application like a normal Angular App,
// for example declaring which module should be in the
// testing environment.
// However, no imports or providers because it's not a
// real application that gets started here.
TestBed.configureTestingModule({
declarations: [
AppComponent
]
});
});
it('should create the app', async() => {
// Component has to be created in each it-block because
// each is contained in itself.
let fixture = TestBed.createComponent(AppComponent);
let app = fixture.debugElement.componentInstance;
// "Truthy" = "exists"
expect(app).toBeTruthy();
}));
it('should have as title 'app workds!'', async() => {
let fixture = TestBed.createComponent(AppComponent);
let app = fixture.debugElement.componentInstance;
expect(app.title).toEqual('app workds!');
}));
it('should render title in a H1 tag', async() => {
let fixture = TestBed.createComponent(AppComponent);
// necessary to have the template rendered
fixture.detectChanges();
let compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('h1').textContent).toContain('app workds!');
}));
});
-
running tests via
ng test
-
services tested in tests for components that use these services:
it('should use the user name from the service'), () => {
let fixture = TestBed.createComponent(UserComponent);
let app = fixture.debugElement.componentInstance;
let userService = fixture.debugElement.injector.get(UserService);
// important to have injected service updated
fixture.detectChanges();
expect(userService.user.name).toEqual(app.user.name);
});
-
Best Practice: Angular unit tests should not reach out to a server, instead there should be mocked data to be used in tests
-
example:
export class DataService {
getDetails() {
const resultPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Data');
}, 1000);
});
}
}
-
this DataService is used in a component and tested there:
// This is not a "real" test that should be written in a real application because // it only tests the Angular setup. It's kind of a pairing test for the second test, // see below. it('shouldn\'t fetch data successfully if not called asynchronously', () => { let fixture = TestBed.createComponent(UserComponent); let app = fixture.debugElement.componentInstance; // DataService instance is getted and mocked to not actually call getDetails(): let dataService = fixture.debugElement.injector.get(DataService); let spy = spyOn(dataService, 'getDetails') .and.returnValue(Promise.resolve('Data')); fixture.detectChanges(); expect(app.data).toBe(undefined); }); // This test fakes an environment that allows for asynchronous tests it('shouldn fetch data successfully if not called asynchronously', async(() => { let fixture = TestBed.createComponent(UserComponent); let app = fixture.debugElement.componentInstance; // DataService instance is getted and mocked to not actually call getDetails(): let dataService = fixture.debugElement.injector.get(DataService); let spy = spyOn(dataService, 'getDetails') .and.returnValue(Promise.resolve('Data')); fixture.detectChanges(); // only when all asynchronous tasks are finished fixture.whenStable().then(() => { expect(app.data).toBe('Data'); }); })); // This is an alternative kind of writing the test above it('shouldn fetch data successfully if not called asynchronously', fakeAsync(() => { let fixture = TestBed.createComponent(UserComponent); let app = fixture.debugElement.componentInstance; // DataService instance is getted and mocked to not actually call getDetails(): let dataService = fixture.debugElement.injector.get(DataService); let spy = spyOn(dataService, 'getDetails') .and.returnValue(Promise.resolve('Data')); fixture.detectChanges(); // With fakeAsync, the whenStable()-method is unnecessary, however now the tick()-method // has to be here. It has the same meaning. tick(); expect(app.data).toBe('Data'); }));
-
both approaches, async and fakeAsync, take the same time to execute
-
"good" deployment includes build and minify code which is done by
ng build --prod --aot
-
aot = ahead of time compiler
-
creates dist-folder which has to be deployed
A huge thank you to Maximilian Schwarzmüller, who created such a great Udemy workshop that was the base for this repository, and who agreed to the contents of this repo.
-
best practice: one module per feature, each with 7 +- 1 components
-
in bigger applications, just having folders for features not sufficient
-
solutions:
-
npm-Packages
-
Monorepo
-
Microservice
-
Majestic Monolith
-
-
goal: cutting project down into small libraries that can easily be used and replaced
-
npm-package consists of
-
/node_modules
-
business packages (that do the actual work)
-
package.json with metadata
-
-
Angular Package Format (detailed specification)
-
building packages according to this specification cumbersome
-
ng-packagr automates most of this process
-
with Angular 6: ng-packagr part of CLI
-
creating sub projects:
ng generate library logger-lib
-
creating applications (within an existing Angular application):
ng generate application playground-app
-
(Libs are imported, applications are executed)
-
best practice for libs: create demo-application that shows how to use lib
-
folder structure for project with subprojects:
project |-- node_modules |-- projects | |-- logger-lib | |-- playground-app | |-- playground-app-e2e |-- src ==> DELETE! |-- angular.json |-- package-lock.json |-- package.json
-
when using subprojects, delete src-folder
-
"Either one main project or subprojects"
-
in logger-lib: public_api.ts defines interface for using the lib:
export * from '.lib/bla';
-
= "Barrel" = place
-
used with
import { LoggerService } from '@my/logger-lib' ... constructor(private logger: LoggerService) {
-
Import readable because of this (in tsconfig.json):
"paths": { "@my/logger-lib": [ // "projects/logger-lib/src/public_api" "dist/projects/logger-lib/src/public_api" ] }
-
commented line used during development, uncommented line for delivery
-
= mapped Namespace
-
npm publish publishes the code in internet, npm publish --registry http://…; only internally (depending on given URL)
-
best practice: in project root .npmrc so that publish-command doesn’t have to get the parameter (which can be forgotten easily)
-
npm registries:
-
Nexus
-
Artifactory
-
Team Foundation Server
-
Verdaccio (very small)
-
-
"Monorepo" = multiple projects in one Git-Repo
-
= slice application in sub-projects so that application only consists of those sub-projects
-
similar to lib + playground-app, but different on organizational layer because sub-projects not just libs and playground-apps, but "real", full-grown applications
-
node_modules only 1x and set for all sub-projects
-
hence: all sub-projects same Angular-version
-
Monorepo good approach if huge application only sliced in smaller chunks without the need to be deployed separately
-
potential problems: all applications have to use same Angular-version, hence have to be updated all at once
-
in sub-projekts no package.json, just one in main root for all sub-projects
-
"basically a renamed src-folder"
-
approach not new, just new name
-
switch between Monorepo and example with lib + playground-app in tsconfig.json with Mapping of the submodules
-
no best practice for structure of subfolders of sub-projects
-
"barrel" (see above) in ever sub-project for information hiding (public_api.ts)
project |-- node_modules |-- projects | |-- admin-app | |-- customer1-app | |-- customer2-app |-- angular.json |-- package-lock.json |-- package.json
-
extension for CLI
-
toolkit to build enterprise-grade Angular applications
-
graphical output for dependencies between modules
-
definition of rulesets for access between modules possible
-
problem with monorepo: architectural decisions have to be followed by all sub-projects
-
Microservice-approach = separation of different applications, maximal independence
-
however, in frontend: all Micro-Apps have to be composed into one application
-
in frontend: "Micro-App" or "Micro-Frontend"
-
simplest solution
-
Disadvantages:
-
loss of state between applications, hence only good if little shared state / communication
-
-
use when
-
product-suite like Google (Search, Maps, …), when different applications don’t need to know much about each other
-
-
= browser-standard, framework-agnostic
-
dynamic loading possible
-
Shadow-DOM: CSS of different apps don’t cause problems
-
since Angular 6 full support of WebComponents
-
supports different technology of applications
-
implementation with Angular Elements (since Angular 6)
-
with HTTP Interceptors, see sample above at HTTP Interceptors
-
best practice: don’t send auth token with every request, filter with if(req.url.startsWith(…))
-
OAuth 2 = most-used protocol
-
OpenIDConnect = additional standard to OAuth 2, so that client gets a second token: Identity-Token. That is readable to the client (in contrast to the Access-Token, which is supposed to be only for the resource-server and not readable for the client). Identity-token can be used in the client, for example to grant access to menu-items or show meta data.
-
-
= lib to use OAuth 2
-
supports ActiveDirectory so login within the domain seemless
-
-
Redhat Keycloak for Java = Auth-Server for Java-Backend
-
Best Practice: Only get authentification from Auth-Server. Auth-server shouldn’t know business-logic. Only get token and with that token go to business logic server and decide what the token means in respect to rights and privileges.
-
Best Practice: one token per Security-Domain
-
Best Practice: Token only valid for 10 to 20 minutes, not multiple days
-
using Active Directory over well-defined web protocols: Active Directory Federation Services
-
mit Angular CLI out of the box: bundling + minification + enableProdMode()
-
service worker = installed from web app into browser, running there even without app
-
for example implementation of caches: leaving data in browser has same effect as using HTTP Interceptors, however on a totally different layer
-
best practice: use abstractions to work with service workers (like workbox or @angular/service-worker) instead of programming service workers directly
-
Service Worker in tab "Application" in dev-mode in browser visible
-
main usage: working app even during offline phases - however only for temporary
-
pre-render first view to show before Java Script has been loaded
-
perceived performance enhanced (it’s not really faster)
-
in Angular: renderModuleFactory
-
a lot of work for small improvement - only for really huge and publicly available applications
-
Support of plain Angular: only renderModuleFactory, however some community-projects
-
2 solutions:
-
(official solution) Angular Compiler
-
("the working option") ngx-translate
-
-
extract texts from templates to xml-files
-
after translating xml-files: compile them back into templates
-
very good performance because translated texts merged into templates
-
however, one build per language + restart of app to change language
-
server-calls via JSON to get translation data from server + set in template via data-binding
-
performance overhead during runtime, but all disadvantages from angular compile solved
-
Defacto Standard