UI • But you can add third-party libraries • JavaScript preference • JSX for templates • One-way databinding Angular 2+ • In addition for UI you will have lot of other libraries for AJAX, services, modules ... • TypeScript preference • Strong opinions how app should be structured • HTML for templates with add-ons from Angular • Two-way databinding • Learning curve can be steep
Simpler expression syntax, [ ] for property binding, ( ) for event binding • Modularity – core functionality in modules, lighter and faster • Modern Browser only – reduce need for browser compability workarounds • TypeScript Recommendation
• Created by Microsoft • Enhances the language with type annotations • Strong recommendation for Angular • Browsers do not understand TypeScript, but it is possible to compile TypeScript to JavaScript • So in Angular2 you will end up with files ending with .ts rather than .js. In the end the .ts files are compiled to .js • You can implement also React, Express, Vue.js in TypeScript
repeat(5, 5) • Where the function is declared • function repeat(mystring : string, amount : number) : string • You will get an error: • Argument of type '5' is not assignable to parameter of type 'string'
= ${person.firstname}, lastname = ${person.lastname}` } interface Person { firstname : string lastname : string age? : number } let result = display({"firstname": "jack", "lastname": "bauer", age: 30}) Must have firstname and lastname. Age is optional optional property (?)
Bird implements Flyable { fly() { console.log('Bird is flying') } } class Airplane implements Flyable { fly() { console.log('Airplane is flying') } } function flySomething(something : Flyable) : void { something.fly() } flySomething(new Airplane()) flySomething(new Bird()) Must have the fly() method We can pass bird and airplane
_name : string get name(): string { return this._name; } set name(newName : string) { if(newName.length >= 2) { this._name = newName } else { throw new TypeError('Name must be at least 2 chars') } } } // tsc -target ES5 -w src/index.ts --outFile dist/bundle.js let tina = new Person() tina.name = "T" Invoking the set method
3.14 private _radius : number set radius(newRadius : number) { if(newRadius > 0) { this._radius = newRadius } else { throw new TypeError('radius must be > 0') } } get radius() : number { return this._radius } calculateArea() : number { return Circle.PI * c.radius * c.radius } } let c = new Circle() c.radius = 5 console.log(c.calculateArea()) console.log("area = " + Circle.PI * c.radius * c.radius) Common variable to all objects. Class variable
Browser support is weak • TypeScript shares the module support from ES2015 • Modules are executed in their own scope, not in global scope • Variables, functions etc are visible to other modules using export • Module can use other module's "stuff" using import • Any file containing top-level either import or export is considered to be module
install globally • npm install -g @angular/cli • Create skeleton project • ng new my-app • Run your app • ng serve –open • If having permission problems, see • https://docs.npmjs.com/getting -started/fixing-npm- permissions
contain Angular markup • <h1>{{ title }}</h1> • These templates are managed by Application components • Business logic can be wrapped inside of services • All of these are sandboxed into modules • So module contains components (that contain templates) and services • The app is launched by bootstrapping a root module
a view.. which is html • Core building block of Angular 2 apps • Reusable piece of UI that is depicted by a custom html tag • Views? • Navigation links • Content in the web page • Component's application logic is written inside of a class • Class interacts with the view through an API of properties and methods • Angular creates, updates and destroys components when user moves through the app • These classes may have lifecycle hooks / methods
@Component({ selector: 'custom-element', template: `<h1>Hello {{name}}</h1>` }) export class AppComponent { name = 'Angular'; } HTML template. Usuall in separate html file When angular finds <custom-element> </custom-element> use the template instea Component that manages the view
} from '@angular/core'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { title = 'app'; } Now template and css in separate files
NgModules • Every app has at least one module: the root module, named AppModule • In large scale app you will end up with several feature modules • Module is a class with @NgModule decorator • Decorators are functions that modify JavaScript Classes • You will use both standard EcmaScript module- and Angular module system
• declarations • The view classes that belong to module: component, directive or pipe view class • imports • Other modules that are needed by component templates declared in this module • bootstrap • The main application view; root component, that hosts all other app views. Only root module should set this.
} from '@angular/core'; import { AppComponent } from './app.component'; @NgModule({ declarations: [AppComponent], imports: [BrowserModule], providers: [], bootstrap: [AppComponent] }) export class AppModule { } By using JavaScript Module system we import modules. This is standard JS Declare all the components that are part of this module. Can be also pipes and directives AppComponent will host all other views. This is done only in the root module All components can now use BrowserModule. You can add here forms, routing, http...
{ platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/app.module'; import { environment } from './environments/environment'; if (environment.production) { enableProdMode(); } platformBrowserDynamic().bootstrapModule(AppModule) .catch(err => console.log(err)); Sets AppModule to be the main module of our app
<meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="icon" type="image/x-icon" href="favicon.ico"> </head> <body> <app-root></app-root> </body> </html> Custom element which angular will change dynamically Not a single import for javascript? <script> tags are injected by runtime here
'my-app', template: `<h1>{{title}}</h1> <planet-list></planet-list>` }) export class AppComponent { title : string = "Planets"; } Custom element again! This needs to be defined in other component
1 + 1 }} • Many JavaScript expressions are allowed, but not all • assignments (=) • new • chaining expressions (;) • incrementing, decrementing (++, --) • Expression context is typically the component instance • {{ title }} refers to properties of the instance of the component • Should not have visible side effects, quick execution, simplicity ...
• <img bind-src="heroImageUrl"> • <img src="{{heroImageUrl}}"> • One way event • <button (click)="onSave()">Save</button> • <button on-click="onSave()">Save</button> • Two way binding in Forms • <input [(ngModel)]="name"> All the same
{ OnInit } from '@angular/core'; import { OnDestroy } from '@angular/core'; @Component({ selector: 'life-cycle', template: `<p>Testing lifecycle</p>` }) export class LifeCycleComponent implements OnInit, OnDestroy { ngOnInit() { console.log("init") } ngOnDestroy() { console.log("destroy") } } Interfaces are voluntary here, you do not need them
construction • To set up component after Angular sets input properties • So if for example your component fetches data from server do NOT do this in constructor • In Destroy you can do clean ups if necessary
or won't detect of its own. • So for example if user input changes the view, the ngDoCheck() is called • When view is changed a lot of "sub-lifecycle" hooks are called • ngAfterContentChecked() • ngAfterViewChecked()
Input } from '@angular/core'; @Component({ selector: 'point', template: `<li>xPos = {{xPos}}, yPos = {{yPos}}</li>` }) export class PointComponent { @Input() xPos: number @Input() yPos: number } Injecting the values given
'@angular/core'; import { Input } from '@angular/core'; import { Output } from '@angular/core'; import { EventEmitter } from '@angular/core'; @Component({ selector: 'point', template: `<li>xPos = {{xPos}}, yPos = {{yPos}} <button (click)="delete()">Delete</button></li>` }) export class PointComponent { @Input() xPos: number @Input() yPos: number @Output() onDeleteClicked = new EventEmitter<string>(); delete() { this.onDeleteClicked.emit('delete clicked') } } When delete button is pressed, send the event The parent listenes to this Triggering the event
selector: '[appDosomething]' }) export class DosomethingDirective { constructor(el: ElementRef) { el.nativeElement.style.backgroundColor = 'yellow'; } } Importing reference to the element that has the attribute Native DOM - element
template: '<h1 appDosomething [color]="someColor">Hello</h1>' }) export class AppComponent { title = 'app'; someColor = 'red'; } You can give inpu to the directive
'@angular/core'; @Directive({ selector: '[appDosomething]' }) export class DosomethingDirective { @Input() color: string; ... Using @Input to get the color
'app-root', // template: '<h1 appDosomething [color]="someColor">Hello</h1>' template: '<h1 [appDosomething]="someColor">Hello</h1>' }) export class AppComponent { title = 'app'; someColor = 'red'; } The directive name and the input in the same
'@angular/core'; @Directive({ selector: '[appDosomething]' }) export class DosomethingDirective { @Input() appDosomething: string; ... Change the input name to selector. Can be confusing name?
• Reshape DOM • Apply the directive to host element, just like in attributes • <div *ngIf="something">...</div> • The asterisk * precedes the structural directive as a convenience notation • Three common built-in structural directives • ngIf, ngFor, ngSwitch
selector: 'app-root', template: `<ng-template [ngIf]="display"> <div>Is this displayed?</div> </ng-template>` }) export class AppComponent { display = true; } The end result is like this. *ngFor and *ngSwitch follows the same pattern.
name: string; } @Component({ selector: 'app-root', template: ` <div *ngFor="let person of persons"> {{person.name}} </div>` }) export class AppComponent { persons: Person[] = [{name: 'jack'}, {name: 'tina'}]; } Iterating the list. We have more options here also...
interface Person { name: string; } @Component({ selector: 'app-root', template: `<ng-template ngFor let-person [ngForOf]="persons" let-i="index" let-o="odd"> <div>{{i}} {{person.name}}</div> </ng-template>` }) export class AppComponent { persons: Person[] = [{name: 'jack'}, {name: 'tina'}, {name: 'paul'}, {name: 'sara'}]; } The *ngFor is compiled to this, but as you can see, *ngFor is much cleaner syntax and preferred
Web Animations API • It helps if you know the Animations API when working with Angular animations • Angular animations have a specific modules • import { BrowserAnimationsModule } from '@angular/platform- browser/animations'; • To build a animation you can have states • When state changes, animation occurs
boolean { this._state = (this._state === 'on') ? 'off' : 'on'; return false; } get state() { return this._state; } } State can be whatever. We can attach "listeners" to this. When state changes animation occurs
{ display: inline-block; }'], animations: [ trigger('lightBulbState', [ state('on', style({ backgroundColor: 'yellow', transform: 'scale(1) rotate(360deg)' })), state('off', style({ backgroundColor: 'white', transform: 'scale(0.5) rotate(0deg)' })), transition('on <=> off', animate('500ms linear')) ]) ] }) If the same kind of animate, we can use <=> See http://easings.net/ for details about easy-in etc
['p { display: inline-block; }', 'a { display: block; }'], animations: [ trigger('lightBulbState', [ transition(':enter', [ style({transform: 'translateX(-200%)'}), animate(500) ]), transition(':leave', [ animate(500, style({opacity: 0, transform: 'translateX(500%)'})) ]) ]) ] }) export class LightbulbComponent { private _state = true; ... Will toggle the lightbulb off and on of the view state is now boolean because of the *ngIf When entering 1) set lightbulb off the screen 2)Animate it to original position When leaving, animate it to right and set opacity to 0 (invisible)
URLs • Clicking links • Forward and back • Display particular component for a given URL • Use @Angular/Router library package • Then configure (which view is displayed on what URL)
<a routerLink="/view" routerLinkActive="active">View</a> </nav> <div> <router-outlet></router-outlet> </div> Depending on the URL different components are injected after the router- outlet Creating links adds "active" CSS class so you can style the link differently
• 1) List of posts • 2) When clicking a post a detailed information about the post • in routes • http://localhost:4200/view • http://localhost:4200/view/1 • We can define own module and own routing module for this • Can be overkill in small applications
} from '@angular/common/http'; import { CommonModule } from '@angular/common'; import { ViewComponent } from './view.component'; import { ViewDetailComponent } from './view-detail.component'; import { ViewRoutingModule } from './view-routing.module'; @NgModule({ imports: [ CommonModule, HttpClientModule, ViewRoutingModule ], declarations: [ ViewComponent, ViewDetailComponent ] }) export class ViewModule {} The view module has it's own routing Just the view components
'my-selector', template: `<h1>{{title}}</h1> <p>Today is {{ todayDate | date:'dd.mm.yyyy'}} <planet-list></planet-list>` }) export class AppComponent { title : string = 'Planets' todayDate : Date = new Date() } Displays date in the following format
export class MyFilterPipe implements PipeTransform { transform(items: any[], filter: string): any { // If no filter text given if(!filter) { return items } let newItems : any[] = [] for(let itemObject of items) { for(let key in itemObject) { // Make regex to the value let value : string = String(itemObject[key]) if(value.match(filter)) { newItems.push(itemObject) } } } return newItems } } items here contains the planets. The filter contains "Alderaan". Returns a array containing only planets that has "Alderaan
sort of functionality and provides a service to the rest of the application • In AngularJS 1.X you had service, factory, provider but no more. Just class.
{ Planet } from './planet'; @Injectable() export class PlanetService { fetch() : Planet[] { return [{"name": "Yavin IV", "diameter": 10200, population: 1000}, {"name": "Alderaan", "diameter": 12500, population: 2000000000}]; } } Now the planets are coming from a service
Planet } from '../planet'; import { PlanetService } from '../planet.service'; @Component({ selector: 'planet-list', template: ` <ul> <li *ngFor="let planet of planets"> {{planet.name}} </li> </ul>`, providers: [PlanetService] }) export class PlanetListComponent { planets: Planet[] = [] constructor(planetService : PlanetService) { this.planets = planetService.fetch(); } } When creating the object from PlaneListComponent Angular will inject the service The Service is available only in this component. Declare providers in main component or even module if you want it to be available throughout your app.
BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { PlanetListComponent } from './planet-list/planet-list.component'; import { AppComponent } from './app.component'; import { PlanetService } from './planet.service'; @NgModule({ imports: [ BrowserModule, FormsModule ], declarations: [ AppComponent, PlanetListComponent ], providers: [ PlanetService ], bootstrap: [ AppComponent ] }) export class AppModule { } Now the service available throughout your app
using HTTP • Two different APIs: XMLHttpRequest and Fetch API • Angular provides HttpClient • Build on top of XMLHttpRequest • HttpClient is implemented in HttpClientModule which needs to be imported first • Once installed you can inject the service
from '@angular/core'; import { HttpClientModule } from '@angular/common/http'; import { PlanetListComponent } from './planet-list/planet-list.component'; @NgModule({ declarations: [ PlanetListComponent ], imports: [ BrowserModule, HttpClientModule ], providers: [], bootstrap: [PlanetListComponent] }) export class AppModule { } Now you can inject HttpClient in all declared components
.subscribe(jsonObject => { this.planets = jsonObject.results }); } Define the type of given json object. This is an interface that we have to implement It should now work
body).subscribe((result) => {}) • You can also give configuration object • http.post(url, body, {observe: 'response'}).subscribe((response) => {}) • And use interfaces • http.post<SomeInterface>(url, body).subscribe((result) => {})
• Add error handler • http.post(url).subscribe(successFunc, errFunc) • You will get argument of type HttpErrorMessage to the errFunc • The errFunc is called if backend responds with 4xx, 5xx • Also if frontend creates exception
= "" private url = "http://localhost:8080/api/locations/" constructor(private http: HttpClient) {} sendToServer() { let body = {"latitude": this.latitude, "longitude": this.longitude} this.http.post<LocationResponse>(this.url, body, {observe: 'response'} ) .subscribe(this.success, this.error) } error(err: HttpErrorResponse) { this.msg = "Some problem with saving data." } success(response : HttpResponse<LocationResponse>) { let locationObject : LocationResponse = response.body this.msg = `Saved with an id of ${locationObject.id}` } } Modify the UI Modify the UI Binded to view Does not work!
this.longitude} this.http.post<LocationResponse>(this.url, body, {observe: 'response'} ) .subscribe((response) => { let locationObject : LocationResponse = response.body this.msg = `Saved with an id of ${locationObject.id}` }) } How come the anonymous arrow function work? It uses closures...
"longitude": this.longitude} let _this = this this.http.post<LocationResponse>(this.url, body, {observe: 'response'} ) .subscribe((response) => { let locationObject : LocationResponse = response.body _this.msg = `Saved with an id of ${locationObject.id}` }) } _this is still in memory because closure
by Google • Most of Google's mobile apps had applied this style • Gmail, Youtube, Docs.. • Also Web interfaces of these apps! • Google has developed prebuilt UI components for Angular that follows this Material Design • Although it's possible to override the styling of these • In addition of providing look and feel you will have basic implementation of various UI components
Development Kit) • npm install --save @angular/material @angular/cdk • Include BrowserAnimationsModule in your app • imports: [BrowserAnimationsModule] • Include Modules from Angular Material • import {MatButtonModule, MatCheckboxModule} from '@angular/material'; • ...imports: [MatButtonModule, MatCheckboxModule] • Include a theme in styles.css • @import "~@angular/material/prebuilt-themes/indigo-pink.css"; • Install gesture support (needed for couple of the components) • npm install --save hammerjs • Add Material Icons in index.html • <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
• The documentation is really basic, rely on the examples given! • Different Components for • Form, Navigation, Layout, Buttons, Popups & Modals, Data table
event • <button (click)="onClickMe()">Click me!</button> • Using $event • <input (keyup)="onKey($event)"> • The corresponding event object is given to the onKey – method • The properties of the $event object depends on the type of DOM event • Mouse event is different than key event
template: `<input (keyup)="doSomething($event)" placeholder="Write"> <p (click)="doSomething($event)">Click me</p>` }) export class AppComponent { doSomething(event: Event) { console.log(event); } } Base class for all the DOM Events Either MouseEvent or KeyboardEvent
template: `<input (keyup)="doSomething($event)" placeholder="Write"> <p (click)="doSomething($event)">Click me</p>` }) export class AppComponent { doSomething(event: Event) { console.log(event.target.value); } } Angular will warn about this. The value is not present in event.target
`<input (keyup)="doSomething($event)" placeholder="Write">` }) export class AppComponent { doSomething(event: KeyboardEvent) { console.log(event.key); // const givenTarget: HTMLInputElement = event.target as HTMLInputElement; // console.log(givenTarget.value); } } If we use KeyboardEvent directly, then we can use it's properties..
selector: 'app-root', template: `<input #donaldduck (keyup)="0" placeholder="Write"> {{donaldduck.value}}` }) export class AppComponent { /* doSomething(event: KeyboardEvent) { const givenTarget: HTMLInputElement = event.target as HTMLInputElement; console.log(givenTarget.value); }*/ } Create new variable that refers to the <input>. No need for the event function! Must have something in here
template: `<input (keyup.enter)="enterIsPressed($event)" placeholder="Write"> {{value}}` }) export class AppComponent { value = ''; enterIsPressed(event: KeyboardEvent) { const givenTarget: HTMLInputElement = event.target as HTMLInputElement; this.value = givenTarget.value; } } Angular's own event When enter is pressed, display the results
template: `<input #donaldduck (keyup.enter)="enterIsPressed(donaldduck.value)" placeholder="Write" (blur)="enterIsPressed(donaldduck.value)"> {{value}}` }) export class AppComponent { value = ''; enterIsPressed(value: string) { this.value = value; } } If user is leaving the input without typing enter, do exactly the same
control has been visited. ng-touched ng-untouched The control's value has changed. ng-dirty ng-pristine The control's value is valid. ng-valid ng-invalid
control is determined by using standard HTML5 • Is valid if text given • <input type="text" ... required> • Is valid if text follows the regex • <input type="text" pattern="[a-z0-9+-]+@[a-z0-9.-]+\.[a-z]{2,3}$" ... required> • Is valid if given 6 chars • <input type="text" minlength="6" maxlength="6" ... required> • See • https://developer.mozilla.org/en- US/docs/Learn/HTML/Forms/Form_validation
simple scenarios • Two way databinding • Minimal Component code • Unit testing can be hard Reactive • More flexible • For more complex scenarios • No automatic databinding • More component code, less HTML • Easier unit testing
FormGroup } from '@angular/forms'; interface Person { name: string; age: number; } @Component({ selector: 'app-root', template: ` <h2>Person Detail</h2> <h3>Person Form</h3> <form [formGroup]="personForm"> <label>Name:</label> <input type="text" formControlName="name"> <label>Age:</label> <input type="text" formControlName="age"> </form>` }) export class AppComponent { personForm = new FormGroup({name: new FormControl(), age: new FormControl()}); persons: Person[] = [{name: 'jack', age: 30}, {name: 'tina', age: 20}]; } Link the the whole form to FormGroup Because input is not standalone, instead of using [formControl] use formControlName
FormControl, FormGroup, FormBuilder, Validators, AbstractControl, ValidationErrors } from '@angular/forms'; function forbiddenNameValidator(control: AbstractControl): ValidationErrors | null { const value = control.value; if (value === 'Jussi') { return {'error': 'wrong name'}; } else { return null; } } Will receive the control If value is wrong, return error object, type is ValidationError which basically means a object with one key value pair and value is string return null if validation was success
the validity of a FormControl. Possible values: VALID, INVALID, PENDING, or DISABLED. myControl.pristine true if the user has not changed the value in the UI. Its opposite is myControl.dirty. myControl.untouched true if the control user has not yet entered the HTML control and triggered its blur event. Its opposite is myControl.touched.
// textbox, slider, ...? controlType?: string; // key to differentiate the form elements key?: string; // Is it required or not? required?: boolean; TextboxQuestion controlType: string = "textbox" // numeric, text...? textboxtype: string;
UI label?: string; // textbox, slider, ...? controlType?: string; // key to differentiate the form elements key?: string; // Is it required or not? required?: boolean; } /** * Base class for all the form input elements */ export class QuestionBase { label: string; controlType: string; key: string; required: boolean; constructor(options: Options) { this.label = options.label; this.controlType = options.controlType; this.key = options.key; this.required = options.required; } }
- element */ export class TextboxQuestion extends QuestionBase { // Will be textbox, overrides the base classes control type controlType = 'textbox'; // Is it numeric, text ..? textboxtype: string; constructor(options: Object) { super(options); this.textboxtype = options['textboxtype']; } }
Validators } from '@angular/forms'; import { QuestionBase } from './question-base'; /** * Service that transforms questions to FormGroup. */ @Injectable() export class QuestionControlService { /** * Transforms questions-array to FormGroup. * * @param questions list of questions. * @return FormGroup containing the questions. */ toFormGroup(questions: QuestionBase[] ): FormGroup { const group: any = {}; for (const question of questions) { if (question.required) { group[question.key] = new FormControl('', Validators.required); } else { group[question.key] = new FormControl(); } } // return new FormGroup({name: new FormControl(), age: new FormControl()}) return new FormGroup(group); } } Creates on object that contains FormControls Returns the FormGroup
FormGroup } from '@angular/forms'; import { TextboxQuestion } from '../question-textbox'; import { QuestionBase } from '../question-base'; import { Form } from '@angular/forms'; @Component({ selector: 'app-question', template: `<div [formGroup]="form"><label>{{question.label}}</label> <input [type]="question.textboxtype" [formControlName]="question.key">{{question.key}}</div>` }) export class QuestionComponent implements OnInit { @Input() question: QuestionBase; @Input() form: FormGroup; constructor() { } ngOnInit() { } } For each question create input. You could do ngSwich here and create different input elements by the type of the question formControlName requires that formGroup is present in the component