Angular: Difference between revisions
(25 intermediate revisions by the same user not shown) | |||
Line 585: | Line 585: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
Directives, like components need to added to the declarations for the module. | Directives, like components need to added to the declarations for the module. | ||
==HostListener and HostBinding== | |||
We can listens for changes in our directive using hostlistener. Using the example above we can implement mouseover and mouseleave | |||
<syntaxhighlight lang="typescript"> | |||
import { Directive, ElementRef, HostListener, Renderer2} from "@angular/core" | |||
@Directive({ | |||
selector: `[appChbgcolor]` | |||
}) | |||
export class ChangeBgColorDirective { | |||
constructor( | |||
private el: ElementRef, | |||
private renderer: Renderer2) { | |||
} | |||
ChangeBgColor(color: string): void { | |||
this.renderer.setStyle( | |||
this.el.nativeElement, 'color', color) | |||
} | |||
@HostListener('mouseover') onMouseOver() { | |||
this.ChangeBgColor('red') | |||
} | |||
@HostListener('mouseleave') onMouseLeave() { | |||
this.ChangeBgColor('black') | |||
} | |||
} | |||
</syntaxhighlight> | |||
In Angular, the @HostBinding() function decorator allows you to set the properties of the host element from the directive class. The @HostBinding() decorator takes one parameter, the name of the host element property which value we want to assign in the directive.<br> | |||
<br> | |||
In our example, our host element is an HTML div element. If you want to set border properties of the host element, you can do that using @HostBinding() decorator as shown below: | |||
<syntaxhighlight lang="typescript"> | |||
@HostBinding(`style.border`) border: string | |||
@HostListener('mouseover') onMouseOver() { | |||
this.border = '5px solid green' | |||
} | |||
</syntaxhighlight> | |||
=Event Handling= | |||
==Introduction== | |||
Event bubbling and capture are two mechanisms that describe what happens when two handlers of the same event type are activated on one element. <br> | |||
This is a pretty simple example that shows and hides a <div> with a <video> element inside it: | |||
<syntaxhighlight lang="html"> | |||
<button>Display video</button> | |||
<div class="hidden"> | |||
<video> | |||
<source src="rabbit320.mp4" type="video/mp4"> | |||
<source src="rabbit320.webm" type="video/webm"> | |||
<p>Your browser doesn't support HTML5 video. Here is a <a href="rabbit320.mp4">link to the video</a> instead.</p> | |||
</video> | |||
</div> | |||
</syntaxhighlight> | |||
When the <button> is selected, the video is displayed, by changing the class attribute on the <div> from hidden to showing (the example's CSS contains these two classes, which position the box off the screen and on the screen, respectively):<br> | |||
<syntaxhighlight lang="ts"> | |||
btn.onclick = function() { | |||
videoBox.setAttribute('class', 'showing'); | |||
} | |||
</syntaxhighlight> | |||
We then add a couple more onclick event handlers — the first one to the <div> and the second one to the <video>. Now, when the area of the <div> outside the video is selected, the box should be hidden again and when the video itself is selected, the video should start to play.<br> | |||
<syntaxhighlight lang="ts"> | |||
videoBox.onclick = function() { | |||
videoBox.setAttribute('class', 'hidden'); | |||
}; | |||
video.onclick = function() { | |||
video.play(); | |||
}; | |||
</syntaxhighlight> | |||
But there's a problem as currently, when you select the video it starts to play, but it causes the <div> to be hidden at the same time. This is because the video is inside the <div> as it is part of it so selecting the video actually runs both the above event handlers. | |||
==Bubbling and capturing explained== | |||
When an event is fired on an element that has parent elements (in this case, the <video> has the <div> as a parent), modern browsers run two different phases — the capturing phase and the bubbling phase. | |||
<br> | |||
In the capturing phase: | |||
*The browser checks to see if the element's outer-most ancestor (<html>) has an onclick event handler registered on it for the capturing phase, and runs it if so. | |||
*Then it moves on to the next element inside <html> and does the same thing, then the next one, and so on until it reaches the element that was actually selected. | |||
In the bubbling phase, the exact opposite occurs: | |||
*The browser checks to see if the element selected has an onclick event handler registered on it for the bubbling phase, and runs it if so. | |||
*Then it moves on to the next immediate ancestor element and does the same thing, then the next one, and so on until it reaches the <html> element. | |||
[[File: Event capturing and bubbling.png | 700px ]] | |||
<br> | |||
In modern browsers, by default, all event handlers are registered for the bubbling phase. So in our current example, when you select the video, the event bubbles from the <video> element outwards to the <html> element. Along the way: | |||
*It finds the video.onclick... handler and runs it, so the video first starts playing. | |||
*It then finds the videoBox.onclick... handler and runs it, so the video is hidden as well. | |||
'''Fixing the problem with stopPropagation()''' | |||
This is a very annoying behavior, but there is a way to fix it! The standard Event object has a function available on it called stopPropagation() which, when invoked on a handler's event object, makes it so that first handler is run but the event doesn't bubble any further up the chain, so no more handlers will be run. | |||
We can, therefore, fix our current problem by changing the second handler function in the previous code block to this: | |||
<syntaxhighlight lang="ts"> | |||
video.onclick = function(e) { | |||
e.stopPropagation(); | |||
video.play(); | |||
}; | |||
</syntaxhighlight> | |||
==Event delegation== | |||
Bubbling also allows us to take advantage of event delegation — this concept relies on the fact that if you want some code to run when you select any one of a large number of child elements, you can set the event listener on their parent and have events that happen on them bubble up to their parent rather than having to set the event listener on every child individually. Remember, bubbling involves checking the element the event is fired on for an event handler first, then moving up to the element's parent, etc. | |||
<br> | |||
A good example is a series of list items as if you want each one to pop up a message when selected, you can set the click event listener on the parent <nowiki><ul></nowiki>, and events will bubble from the list items to the <nowiki><ul></nowiki>. | |||
=HTTP Management= | =HTTP Management= | ||
Line 1,526: | Line 1,627: | ||
*Improved build times, enabling AOT on by default | *Improved build times, enabling AOT on by default | ||
*Improved Internationalization | *Improved Internationalization | ||
=CORS= | |||
Put this here to remind myself of how to do this. You cannot call an endpoint on the same domain without implementing CORs. CORs needs to be implemented on the server so if you are on the client and do not have access to the server code you need to use a proxy.<br><br> | |||
In the case we are going to implement<br> | |||
==Example== | |||
We need to do 3 steps | |||
*Identify Configuration | |||
*Create a Proxy config | |||
*Implement in Project | |||
*Change Angular config | |||
*What Good looks like | |||
===Identify Configuration=== | |||
This is the set up I needed to solve | |||
<syntaxhighlight> | |||
The server end point was http://localhost:8080 | |||
The client angular project was http://localhost:4200 | |||
</syntaxhighlight> | |||
===Create a Proxy config=== | |||
This example config '''proxy.conf.json''' is usually in the '''<project>src'' directory. In this example we are looking to redirect all http://localhost:8080/api calls. So in this file we specify the server current endpoint, address and port. | |||
<syntaxhighlight lang="json"> | |||
{ | |||
"/api/*": { | |||
"target": "http://localhost:8080", | |||
"secure": false, | |||
"logLevel": "debug" | |||
} | |||
} | |||
</syntaxhighlight> | |||
===Implement in Project=== | |||
We now need to implement this in the project by specifying the port the application is running on for the endpoint. | |||
<syntaxhighlight lang="ts"> | |||
export class SearchService { | |||
mediaFiles$ = this.http.get<IMediaFile[]>( | |||
'http://localhost:4200/api/mediaFiles' | |||
) | |||
</syntaxhighlight> | |||
===Change Angular config=== | |||
We need to change the '''angular.json''' configuration in the root directory of the project assuming we are using '''ng serve'''. | |||
<syntaxhighlight lang="json" highlight="4-6"> | |||
"serve": { | |||
"builder": "@angular-devkit/build-angular:dev-server", | |||
"options": { | |||
"browserTarget": "project-name:build", | |||
"proxyConfig": "src/proxy.conf.json" | |||
}, | |||
... | |||
</syntaxhighlight> | |||
===What Good looks like=== | |||
Finally when we run the software this is what it looks like when it works | |||
<syntaxhighlight highlight="5-7,21-25"> | |||
iwiseman@OLIVER:~/dev/projects/bibblempng$ npm start | |||
> bibblempng@0.0.0 start | |||
> ng serve | |||
⠋ Generating browser application bundles (phase: setup)...[HPM] Proxy created: /api -> http://localhost:8080 | |||
[HPM] Subscribed to http-proxy events: [ 'error', 'close' ] | |||
✔ Browser application bundle generation complete. | |||
Initial Chunk Files | Names | Size | |||
vendor.js | vendor | 2.51 MB | |||
polyfills.js | polyfills | 128.50 kB | |||
main.js | main | 67.34 kB | |||
runtime.js | runtime | 6.62 kB | |||
styles.css | styles | 736 bytes | |||
| Initial Total | 2.71 MB | |||
Build at: 2021-09-16T20:08:05.938Z - Hash: 1e2e2f6bdba2d861581f - Time: 6355ms | |||
** Angular Live Development Server is listening on localhost:4200, open your browser on http://localhost:4200/ ** | |||
✔ Compiled successfully. | |||
[HPM] GET /api/mediaFilesx -> http://localhost:8080 | |||
</syntaxhighlight> | |||
=Apache Setup= | =Apache Setup= | ||
==Virtual Site == | ==Virtual Site == | ||
Line 1,574: | Line 1,754: | ||
SSLProxyEngine On | SSLProxyEngine On | ||
ProxyPass /v1/api/myEndpoint https://site.com/v1/api/myEndpoint | ProxyPass /v1/api/myEndpoint https://site.com/v1/api/myEndpoint | ||
</syntaxhighlight> | |||
==Switching from CSS to SCCS== | |||
In Angular.json | |||
*Replace styles.css with style.sccs (x2) | |||
*Convert src/styles.css to src/styles.scss | |||
*Rename existing components and convert their css to sccs | |||
*Set schematics | |||
<syntaxhighlight lang="js"> | |||
"schematics": { | |||
"@schematics/angular:component": { | |||
"styleext": "scss" | |||
} | |||
} | |||
</syntaxhighlight> | </syntaxhighlight> |
Latest revision as of 20:14, 16 September 2021
Introduction
Angular Versions
- Angular 1.0 is the old version of angular along with AngularJS
- Angular, starts at 2 and skips 3 and is now at 9 is the version this page is about
Benefits
- Compiles with ES6
- Use modules
- Built in Internationalisation and Accessibility
- Comes with Router, http, forms, rxjs, etc
- Supports Progress Web Apps, (Mobile, Web like React)
- Server-side rendering (render page on server)
- One-way data flow (like React update from parent down)
- Dependency injection
- Uses zone.js to detect change before rendering
- Like React, multiple rendering targets, Browser/DOM, Server Side, Mobile Apps, Desktops
Angular CLI
When we build JavaScript we have to manage
- Module Handling
- Minifying
- Shims (compatibility to legacy code)
- Zone.js wrapper
- Bundling
- Transpilation, compiling to ESx (babel)
Then Angular CLI can
- Create new application
- New components/service/pipe
- Serve up the Application
- Linting
- Testing
- Building
Sample App
This is the sample app I built
The files for the example can be found at [1]
Components
Introduction
- Template
- Class
- Metadata
How An Application Starts
Here is the index.html which has the pm-root tag inside it to denote a component.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>APM</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
<pm-root></pm-root>
</body>
</html>
Here is a sample app.module (possibly main) used.
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule
],
bootstrap: [AppComponent]
})
export class AppModule { }
And finally the component we defined.
import { Component } from '@angular/core';
@Component({
selector: 'pm-root',
template: `
<div>
<h1>{{pageTitle}}</h1>
<div>My First Component</div>
</div>
`
})
export class AppComponent {
pageTitle: string = 'Angular: Getting Started 2';
}
Interfaces
Defining
We can define interfaces in Angular
export interface IProduct {
productId: number;
productName: string;
...
calculateDiscount(percent: number): number;
}
Usage
So we just need to change our any definition in the product component to be IProduct
import { Component } from '@angular/core';
import { IProduct} from './product'
@Component({
selector: 'pm-products',
templateUrl: './product-list.component.html'
})
export class ProductListComponent {
pageTitle: string = "Product List"
imageWidth: number = 50;
imageMargin: number = 2;
showImage: boolean = false;
listFilter: string = "cart"
products: IProduct[] = [
{
"productId": 1,
"productName": "Leaf Rake",
...
LifeCyle Hooks
Introduction
In Angular, every component has a life-cycle, a number of different stages it goes through. There are 8 different stages in the component lifecycle. Every stage is called as lifecycle hook event.
ngOnChanges
This event executes every time when a value of an input control within the component has been changed. Actually, this event is fired first when a value of a bound property has been changed. It always receives a change data map, containing the current and previous value of the bound property wrapped in a SimpleChange.
ngOnInit
This event initializes after Angular first displays the data-bound properties or when the component has been initialized. This event is basically called only after the ngOnChanges()events. This event is mainly used for the initialize data in a component.
ngDoCheck
This event is triggered every time the input properties of a component are checked. We can use this hook method to implement the check with our own logic check. Basically, this method allows us to implement our own custom change detection logic or algorithm for any component.
ngAfterContentInit
This lifecycle method is executed when Angular performs any content projection within the component views. This method executes when all the bindings of the component need to be checked for the first time. This event executes just after the ngDoCheck() method. This method is basically linked with the child component initializations.
ngAfterContentChecked
This lifecycle hook method executes every time the content of the component has been checked by the change detection mechanism of Angular. This method is called after the ngAfterContentInit() method. This method is also called on every subsequent execution of ngDoCheck(). This method is also mainly linked with the child component initializations.
ngAfterViewInit
This lifecycle hook method executes when the component’s view has been fully initialized. This method is initialized after Angular initializes the component’s view and child views. It is called after ngAfterContentChecked(). This lifecycle hook method only applies to components.
ngAfterViewChecked
This method is called after the ngAterViewInit() method. It is executed every time the view of the given component has been checked by the change detection algorithm of Angular. This method executes after every subsequent execution of the
ngAfterContentChecked()
This method also executes when any binding of the children directives has been changed. So this method is very useful when the component waits for some value which is coming from its child components.
ngOnDestroy
This method will be executed just before Angular destroys the components. This method is very useful for unsubscribing from the observables and detaching the event handlers to avoid memory leaks. Actually, it is called just before the instance of the component is finally destroyed. This method is called just before the component is removed from the DOM.
Nested Components
Introduction
We often want to put component within other component. The nested component receives information from it's input properties and talks to the container (parent) via events. In the example we create a
- component,
- style,
- template
Component
import { Component, OnChanges, Input, EventEmitter, Output } from '@angular/core';
@Component({
selector: 'pm-star',
templateUrl: './star.component.html',
styleUrls: ['./star.component.css']
})
export class StarComponent implements OnChanges {
@Input() rating = 0;
starWidth = 0;
@Output() ratingClicked: EventEmitter<string> =
new EventEmitter<string>();
ngOnChanges(): void {
this.starWidth = this.rating * 75 / 5;
}
onClick(): void {
this.ratingClicked.emit(`The rating ${this.rating} was clicked!`);
}
}
Style
.crop {
overflow: hidden;
}
div {
cursor: pointer;
}
Template
<div class="crop"
[style.width.px]="starWidth"
[title]="rating"
(click)="onClick()">
<div style="width: 75px">
<span class="fa fa-star"></span>
<span class="fa fa-star"></span>
<span class="fa fa-star"></span>
<span class="fa fa-star"></span>
<span class="fa fa-star"></span>
</div>
</div>
Using the Component
To use the component we need to
- add the component to the app module
- add the tag to the template and pass the relevant parameters
Add to App Module
import { StarComponent } from './shared/star.component';
import { ConvertToSpacesPipe } from './shared/convert-to-spaces.pipe';
...
@NgModule({
declarations: [
AppComponent,
ProductListComponent,
ConvertToSpacesPipe,
StarComponent,
],
imports: [
BrowserModule,
...
Add to the template
Note the starRating is bound to the @input property of the component. This is similar to props used in react.
<td><pm-star [rating]='product.starRating'></pm-star></td>
Outputting from Nested Component
We can only use events to talk to the parent container but we can do this by creating events within the component. In the example we created
@Output() ratingClicked: EventEmitter<string> = new EventEmitter<string>()
We can bind to this in the parent where ratingClicked is the name of the output function and OnRatingClicked is the parents function to execute.
In the parent template
<td>
<pm-star [rating]='product.starRating'
(ratingClicked)='onRatingClicked($event)'>
</pm-star>
</td>
In the parent class
onRatingClicked(message: string): void {
this.pageTitle = 'Product List Me: ' + message;
}
Styles
Styles can be added either by adding an array of styles using the keyword styles or by referencing a style sheet using styleUrls.
import { Component, OnInit } from '@angular/core';
import { IProduct } from './product';
import { ProductService } from './product.service';
@Component({
templateUrl: './product-list.component.html',
styleUrls: ['./product-list.component.css']
})
....
Example ./product-list.component.css
thead {
color: #337AB7;
}
Templates
Inline Templates
We can define templates using double quotes on one line or by using back ticks.
template: `
<div>
<h1>{{pageTitle}}</h1>
<div>My First Component</div>
</div>
`
Linked Templates
In general it would look like using and linked template would be better. You simply reference the template using the templateUrl keyword instead of template.
Adding Styling Components
We used bootstrap and font-awesome. To make them available in angular we added the imports to the styles.css
@import "~bootstrap/dist/css/bootstrap.min.css";
@import "~font-awesome/css/font-awesome.min.css";
Creating the Template
We call the template the same name as the component. In the same app this was product-list.component.html. This starts with html
<div class='card'>
<div class='card-header'>
Page Title
</div>
<div class='card-body'>
<div class='row'>
<div class='col-md-2'>Filter by:</div>
<div class='col-md-4'>
<input type='text'/>
</div>
</div>
<div class='row'>
<div class='col-md-6'>
<h4>Filtered by: </h4>
</div>
</div>
<div class='table-responsive'>
<table class='table'>
<thead>
<tr>
<th>
<button class='btn btn-primary'>
Show Image
</button>
</th>
<th>Product</th>
<th>Code</th>
<th>Available</th>
<th>Price</th>
<th>5 Star Rating</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
Creating the Component
So we create the component
import { Component } from '@angular/core';
@Component({
selector: 'pm-products',
templateUrl: './product-list.component.html'
})
export class ProductListComponent {
}
Add Component to App
Seems a popular error but whenever we use a component we must add it to the app. In this case it was app.modules.ts with an import. Note the import AND the add to declarations
import { ProductListComponent } from './products/product-list.component';
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent,
ProductListComponent
],
imports: [
BrowserModule
],
bootstrap: [AppComponent]
})
export class AppModule { }
Interpolation
We can use this to bind the properties to functions to the component in the template. E.g. This gets the property pageTitle from the class component
<div>{{pageTitle}}</div>
This calls foo() in the class
<div>{{getFoo()}}</div>
Directives
These are ways to control template logic. Below are the 3 of the key ones
- NgIf—conditionally creates or destroys subviews from the template.
- NgFor—repeat a node for each item in a list.
- NgSwitch—a set of directives that switch among alternative views.
You can find the docs at [2]
ngIf
Example
<table class='table' *ngIf='products && products.length'>
ngFor
Within javascript there is a for...of and and a for...in. The for..of iterates over the objects. The for..in loop iterates over the properties of an object. Example
<tbody>
<tr *ngFor="let product of products">
<td></td>
<td>{{product.productName}}</td>
<td>{{product.productCode}}</td>
<td>{{product.releaseData}}</td>
<td>{{product.price}}</td>
<td>{{product.starRating}}</td>
</tr>
</tbody>
Data Binding & Pipes
Property Binding
Property binding is binding properties and should not be confused with the interpolation. Property binding is one-way. Note some of the values need product because they are from the variable in the for loop. The values such as imageWidth are a property of the component and do not need the component.
<td><img [src]='product.imageUrl'
[title]='product.imageUrl'
[style.width.px]='imageWidth'
[style.margin.px]='imageMargin'>
</td>
Event Binding
No surprises,
In the template define an event on the left and a function to call on the right.
<button class="btn btn-primary"
(click)='toggleImage()'>
Show Image
</button>
In the class create a function
export class ProductListComponent {
...
// Generally at the end
toggleImage(): void {
this.showImage = !this.showImage
}
}
Two-way Binding
To implement two-way binding we use ngModel. We will need to import the FormsModule to use this. We do this by specifying the module in the imports array as it is a third-party component not made by us.
import { ProductListComponent } from './products/product-list.component';
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent,
ProductListComponent
],
imports: [
BrowserModule,
FormsModule
],
bootstrap: [AppComponent]
})
export class AppModule { }
Add Out Listfilter to the class
export class ProductListComponent {
pageTitle: string = "Product List"
imageWidth: number = 50;
imageMargin: number = 2;
showImage: boolean = false;
listFilter: string = "cart"
...
Now we can add our filter to the page using the ngModel.
<input type='text'
[(ngModel)]="listFilter"/>
</div>
Pipes
Introduction
Pipes are used to transform data such as bound properties. There are several built-in data types for dates, numbers,json, slice etc or build our own. As the name suggests, it is achieved by using the "|" character. Parameters can be passed by using the full colon.
{{product.price | currency:'USD':'symbol':'1.2-2' }}
Custom Pipes
We can build our own pipe by
- implementing the PipeTransform interface on a class using the @Pipe decorator
- adding the component to our app module
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'convertToSpaces'
})
export class ConvertToSpacesPipe implements PipeTransform {
transform(value: string, character: string): string {
return value.replace(character, ' ');
}
}
Filtering
It is suggested that it is better to filter within the component rather than to have pipes which could do this for us. In the example code we
- Added a variable to hold the filtered products
- Added a getter/setter to the filter property to encapsulate access
- Create a function to do the filtering
- Add constructor to default values
constructor() {
this.filteredProducts = this.products
this.listFilter = 'cart'
}
filteredProducts: IProduct[]
_listFilter = '';
get listFilter(): string {
return this._listFilter;
}
set listFilter(value: string) {
this._listFilter = value;
this.filteredProducts = this.listFilter ? this.performFilter(this.listFilter) : this.products;
}
performFilter(filterBy: string): IProduct[] {
filterBy = filterBy.toLocaleLowerCase();
return this.products.filter((product: IProduct) =>
product.productName.toLocaleLowerCase().indexOf(filterBy) !== -1);
}
Directives
Overview
At the core, a directive is a function that executes whenever the Angular compiler finds it in the DOM. Angular directives are used to extend the power of the HTML by giving it new syntax. Each directive has a name — either one from the Angular predefined like ng-repeat, or a custom one which can be called anything. And each directive determines where it can be used: in an element, attribute, class or comment.
By default, from Angular versions 2 and onward, Angular directives are separated into three different types
Components
As we saw earlier, components are just directives with templates. Under the hood, they use the directive API and give us a cleaner way to define them.
The other two directive types don’t have templates. Instead, they’re specifically tailored to DOM manipulation.
Attribute directives
Attribute directives manipulate the DOM by changing its behavior and appearance.
We use attribute directives to apply conditional style to elements, show or hide elements or dynamically change the behavior of a component according to a changing property.
Structural directives
These are specifically tailored to create and destroy DOM elements.
Some attribute directives — like hidden, which shows or hides an element — basically maintain the DOM as it is. But the structural Angular directives are much less DOM friendly, as they add or completely remove elements from the DOM. So, when using these, we have to be extra careful, since we’re actually changing the HTML structure.
These allow you to add logic to you components
Example Attribute Directive
To visualize an attribute directive here is a simple example
The code for the directive is created similar to a component but uses the @Directive decorator. Like components you specify the selector to use when referring to your directive.
import { Directive, ElementRef, Renderer2} from "@angular/core"
@Directive({
selector: `[appChbgcolor]`
})
export class ChangeBgColorDirective {
constructor(
private el: ElementRef,
private renderer: Renderer2) {
console.log(`Did this gov`)
this.ChangeBgColor(`red`)
}
ChangeBgColor(color: string): void {
this.renderer.setStyle(
this.el.nativeElement, 'color', color)
}
}
You then create a component and add your directive to the html. For example
<h1>Test</h1>
<div appChbgcolor>
<div>{{ title }}</div>
</div>
Directives, like components need to added to the declarations for the module.
HostListener and HostBinding
We can listens for changes in our directive using hostlistener. Using the example above we can implement mouseover and mouseleave
import { Directive, ElementRef, HostListener, Renderer2} from "@angular/core"
@Directive({
selector: `[appChbgcolor]`
})
export class ChangeBgColorDirective {
constructor(
private el: ElementRef,
private renderer: Renderer2) {
}
ChangeBgColor(color: string): void {
this.renderer.setStyle(
this.el.nativeElement, 'color', color)
}
@HostListener('mouseover') onMouseOver() {
this.ChangeBgColor('red')
}
@HostListener('mouseleave') onMouseLeave() {
this.ChangeBgColor('black')
}
}
In Angular, the @HostBinding() function decorator allows you to set the properties of the host element from the directive class. The @HostBinding() decorator takes one parameter, the name of the host element property which value we want to assign in the directive.
In our example, our host element is an HTML div element. If you want to set border properties of the host element, you can do that using @HostBinding() decorator as shown below:
@HostBinding(`style.border`) border: string
@HostListener('mouseover') onMouseOver() {
this.border = '5px solid green'
}
Event Handling
Introduction
Event bubbling and capture are two mechanisms that describe what happens when two handlers of the same event type are activated on one element.
This is a pretty simple example that shows and hides a
<button>Display video</button>
<div class="hidden">
<video>
<source src="rabbit320.mp4" type="video/mp4">
<source src="rabbit320.webm" type="video/webm">
<p>Your browser doesn't support HTML5 video. Here is a <a href="rabbit320.mp4">link to the video</a> instead.</p>
</video>
</div>
btn.onclick = function() {
videoBox.setAttribute('class', 'showing');
}
videoBox.onclick = function() {
videoBox.setAttribute('class', 'hidden');
};
video.onclick = function() {
video.play();
};
Bubbling and capturing explained
When an event is fired on an element that has parent elements (in this case, the <video> has the
In the capturing phase:
- The browser checks to see if the element's outer-most ancestor (<html>) has an onclick event handler registered on it for the capturing phase, and runs it if so.
- Then it moves on to the next element inside <html> and does the same thing, then the next one, and so on until it reaches the element that was actually selected.
In the bubbling phase, the exact opposite occurs:
- The browser checks to see if the element selected has an onclick event handler registered on it for the bubbling phase, and runs it if so.
- Then it moves on to the next immediate ancestor element and does the same thing, then the next one, and so on until it reaches the <html> element.
In modern browsers, by default, all event handlers are registered for the bubbling phase. So in our current example, when you select the video, the event bubbles from the <video> element outwards to the <html> element. Along the way:
- It finds the video.onclick... handler and runs it, so the video first starts playing.
- It then finds the videoBox.onclick... handler and runs it, so the video is hidden as well.
Fixing the problem with stopPropagation() This is a very annoying behavior, but there is a way to fix it! The standard Event object has a function available on it called stopPropagation() which, when invoked on a handler's event object, makes it so that first handler is run but the event doesn't bubble any further up the chain, so no more handlers will be run.
We can, therefore, fix our current problem by changing the second handler function in the previous code block to this:
video.onclick = function(e) {
e.stopPropagation();
video.play();
};
Event delegation
Bubbling also allows us to take advantage of event delegation — this concept relies on the fact that if you want some code to run when you select any one of a large number of child elements, you can set the event listener on their parent and have events that happen on them bubble up to their parent rather than having to set the event listener on every child individually. Remember, bubbling involves checking the element the event is fired on for an event handler first, then moving up to the element's parent, etc.
A good example is a series of list items as if you want each one to pop up a message when selected, you can set the click event listener on the parent <ul>, and events will bubble from the list items to the <ul>.
HTTP Management
Introduction
At Angular 7, the default implementation for Http client was rxJs 6.3.3.
Http Client
Set up
Angular provides its own Http client. This can be found in the httpclientmodule. You need to add the to your app in the bootstrap module, usually app.module.ts.
import { HttpClientModule } from '@angular/common/http';
...
imports: [
BrowserModule,
AppRoutingModule,
FormsModule,
CoreModule,
HttpClientModule,
...
Passing Headers
We can do this by creating an object and passing it the http request.
this.http.get<Book>(`./api/books/${id}`, {
headers: new HttpHeaders({
'Accept': 'application/json',
'Authorization': 'my-token',
})
});
REST API
Introduction
The http client provides the API for the REST API functions
Add
addBook(book: Book): Observable<Book> {
return this.http.post<Book>(
`./api/books`,
book,
{
headers: new HttpHeaders({
'Accept': 'application/json',
})
});
}
Update
updateBook(updateBook: Book): Observable<void> {
return this.http.put<void>(
`./api/books/${updateBook.bookID}`,
updateBook,
{
headers: new HttpHeaders({
'Accept': 'application/json',
})
});
}
Delete
deleteBook(bookID: number): Observable<void> {
return this.http.delete<void>(`./api/books/${bookID}`);
}
Handling Errors
The examples recommended catching the errors and wrapping them in an application specific object. There are many ways to handle errors but here is the approach suggested.
Object For Error
export class BookTrackerError {
errorNumber: number;
message: string;
friendlyMessage: string;
}
Error Handler
handleHttpError(error: HttpErrorResponse) : Observable<BookTrackerError> {
let dataError = new BookTrackerError();
dataError.errorNumber = 100;
dataError.message = error.statusText;
dataError.friendlyMessage = "An Error received retrieving data"
return throwError(dataError);
}
Implemented on catch on Data Service
We need to use the catchError operator within the pipe and make sure the return argument allows for both return types.
getAllBooks(): Observable<Book[] | BookTrackerError> {
return this.http.get<Book[]>('./api/books').pipe(
catchError(err => this.handleHttpError(err))
)
}
Implemented on call to Data Service
So now when we use the data service the error is output to the console. Obviously we might want something else in the app.
this.dataService.getAllBooks()
.subscribe(
(data: Book[]) => this.allBooks = data,
(err: BookTrackerError) => console.log(err.friendlyMessage),
()=> console.log("Completes")
);
Using Observable Operators
Because we are using the rxjs we can use any of the operators which work with them. In this example we are transforming a new book type to an old book type and reporting it at the end using tap. Note with rxjs it was necessary to import using the notation used below.
import { map,tap } from "rxjs/operators";
...
getOldBookById(id: number): Observable<OldBook> {
return this.http.get<Book>(`./api/books/${id}`)
.pipe(
map(b => <OldBook>{
bookID: b.bookID,
bookTitle: b.title,
year: b.publicationYear
}),
tap(classicBook => console.log(classicBook)),
);
}
Resolvers
Resolvers allow you to get the data prior to navigating to the screen. This perhaps, provides a better experience for the user and definitely in the case or error. Resovlers are implemented as a service.
Creating Resolver
Here is the example of the resolver. This wraps data from the data service
import { DataService } from 'app/core/data.service';
import { BookTrackerError } from './../models/bookTrackerError';
import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { Book } from 'app/models/book';
import { Observable } from 'rxjs';
@Injectable({
providedIn : 'root'
})
export class BooksResolverService implements Resolve<Book[] | BookTrackerError> {
constructor(private dataservice : DataService) {}
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) : Observable<Book[] | BookTrackerError> {
return this.dataservice.getAllBooks();
}
}
Change Router
We need to tell the router we want to use a resolver and we need to provide, the resolver and label the data for retrieval in the page.
import { BooksResolverService } from './core/books-resolver.service';
...
const routes: Routes = [
{ path: 'dashboard', component: DashboardComponent, resolve: {resolvedBooks: BooksResolverService} },
...
Change Page to Get Data from Resolver
We need to, add the activatedRoute to the page, get the data using the name from above and handle if we got data or an error.
...
constructor(private dataService: DataService,
private title: Title,
private route: ActivatedRoute) { }
ngOnInit() {
let resolvedData : Book [] | BookTrackerError = this.route.snapshot.data["resolvedBooks"]
if(resolvedData instanceof BookTrackerError)
{
console.log(`Dashboard error for ${resolvedData.friendlyMessage}`)
} else {
this.allBooks = resolvedData
}
...
Interceptors
Introduction
Interceptors are useful for
- Adding headers to all requests
- Logging
- Reporting Progress
- Client-side caching
Defining
Interceptors are defined by implementing a HttpInterceptor interface. There can be more than one interceptor and therefore can be chained together
export class FirstInterceptor implements HttpInterceptor {
intercept(req: HtttpRequest<any>, next: HttpHandler) : Observable<HttpEvent<any>> {
// Request is immutable so need to copy
const modifiedRequest = req.clone
// Make changes
...
// Just Return
return next.handle(modifiedRequest)
// Or We can add pipes to manipulate.
.pipe(
tap(event => {
if(event instanceof HttpResponse) {
// Modify here
}
})
}
}
The specifying of providers was a little different but here is how to do it within app.module.ts
providers: [
{provide: HTTP_INTERCEPTORS, useClass: FirstInterceptor, multi: true },
{provide: HTTP_INTERCEPTORS, useClass: SecondInterceptor, multi: true },
],
Examples
Ordering
Note that the order in which you execute the interceptors is determined by the order in the modules providers.
...
{ provide: HTTP_INTERCEPTORS, useClass: AddHeaderInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: LogResponseInterceptor, multi: true },
...
Example 1 Add Header
Here is an example of adding the content type to the request. Don't for to add it to the providers.
import { Injectable } from "@angular/core";
import { HttpInterceptor, HttpHandler, HttpEvent, HttpRequest } from "@angular/common/http";
import { Observable } from "rxjs";
@Injectable()
export class AddHeaderInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
let myReq: HttpRequest<any> = req.clone({
setHeaders: {'ContentType' : 'application/json'}
})
return next.handle(myReq)
}
}
Example 2 Log Response
This example logs the response to the console.
import { Injectable } from "@angular/core";
import { HttpInterceptor, HttpHandler, HttpEvent, HttpRequest, HttpResponse, HttpEventType } from "@angular/common/http";
import { Observable } from "rxjs";
import { tap} from "rxjs/operators"
@Injectable()
export class LogResponseInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(req).pipe(
tap(event => {
if(event.type === HttpEventType.Response) {
console.log(event.body)
}
})
)
}
}
Caching With Interceptors
Role of The Cache Service
The role of the cache will be
- Provides a data structure for the cached items
- Add Items to the cache
- Retrieve items from the cache
- Remove items from the cache (cache invalidation)
Create the Cache Service
Let use the Angular CLI and not generate unit tests
ng g service services/HttpCache --spec false
And implement the cache
import { HttpResponse } from '@angular/common/http';
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class HttpCacheService {
private requests : any = {}
constructor() { }
put(url: string , repsonse: HttpResponse<any>) : void {
this.requests[url] = repsonse;
}
get(url: string) : HttpResponse<any> | undefined {
return this.requests[url];
}
invalidate(url: string) : void {
this.requests[url] = undefined;
}
invalidateCache() : void {
this.requests = {};
}
}
Create the Interceptor
This creates an interceptor to use the cache.
import { Injectable } from "@angular/core";
import { HttpInterceptor, HttpHandler, HttpEvent, HttpRequest, HttpResponse, HttpEventType } from "@angular/common/http";
import { Observable, of } from "rxjs";
import { tap} from "rxjs/operators"
import { HttpCacheService } from "app/services/http-cache.service";
import { url } from "inspector";
@Injectable()
export class CacheInterceptor implements HttpInterceptor {
constructor(private cachedService : HttpCacheService) {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// Pass along non-cacheable requests and invalidates the cache
if(req.method !== 'GET') {
console.log("Clearing the cache")
this.cachedService.invalidateCache()
return next.handle(req)
}
// Get have a get request
// attempt to retrieve a cached HttpRequest
let cachedResponse : HttpResponse<any> = this.cachedService.get(req.url)
if(cachedResponse) {
// Wrapping in an observable using of method
// This is not going to the next interceptor because we have what we want.
console.log("Got a cached item")
return of(cachedResponse)
}
// send request to server add response to CacheInterceptor
return next.handle(req).pipe(
tap(event => {
if(event.type === HttpEventType.Response) {
console.log("Adding to the cache")
this.cachedService.put(req.url, event)
}
})
)
}
}
Unit Tests
Introduction
The demo showed the basics of using jasmine where the structure of the tests amounted to
describe('DataService Test Suite', () => {
beforeEach(() => {
// Do some stuff
})
it('test 1', () => {
// Do some stuff
})
it('test 2', () => {
// Do some stuff
})
afterEach(() => {
// Do some stuff
})
}
Example
Here is the full example for testing part of the data service
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController, TestRequest } from '@angular/common/http/testing';
import { DataService } from './data.service';
import { Book } from 'app/models/book';
import { BookTrackerError } from 'app/models/bookTrackerError';
describe('DataService Tests', () => {
let dataService: DataService;
let httpTestingController: HttpTestingController;
let testBooks: Book[] = [
{ bookID: 1, title: 'Goodnight Moon', author: 'Margaret Wise Brown', publicationYear: 1953 },
{ bookID: 2, title: 'Winnie-the-Pooh', author: 'A. A. Milne', publicationYear: 1926 },
{ bookID: 3, title: 'The Hobbit', author: 'J. R. R. Tolkien', publicationYear: 1937 }
];
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ HttpClientTestingModule ],
providers: [ DataService ]
});
dataService = TestBed.get(DataService);
httpTestingController = TestBed.get(HttpTestingController);
});
afterEach(() => {
httpTestingController.verify();
});
it('should GET all books', () => {
dataService.getAllBooks()
.subscribe((data: Book[]) => {
expect(data.length).toBe(3);
});
let booksRequest: TestRequest = httpTestingController.expectOne('/api/books');
expect(booksRequest.request.method).toEqual('GET');
booksRequest.flush(testBooks);
});
it('should return a BookTrackerError', () => {
dataService.getAllBooks()
.subscribe(
(data: Book[]) => fail('this should have been an error'),
(err: BookTrackerError) => {
expect(err.errorNumber).toEqual(100);
expect(err.friendlyMessage).toEqual('An error occurred retrieving data.');
}
);
let booksRequest: TestRequest = httpTestingController.expectOne('/api/books');
booksRequest.flush('error', {
status: 500,
statusText: 'Server Error'
});
});
});
Services
Introduction
Angular provides the management of services. We can use dependency injections to provide services.
Building a Service
To build a service we need to define a class and add the @Injectable decorator.
import { Injectable } from '@angular/core';
import { IProduct } from './product';
@Injectable()
export class ProductService {
getProducts(): IProduct[] { return [
{
"productId": 1,
"productName": "Leaf Rake",
"productCode": "GDN-0011",
"releaseDate": "March 19, 2019",
"description": "Leaf rake with 48-inch wooden handle.",
"price": 19.95,
"starRating": 3.2,
"imageUrl": "assets/images/leaf_rake.png"
},
{
"productId": 2,
"productName": "Garden Cart",
"productCode": "GDN-0023",
"releaseDate": "March 18, 2019",
"description": "15 gallon capacity rolling garden cart",
"price": 32.99,
"starRating": 4.2,
"imageUrl": "assets/images/garden_cart.png"
},
....
]
}
}
Registering a Service
You can register services to either the root or just to component and its children. To register it with the root we set the providedIn: to root on the @Injectable decorator.
@Injectable({
providedIn: 'root'
})
Injecting A Service
We do this the same way as we do it in javascript. We create a constructor, pass in the service and assign it to a variable in the class. This is not much different to C#
export class ProductListComponent {
private _productService : ProductService;
constructor(inProductService: ProductService) {
this._productService = inProductService
}
// For short
constructor(private _productService: ProductService)
}
We also need to initialize our products from this service. To do this once then we should use the OnInit function.
ngOnInit(): void {
this.products = this._productService.getProducts();
this.filteredProducts = this.products
}
Retrieving Data
Add the HttpClientModule
No surprises
import { HttpClientModule } from '@angular/common/http';
...
imports: [
BrowserModule,
FormsModule,
HttpClientModule
],
Inject The Client the Service
Using the short code we learned in services we add a constructor to add the service to our service along with the imports.
constructor(private _httpClient: HttpClient) {}
Make the Http Call
This is as simple as passing the Url to the client.
getProducts(): Observable<IProduct[]> {
return this._httpClient.get<IProduct[]>(this.productUrl)
}
Subscribing
We need to subscribe to the returning observable and make sure the filtered products are set post deliver of the data (which is async)
ngOnInit(): void {
this._productService.getProducts().subscribe({
next: products => {
this.products = products;
this.filteredProducts = this.products;
},
});
}
Routing
Enable the Router
As ever we need to specify our router in the apps module. But with this we also need to specify our list of roots too. See [3] for more information.
import { RouterModule } from '@angular/router';
// And
....
imports: [
BrowserModule,
FormsModule,
HttpClientModule,
RouterModule.forRoot([]),
],
Configuring Example Routing
Here is what was used to define the routes for the example app. First set the base root in the index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>APM</title>
<base href="/">
....
</html>
And now the routes in app.module.ts
RouterModule.forRoot([
{ path: 'products', component: ProductListComponent },
{ path: 'products/id', component: ProductDetailComponent },
{ path: 'welcome', component: WelcomeComponent },
{ path: '', redirectTo: 'welcome', pathMatch: 'full' },
{ path: '**', redirectTo: 'welcome', pathMatch: 'full' }
]),
Binding to Controls
This is done via the routerLink element. For example
<ul class=='nav navabar-nav>
<li><a [routerLink]="['welcome']">Home</a></li>
<li><a [routerLink]="['products']">Home</a></li>
</ul>
Displaying the View
To display the view we also need to put the routers selector (router-outlet) into some html. In our case this was app.component.ts
template: `
<nav class='navbar navbar-expand navbar-light bg-light'>
<a class='navbar-brand'>{{pageTitle}}</a>
<ul class='nav nav-pills'>
<li><a class='nav-link' routerLinkActive='active' [routerLink]="['/welcome']">Home</a></li>
<li><a class='nav-link' routerLinkActive='active' [routerLink]="['/products']">Product List</a></li>
</ul>
</nav>
<div class='container'>
<router-outlet></router-outlet>
</div>
`,
Implementing Details Click
To display the detail behind a product read from our list we set up an anchor tag
<a [routerLink]="['/products',product.productId]">
{{product.productName}}
</a>
Now within the detail component we need to import the ActivatedRoute component to read the id of the product passed. We do this be injection using the shorthand.
import {ActivatedRoute} form '@angular/router'
...
constructor(private route: ActivatedRoute) {};
...
// Note the plus is a shortcut to convert string to number in javascript
ngOnInit(): void {
const param = +this.route.snapshot.paramMap.get("id");
if (param) {
const id = +param;
this.getProduct(id);
}
}
Import the router service as a dependencies and call navigate['products'];
import { ActivatedRoute, Router } from '@angular/router';
constructor(
private route: ActivatedRoute,
private router: Router) {};
onBack(): void {
this.router.navigate(['/products']);
}
Then stick a button on to activate
<div class='card-footer'>
<button class='btn btn-outline-secondary'
(click)='onBack()'
style='width:80px'>
<i class='fa fa-chevron-left'></i> Back
</button>
</div>
Guarding Routes
Angular provides methods to prevent usage of routes they include
- CanActivate Guard navigation to route
- CanDeactivate Guard navigation from a route
- Resolve Pre-fetch data before activating a route
- CanLoad Prevent asynchronous routing
To implement derive a service from the relevant guard and attach it to the routing. We can generate the code using the cli and selecting the appropriate guard.
ng g g products/product-detail
This creates the skeleton of a class and all that is required is to add the router as a service and the logic to determine if ok. In the example this is just bogus.
...
export class ProductDetailGuard implements CanActivate {
constructor(private router: Router) { }
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
const id = +next.url[1].path;
// const id2 = next.paramMap.get('id');
// console.log(id2);
if (isNaN(id) || id < 1) {
alert('Invalid product Id');
this.router.navigate(['/products']);
return false;
}
return true;
}
}
We do, of course, need to add it to the code which does the routing in app.module
RouterModule.forRoot([
{ path: 'products', component: ProductListComponent },
{
path: 'products/id',
canActivate: [ProductDetailGuard],
component: ProductDetailComponent
},
Modules
Creating the Template
We can create a module template using the cli
ng g m products/product --flat -m app
The --flat is because we have our content and this, by default, assumes a new module/folder. The -m app adds the module to our app.module.ts.
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
@NgModule({
declarations: [],
imports: [
CommonModule
]
})
export class ProductModule { }
Adding Our Modules
Now we can add our code to the modules declarations array along with the imports
...
import { ProductListComponent } from './product-list.component';
import { ConvertToSpacesPipe } from '../shared/convert-to-spaces.pipe';
import { StarComponent } from '../shared/star.component';
import { ProductDetailComponent } from './product-detail.component';
...
@NgModule({
declarations: [
ProductListComponent,
ConvertToSpacesPipe,
StarComponent,
ProductDetailComponent,
],
imports: [
CommonModule
...
Other Third-Party/External Modules
These need to be added to the imports if required. In our case this was FormsModule and RouterModule.
Routing Considerations
The router does not need to be registered in the module as it will need to be registered with the app. Because of this the product routes, which are owned by this module, must be declared using the forChild and not forRoot
imports: [
CommonModule,
FormsModule,
RouterModule.forChild([
{ path: 'products', component: ProductListComponent },
{
path: 'products/id',
canActivate: [ProductDetailGuard],
component: ProductDetailComponent
},
]),
]
We want share common code, often this is modules like CommonModule and FormsModule. Specifying these in one shared modules means we do not have to repeat this for each module. Below is an example of a shared module which does this.
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { StarComponent } from './star.component';
import { FormsModule } from '@angular/forms';
@NgModule({
declarations: [
StarComponent
],
imports: [
CommonModule
],
exports: [
StarComponent,
CommonModule,
FormsModule
]
})
export class SharedModule { }
Deployment
To deploy using apache I
- Made an Apache conf
- Added .htaccess
- Changed the base
Apache Conf
Here is the conf.
# Those aliases do not work properly with several hosts on your apache server
# Uncomment them to use it or adapt them to your configuration
Alias /APM /mnt/nosaying/APM
<Directory /mnt/nosayingAPM>
Options +FollowSymlinks
AllowOverride All
Require all granted
</Directory>
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
htaccess
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /APM/
RewriteRule ^index\.html$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . index.html [L]
</IfModule>
Changed the base
For index.html I changed the base to point to the subfolder.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>APM</title>
<base href="/APM/">
...
Angular Animation Example
This is an example of how to make things work using Angular
CCS Approach
The css code
.thing {
transition: transform .25s ease-in;
}
.triggername .thing {
transform: translateX(100px);
}
The html code
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="test.css">
<script type="text/javascript" src="jquery-3.5.1.min.js"></script>
<script type="text/javascript" src="test.js"></script>
</head>
<body>
<div class="thing">FRED</div>
</body>
</html>
The js code
$(window).on('load',function(){
$("body").addClass("triggername");
});
The Angular Approach
In Angular I tried to add an animation using the java script code but it failed miserably so I was forced to read the documentation The HTML
<div [@triggerName]="isTriggerActive ? 'ACTIVE' : 'DEACTIVE'">
<div>FRED</div>
</div>
<button (click)='toggle()'> Click</button>
The typescript
import { Component } from '@angular/core';
import { trigger, state, style, transition, animate } from '@angular/animations';
@Component({
animations: [
trigger('triggerName', [
state('ACTIVE', style({
opacity: 0.5,
transform: 'translateX(0px)'
})),
state('DEACTIVE', style({
opacity: 1,
transform: 'translateX(100px)'
})),
transition('ACTIVE => DEACTIVE', [
animate('1s')
]),
transition('DEACTIVE => ACTIVE', [
animate('0.2s')
]),
]),
],
templateUrl: 'test.component.html',
styleUrls: ['test.component.css']
})
export class TestComponent {
isTriggerActive : boolean
toggle() {
this.isTriggerActive = !this.isTriggerActive;
}
}
Angular Releases
Version 4
On 13 December 2016 Angular 4 was announced, skipping 3 to avoid a confusion due to the misalignment of the router package's version which was already distributed as v3.3.0.[14] The final version was released on March 23, 2017.[15] Angular 4 is backward compatible with Angular 2.[16]
Angular version 4.3 is a minor release, meaning that it contains no breaking changes and that it is a drop-in replacement for 4.x.x.
Features in version 4.3
Introducing HttpClient, a smaller, easier to use, and more powerful library for making HTTP Requests.
New router life cycle events for Guards and Resolvers. Four new events: GuardsCheckStart, GuardsCheckEnd, ResolveStart, ResolveEnd join the existing set of life cycle event such as NavigationStart.
Conditionally disable animations.
Version 5
Angular 5 was released on November 1, 2017.[17] Key improvements in Angular 5 include support for progressive web apps, a build optimizer and improvements related to Material Design.[18]
Version 6
Angular 6 was released on May 4, 2018.[19]. This is a major release focused less on the underlying framework, and more on the toolchain and on making it easier to move quickly with Angular in the future, like: ng update, ng add, Angular Elements, Angular Material + CDK Components, Angular Material Starter Components, CLI Workspaces, Library Support, Tree Shakable Providers, Animations Performance Improvements, and RxJS v6.
Version 7
Angular 7 was released on October 18, 2018. Updates regarding Application Performance, Angular Material & CDK, Virtual Scrolling, Improved Accessibility of Selects, now supports Content Projection using web standard for custom elements, and dependency updates regarding Typescript 3.1, RxJS 6.3, Node 10 (still supporting Node 8).[20]
Version 8
Angular 8 was released on May 28, 2019. Featuring Differential loading for all application code, Dynamic imports for lazy routes, Web workers, TypeScript 3.4 support, and Angular Ivy as an opt-in preview. Angular Ivy opt-in preview includes:[21]
- Generated code that is easier to read and debug at runtime
- Faster re-build time
- Improved payload size
- Improved template type checking
- Backwards compatibility
Version 9
Angular 9 was released on February 6, 2020. Version 9 moves all applications to use the Ivy compiler and runtime by default. Angular has been updated to work with TypeScript 3.6 and 3.7. In addition to hundreds of bug fixes, the Ivy compiler and runtime offers numerous advantages:
- Smaller bundle sizes
- Faster testing
- Better debugging
- Improved CSS class and style binding
- Improved type checking
- Improved build errors
- Improved build times, enabling AOT on by default
- Improved Internationalization
CORS
Put this here to remind myself of how to do this. You cannot call an endpoint on the same domain without implementing CORs. CORs needs to be implemented on the server so if you are on the client and do not have access to the server code you need to use a proxy.
In the case we are going to implement
Example
We need to do 3 steps
- Identify Configuration
- Create a Proxy config
- Implement in Project
- Change Angular config
- What Good looks like
Identify Configuration
This is the set up I needed to solve
The server end point was http://localhost:8080
The client angular project was http://localhost:4200
Create a Proxy config
This example config proxy.conf.json' is usually in the <project>src directory. In this example we are looking to redirect all http://localhost:8080/api calls. So in this file we specify the server current endpoint, address and port.
{
"/api/*": {
"target": "http://localhost:8080",
"secure": false,
"logLevel": "debug"
}
}
Implement in Project
We now need to implement this in the project by specifying the port the application is running on for the endpoint.
export class SearchService {
mediaFiles$ = this.http.get<IMediaFile[]>(
'http://localhost:4200/api/mediaFiles'
)
Change Angular config
We need to change the angular.json configuration in the root directory of the project assuming we are using ng serve.
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "project-name:build",
"proxyConfig": "src/proxy.conf.json"
},
...
What Good looks like
Finally when we run the software this is what it looks like when it works
iwiseman@OLIVER:~/dev/projects/bibblempng$ npm start
> bibblempng@0.0.0 start
> ng serve
⠋ Generating browser application bundles (phase: setup)...[HPM] Proxy created: /api -> http://localhost:8080
[HPM] Subscribed to http-proxy events: [ 'error', 'close' ]
✔ Browser application bundle generation complete.
Initial Chunk Files | Names | Size
vendor.js | vendor | 2.51 MB
polyfills.js | polyfills | 128.50 kB
main.js | main | 67.34 kB
runtime.js | runtime | 6.62 kB
styles.css | styles | 736 bytes
| Initial Total | 2.71 MB
Build at: 2021-09-16T20:08:05.938Z - Hash: 1e2e2f6bdba2d861581f - Time: 6355ms
** Angular Live Development Server is listening on localhost:4200, open your browser on http://localhost:4200/ **
✔ Compiled successfully.
[HPM] GET /api/mediaFilesx -> http://localhost:8080
Apache Setup
Virtual Site
Set up a standard virtual site
</VirtualHost>
<VirtualHost *:443>
ServerName site.com
DocumentRoot /var/www/site.com/
<Directory /var/www/site.com/>
Require all granted
</Directory>
#
# SSL
#
SSLCertificateFile /etc/ssl/certs/server.crt
SSLCertificateKeyFile /etc/ssl/private/server.key
Include /etc/letsencrypt/options-ssl-apache.conf
#
# Log definitions
#
ErrorLog ${APACHE_LOG_DIR}/site.com-error.log
CustomLog ${APACHE_LOG_DIR}/site.com-access.log combined
</VirtualHost>
Set up the rewrite Rules
Add an entry for each conf you want to exclude. In my case mediawiki and phpldapadmin
# Rewrite rules
RewriteEngine On
# Do not redirect existing files and directories
RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI} -f [OR]
RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI} -d [OR]
# Do not redirect confs
RewriteCond %{REQUEST_URI} ^/mediawiki [OR]
RewriteCond %{REQUEST_URI} ^/phpldapadmin
# If the requested resource doesn't exist, use index.html
RewriteRule ^ - [L]
RewriteRule ^ /index.html
Proxy
You can set up the proxy for CORs issues with proxy pass
SSLProxyEngine On
ProxyPass /v1/api/myEndpoint https://site.com/v1/api/myEndpoint
Switching from CSS to SCCS
In Angular.json
- Replace styles.css with style.sccs (x2)
- Convert src/styles.css to src/styles.scss
- Rename existing components and convert their css to sccs
- Set schematics
"schematics": {
"@schematics/angular:component": {
"styleext": "scss"
}
}