מעבר מאנגולר 15 לאנגולר 18

כיום (22.10.24) הגרסה העדכנית של אנגולר היא 18. מכיוון שעד עכשיו כתבתי באנגולר 15, אכתוב על המעבר ומה שונה בגרסאות שעברו עד עכשיו.

אנגולר 16

Standalone Components by Default

אנגולר 15 – standalone components היו זמינים, אבל לא כברירת מחדל. עדיין היה חייב להיות מודול אחד לפחות.

// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { MyComponent } from './my.component';

@NgModule({
  declarations: [AppComponent, MyComponent],
  imports: [BrowserModule],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

אנגולר 16 – standalone components נוצרים כברירת מחדל.

// my.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-my-component',
  template: `<p>My Component works!</p>`,
  standalone: true,
})
export class MyComponent {}

// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app.component';

bootstrapApplication(AppComponent);

Signal-Based Reactivity

אנגולר 15 – שימוש ב-rxjs על מנת לעקוב אחרי שינויים שקורים בדף, שיתוף של מידע בין קומפוננטות, קבלת מידע מ-API וכו' כ-reactive.

// counter.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-counter',
  template: `
    <p>Count: {{ count }}</p>
    <button (click)="increment()">Increment</button>
  `,
})
export class CounterComponent {
  count = 0;

  increment() {
    this.count++;
  }
}

אנגולר 16 – שימוש ב-Signals.

// counter.component.ts
import { Component, signal } from '@angular/core';

@Component({
  selector: 'app-counter',
  template: `
    <p>Count: {{ count() }}</p>
    <button (click)="increment()">Increment</button>
  `,
  standalone: true,
})
export class CounterComponent {
  count = signal(0);

  increment() {
    this.count.update((value) => value + 1);
  }
}

Dependency Injection Enhancements with DestroyRef

אנגולר 15 – ניקיון של משאבים בפונקצית ngOnDestroy

// my.service.ts
import { Injectable, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';

@Injectable({
  providedIn: 'root',
})
export class MyService implements OnDestroy {
  private subscription: Subscription;

  constructor() {
    this.subscription = someObservable.subscribe();
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }
}

אנגולר 16 – ניקיון יעיל יותר של משאבים על ידי שימוש ב-DestroyRef

// my.service.ts
import { Injectable, inject, DestroyRef } from '@angular/core';

@Injectable({
  providedIn: 'root',
})
export class MyService {
  private destroyRef = inject(DestroyRef);
  private subscription = someObservable.subscribe();

  constructor() {
    this.destroyRef.onDestroy(() => {
      this.subscription.unsubscribe();
    });
  }
}

Route-Based Code Splitting with canLoad Guards

אנגולר 15

// app-routing.module.ts
const routes: Routes = [
  {
    path: 'admin',
    loadChildren: () =>
      import('./admin/admin.module').then((m) => m.AdminModule),
    canLoad: [AuthGuard],
  },
];

אנגולר 16 – מנגנון lazy loading משופר.

// app-routing.module.ts
import { inject } from '@angular/core';

const routes: Routes = [
  {
    path: 'admin',
    loadComponent: () =>
      import('./admin/admin.component').then((m) => m.AdminComponent),
    canMatch: [
      () => inject(AuthService).isLoggedIn(),
    ],
  },
];

Required Inputs

אנגולר 15 – אין דרך לאכוף בזמן כתיבת הקוד שליחה של ערך ב-@Input.

// my-component.component.ts
import { Component, Input } from '@angular/core';

@Component({
  selector: 'my-component',
  template: `<p>{{ name }}</p>`,
})
export class MyComponent {
  @Input() name!: string;
}

אנגולר 16 – אפשר לסמן ערך של @Input כ-required ולתפוס שגיאות כבר בזמן קומפילציה.

// my-component.component.ts
import { Component, Input } from '@angular/core';

@Component({
  selector: 'my-component',
  template: `<p>{{ name }}</p>`,
})
export class MyComponent {
  @Input({ required: true }) name!: string;
}

Router Inputs

אנגולר 15 – כדי לקבל פרמטרים שעוברים בכתובת, צריך לחלץ אותם לתוך משתנה.

// article.component.ts
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

@Component({
  selector: 'app-article',
  template: `<p>Article ID: {{ articleId }}</p>`,
})
export class ArticleComponent implements OnInit {
  articleId?: string;

  constructor(private route: ActivatedRoute) {}

  ngOnInit() {
    this.articleId = this.route.snapshot.paramMap.get('articleId') ?? undefined;
  }
}

אנגולר 16 – אפשר לקבל משתנים שעוברים בכתובת ישירות לתוך משתנה input.

// app.module.ts
import { RouterModule, withComponentInputBinding } from '@angular/router';

@NgModule({
  imports: [
    RouterModule.forRoot(routes, {
      bindToComponentInputs: true,
    }),
    // or
    RouterModule.forRoot(routes, withComponentInputBinding()),
  ],
})
export class AppModule {}
// app-routing.module.ts
const routes: Routes = [
  {
    path: 'articles/:articleId',
    component: ArticleComponent,
  },
];
// article.component.ts
import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-article',
  template: `<p>Article ID: {{ articleId }}</p>`,
})
export class ArticleComponent {
  @Input() articleId?: string;
}

הרחבה – Standalone Components

Standalone Components הן קומפוננטות שלא מוגדרות במודול ומסומנות ב-standalone: true. ההגדרה הזאת מאפשרת להן להיות עצמאיות ומפשטת את מבנה האפליקציה.

קומפוננטה שהיא Standalone צריכה לדאוג לבד לייבוא של כל מה שהיא משתמשת בו, אין לה קובץ מודול לסמוך עליו.

אנגולר 15

app.module.ts

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { HomeComponent } from './home/home.component';

@NgModule({
  declarations: [AppComponent, HomeComponent],
  imports: [BrowserModule],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

home.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-home',
  template: `<h1>Welcome to the Home Component!</h1>`,
})
export class HomeComponent {}

אנגולר 16

home.component.ts

import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-home',
  template: `<h1>Welcome to the Standalone Home Component!</h1>`,
  standalone: true,
  imports: [CommonModule],
})
export class HomeComponent {}

כדי להשתמש ב-Standalone Component בקומפוננטות אחרות, צריך לייבא אותה.

parent.component.ts

import { Component } from '@angular/core';
import { MyStandaloneComponent } from './my-standalone.component';

@Component({
  selector: 'app-parent',
  template: `
    <h1>Parent Component</h1>
    <app-my-standalone></app-my-standalone>
  `,
  standalone: true,
  imports: [MyStandaloneComponent],
})
export class ParentComponent {}

אפשר להגדיר לאפליקציה מה תהיה הקומפוננטה ההתחלתית.

main.ts

import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app.component';

bootstrapApplication(AppComponent);

שימוש ב-Lazy Loading

app.routes.ts

import { Routes } from '@angular/router';

export const routes: Routes = [
  {
    path: 'dashboard',
    loadComponent: () =>
      import('./dashboard/dashboard.component').then((m) => m.DashboardComponent),
  },
];

הזרקת services

// component-with-provider.component.ts
import { Component } from '@angular/core';
import { MyService } from './my-service.service';

@Component({
  selector: 'app-component-with-provider',
  template: `<p>Component with a service provider.</p>`,
  standalone: true,
  providers: [MyService],
})
export class ComponentWithProviderComponent {}

Standalone Directive

// highlight.directive.ts
import { Directive, ElementRef, Renderer2, HostListener } from '@angular/core';

@Directive({
  selector: '[appHighlight]',
  standalone: true,
})
export class HighlightDirective {
  constructor(private el: ElementRef, private renderer: Renderer2) {}

  @HostListener('mouseenter') onMouseEnter() {
    this.highlight('yellow');
  }
  @HostListener('mouseleave') onMouseLeave() {
    this.highlight(null);
  }
  private highlight(color: string | null) {
    this.renderer.setStyle(this.el.nativeElement, 'backgroundColor', color);
  }
}

Standalone Pipe

// truncate.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'truncate',
  standalone: true,
})
export class TruncatePipe implements PipeTransform {
  transform(value: string, limit: number = 20): string {
    return value.length > limit ? value.substring(0, limit) + '...' : value;
  }
}

מכיוון שאין מודולים בתוכנית, ארגון הקבצים לא יהיה לפי מודולים אלא לפי features.

src/
└── app/
    ├── features/
    │   ├── feature1/
    │   │   ├── feature1.component.ts
    │   │   ├── feature1.component.html
    │   │   ├── feature1.service.ts
    │   ├── feature2/
    │       ├── feature2.component.ts
    │       ├── feature2.component.html
    │       ├── feature2.service.ts
    ├── shared/
        ├── components/
        ├── directives/
        ├── pipes/

אפשר לשלב בתוכנית שימוש במודולים וב-standalone components.

אנגולר 17

New Built-in Control Flow Directives

שימוש ב-directives שמובנים בתוך הקוד.

אנגולר 15

<!-- Using *ngIf -->
<div *ngIf="condition">
  Content displayed if condition is true.
</div>

<!-- Using *ngFor -->
<ul>
  <li *ngFor="let item of items; trackBy: trackByFn">
    {{ item }}
  </li>
</ul>

<!-- Using *ngSwitch -->
<div [ngSwitch]="value">
  <div *ngSwitchCase="'case1'">Case 1 content</div>
  <div *ngSwitchCase="'case2'">Case 2 content</div>
  <div *ngSwitchDefault>Default content</div>
</div>

אנגולר 17

<!-- Using @if -->
<div @if="condition">
  Content displayed if condition is true.
</div>

<!-- Using @for -->
<ul>
  <li @for="let item of items; track item.id">
    {{ item }}
  </li>
</ul>

<!-- Using @switch -->
<div @switch="value">
  <div @case="'case1'">Case 1 content</div>
  <div @case="'case2'">Case 2 content</div>
  <div @default>Default content</div>
</div>

Enhanced Deferred Loading

שימוש ב-lazy loading משופר של ידי שימוש ב-@defer.

אנגולר 15

// app-routing.module.ts
const routes: Routes = [
  {
    path: 'feature',
    loadChildren: () =>
      import('./feature/feature.module').then((m) => m.FeatureModule),
  },
];

אנגולר 17

<!-- Template -->
<div @defer (onIdle)="loadComponent()">
  <!-- Content to be loaded lazily -->
</div>
// app-routing.module.ts
const routes: Routes = [
  {
    path: 'feature',
    loadComponent: () =>
      import('./feature/feature.component').then((c) => c.FeatureComponent),
    canActivate: ['defer'],
  },
];

Support for Custom Element Bindings and Providers

אינטגרציה טובה יותר לאלמנטים מיוצרים אישית.

אנגולר 15

// Defining a custom element
import { Component, NgModule, Injector } from '@angular/core';
import { createCustomElement } from '@angular/elements';

@Component({
  selector: 'my-element',
  template: `<p>My Custom Element</p>`,
})
export class MyElementComponent {}

// Module declaration
@NgModule({
  declarations: [MyElementComponent],
  entryComponents: [MyElementComponent],
})
export class AppModule {
  constructor(private injector: Injector) {
    const el = createCustomElement(MyElementComponent, { injector });
    customElements.define('my-element', el);
  }
}

אנגולר 17

// Using standalone components
import { Component, inject } from '@angular/core';

@Component({
  selector: 'my-element',
  template: `<p>My Custom Element</p>`,
  standalone: true,
})
export class MyElementComponent {}

// Defining the custom element
const el = createCustomElement(MyElementComponent, {
  injector: inject(Injector),
});
customElements.define('my-element', el);

הרחבה – @defer

@defer הוא directive שמאפשר lazy loading לחלקים של התבנית. כלומר לא לטעון חלקים מסויימים עד שתנאי מתקיים. מה שמייצר טעינה ראשונית מהירה יותר של הדף.

Load Content When Browser is Idle

התוכן יטען רק כשהדפדפן פנוי, מה שמאפשר טעינה מהירה של חלקים של הדף והתוכן הכבד יטען רק אחר כך.

<div @defer (onIdle)>
  <!-- Content to load when the browser is idle -->
  <app-heavy-component></app-heavy-component>
  <ng-template #loading>
    <p>Loading component...</p>
  </ng-template>
</div>

Load Content When Element is Visible

טעינה של חלק רק כאשר המשתמש גלל והגיע לחלק הזה בדף.

<div @defer (onVisible)>
  <!-- Content to load when visible in the viewport -->
  <img src="large-image.jpg" alt="Large Image" />
  <ng-template #loading>
    <p>Image is loading...</p>
  </ng-template>
</div>

Load Content After a Delay

טעינת תוכן רק אחרי זמן מסויים.

<div @defer (onTimer)="3000">
  <!-- Content to load after 3 seconds -->
  <app-delayed-content></app-delayed-content>
</div>

Load Content on User Interaction

טעינת תוכן רק אחרי פעולה של המשתמש.

<button @defer (onInteraction)>
  Show Details
  <ng-template>
    <!-- Deferred content -->
    <app-details></app-details>
  </ng-template>
</button>

Best Practices

  1. להשתמש ב-@defer על מנת לגרום לתוכן החשוב של הדף להטען ראשון ולקצר את זמן הטעינה הראשוני.
  2. @defer אמנם משפר ביצועים, אבל שימוש יתר יכול דוקא להזיק להם.
  3. להשתמש ב-loading על מנת הודיע למשתמש מה קורה, כדי שידע שהתוכן נטען.
  4. לבדוק ביצועים עם ובלי @defer.
  5. @defer יכול להשפיע על SEO מכיוון שחלק מהתוכן לא נטען בפעם הראשונה.
  6. נגישות – לראות שהשימוש ב-@defer לא פוגע בנגישות.

אנגולר 18

Route Redirects as Functions

אנגולר 15

// app-routing.module.ts (Angular 15)
const routes: Routes = [
  { path: 'home', component: HomeComponent },
  { path: 'dashboard', redirectTo: '/user/dashboard' },
];

אנגולר 18

// app.routes.ts (Angular 18)
import { Routes, Router } from '@angular/router';
import { inject } from '@angular/core';

const routes: Routes = [
  { path: 'home', component: HomeComponent },
  {
    path: 'dashboard',
    redirectTo: (route) => {
      const queryParams = route.queryParams;
      const isAdmin = queryParams['admin'];
      const router = inject(Router);

      if (isAdmin === 'true') {
        return '/admin/dashboard';
      } else if (isAdmin === 'false') {
        return '/user/dashboard';
      } else {
        // Handle error or redirect to not-found
        return '/not-found';
      }
    },
  },
];

Fallback Content for ng-content

אנגולר 15

// custom-widget.component.ts (Angular 15)
import { Component } from '@angular/core';

@Component({
  selector: 'app-custom-widget',
  template: `
    <ng-content select=".header"></ng-content>
    <ng-content></ng-content>
  `,
})
export class CustomWidgetComponent {}
<app-custom-widget>
  <span class="header">Custom Header</span>
  <p>Custom Content</p>
</app-custom-widget>

אנגולר 18

// custom-widget.component.ts (Angular 18)
import { Component } from '@angular/core';

@Component({
  selector: 'app-custom-widget',
  template: `
    <ng-content select=".header">Default Header</ng-content>
    <ng-content>Default Content</ng-content>
  `,
})
export class CustomWidgetComponent {}
<!-- Using default content -->
<app-custom-widget></app-custom-widget>

<!-- Overriding default header -->
<app-custom-widget>
  <span class="header">New Header</span>
</app-custom-widget>

Form's New Control State Change Events

אנגולר 15

// form.component.ts (Angular 15)
import { Component } from '@angular/core';
import { FormControl, Validators } from '@angular/forms';

@Component({
  selector: 'app-form',
  template: `<input [formControl]="nameControl" />`,
})
export class FormComponent {
  nameControl = new FormControl('', Validators.required);

  constructor() {
    this.nameControl.valueChanges.subscribe((value) => {
      console.log('Value changed:', value);
    });

    this.nameControl.statusChanges.subscribe((status) => {
      console.log('Status changed:', status);
    });
  }
}

אנגולר 18

// form.component.ts (Angular 18)
import { Component } from '@angular/core';
import { FormControl, Validators } from '@angular/forms';

@Component({
  selector: 'app-form',
  template: `<input [formControl]="nameControl" />`,
})
export class FormComponent {
  nameControl = new FormControl('', Validators.required);

  constructor() {
    this.nameControl.events.subscribe((event) => {
      if (event.type === 'valueChange') {
        console.log('Value changed:', event.value);
      } else if (event.type === 'statusChange') {
        console.log('Status changed:', event.status);
      }
    });
  }
}

הרחבה – signals

signals הוא סוג משתנה שמאפשר ריאקטיביות, כלומר כשהערך של המשתנה משתנה, תהיה השפעה על כל מי שתלוי במשתנה הזה.

דוגמאות לשימוש ב-signals.

Fetching Data from an API and Displaying It in a Component

אנגולר 15 – שימוש ב-rxjs על מנת לקבל מידע מ-API.

data.service.ts

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

export interface Item {
  id: number;
  name: string;
}

@Injectable({
  providedIn: 'root',
})
export class DataService {
  private apiUrl = 'https://api.example.com/items';

  constructor(private http: HttpClient) {}

  getItems(): Observable<Item[]> {
    return this.http.get<Item[]>(this.apiUrl);
  }
}

item-list.component.ts

import { Component, OnInit } from '@angular/core';
import { DataService, Item } from '../data.service';

@Component({
  selector: 'app-item-list',
  template: `
    <h2>Items</h2>
    <ul *ngIf="items">
      <li *ngFor="let item of items">{{ item.name }}</li>
    </ul>
  `,
})
export class ItemListComponent implements OnInit {
  items: Item[] = [];

  constructor(private dataService: DataService) {}

  ngOnInit() {
    this.dataService.getItems().subscribe((data) => (this.items = data));
  }
}

אנגולר 17 – שימוש ב-signals.

data.service.ts

import { Injectable, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';

export interface Item {
  id: number;
  name: string;
}

@Injectable({
  providedIn: 'root',
})
export class DataService {
  private apiUrl = 'https://api.example.com/items';
  items = signal<Item[]>([]);

  constructor(private http: HttpClient) {
    this.fetchItems();
  }

  fetchItems() {
    this.http.get<Item[]>(this.apiUrl).subscribe((data) => this.items.set(data));
  }
}

item-list.component.ts

import { Component } from '@angular/core';
import { DataService } from '../data.service';

@Component({
  selector: 'app-item-list',
  template: `
    <h2>Items</h2>
    <ul>
      <li *ngFor="let item of items()">{{ item.name }}</li>
    </ul>
  `,
})
export class ItemListComponent {
  items = this.dataService.items;

  constructor(private dataService: DataService) {}
}

דוגמא נוספת – שימוש בפונקציית post.

אנגולר 15

data.service.ts

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

export interface ItemDetails {
  id: number;
  name: string;
  description: string;
  // Other properties...
}

@Injectable({
  providedIn: 'root',
})
export class DataService {
  private apiUrl = 'https://api.example.com/item-details';

  constructor(private http: HttpClient) {}

  getItemDetails(id: number): Observable<ItemDetails> {
    const body = { id: id };
    return this.http.post<ItemDetails>(this.apiUrl, body);
  }
}

item-detail.component.ts

import { Component, OnInit } from '@angular/core';
import { DataService, ItemDetails } from '../data.service';

@Component({
  selector: 'app-item-detail',
  template: `
    <h2>Item Details</h2>
    <div *ngIf="itemDetails">
      <p><strong>ID:</strong> {{ itemDetails.id }}</p>
      <p><strong>Name:</strong> {{ itemDetails.name }}</p>
      <p><strong>Description:</strong> {{ itemDetails.description }}</p>
      <!-- Display other properties as needed -->
    </div>
  `,
})
export class ItemDetailComponent implements OnInit {
  itemDetails?: ItemDetails;
  itemId = 1; // Example id; in a real app, this might come from route parameters or user input.

  constructor(private dataService: DataService) {}

  ngOnInit() {
    this.dataService.getItemDetails(this.itemId).subscribe(
      (data) => {
        this.itemDetails = data;
      },
      (error) => {
        console.error('Error fetching item details:', error);
      }
    );
  }
}

אנגולר 17

data.service.ts

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { ItemDetails } from './item-details.model';

@Injectable({
  providedIn: 'root',
})
export class DataService {
  private apiUrl = 'https://api.example.com/item-details';

  constructor(private http: HttpClient) {}

  getItemDetails(id: number) {
    const body = { id: id };
    return this.http.post<ItemDetails>(this.apiUrl, body);
  }
}

item-detail.component.ts

import { Component, signal, effect } from '@angular/core';
import { DataService } from '../data.service';
import { ItemDetails } from '../item-details.model';
import { toSignal } from '@angular/core/rxjs-interop';

@Component({
  selector: 'app-item-detail',
  template: `
    <h2>Item Details</h2>
    <div *ngIf="itemDetails()">
      <p><strong>ID:</strong> {{ itemDetails().id }}</p>
      <p><strong>Name:</strong> {{ itemDetails().name }}</p>
      <p><strong>Description:</strong> {{ itemDetails().description }}</p>
    </div>
  `,
})
export class ItemDetailComponent {
  itemId = signal<number>(1); // Signal holding the ID
  itemDetails = signal<ItemDetails | null>(null); // Signal for item details

  constructor(private dataService: DataService) {
    effect(() => {
      const id = this.itemId();
      if (id) {
        // Fetch data when itemId changes
        const itemDetails$ = this.dataService.getItemDetails(id);
        const itemDetailsSignal = toSignal(itemDetails$, { initialValue: null });
        this.itemDetails.set(itemDetailsSignal());
      }
    });
  }
}

Reactive Form Validation

אנגולר 15

form.component.ts

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { debounceTime } from 'rxjs/operators';

@Component({
  selector: 'app-form',
  template: `
    <form [formGroup]="myForm">
      <input formControlName="name" placeholder="Enter your name" />
      <div *ngIf="nameError">{{ nameError }}</div>
    </form>
  `,
})
export class FormComponent implements OnInit {
  myForm: FormGroup;
  nameError: string = '';

  constructor(private fb: FormBuilder) {
    this.myForm = this.fb.group({
      name: ['', Validators.required],
    });
  }

  ngOnInit() {
    this.myForm
      .get('name')
      ?.valueChanges.pipe(debounceTime(300))
      .subscribe((value) => {
        if (value.length < 3) {
          this.nameError = 'Name must be at least 3 characters';
        } else {
          this.nameError = '';
        }
      });
  }
}

אנגולר 17

form.component.ts

import { Component, signal, effect } from '@angular/core';
import { FormControl } from '@angular/forms';

@Component({
  selector: 'app-form',
  template: `
    <form>
      <input [formControl]="nameControl" placeholder="Enter your name" />
      <div *ngIf="nameError()">{{ nameError() }}</div>
    </form>
  `,
})
export class FormComponent {
  nameControl = new FormControl('');
  nameError = signal<string>('');

  constructor() {
    effect(() => {
      const value = this.nameControl.value;
      if (value.length < 3) {
        this.nameError.set('Name must be at least 3 characters');
      } else {
        this.nameError.set('');
      }
    });
  }
}

Shared State Across Components

שיתוף מידע בין קומפוננטות.

אנגולר 15 – שימוש ב-RxJS BehaviorSubject

counter.service.ts

import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';

@Injectable({
  providedIn: 'root',
})
export class CounterService {
  private _counter = new BehaviorSubject<number>(0);
  counter$ = this._counter.asObservable();

  increment() {
    this._counter.next(this._counter.value + 1);
  }
}

Component A: counter-display.component.ts

import { Component, OnInit } from '@angular/core';
import { CounterService } from '../counter.service';

@Component({
  selector: 'app-counter-display',
  template: `<p>Counter: {{ counter }}</p>`,
})
export class CounterDisplayComponent implements OnInit {
  counter: number = 0;

  constructor(private counterService: CounterService) {}

  ngOnInit() {
    this.counterService.counter$.subscribe((value) => (this.counter = value));
  }
}

Component B: counter-button.component.ts

import { Component } from '@angular/core';
import { CounterService } from '../counter.service';

@Component({
  selector: 'app-counter-button',
  template: `<button (click)="increment()">Increment</button>`,
})
export class CounterButtonComponent {
  constructor(private counterService: CounterService) {}

  increment() {
    this.counterService.increment();
  }
}

אנגולר 17 – שימוש ב-Signals

counter.service.ts

import { Injectable, signal } from '@angular/core';

@Injectable({
  providedIn: 'root',
})
export class CounterService {
  counter = signal<number>(0);

  increment() {
    this.counter.update((value) => value + 1);
  }
}

Component A: counter-display.component.ts

import { Component } from '@angular/core';
import { CounterService } from '../counter.service';

@Component({
  selector: 'app-counter-display',
  template: `<p>Counter: {{ counter() }}</p>`,
})
export class CounterDisplayComponent {
  counter = this.counterService.counter;

  constructor(private counterService: CounterService) {}
}

Component B: counter-button.component.ts

import { Component } from '@angular/core';
import { CounterService } from '../counter.service';

@Component({
  selector: 'app-counter-button',
  template: `<button (click)="increment()">Increment</button>`,
})
export class CounterButtonComponent {
  constructor(private counterService: CounterService) {}

  increment() {
    this.counterService.increment();
  }
}

מקרים שבהם עדיף להשתמש ב-RxJS

קריאה מרובה למידע א-סינכרוני או צורך בטרנספורמציה מורכבת למידע שמתקבל מה-API.

// data.service.ts (Angular 17)
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, forkJoin } from 'rxjs';
import { map } from 'rxjs/operators';

export interface User {
  id: number;
  name: string;
}

export interface Orders {
  userId: number;
  orders: Order[];
}

@Injectable({
  providedIn: 'root',
})
export class DataService {
  constructor(private http: HttpClient) {}

  getUserWithOrders(userId: number): Observable<{ user: User; orders: Orders }> {
    return forkJoin({
      user: this.http.get<User>(`/api/users/${userId}`),
      orders: this.http.get<Orders>(`/api/orders?userId=${userId}`),
    });
  }
}

צורך בעיבוד מהיר של מידע שמגיע מהקלט כמו למשל בחיפוש על פי קלט המשתמש ושימוש בהשהייה על מנת לאמת קלט.

import { Component } from '@angular/core';
import { FormControl } from '@angular/forms';
import { Observable } from 'rxjs';
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
import { SearchService } from '../search.service';

@Component({
  selector: 'app-search',
  template: `
    <input type="text" [formControl]="searchControl" placeholder="Search..." />
    <ul>
      <li *ngFor="let item of results$ | async">{{ item.name }}</li>
    </ul>
  `,
})
export class SearchComponent {
  searchControl = new FormControl('');
  results$: Observable<SearchResult[]>;

  constructor(private searchService: SearchService) {
    this.results$ = this.searchControl.valueChanges.pipe(
      debounceTime(300),
      distinctUntilChanged(),
      switchMap((term) => this.searchService.search(term))
    );
  }
}

תפיסת שגיאות מורכבת והפעלה של לוגיקה בתוך תפיסת השגיאות.

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, throwError, timer } from 'rxjs';
import { retryWhen, mergeMap } from 'rxjs/operators';

@Injectable({
  providedIn: 'root',
})
export class DataService {
  constructor(private http: HttpClient) {}

  getData(): Observable<Data> {
    return this.http.get<Data>('/api/data').pipe(
      retryWhen((errors) =>
        errors.pipe(
          mergeMap((error, i) => {
            if (i >= 3) {
              return throwError(error);
            }
            return timer(1000 * (i + 1));
          })
        )
      )
    );
  }
}

פונקציונליות מורכבת בתפיסת events. בדוגמא פונקציונליות של drop-down.

import { Directive, ElementRef, OnInit } from '@angular/core';
import { fromEvent } from 'rxjs';
import { switchMap, takeUntil, pairwise } from 'rxjs/operators';

@Directive({
  selector: '[appDraggable]',
})
export class DraggableDirective implements OnInit {
  constructor(private element: ElementRef) {}

  ngOnInit() {
    const mouseDown$ = fromEvent<MouseEvent>(this.element.nativeElement, 'mousedown');
    const mouseMove$ = fromEvent<MouseEvent>(document, 'mousemove');
    const mouseUp$ = fromEvent<MouseEvent>(document, 'mouseup');

    mouseDown$
      .pipe(
        switchMap(() =>
          mouseMove$.pipe(
            takeUntil(mouseUp$),
            pairwise()
          )
        )
      )
      .subscribe(([prev, curr]) => {
        const deltaX = curr.clientX - prev.clientX;
        const deltaY = curr.clientY - prev.clientY;
        // Update element position
        const element = this.element.nativeElement;
        element.style.left = element.offsetLeft + deltaX + 'px';
        element.style.top = element.offsetTop + deltaY + 'px';
      });
  }
}

שימוש באופרטורים מורכבים.

import { Observable, timer, throwError } from 'rxjs';
import { mergeMap } from 'rxjs/operators';

export function retryWithDelay(retryCount: number, delayMs: number) {
  return (src: Observable<any>) =>
    src.pipe(
      retryWhen((errors) =>
        errors.pipe(
          mergeMap((error, i) => {
            if (i >= retryCount) {
              return throwError(error);
            }
            return timer(delayMs);
          })
        )
      )
    );
}

המרה בין Observables ל-Signals.

import { Component } from '@angular/core';
import { interval } from 'rxjs';
import { toSignal } from '@angular/core/rxjs-interop';

@Component({
  selector: 'app-timer',
  template: `<p>Seconds elapsed: {{ secondsElapsed() }}</p>`,
})
export class TimerComponent {
  secondsElapsed = toSignal(interval(1000));
}