Skip to content

msg-DAVID-GmbH/AngularWorkshop

Repository files navigation

Angular Workshop

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.

1. Introduction

  • 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

2. Setup Option 1: Just Angular

  • this option: only little Angular project for learning plain angular

  • following steps are for Windows machines; Linux can slightly diver

2.1. Install NodeJS

  • https://nodejs.org/en/

  • …​ because Angular CLI needs it …​

  • manages packages and loads a local development server

2.2. Install Angular CLI

  • 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

2.3. Create new Angular Project

  • 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

2.4. Run project

  • 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

  • this option: generate complete and deployable application with Angular UI

  • generated with help of Yeoman, which is a scaffolding tool that can generate different projects using best practices, for example Angular or Node.js

  • JHipster = Yeoman-Generator that creates a Spring Boot + Angular Project

3.1. Install Yarn

  • Yarn = Dependency Manager

  • "yarn global add generator-jhipster" in Terminal will install Yarn

3.2. Create Project Folder

mkdir testfolder && cd testfolder

3.3. Generate Project

  • in Terminal:

    jhipster

3.4. Running Project locally

  • 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.

3.5. Deployment to Pivotal Cloud Foundry

  • 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.

3.6. Generating Entities with JDL-Studio

4. Package Management

4.1. NPM

  • https://www.npmjs.com

  • 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

4.2. Yarn

  • = 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

4.2.1. Error: command xyz not found when running "yarn start"

  • 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"

4.3. Babel

  • JavaScript has different versions

  • Babel converts new JavaScript code into older versions

  • enables development with newest JS version without worrying about browser support

4.4. Webpack

  • usage of for example SASS, PostCSS, minimizing CSS and minimizing JavaScript code with file webpack.config.js plus CLI command:

    webpack
  • Webpack = modular build tool

  • loaders transform source code, for example style-loader adds CSS to DOM

  • plugins like UglifyJS minimizes output of webpack

5. Tooling

  • IntelliJ IDEA supports Angular right from the start:

angularSupportInWebStorm
  • Reference search also working:

referenceSearchInIDEA

5.1. Emmet

  • https://emmet.io

  • = 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":

emmetAbbreviationPreview

6. Debugging

6.1. Developer Tools

  • 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

6.2. Augury

7. Bootstrap

7.1. Usage in this course

  • 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"
          ],

8. Writing Components

  • 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']
    })

8.1. Creating minimal Component

  1. create new directory in src/app, for example "server"

  2. create server.component.ts with a (unique!) selector and a reference to a template

  3. create template server.component.html

  4. 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)

  5. use new component in app.component.html - NOT in the index.html because of best practice

8.2. Creating a Component via CLI

  • 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

9. Databinding

  • = 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;">

10. Directives

  • = 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 {
      ...
    }

10.1. Structural Directives

  • 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

10.2. Attribute Directives

  • 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>

10.3. Building own attribute Directive

  • 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>

10.3.1. React on events with @HostListener

@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

10.3.2. Bind properties with @HostBinding

@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

10.3.3. Setting values to custom directives

@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>

10.4. Building own structural Directive

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>

11. Models

  • 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"

12. Components & Databinding

  • 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

12.1. Sending data from parent component to child component

  • 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};
}

12.2. Sending data from child-component to parent-component

  • = 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.

12.3. Sending data between neighboring components

  • shown methods only allow data-passing between neighboring components via a parent-component that acts as a proxy

  • especially unpractical when components are located "far away" from each other

  • later another approach with Services shown

13. View Encapsulation

  • 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

13.1. Overwriting View Encapsulation

  • 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

14. Local References

  • (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>

15. Accessing DOM Elements via ElementRef

  • 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

16. Component Lifecycle

  • every lifecycle-step = hook that can be used to do things

  • Lifecycle of every component:

    1. ngOnChanges - whenever bound input property changes

    2. ngOnInit - initialization

    3. ngDoCheck - every change detection run (often!)

    4. ngAfterContentInit - content projected into view

    5. ngAfterContentChecked - content checked

    6. ngAfterViewInit - view has been initialized

    7. ngAfterViewChecked - view checked

    8. ngOnDestroy - called before destroying an object

  • ngOnChanges = only hook that recives an argument with some information:

 ngOnChanges(changes: SimpleChanges) {
  ...
 }

17. Services and Dependency Injection

  • 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

17.1. Simple 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:

    1. provider with type of service

    2. 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) {}

  ...
}

17.2. Data-holding Service

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!

17.3. Hierarchical Injection

  • 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

17.4. Injecting Services into Services

  • @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

17.5. ProvidedIn

  • 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

18. Routing

  • 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

18.1. Setup

  • 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>

18.3. Programmatically visit Routes

<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});
}

18.4. Passing Parameters as/into Dynamic Routes

  • 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}

18.4.1. Calling Routes with Parameters programmatically

<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'];
    }
  );
}

18.5. Passing Parameters as Query Parameters

  • 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'];

18.6. Child-Routing

  • 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

18.7. Preserving parameters when routing

  • 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

18.8. Redirect

  • 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.

18.9. Route Guards

  • 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

18.10. Passing static Data to a Route

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'];
      }
    );
  }
}

18.11. Passing dynamic Data to a Route

  • 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'];
      }
      );
}
...

18.12. Location Strategies

  • 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)

19. Observables

  • 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

19.1. Building an Observable

  • 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!');
  }
);

19.2. Unsubscribing

  • 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

19.3. Subject

  • 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);
}

19.4. Operators

  • a lot of operators available, see RxJS docs

  • one example:

const myNumbers = Observable.interval(1000)
  .pipe(map(
    (data: number) => {
      return data * 2;
    }
  ));

19.5. RXJS 5 vs 6

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';

20. Forms

  • 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)

20.1. Template-Driven

  • 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

20.1.1. Validation

  • 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
  >
  • list of all validators

  • 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]*$"
>

20.1.2. Default Texts

<select
  id="secret"
  class="form-control"
  [ngModel]="'default-value'"
  name="secret">
  • This can also be bound (one-way!) to a property: [ngModel]="myProperty"

20.1.3. Binding

  • 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>

20.1.4. Setting Value of Input programmatically

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

20.1.5. Grouping

  • goal: groups of inputs in result object

<div ... ngModelGroup="userData">
  ... some components ...
</div>
  • ngModelGroup forms a group of all the inputs in the div in the field "userData"

  • group also has properties like valid or touched, so whole groups can be validated

20.2. Reactive Forms

  • 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>

20.2.1. Validation

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);
  }
}

20.2.2. Getting access to Data

<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>

20.2.3. Grouping

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>

20.2.4. Dynamically adding Components

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>

20.2.5. Custom Validators

  • 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

20.2.6. Asynchronous Validation

  • 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;
}

20.2.7. Listening to Changes

// 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);
);

21. Best Practices

21.2. Returning "Defensive Copies" of Data

  • 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

21.3. Outsourcing Route Configuration

  • 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
],
...

22. Pipes

  • 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}}

22.1. Creating own Pipes

  • 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 {
 ...

23. Http Requests

  • new in Angular 6: HttpClient, see below. However, using Http as shown here also valid.

23.1. Sending Requests

  • 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

23.2. Getting Data back from Server

@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)
    );
}
...

23.3. Using Observables

  • 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)
    );
}
...

23.4. Catching errors

@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);
        }
      ));
  }
}

23.5. Angular 6: HttpClient

  • 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;
        }
      ));
  }
  }
}

23.5.1. Additional Options

  • 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
      }
    );
}
...

23.5.2. Setting Query Params

  • 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)
  });
}

23.5.3. Progress

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)

23.5.4. Interceptors

  • 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

24. Authentification

  • 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

24.1. JWT

  • introduction

  • "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

25. Modules

  • 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

25.1. Creating new Modules

  1. new file: myname.module.ts

  2. decorate with @NgModule()

  3. add CommonModule as import (contains common directives)

  4. 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

  5. add custom declarations, imports, providers and the main module (bootstrap) - syntax see app.module.ts

  6. add new module to import in app.module.ts

  7. 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

25.2. Shared Modules

  • 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

25.3. 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 }
  ...

25.4. Injection of Services

  • 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.

25.5. Core Module

  • = module that can be created to make AppModule leaner by collecting everything that is only used in the AppModule

  • possible contents:

    • header

    • HomeComponent

25.6. Ahead-of-Time Compilation

  • 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)

26. Unit Tests

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

26.1. Testing Services

  • 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);
});

26.1.1. Testing asynchronous tasks

  • 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

27. Deploying Angular Applications

27.1. Building

  • "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

27.2. Server Setup

  • set correct <base>-element, for example <base href="/my-app/">

  • server should always return index.html because routing is managed by Angular

28. Acknowledgements

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.

29. Structuring Angular Applications

  • best practice: one module per feature, each with 7 +- 1 components

  • in bigger applications, just having folders for features not sufficient

  • solutions:

    1. npm-Packages

    2. Monorepo

    3. Microservice

    4. Majestic Monolith

29.1. npm Packages

  • goal: cutting project down into small libraries that can easily be used and replaced

  • npm-package consists of

    1. /node_modules

    2. business packages (that do the actual work)

    3. package.json with metadata

29.1.1. Generating npm Packages with ng-packagr

  • 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

29.1.2. Folder Structure

  • 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"

29.1.3. Defining Interface of Library

  • 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

29.1.4. Deployment

  • 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)

29.2. Monorepos

  • "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)

29.2.1. Folder-Structure

project
|-- node_modules
|-- projects
|   |-- admin-app
|   |-- customer1-app
|   |-- customer2-app
|-- angular.json
|-- package-lock.json
|-- package.json

29.2.2. Nx

  • https://nrwl.io

  • extension for CLI

  • toolkit to build enterprise-grade Angular applications

  • graphical output for dependencies between modules

  • definition of rulesets for access between modules possible

29.3. Microservice

  • 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

29.3.2. iFrame

  • ugly solution because different problems like scaling

29.3.3. WebComponents

  • = 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)

29.4. Majestic Monolith

  • basically a "good monolith" consisting of libs and Monorepo

30. Cross-Cutting Concerns

30.1. Authorization

  • 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(…​))

30.2. Login & Access Control

  • 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.

  • angular-oauth2-oidc

    • = 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

30.3. Performance

  • mit Angular CLI out of the box: bundling + minification + enableProdMode()

30.3.1. Preloading

  • preloading = load contents asynchronously that are not needed yet, but maybe in the future

  • chunks to load should be per-feature so that when loading, whole features are loaded

  • best practice: use lazy loading and preloading from the beginning instead of adding it later

30.4. Caching with Service Worker

  • 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

30.5. Server Side Rendering

  • 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

  • https://universal.angular.io

30.6. I18N

  • 2 solutions:

    1. (official solution) Angular Compiler

    2. ("the working option") ngx-translate

30.6.1. Angular Compile

  • 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

30.6.2. ngx-translate

  • http://www.ngx-translate.com

  • 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

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published