Introduction
Angular is a powerful TypeScript-based front-end web application framework maintained by Google. It provides a comprehensive solution for building single-page applications (SPAs) with its component-based architecture, powerful templating system, and robust tooling. Angular’s opinionated structure promotes consistent code organization, making it ideal for large-scale enterprise applications.
Core Concepts & Principles
Angular Building Blocks
Building Block | Description |
---|
Components | The fundamental UI building blocks in Angular; encapsulate data, HTML templates, and styling |
Modules | Containers for organizing related components, directives, pipes, and services |
Services | Reusable classes for sharing functionality across components through dependency injection |
Directives | Add behavior to DOM elements (e.g., *ngIf , *ngFor ) |
Pipes | Transform displayed values within templates (e.g., formatting dates, filtering lists) |
Guards | Control access to routes based on conditions |
Interceptors | Intercept and modify HTTP requests and responses |
Decorators | Metadata annotations that configure Angular features (e.g., @Component , @Injectable ) |
Component Lifecycle Hooks
Hook | Execution Timing | Use Case |
---|
ngOnChanges() | Before ngOnInit and when input properties change | React to input property changes |
ngOnInit() | Once, after the first ngOnChanges | Initialize component after Angular sets inputs |
ngDoCheck() | During change detection | Implement custom change detection logic |
ngAfterContentInit() | After content projection | Initialize projected content |
ngAfterContentChecked() | After checking projected content | Respond to projected content changes |
ngAfterViewInit() | After component’s view is initialized | Access and manipulate child views |
ngAfterViewChecked() | After checking component’s view | Respond to view changes |
ngOnDestroy() | Before component is destroyed | Clean up subscriptions, event handlers, etc. |
Setting Up Angular Projects
Installation & Project Creation
# Install Angular CLI
npm install -g @angular/cli
# Create a new Angular project
ng new my-app-name
# Add routing during creation
ng new my-app-name --routing
# Specify CSS preprocessor
ng new my-app-name --style=scss
# Generate components, services, etc.
ng generate component component-name
ng generate service service-name
ng generate module module-name
ng generate pipe pipe-name
ng generate directive directive-name
ng generate guard guard-name
ng generate interface interface-name
ng generate enum enum-name
ng generate class class-name
# Development server
ng serve
ng serve --open # Opens browser automatically
ng serve --port 4201 # Custom port
# Build for production
ng build --configuration production
Angular Templates Syntax
Data Binding
<!-- Interpolation -->
<p>{{ expression }}</p>
<p>Hello, {{ user.name }}</p>
<!-- Property Binding -->
<img [src]="imageUrl">
<button [disabled]="isDisabled">Click me</button>
<!-- Event Binding -->
<button (click)="onClick()">Click me</button>
<input (keyup)="onKeyUp($event)">
<!-- Two-way Binding -->
<input [(ngModel)]="name">
Built-in Directives
<!-- Conditional Rendering -->
<div *ngIf="condition">Content to show if condition is true</div>
<div *ngIf="condition; else elseBlock">Content if true</div>
<ng-template #elseBlock>Content if false</ng-template>
<!-- List Rendering -->
<ul>
<li *ngFor="let item of items; let i = index; trackBy: trackByFn">
{{ i }} - {{ item.name }}
</li>
</ul>
<!-- Switch Cases -->
<div [ngSwitch]="condition">
<div *ngSwitchCase="'case1'">Content for case1</div>
<div *ngSwitchCase="'case2'">Content for case2</div>
<div *ngSwitchDefault>Default content</div>
</div>
<!-- Class Binding -->
<div [ngClass]="{'active': isActive, 'disabled': isDisabled}"></div>
<!-- Style Binding -->
<div [ngStyle]="{'color': textColor, 'font-size': fontSize + 'px'}"></div>
Template Reference Variables
<input #nameInput type="text">
<button (click)="greet(nameInput.value)">Greet</button>
Pipes
<!-- Built-in Pipes -->
{{ dateValue | date:'short' }}
{{ price | currency:'USD' }}
{{ text | uppercase }}
{{ text | lowercase }}
{{ number | number:'1.2-2' }}
{{ object | json }}
{{ array | slice:1:3 }}
{{ text | titlecase }}
{{ value | percent:'2.2-2' }}
<!-- Pipe Chaining -->
{{ dateValue | date:'fullDate' | uppercase }}
<!-- Parameterized Pipes -->
{{ dateValue | date:'MM/dd/yyyy' }}
Component Communication
Parent to Child: Input Properties
// Child component
@Component({...})
export class ChildComponent {
@Input() data: string;
@Input('aliasName') originalName: string;
}
// Parent template
<app-child [data]="parentData" [aliasName]="parentProperty"></app-child>
Child to Parent: Output Properties & EventEmitter
// Child component
@Component({...})
export class ChildComponent {
@Output() dataChange = new EventEmitter<string>();
sendToParent() {
this.dataChange.emit('Data to parent');
}
}
// Parent template
<app-child (dataChange)="handleData($event)"></app-child>
View Child and Content Child
@Component({...})
export class ParentComponent implements AfterViewInit {
@ViewChild(ChildComponent) childComp: ChildComponent;
@ViewChild('inputRef') inputElement: ElementRef;
@ViewChildren(ChildComponent) childrenComps: QueryList<ChildComponent>;
@ContentChild(ContentChildComponent) contentChild: ContentChildComponent;
@ContentChildren(ContentChildComponent) contentChildren: QueryList<ContentChildComponent>;
ngAfterViewInit() {
// Access view child elements after view initialization
this.childComp.someMethod();
this.inputElement.nativeElement.focus();
}
}
Angular Services & Dependency Injection
Creating and Injecting Services
// Service definition
@Injectable({
providedIn: 'root' // Application-wide singleton
})
export class DataService {
getData() {
return ['data1', 'data2'];
}
}
// Component using the service
@Component({...})
export class MyComponent {
data: string[];
constructor(private dataService: DataService) {
this.data = this.dataService.getData();
}
}
Injection Providers & Hierarchies
// Module-level provider
@NgModule({
providers: [
DataService,
{ provide: API_URL, useValue: 'https://api.example.com' },
{ provide: LoggerService, useClass: ProductionLoggerService },
{ provide: CacheService, useFactory: cacheFactory, deps: [StorageService] }
]
})
// Component-level provider (instance only for this component and its children)
@Component({
providers: [DataService]
})
Angular Routing
Basic Routing Configuration
// app-routing.module.ts
const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'products', component: ProductListComponent },
{ path: 'products/:id', component: ProductDetailComponent },
{ path: 'admin', loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule) },
{ path: '**', component: NotFoundComponent }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
Router Directives and Links
<!-- Router outlet where components will be rendered -->
<router-outlet></router-outlet>
<!-- Router links -->
<a routerLink="/products">Products</a>
<a [routerLink]="['/products', product.id]">{{ product.name }}</a>
<!-- Active link styles -->
<a routerLink="/products" routerLinkActive="active-link">Products</a>
<a routerLink="/admin" [routerLinkActiveOptions]="{exact: true}" routerLinkActive="active">Admin</a>
Route Parameters & Query Parameters
// Accessing route parameters
constructor(private route: ActivatedRoute) {}
ngOnInit() {
// Snapshot approach
const id = this.route.snapshot.paramMap.get('id');
// Observable approach (recommended for params that can change while component is active)
this.route.paramMap.subscribe(params => {
const id = params.get('id');
});
// Query parameters
this.route.queryParamMap.subscribe(params => {
const page = params.get('page');
const sort = params.get('sort');
});
}
Router Navigation
constructor(private router: Router) {}
navigate() {
// Simple navigation
this.router.navigate(['/products']);
// With params
this.router.navigate(['/products', productId]);
// With query params
this.router.navigate(['/products'], {
queryParams: { page: 1, sort: 'name' }
});
// Relative navigation
this.router.navigate(['details'], { relativeTo: this.route });
// Preserve query params
this.router.navigate(['/list'], {
queryParamsHandling: 'preserve' // or 'merge'
});
}
Route Guards
// CanActivate guard
@Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate {
constructor(private authService: AuthService, private router: Router) {}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
if (this.authService.isLoggedIn()) {
return true;
}
this.router.navigate(['/login'], { queryParams: { returnUrl: state.url } });
return false;
}
}
// Route with guards
const routes: Routes = [
{
path: 'admin',
component: AdminComponent,
canActivate: [AuthGuard],
canDeactivate: [CanDeactivateGuard],
canLoad: [AuthGuard],
resolve: {
data: DataResolver
}
}
];
Forms in Angular
Template-Driven Forms
<!-- Template-driven form -->
<form #userForm="ngForm" (ngSubmit)="onSubmit(userForm.value)">
<div>
<label for="name">Name</label>
<input id="name" name="name" [(ngModel)]="user.name" required #name="ngModel">
<div *ngIf="name.invalid && (name.dirty || name.touched)">
<div *ngIf="name.errors?.['required']">Name is required</div>
</div>
</div>
<div>
<label for="email">Email</label>
<input id="email" name="email" [(ngModel)]="user.email" email #email="ngModel">
<div *ngIf="email.invalid && (email.dirty || email.touched)">
<div *ngIf="email.errors?.['email']">Invalid email format</div>
</div>
</div>
<button type="submit" [disabled]="!userForm.valid">Submit</button>
</form>
Reactive Forms
// Component
import { FormBuilder, FormGroup, Validators, FormArray } from '@angular/forms';
@Component({...})
export class UserFormComponent implements OnInit {
userForm: FormGroup;
constructor(private fb: FormBuilder) {}
ngOnInit() {
this.userForm = this.fb.group({
name: ['', [Validators.required, Validators.minLength(2)]],
email: ['', [Validators.required, Validators.email]],
address: this.fb.group({
street: [''],
city: [''],
zipcode: ['', Validators.pattern(/^\d{5}$/)]
}),
skills: this.fb.array([
this.fb.control('')
])
});
}
get skills() {
return this.userForm.get('skills') as FormArray;
}
addSkill() {
this.skills.push(this.fb.control(''));
}
removeSkill(index: number) {
this.skills.removeAt(index);
}
onSubmit() {
if (this.userForm.valid) {
console.log(this.userForm.value);
}
}
}
<!-- Reactive form template -->
<form [formGroup]="userForm" (ngSubmit)="onSubmit()">
<div>
<label for="name">Name</label>
<input id="name" formControlName="name">
<div *ngIf="userForm.get('name')?.invalid && userForm.get('name')?.touched">
<div *ngIf="userForm.get('name')?.errors?.['required']">Name is required</div>
<div *ngIf="userForm.get('name')?.errors?.['minlength']">Name must be at least 2 characters</div>
</div>
</div>
<div formGroupName="address">
<h3>Address</h3>
<input formControlName="street" placeholder="Street">
<input formControlName="city" placeholder="City">
<input formControlName="zipcode" placeholder="Zipcode">
</div>
<div>
<h3>Skills</h3>
<div formArrayName="skills">
<div *ngFor="let skill of skills.controls; let i = index">
<input [formControlName]="i">
<button type="button" (click)="removeSkill(i)">Remove</button>
</div>
</div>
<button type="button" (click)="addSkill()">Add Skill</button>
</div>
<button type="submit" [disabled]="userForm.invalid">Submit</button>
</form>
Custom Validators
// Custom validator function
function forbiddenNameValidator(nameRe: RegExp): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const forbidden = nameRe.test(control.value);
return forbidden ? { forbiddenName: { value: control.value } } : null;
};
}
// Using custom validator in form
this.userForm = this.fb.group({
name: ['', [Validators.required, forbiddenNameValidator(/admin/i)]],
// other fields...
});
// Custom async validator
function uniqueEmailValidator(userService: UserService): AsyncValidatorFn {
return (control: AbstractControl): Observable<ValidationErrors | null> => {
return userService.checkEmailExists(control.value).pipe(
map(exists => exists ? { emailExists: true } : null),
catchError(() => of(null))
);
};
}
// Using async validator
this.userForm = this.fb.group({
email: ['', {
validators: [Validators.required, Validators.email],
asyncValidators: [uniqueEmailValidator(this.userService)],
updateOn: 'blur'
}],
// other fields...
});
HTTP Client
Basic HTTP Requests
@Injectable({
providedIn: 'root'
})
export class DataService {
private apiUrl = 'https://api.example.com/items';
constructor(private http: HttpClient) {}
// GET request
getItems(): Observable<Item[]> {
return this.http.get<Item[]>(this.apiUrl);
}
// GET request with parameters
getItemById(id: number): Observable<Item> {
return this.http.get<Item>(`${this.apiUrl}/${id}`);
}
// POST request
addItem(item: Item): Observable<Item> {
return this.http.post<Item>(this.apiUrl, item);
}
// PUT request
updateItem(id: number, item: Item): Observable<Item> {
return this.http.put<Item>(`${this.apiUrl}/${id}`, item);
}
// DELETE request
deleteItem(id: number): Observable<void> {
return this.http.delete<void>(`${this.apiUrl}/${id}`);
}
}
HTTP Headers & Parameters
import { HttpHeaders, HttpParams } from '@angular/common/http';
// With headers
getItems(): Observable<Item[]> {
const headers = new HttpHeaders()
.set('Content-Type', 'application/json')
.set('Authorization', 'Bearer ' + this.authService.getToken());
return this.http.get<Item[]>(this.apiUrl, { headers });
}
// With URL parameters
searchItems(term: string, page: number): Observable<Item[]> {
const params = new HttpParams()
.set('query', term)
.set('page', page.toString())
.set('limit', '10');
return this.http.get<Item[]>(`${this.apiUrl}/search`, { params });
}
HTTP Interceptors
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
constructor(private authService: AuthService) {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// Clone the request and add auth header
const authToken = this.authService.getToken();
const authReq = req.clone({
headers: req.headers.set('Authorization', `Bearer ${authToken}`)
});
// Pass the cloned request to the next handler
return next.handle(authReq);
}
}
// Register in AppModule
@NgModule({
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }
]
})
Error Handling
getItems(): Observable<Item[]> {
return this.http.get<Item[]>(this.apiUrl).pipe(
catchError(this.handleError<Item[]>('getItems', []))
);
}
private handleError<T>(operation = 'operation', result?: T) {
return (error: HttpErrorResponse): Observable<T> => {
console.error(`${operation} failed: ${error.message}`);
// Send error to remote logging service
this.logService.logError(error);
// Let the app keep running by returning an empty result
return of(result as T);
};
}
RxJS in Angular
Common RxJS Operators
import {
map, filter, tap, catchError, switchMap,
debounceTime, distinctUntilChanged, mergeMap,
concatMap, takeUntil, take, skip, throttleTime
} from 'rxjs/operators';
import { of, from, Observable, Subject, combineLatest, forkJoin } from 'rxjs';
// Example usage in a search component
@Component({...})
export class SearchComponent implements OnInit, OnDestroy {
searchTerms = new Subject<string>();
results$: Observable<Result[]>;
private destroy$ = new Subject<void>();
constructor(private searchService: SearchService) {}
ngOnInit() {
this.results$ = this.searchTerms.pipe(
// Wait 300ms after each keystroke
debounceTime(300),
// Ignore if same as previous search term
distinctUntilChanged(),
// Switch to new search observable each time the term changes
switchMap(term => term
? this.searchService.search(term)
: of([])
),
// Handle errors
catchError(error => {
console.error(error);
return of([]);
}),
// Unsubscribe when component is destroyed
takeUntil(this.destroy$)
);
}
search(term: string): void {
this.searchTerms.next(term);
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}
Combining Observables
// combineLatest - emits when any source observable emits
combineLatest([observable1$, observable2$]).subscribe(([result1, result2]) => {
console.log(result1, result2);
});
// forkJoin - waits for all observables to complete, then emits last values
forkJoin({
users: this.userService.getUsers(),
config: this.configService.getConfig()
}).subscribe(({ users, config }) => {
console.log(users, config);
});
// merge - flattens multiple Observables together
merge(observable1$, observable2$).subscribe(result => {
console.log(result); // Could be from either observable
});
Unsubscribing Patterns
// Option 1: Using takeUntil with a destroy Subject
@Component({...})
export class MyComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
ngOnInit() {
this.someObservable$.pipe(
takeUntil(this.destroy$)
).subscribe(data => {
// Handle data
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}
// Option 2: Manually tracking subscriptions
@Component({...})
export class MyComponent implements OnDestroy {
private subscriptions = new Subscription();
someMethod() {
const sub = this.someObservable$.subscribe(data => {
// Handle data
});
this.subscriptions.add(sub);
}
ngOnDestroy() {
this.subscriptions.unsubscribe();
}
}
// Option 3: Using the async pipe in templates
@Component({
template: `<div *ngFor="let item of items$ | async">{{item.name}}</div>`
})
export class MyComponent {
items$: Observable<Item[]>;
constructor(private dataService: DataService) {
this.items$ = this.dataService.getItems();
}
}
State Management
Component State
@Component({...})
export class CounterComponent {
count = 0;
increment() {
this.count++;
}
decrement() {
this.count--;
}
}
Services for Shared State
@Injectable({
providedIn: 'root'
})
export class CartService {
private items: Product[] = [];
private cartSubject = new BehaviorSubject<Product[]>([]);
cart$ = this.cartSubject.asObservable();
addToCart(product: Product) {
this.items = [...this.items, product];
this.cartSubject.next(this.items);
}
removeFromCart(productId: number) {
this.items = this.items.filter(item => item.id !== productId);
this.cartSubject.next(this.items);
}
getItems() {
return this.items;
}
clearCart() {
this.items = [];
this.cartSubject.next(this.items);
}
}
NgRx Store
// Actions
export const increment = createAction('[Counter] Increment');
export const decrement = createAction('[Counter] Decrement');
export const reset = createAction('[Counter] Reset');
export const incrementBy = createAction(
'[Counter] Increment By',
props<{ amount: number }>()
);
// Reducer
export const initialState = 0;
export const counterReducer = createReducer(
initialState,
on(increment, state => state + 1),
on(decrement, state => state - 1),
on(reset, state => 0),
on(incrementBy, (state, { amount }) => state + amount)
);
// Selectors
export const selectCount = (state: AppState) => state.count;
// Store in component
@Component({...})
export class CounterComponent {
count$: Observable<number>;
constructor(private store: Store<AppState>) {
this.count$ = this.store.select(selectCount);
}
increment() {
this.store.dispatch(increment());
}
decrement() {
this.store.dispatch(decrement());
}
reset() {
this.store.dispatch(reset());
}
incrementBy(amount: number) {
this.store.dispatch(incrementBy({ amount }));
}
}
Testing Angular Applications
Component Testing
// component.spec.ts
describe('CounterComponent', () => {
let component: CounterComponent;
let fixture: ComponentFixture<CounterComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [CounterComponent]
}).compileComponents();
fixture = TestBed.createComponent(CounterComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should increment count', () => {
const initialValue = component.count;
component.increment();
expect(component.count).toBe(initialValue + 1);
});
it('should render count value', () => {
component.count = 42;
fixture.detectChanges();
const compiled = fixture.nativeElement;
expect(compiled.querySelector('.count-value').textContent).toContain('42');
});
it('should call increment when increment button is clicked', () => {
spyOn(component, 'increment');
const button = fixture.nativeElement.querySelector('.increment-button');
button.click();
expect(component.increment).toHaveBeenCalled();
});
});
Service Testing
// service.spec.ts
describe('DataService', () => {
let service: DataService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [DataService]
});
service = TestBed.inject(DataService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpMock.verify();
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('should get items', () => {
const mockItems = [{ id: 1, name: 'Item 1' }, { id: 2, name: 'Item 2' }];
service.getItems().subscribe(items => {
expect(items).toEqual(mockItems);
});
const req = httpMock.expectOne('https://api.example.com/items');
expect(req.request.method).toBe('GET');
req.flush(mockItems);
});
it('should handle errors', () => {
service.getItems().subscribe({
next: () => fail('should have failed'),
error: error => {
expect(error.status).toBe(404);
}
});
const req = httpMock.expectOne('https://api.example.com/items');
req.flush('Not found', { status: 404, statusText: 'Not Found' });
});
});
Testing with Mocks
// Mock service
const mockDataService = {
getItems: jasmine.createSpy('getItems').and.returnValue(of([
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' }
]))
};
// Test with mocked service
describe('ItemListComponent', () => {
let component: ItemListComponent;
let fixture: ComponentFixture<ItemListComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ItemListComponent],
providers: [
{ provide: DataService, useValue: mockDataService }
]
}).compileComponents();
fixture = TestBed.createComponent(ItemListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should fetch items on init', () => {
expect(mockDataService.getItems).toHaveBeenCalled();
expect(component.items.length).toBe(2);
});
});
Performance Optimization
Change Detection Strategies
@Component({
selector: 'app-item',
template: '...',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ItemComponent {
@Input() item: Item;
// Component will only update when:
// 1. Input references change (not just properties of the input)
// 2. Component events trigger
// 3. Observable with async pipe emits
// 4. Manually triggering change detection
}
TrackBy Function for NgFor
<div *ngFor="let item of items; trackBy: trackByItemId">
{{ item.name }}
</div>
trackByItemId(index: number, item: Item): number {
return item.id;
}
Pure Pipes for Transformations
@Pipe({
name: 'filter',
pure: true // Default is true
})
export class FilterPipe implements PipeTransform {
transform(items: any[], field: string, value: any): any[] {
if (!items) return [];
if (!value) return items;
return items.filter(item => item[field] === value);
}
}
Lazy Loading Modules
// app-routing.module.ts
const routes: Routes = [
{ path: '', component: HomeComponent },
{
path: 'admin',
loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule)
},
{
path: 'products',
loadChildren: () => import('./products/products.module').then(m => m.ProductsModule)
}
];
Common Angular Best Practices
Architecture
- Follow Angular Style Guide: Use the official Angular style guide for consistent code structure
- Modular Design: Organize by feature modules, shared modules, and core modules
- Smart vs. Presentational Components:
- Smart (container) components: Fetch data, manage state
- Presentational components: Display data, emit events
Performance
- Use OnPush Change Detection for pure components
- Lazy load modules for large applications
- Implement trackBy functions with *ngFor
- Virtual scrolling for long lists with ScrollingModule
- Optimize bundle size with tree-shaking
State Management
- Services with Observables for simpler applications
- NgRx/Redux pattern for complex applications
- Keep component state minimal and lift shared state up
Security
- Always sanitize user input
- Use Angular’s built-in XSS protection
- Implement proper authentication and authorization
- Use HttpInterceptors for adding auth tokens
- Avoid direct DOM manipulation when possible
Common Challenges and Solutions
Challenge | Solution |
---|
Circular Dependencies | Use forwardRef() in constructor params or redesign service relationships |
Complex Forms | Use FormBuilder with nested FormGroups and FormArrays |
Memory Leaks | Always unsubscribe from observables, use takeUntil operator |
Large Bundle Size | Implement lazy loading, code splitting, tree-shaking |
Slow Change Detection | Use OnPush strategy, pure pipes, detach components when needed |
Server-Side Rendering | Implement Angular Universal for SEO and performance |
IE11 Compatibility | Add polyfills and limit use of modern JavaScript features |
Debugging | Use Angular DevTools, Augury, or Redux DevTools |
Testing Components | Use TestBed, shallow rendering, and component harnesses |
Mobile Performance | Optimize bundle size, minimize DOM operations, use PWA features |
Resources for Further Learning
Official Documentation
Books
- “Angular: Up and Running” by Shyam Seshadri
- “Angular in Action” by Jeremy Wilken
- “NgRx in Angular” by Gion Kunz
Courses and Tutorials
- Angular University (angular-university.io)
- Pluralsight Angular Path
- Udemy – Angular: The Complete Guide
Tools
- Angular DevTools
- Augury
- NgRx DevTools
- Angular Language Service