{% embed url="https://github.com/PatrickJS/awesome-angular" %}
- Angular is a Javascript Framework which allows you to create reactive Single-Page-Applications (SPAs)
- Angular 1 (now referred as Angular JS it's totally different) Obsolete now it has been rewritten
- Angular 2 -> Angular 4 -> ... -> Angular 9 -> Angular 11 (Nothing changed since Angular 2)
- Install Angular CLI using npm
- create new project using
ng new my-first-project
- It's advanced version of Javascript. Simple Janilla Javascript lacks types, classes, Interfaces
- Typescript gets compiled to Javascript then run in browser but this compilation is very fast
- npm install --save bootstrap
- Then in angular.json in styles add path
"node_modules/bootstrap/dist/css/bootstrap.min.css"
Before default styles.css
- Angular uses bunch of modules, default is app module which has declaration of all components within that module, imports are external modules which are imported.
- App Component is then called, each component has seperate css, ts, html and .spec.ts (for testing)
- You can manually create a new component, put in new folder take care of naming convention and add it to module or type
ng generate component servers
- by default it will create css, ts, html and .spec.ts file for every component use
ng g c comp_name --inlineTemplate --inlineStyle
to create inline component. You can set it as default behaviour by going to angular.json file and edit schematics property
- --flat flag don't make a new folder
- Shows data from typescript to html
export class ServerComponent implements OnInit {
serverId: number = 10;
serverStatus: string = 'offline';
constructor() { }
ngOnInit() { }
}
// In HTML
<p>Server with {{ serverId }} is {{ serverStatus }}</p>
- Change some property of Element/Component (uses [...])
- Do some event (uses (...))
export class ServersComponent implements OnInit {
allowNewServer = false;
constructor() {
setTimeout(() => {
this.allowNewServer = true;
}, 2000);
}
ngOnInit(): void {
}
}
// In HTML
<button
class="btn btn-primary"
[disabled]="!allowNewServer"
(click)="onClickAddServer($event)"
>
Add Server
</button>
- Two way bind data say for textbox, here it uses a special directive ng-model
// HTML
<input type="text" class="form-control" [(ngModel)]="serverName" />
// Make sure to import this in App module
import { FormsModule } from '@angular/forms';
// And put this inside imports of App module
FormsModule
- Directives are instructions in the DOM
#noServer in below is a reference to an HTML component which can be refereed within HTML document. To use it in TS
@ViewChild('name-of-ref') refVariable: ElementRef;
Now we can use refVariable. Get element using -refVariable.nativeElement.value;
// * because it's a structural directive
<p *ngIf="serverCreated; else noServer">Server was created!</p>
<ng-template #noServer>
<p>No Server was created!</p>
</ng-template>
/* severCreated is a bool defined in component.ts
ng-template marks a portion with some tag, so that it can be used later
*/
// ngStyle derrivative, here it's binded with getColor method that's why inside []
<p [ngStyle]="{backgroundColor: getColor()}">{{ status }}</p>
// ngClass, it adds css class based on condition specified
<p [ngClass]="{online: isOnline, offline: !isOnline}">{{ status }}</p>
// ngFor
<p *ngFor="let item of items">{{ item }}</p>
You can also create nameOfComponent.model.ts file to store some typeScript class maybe like a data class which is getting instanced in the original component.ts
Install Augury Chrome Extension
- ng g i habbitInterface
export interface Habit {
id: number;
title: string;
count: number;
streak?: boolean; // ? means optional
}
// inside a class ts, HabbitService is just a class that
// does http calls and returns list of habbits
ngOnInit(): void {
this.habbits = this.habitService.getHabits().pipe(map(habits => {
return habits.map(habit => {
habit.stream = habit.count > 5 : true : false;
return habit;
})
}));
}
/* basically streak is computed property that is set
automatically */
// parent.html
<app-server-element
*ngFor="let serverElement of serverElements"
[element]="serverElement"
></app-server-element>
// child.ts
export class ServerElementComponent implements OnInit {
@Input() element: { type: string, name: string, content: string };
constructor() { }
ngOnInit(): void { }
}
// child.html
{{ element.name }}
- Without @Input decorator the variable won't be accessible as property
- You can set an alias instead of variable name by
@Input('alias-instead-of-variable-name')
// child.ts
export class CockpitComponent implements OnInit {
@Output() serverCreated = new EventEmitter<{ serverName: string, serverContent: string }>();
@Output() blueprintCreated = new EventEmitter<{ blueprintName: string, blueprintContent: string }>();
newServerName = '';
newServerContent = '';
constructor() { }
ngOnInit(): void { }
onAddServer() { // called by button in child.html
this.serverCreated.emit({ serverName: this.newServerName, serverContent: this.newServerContent });
}
onAddBlueprint() { // called by button in child.html
this.blueprintCreated.emit({ blueprintName: this.newServerName, blueprintContent: this.newServerContent });
}
}
// parent.html
<app-cockpit
(serverCreated)="onServerAdded($event)"
(blueprintCreated)="onBlueprintAdded($event)"
></app-cockpit>
- You can set an alias instead of variable name by
@Output('alias-instead-of-variable-name')
CSS defined in a component say for p, then it gets applied for all p element However in Angular it gets applied to only that component. It's a great behaviour as it ensures that whichever css we apply gets applied to the same css.
How Angular does that, is it basically adds attribute to each element based on what component it belongs to like ng0, ng1 then it uses that as selector. There's also a shadow DOM method which is only supported in new browsers
// component.ts
@Component({
selector: 'app-server-element',
templateUrl: './server-element.component.html',
styleUrls: ['./server-element.component.css'],
encapsulation: ViewEncapsulation.Emulated // use this line
})
/* encapsulation - Emulated is default behaviour
Native is same as Emulated but it uses shadow DOM method which is not supported
by older browsers so Native is not recommended.
Last, there's None which gives default encapsulation behaviour and attribute of
component wont get set. */
By default everything placed inside the custom-component-tag in HTML is lost
// some-component.html
...
<ng-content></ng-content>
...
This hook puts all the content placed inside while calling <some-component>....</some-component> to the ng-content place
Say this content has some #ref and you want to use it then just like @ViewChild there's @ContentChild
@ContentChild('name-of-ref') refVariable: ElementRef;
- There are 2 types of directives - Attribute & Structural directives
- Attribute directives changes the attribute like style and stuff, eg: ngStyle, ngClass
- Structural directives changes the structure (can remove), eg: *ngIf, *ngFor
- To create a new directive
ng generate directive name-of-derrivate
<p appBetterHighlight [defaultColor]="'yellow'" [highlightColor]="'red'">...</p>
import { Directive, ElementRef, HostBinding, HostListener, Input, OnInit, Renderer2 } from '@angular/core';
@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() { }
@HostListener('mouseenter') mouseEnter(eventData: Event) {
this.backgroundColor = this.highlightColor;
}
@HostListener('mouseleave') mouseLeave(eventData: Event) {
this.backgroundColor = this.defaultColor;
}
}
<div *appUnless="condition">...</div>
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
@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) { }
}
<div [ngSwitch]="value">
<p *ngSwitchCase="5">Value is 5</p>
<p *ngSwitchCase="10">Value is 10</p>
<p *ngSwitchDefault>Value is 10</p>
</div>
import { Directive, HostBinding, HostListener } from '@angular/core';
@Directive({
selector: '[appDropdown]'
})
export class DropdownDirective {
@HostBinding('class.open') isOpen = false;
@HostListener('click') toggleOpen() {
this.isOpen = !this.isOpen;
}
}
- Use
ng generate service name-of-service
- To use the new service in say your xyz.ts file add
providers: [LoggingService]
to @Component or whole module and thenconstructor(private loggingService: LoggingService) {}
then you can simply call the service bythis.loggingService.logStatusChange(accountStatus)
// LoggingService.ts
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class LoggingService {
logStatusChange(msg: string) { console.log(msg); }
}
This provider dependency injector is hierarchial in nature that means if we provide the service (using providers) to a component than that component and all it's child will get that service and that too of same instance, so data within service will be shared.
// app.module.ts
const appRoutes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'users/:id', component: UsersComponent },
{ path: 'servers', component: ServersComponent }
];
// Then in it's imports array add
RouterModule.forRoot(appRoutes)
// Finally in app.component.html add
<router-outlet></router-outlet>
// to the place where you want the router to show
To get route param, in the component.ts file of that route add
private route: ActivatedRoute;
to the constructor and then get param bythis.route.snapshot.params['id']
Inside the ngOnInit
- Then
<a href="/servers">..</a>
This works however what it does is it restarts the entire app instead of href use routerLink this will work just like that but doesnt restart app hence more fast /path
goes to rootpath/path whereas if it'spath
and say user is already at /path then it will go to /path/path so it basically will append.../path
This will go back one from current path and then add
<ul class="nav nav-tabs">
<li
role="presentation"
routerLinkActive="active"
[routerLinkActiveOptions]="{ exact: true }"
>
<a routerLink="/">Home</a>
</li>
<li role="presentation" routerLinkActive="active">
<a routerLink="/servers">Servers</a>
</li>
<li role="presentation" routerLinkActive="active">
<a routerLink="/users">Users</a>
</li>
</ul>
routerLinkActive add the active css class if that route is active and routerLinkActiveOptions exact makes sure that it is exact since / and /servers both has / in it but we only want home to be active in case of / so that's why exact is required.
- To programmatically route somewhere
constructor(private router: Router) { }
onLoadServer() {
this.router.navigate(['/servers']);
}
// for relative path 'path' instead of '/path' relativeTo needs to be specified
constructor(private router: Router, private route: ActivatedRoute) { }
onLoadServer() {
this.router.navigate(['/servers'], {relativeTo: this.route});
}
To get params use
this.route.snapshot.param['param_name']
inside ngOnInit however this might not work in a scenerio say there's a button on that component which routes to the same component but with different param. Angular will not reload the component since it knows that component is already there.
Add this in ngOnInit
paramsSubscription: Subscription;
ngOnInit() {
this.id = this.route.snapshot.params['id'];
this.paramsSubscription = this.route.params.subscribe((params: Params) => {
this.id = params['id'];
});
}
ngOnDestroy() {
this.paramsSubscription.unsubscribe();
}
// this params.suscribe is something called observable which is called when
// the event is observed
- To set additional query params. Below will go to - /servers/5/edit?query1=1#loading
<a
[routerLink]="['/servers', 5, 'edit']"
[queryParams]="{query1: '1'}"
fragment="loading">
...
</a>
this.router.navigate(['/servers', 5, 'edit'],
{queryParams: {query1: '1'}, fragment: 'loading'});
- To get the queryParams and framents just like snapshot & params.subscribe simillary do it for queryParams and fragments.
Parsed params and queryParams are always string to make them int add + in their initialization
- To add nested routing
const addRoutes: Routes = [
{ path: 'servers', component.ServersComponent, children: [
{ path: ':id', component: ServerComponent },
{ path: ':id/edit', component: EditServerComponent },
] }
]
// Inside ServersComponent.html add <router-outlet></router-outlet>
const appRoutes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'not-found', component: NotFound },
{ path: '**', redirectTo: '/not-found' }
];
- If the size of router is too large usually create a new module
ng g m AppRoutingModule
// AppRoutingModule.ts
const appRoutes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'users', component: UsersComponent, children: [
{ path: ':id/:name', component: UserComponent }
] },
{
path: 'servers',
// canActivate: [AuthGuard],
canActivateChild: [AuthGuard],
component: ServersComponent,
children: [
{ path: ':id', component: ServerComponent, resolve: {server: ServerResolver} },
{ path: ':id/edit', component: EditServerComponent, canDeactivate: [CanDeactivateGuard] }
] },
// { path: 'not-found', component: PageNotFoundComponent },
{ path: 'not-found', component: ErrorPageComponent, data: {message: 'Page not found!'} },
{ path: '**', redirectTo: '/not-found' }
];
@NgModule({
imports: [
// RouterModule.forRoot(appRoutes, {useHash: true})
RouterModule.forRoot(appRoutes)
],
exports: [RouterModule]
})
export class AppRoutingModule {
}
// In App.module.ts imports simply add AppRoutingModule
- Here this is auth guard
// auth.service.ts
export class AuthService {
loggedIn = false;
isAuthenticated() {
const promise = new Promise(
(resolve, reject) => {
setTimeout(() => {
resolve(this.loggedIn);
}, 800);
}
);
return promise;
}
login() {
this.loggedIn = true;
}
logout() {
this.loggedIn = false;
}
}
// auth-guard.service.ts
import {
CanActivate,
ActivatedRouteSnapshot,
RouterStateSnapshot,
Router,
CanActivateChild
} from '@angular/router';
import { Observable } from 'rxjs/Observable';
import { Injectable } from '@angular/core';
import { AuthService } from './auth.service';
@Injectable()
export class AuthGuard implements CanActivate, CanActivateChild {
constructor(private authService: AuthService, 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(['/']);
}
}
);
}
canActivateChild(route: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
return this.canActivate(route, state);
}
}
// Then finally add guards to the route
{
path: 'servers',
// canActivate: [AuthGuard],
canActivateChild: [AuthGuard],
component: ServersComponent,
children: [...]
}
canActivate has an array of guards which ensures that the component is guarded we are using CanActivateChild instead it will render the parent component but not child so it's like having CanActivate to all the child
For example if user is submitting a form and he accidentally navigate to some other page then a prompt should say do you want to save changes.
// can-deactivate-guard.service.ts
import { Observable } from 'rxjs/Observable';
import { CanDeactivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
export interface CanComponentDeactivate {
canDeactivate: () => Observable<boolean> | Promise<boolean> | boolean;
}
export class CanDeactivateGuard implements CanDeactivate<CanComponentDeactivate> {
canDeactivate(component: CanComponentDeactivate,
currentRoute: ActivatedRouteSnapshot,
currentState: RouterStateSnapshot,
nextState?: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
return component.canDeactivate();
}
}
// edit-server.component.ts
export class EditServerComponent implements OnInit, CanComponentDeactivate {
canDeactivate(): Observable<boolean> | Promise<boolean> | boolean {
// return true if you want navigation guard to go off
return confirm('Do you want to discard the changes?');
}
}
// Then finally add guards to the route
{
path: ':id/edit',
component: EditServerComponent,
canDeactivate: [CanDeactivateGuard]
}
{
path: 'not-found',
component: ErrorPageComponent,
data: {message: 'Page not found!'}
}
/* Now to the component you want to use that data
use snapshot.data['message'] again suscribe to it */
Routing works in local environment but to make it work in web server also need to ensure that the server returns index.html if there's a 404 error. Some older browser and server don't support that for this RouterModule.forRoot(appRoutes, {useHash: true})
private firstObsSubscription: Subscription;
ngOnInit() {
this.firstObsSubscription = interval(1000).subscribe(count => {
console.log(count);
});
}
ngOnDestroy(): void {
this.firstObsSubscription.unsubscribe();
}
private firstObsSubscription: Subscription;
ngOnInit() {
const customIntervalObservable = Observable.create(observer => {
let count = 0;
setInterval(() => {
observer.next(count);
count++;
}, 1000);
});
this.firstObsSubscription = customIntervalObservable.subscribe(data => {
console.log(data);
});
}
ngOnDestroy(): void {
this.firstObsSubscription.unsubscribe();
}
- Just like next there's also complete() and error(new Error("New error"));
- To handle error:-
this.firstObsSubscription = customIntervalObservable.subscribe(data => {
console.log(data);
}, error => {
console.log(error);
}, () => {
console.log("Completed");
});
- Say in above interval count example, we want to have Round 1, Round 2, Round 3... instead of 0, 1, 2...
this.firstObsSubscription = customIntervalObservable
.pipe(map((data: number) => {
return 'Round: ' + (data + 1);
})).subscribe(data => {
console.log(data);
}, error => {
console.log(error);
}, () => {
console.log("Completed");
});
/* Like map there's also filter and many more
https://www.learnrxjs.io/learn-rxjs/operators */
- Simple template driven approach
- Make sure FormsModule is in imports array of app.module.ts
// In html, last 2 attributes are important
<form (ngSubmit)="onSubmit(f)" #f="ngForm">
<input type="email" class="form-control" ngModel name="email">
</form>
// In component.ts
export class AppComponent {
onSubmit(form: NgForm) {
console.log(form.value.email);
}
}
// Instead of passing f to onSubmit, we can also use it directly in ts
@ViewChild('f') signupForm: ngForm;
this.signupForm.value.email;
// Add attribute required or email to form input element
// then in form button
<button class="btn btn-primary" type="submit" [disabled]="!f.valid">Submit</button>
// .css of that component
input.ng-invalid.ng-touched {
border: 1px solid red;
}
/* Now button won't be active unless form input is valid and red border will
appear on input if we leave it (touched dirty) in invalid state */
<input
type="email"
class="form-control"
ngModel
name="email"
required
email
#email="ngModel">
<span class="help-block" *ngIf="!email.valid && email.touched">
Please enter a valid email!
</span>
// for a form element set ngModel
[ngModel]="'default value'"
// or
[ngModel]="someVariable"
You can have a div with attribute ngModelGroup="groupName" then all the element inside it will be grouped as one. Now you can put it as ref and check validity of that group maybe.
Say you want to put value to form (during maybe startup from API)
@ViewChild('f') signupForm: NgForm;
changeVal() {
this.signupForm.setValue({
username: "abc",
email: "abc.xyz.com"
});
}
// or to overwrite part of form
this.signupForm.form.patchValue({/* ... same as above ... */ });
this.signupForm.reset();
Say you have a username which you want to display. You want the username to be uppercase while displaying but you don't want to change its value
{{ username | uppercase }} // ANKIT
{{ dateObj | date:"fullDate" }} // Sunday, Aug 8, 2020
{{ username | uppercase | date }} // Will apply Left to Right
- ng generate pipe shorten
// shorten.pipe.ts
@Pipe({
name: 'shorten'
})
export class ShortenPipe implements PipeTransform {
transform(value: any, limit: number) {
if (value.length > limit)
return value.substr(0, limit);
return value;
}
}
Add HTTPClientModule to imports of module.ts
constructor(private http: HttpClient) {}
onCreatePost(postData: { title: string; content: string}) {
this.http.post('...', postData).subscribe(response => {
console.log(response);
});
// No need to unsuscribe to these observables because it will never leak
}
this.http.get<{ [key: string]: Post }>('...', {
headers: new HTTPHeaders({...}),
params: ...,
responseType: 'json'
});
Usually you want certain header to be applied to many, so use interceptor
// auth-interceptor.service.ts
import { HttpInterceptor, HttpRequest, HttpHandler } from '@angular/common/http';
export class AuthInterceptorService implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler) {
console.log('Request is on its way');
return next.handle(req);
}
}
// module.ts
declarations: [AppComponent],
imports: [BrowserModule, FormsModule, HttpClientModule],
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptorService,
multi: true
}
],
bootstrap: [AppComponent]
Now interceptor will get called before every HTTP request and if you want to filter some behavior then do it inside interceptor you can check url and stuff
- Declarations hold declarations for components, pipes, directive...
- Imports holds other modules dependency
- Providers hold all the services we want to use
- Bootrstrap holds what is starting point of app, it can have multiple starting point too (it's an array) for that you need to have multiple <app-root></app-root> in index.html
Every module works on it's own, they don't communicate with each other
- Instead of BrowserModule import CommonModule to self made modules
- Declarations of certain component, pipes... can be put on only 1 module multiple declarations are not allowed
Using multiple module is pre requisite. Idea is to download smaller bundle of the required module only unless user don't visit it.
// To route add
{
path: 'recipes', loadChildren: () => import('./recipes/recipes.module')
.then(m => m.RecipesModule)
}
// Don't add the RecipesModule to imports now, don't load it eagerly when loading
// lazily
// After lazy loading path of module lazy loaded in it's router becomes relative
Preload all lazy loaded module before hand
// app.routing.module.ts
imports: [
RouterModule.forRoot(routes, {preloadingStrategy: PreloadAllModules})
],
- Constants for development and production in Environment file
import { environment } fromt '../../environments/environment';
// use environment.APIKey;
Angular will automatically swap dev and prod constants
- ng build --prod
- npm i --save @ngrx/store
// shopping-list.actions.ts
import { Action } from '@ngrx/store';
export const ADD_INGREDIENT = 'ADD_INGREDIENT';
export class AddIngredient implements Action {
readonly type = ADD_INGREDIENT;
constructor(public payload: Ingredient) {}
}
export type ShoppingListActions = AddIngredient // | RemoveIngredients;
// shopping-list.reducer.ts
import { Ingredient } from '../shared/ingredient.model';
import * as ShoppingListActions from './shopping-list.actions';
export interface State {
ingredients: Ingredients[];
...
}
export interface AppState {
shoppingList: State;
}
const initialState : State = {
ingredients: [
new Ingredient('Apples', 5),
new Ingredient('Tomatoes', 10),
]
};
export function shoppingListReducer(state: State = initialState,
action: ShoppingListActions.ShoppingListActions) {
switch(action.type) {
case ShoppingListActions.ADD_INGREDIENT:
return {
...state, // always have old state
ingredients: [...state.ingredients, action.payload]
};
default:
return state;
}
}
Then in AppModule
import { StoreModule } from '@ngrx/store';
// Add to imports array - StoreModule.forRoot({shoppingList: shoppingListReducer})
Then to the component you want to use that store
// add to constructor
import * as fromShoppingList from './store/shopping-list.reducer';
constructor(
private store: Store<fromShoppingList.AppState>
) {}
// Then in ngOnInit
this.ingredients = this.store.select('shoppingList');
// ingredients here is an observable
ingredients: Observable<{ ingredients: Ingredient[] }>;
// To fetch data from observable in componenet you can use async pipe
<a *ngFor="let ingredient of (ingredients | async).ingredients">
{{ ingredient.name }} ({{ ingredient.amount }})
</a>
// Angular will subscribe to observable for us
Dispatch action at shopping-edit-component
import * as ShoppingListActions from './shopping-list.actions';
import * as fromShoppingList from './store/shopping-list.reducer';
// add to constructor
constructor(
private store: Store<fromShoppingList.AppState>
) {}
this.store.dispatch(new ShoppingListActions.AddIngredients(...));
Pre render so that user receive rendered pages. Pre rendering helps crawlers and SEO
- ng add @nguniversal/express-engine --clientProject <identifier found under projects of angular.json>
- npm i --save @nguniversal/module-map-ngfactory-loader
- Then add ModuleMapLoaderModule to app.server.module.ts imports array
/* Now the project gets run at server, but the problem is some API are browser
only like LocalStorage so to handle that */
constructor(
@Inject(PLATFORM_ID) private platformId;
) {}
// then on ngOnInit
if (isPlatformBrowser(this.platformId)) {
...
}