Wróć do strony głównej
Angular

Angular 18 – co nowego?

Team Angular nie zwalnia. Wersja 18 frameworka usprawnia wprowadzone w poprzednich wersjach funkcjonalności. Nacisk stawiany jest na dalszą integrację sygnałów i przygotowania do trybu zoneless. Oprócz tego na ng-conf dostaliśmy niespodziankę w postaci przedstawienia kooperacji dwóch teamów w Google – Angular i Wiz. Nie zabrakło też usprawnień DX, takich jak fallback w ng-content czy nowy Observable w formularzach. 

Zapraszam do lektury!

Angular i Wiz

Dużą niespodzianką na tegorocznym ng-conf, największej Angularowej konferencji na świecie, było zaprezentowanie trwającej już ponad rok współpracy dwóch teamów w Google: Angular Team oraz Wiz Team. Jeremy Elbourn i Minko Gechev dali nam mały wgląd na to czego możemy się spodziewać po takiej współpracy.

Czym jest Wiz?

Wiz to wewnętrzny framework Googla wykorzystywany do tworzenia krytycznych pod względem wydajności aplikacji, takich jak Google Search, Google Photos, Google Payments czy YouTube. Ruch na takich stronach jest ogromny i znaczna część użytkowników nie ma dostępu do szybkiego internetu. Wiz skupia się na tym, aby dostarczyć jak najbardziej zoptymalizowaną aplikację z relatywnie małą interaktywnością. SSR jest podstawą działania Wiz – wszystkie komponenty aplikacji są renderowane na zoptymalizowanym rozwiązaniu streamingowym. JavaScript potrzebny do interakcji na stronie jest ładowany dopiero gdy komponent jest widoczny dla użytkownika. 

Natomiast po drugiej stronie mamy znany nam dobrze Angular, który skupia się na wysokiej interaktywności i Developer Experience. Z każdą nową wersją dostarczane są nowe ficzery optymalizujące końcową aplikację, jak np. control flow z blokiem defer- całkiem możliwe, że to właśnie dzięki kooperacji z teamem Wiz. 

Sygnały w Wiz

W ramach współpracy oba frameworki będą zapożyczać od siebie funkcjonalności – długoletnim celem jest scalenie obu rozwiązań. Wiz będzie przechodził na model open-source, co usprawni pracę nad nim poprzez feedback i kontrybucje społeczności. 

Pierwszym owocem współpracy jest wdrożenie Angularowych sygnałów w mobilnej wersji przeglądarkowej portalu YouTube. Nie jest to może najczęściej używany sposób oglądania filmików z kotkami, ale od czegoś trzeba zacząć 🙂

W przyszłości możemy spodziewać się wdrażania zmian z Wiz do Angulara. Sam temat optymalnego SSR już wydaje się bardzo ciekawy. Bez wątpienia będziemy was o tym informować na angular.love, a dzisiaj zapraszam do przeczytania posta na blogu Angulara jak i obejrzenia Keynote z ng-conf.

Dalsza integracja sygnałów

Ficzer wyczekiwany przez wielu developerów. Sygnały, które są z nami od 16. wersji Angulara, w najnowszej jego odsłonie napędzają coraz więcej core-owych mechanizmów. Dotychczas dostępne w developer preview, bazujące na sygnałach inputy, queries oraz modele, w v18 awansują do wersji stabilnej. 

Wszystkie zmiany są kolejnym krokiem do tego, aby Angular działał kompletnie w trybie zoneless.

input()

Uzyskujemy dostęp do nowej funkcji input(), która co prawda nie zastąpi leciwego już dekoratora @Input(), ale będzie zoptymalizowaną jego alternatywą.

Do naszego użytku otrzymujemy dwa typy inputów:

  • opcjonalny (optional) – domyślne zachowanie. Można na nich zdefiniować wartość początkową. Jeśli się tego nie zrobi, Angular zadeklaruje wartość inputu jako undefined.
  • wymagany (required) – w tym przypadku input musi być przekazany z parenta do childa. Nie można też zadeklarować wartości początkowej. 
@Component(...)
export class MyComponent {
  // default: undefined
  optionalInput = input<number>();  

  // default: 5
  optionalInputWithDefaultValue = input<number>(5);           
  
  
  // parent must pass value trough input
  requiredInput = input.required<number>();    
  
  // ERROR - setting initial value to required input is not allowed
  requiredInputWithDefaultValue = input.required<number>(5);  
}

W templatce używamy ich jak innych sygnałów

<p>{{ myInput() }}</p>

W przeciwieństwie do dekoratora, sygnałowe inputy są read-only. Nie możemy zatem zmieniać ich wartości w komponencie. Zapewnia to dodatkową gwarancję prawidłowego przepływu danych. Jednak w świecie Angulara istnieje wiele aplikacji, w których inputy były zmieniane na poziomie komponentu. A zatem nie w każdym przypadku możliwa będzie prosta zamiana dekoratora na sygnał.

Jeżeli faktycznie potrzebujemy zmodyfikowanych wartości przesłanych przez input, możemy użyć funkcji model(), która jest opisana w dalszej części artykułu, lub jednego z poniższych sposobów. 

Jak już wiemy, nowe inputy są napędzane przez sygnały. Czyli identycznie jak z innymi sygnałami, mamy tutaj dostęp do funkcji computed() oraz effect(). Z pomocą funkcji computed możemy stworzyć nowy sygnał, który bazuje na wartości inputu.

@Component(...)
export class MyComponent {
  age = input(0);

  // wiek pomnożony przez 2 
  ageMultiplied = computed(() => this.age() * 2);
}

Podobnie jak w przypadku dekoratora @Input() mamy też dostęp do znanych nam już atrybutów:

  • transform – możemy za jego pomocą modyfikować naszą wartość w inpucie. W poniższym przykładzie za każdym razem gdy wywołamy age(), będzie użyta przekazana wartość pomnożona przez 2
  • alias – zmienia publiczną nazwę inputu. Komponent, który deklaruje input nadal używa jego pierwotnej nazwy. Jednak dla parenta, który korzysta z MyComponent, widoczny jest alias. 
@Component(...)
export class MyComponent {
  age = input(0, {
    transform: (value: number) => value * 2,
    alias: 'userAge' 
  })
}

model()

Krótko mówiąc model() to input() na sterydach. Można z niego korzystać tak samo jak z inputa, ale ma kilka dodatkowych funkcjonalności. 

Po pierwsze, wartość w sygnale, który został utworzony za pomocą model(), można dowolnie zmieniać używając znanej z sygnałów funkcji set()

@Component(...)
export class MyComponent {
  myModel = model(false);        // ModelSignal<boolean> 
  myOtherModel = model<string>() // ModelSignal<string | undefined>

  toggle(): void {
    // model w każdym momencie można zmienić za pomocą set()
    this.myModel.set(!this.myModel());
  }
}

Podobnie jak input, tak i model możemy oznaczyć jako wymagany (required). Możemy też nadać alias. model nie ma jednak dostępu do funkcji transform.

@Component(...)
export class MyComponent {
  myModel = model.required<boolean>(); // ModelSignal<boolean> 
}

Używając model() Angular tworzy dla nas mechanizmy two-way binding. Analogicznie do dotychczasowych rozwiązań, mamy dostęp do specjalnej składni – potocznie zwanej banana-in-a-box [()]. Na istniejącym modelu działa też składnia input []. Jednak gdy jej użyjemy, two-way binding przestaje działać, ale nadal mamy input, który możemy edytować w dziecku. 

Oprócz tego Angular tworzy output w komponencie, gdzie zadeklarowany jest model. Nazwa outputu składa się z nazwy modelu oraz dopisku Change. Przykład: jeśli mój model nazywa się name, to output będzie nazywał się nameChange. Parent może nasłuchiwać eventów za pomocą okrągłych nawiasów ().

// child.component.ts
@Component(...)
export class ChildComponent {
  // W tym miejscu powstaje input, two-way binding i output o nazwie nameChange
  name = model('Marcin');
}

// parent.component.ts
@Component({
  ...,
  template: `
  <app-child
    (nameChange)="logValue($event)"  << Event 
    [(name)]="nameFromParent"        << Two-way binding - banana-in-a-box 
  ></app-child>`,
})
export class AppComponent {
  nameFromParent = 'Martin';

  logValue(value: string): void {
    console.log(value);
  }
}

W tym miejscu warto zaznaczyć, że do two-way binding można wykorzystać proste typy danych jak w przykładzie powyżej, jak również sygnały. Powyższy parent mógłby np. wyglądać tak:

// parent.component.ts
@Component({
  ...,
  template: `
  <app-child
    [(name)]="nameFromParent"        << Two-way binding - banana-in-a-box  ></app-child>`,
})
export class AppComponent {
  nameFromParent = signal('Martin'); // WritableSignal<string>
}

Signal queries

Odświeżenia doczekały się również queries, czyli mechanizmy pozwalające na tworzenie odwołań do komponentów, dyrektyw czy elementów DOM. Zostały dodane cztery nowe funkcje. 

viewChild()

Pierwsza funkcja wprowadza alternatywę dla dekoratora @ViewChild(). Używamy jej gdy szukamy jednego wyniku w naszym komponencie. Analogicznie do input() czy model(), również tutaj możemy użyć opcji required.

@Component({
  ...,
  template: `
    <div #el></div>
    <div #requiredDiv></div>
    <my-child />
`,
})
export class MyComponent { 
  divEl = viewChild<ElementRef>('el'); // Signal<ElementRef|undefined>                      
  requiredDivEl = viewChild.required<ElementRef>('requiredDiv'); // Signal<ElementRef>
  
  cmp = viewChild(ChildComponent); // Signal<ChildComponent|undefined>
}

viewChildren()

Druga funkcja działa podobnie do viewChild. Szuka ona jednak wielu elementów i zwraca nam array z wynikami.

@Component({
  template: `
    <div #el></div>
    <div #el></div>
    <div #el></div>
`,
})
export class MyComponent { 
  firstSelector = viewChildren<ElementRef>('el'); 
  // Signal<readonly ElementRef<any>[]>
  

  secondSelector = viewChildren<ElementRef<HTMLDivElement>>('el'); 
  // Signal<readonly ElementRef<HTMLDivElement>[]>
}

contentChild() i contentChildren()

Ostatnie dwie nowości w Signal Queries to contentChild() i contentChildren(), które działają podobnie do poprzedniczek – różnicą jest to, że nie przeszukują one templatki komponentu, a zawartości zamieszczonego w niej elementu ng-content.

// parent.component.ts
@Component({
  template: `<ng-content></ng-content>`, // Zwróćcie uwagę na tag ng-content
  standalone: true,
  selector: 'app-parent',
})
export class ParentComponent {
  content = contentChild(ChildComponent); 
  // Signal<ChildComponent | undefined>

  contentElements = contentChildren(ChildComponent);
  // Signal<readonly ChildComponent[]>
}

output()

Również outputy zostały usprawnione. Do wersji 17.3 trafiła funkcja output(), która póki co ma status Developer Preview. Wywołując ją otrzymujemy obiekt typu OutputEmitterRef<T>. Aby emitować wartość do parenta, tak jak do tej pory, wywołujemy funkcję emit().

@Component(...)
export class MyComponent {
  valueChanged = output<string>();

  onValueChanged(msg: string): void {
    // emit() działa tak samo. Jednak nie można już emitować undefined
    this.valueChanged.emit(msg);
  }
}

W tym miejscu trzeba podkreślić, że w przeciwieństwie do nowych inputów, outputy nie bazują na sygnałach. 

Po co więc zmiany? Jednym z powodów jest ujednolicenie składni z nowymi inputami – nowa składnia obu konceptów zawiera mniej boilerplate’u i jest po prostu czytelniejsza. 

@Component(...)
export class MyComponent {
  newInput = input<boolean>();  // InputSignal<boolean | undefined>
  newOutput = output<string>(); // OutputEmitterRef<string>
}

Drugim powodem jest type-safety w nowej klasie OutputEmitterRef<T>. Do tej pory używaliśmy klasy EventEmitter<T>, której funkcja emit() akceptowała argument o typie T | undefined. To już nie będzie problemem, gdyż Typescript wyłapie błąd. Rozwiązuje to dosyć aktywny problem na Githubie.

@Component(...)
export class MyComponent {
  @Output() oldOutput = new EventEmitter<string>();
  newOutput = output<string>();

  onEvent(): void {
    this.oldOutput.emit(); // OK
    this.newOutput.emit(); // ERROR: Expected 1 arguments, but got 0.
  }
}

W paczce @angular/core/rxjs-interop znajdziemy dwa nowe helpery do wspomagania pracy z nowymi outputami. 

outputFromObservable() pozwala nam na stworzenie outputa z Observable. Oznacza to, że nie trzeba będzie manualnie tworzyć subskrypcji i w niej emitować wartości, lub co gorsza, oznaczać Observable dekoratorem @Output i zwracać go do parenta – nie było to wspierane rozwiązanie. Korzystając z nowego helpera Angular zadba o to aby automatycznie wyłączyć subskrypcję naszego Observable gdy nasz komponent zostanie zniszczony.

outputToObservable() zamieni natomiast nasz output do Observable

// child.component.ts
@Component({
  selector: 'app-child',
  ...
})
export class ChildComponent {
  active$ = new Observable<boolean>();
  activeChanged = outputFromObservable<boolean>(this.active$);
}

// parent.component.ts
@Component({
  selector: 'app-parent',
  template: `<app-child (activeChanged)="onActiveChanged($event)"></app-child>`,
  ...
})
export class ParentComponent {
  onActiveChanged(val: boolean): void {
    console.log(val);
  }
}

Fallback w ng-content

Kolejna, bardzo ciepło przyjęta przez społeczność nowość to fallback dla tagu ng-content. Jak wiadomo, tag ten służy do projekcji zawartości (ang. Content Projection), która została przekazana z zewnątrz – to oczywiście pozostaje bez zmian. Teraz jednak, gdy nie zostanie przekazana żadna zawartość, możemy obsłużyć taką sytuację, prezentując domyślną treść.

@Component({
  selector: 'my-comp',
  template: `
    Tu będzie użyty fallback
    <ng-content select="header">Default header</ng-content> 

    A tutaj footer z MyApp
    <ng-content select="footer">Default footer</ng-content> 
  `
})
class MyComp {}

@Component({
  template: `
    <my-comp>
      <footer>New footer</footer>
    </my-comp>
  `
})
class MyApp {}

Nowy Observable w formularzach

Reactive Forms, czyli podejście do sterowania formularzami w Angularze za pomocą modeli (model-driven), doczekały się nowego Observable o nazwie events. Emituje on wszelakiego rodzaju zmiany w formularzu łącząc ze sobą subskrypcje valueChanges i statusChanges, jak i również dodaje wydarzenia, której do tej pory nie były dostępne w żadnej z subskrypcji formularzy. 

events emituje poniższe Eventy

  • ValueChangeEvent – gdy wartość inputu się zmieni
  • PristineChangeEvent – gdy status pristine ulegnie zmianie – jest to stan pierwotny
  • TouchedChangeEvent – gdy input jest “dotknięty”
  • StatusChangeEvent – gdy nasz formularz staje się VALID lub INVALID

Jak widać możemy teraz nasłuchiwać na zmiany statusów touched i pristine, co do tej pory nie było możliwe.

Hybrydowe Change Detection

Detekcja zmian w Angularze opiera się na Zone.js, którego zadaniem jest planowanie aktualizacji w odpowiedzi na wywołanie akcji z API przeglądarki, takich jak np. setTimeout, setInterval, Promise.then, addEventListener itd. Jednak to podejście ma pewne ograniczenia, zwłaszcza w obsłudze aktualizacji poza NgZone, co prowadzi do problemów z wyzwalaniem detekcji zmian w odpowiednich momentach. W niektórych przypadkach powoduje to problem z wydajnością aplikacji, przez co developerzy muszą korzystać z funkcji takich jak ngZone.runOutSideAngular().

W v18 wprowadzono eksperymentalne wsparcie dla detekcji zmian bez zone.js, co stanowi znaczącą zmianę w stosunku do dotychczasowego podejścia. Zmiana ta ma na celu poprawę DX oraz wydajności zarówno dla aplikacji korzystających z NgZone, jak i tych działających bez niego.

Zoneless Change Detection bazuje na podejściu, w którym komponenty bezpośrednio poinformują Angulara o tym, że coś się zmieniło bez dodatkowej warstwy jaką jest zone.js.

Aby wypróbować eksperymentalne CD wystarczą dwie zmiany

// main.ts
bootstrapApplication(AppComponent, {
  providers: [
    // ? Add this line to enable Zoneless Change Detection
    provideExperimentalZonelessChangeDetection(),
  ],
});
// angular.json 
{
  "projects": {
    "app": {
      "architect": {
        "build": {
          "options": {
            "polyfills": [
              "zone.js" // ? Remove this line
            ],
          }
        }
      }
    }
  }

Jeśli wasze komponenty używają ChangeDetectionStrategy.OnPush , AsyncPipe i/lub sygnałów aby wyrenderować zawartość, to wszystko powinno działać jak należy.

Już bez aktywacji eksperymentalnego providera w v18 domyślnie będzie aktywna hybrydowa detekcja zmian. W trybie hybrydowym zarówno NgZone, jak i nowy zoneless scheduler są wykorzystywane. To hybrydowe podejście poprawia DX, zapewniając, że detekcja zmian jest zawsze planowana, nawet gdy aktualizacje występują poza NgZone. W razie problemów zawsze można wrócić do sprawdzonego CD

// main.ts
bootstrapApplication(AppComponent, {
  providers: [
    provideZoneChangeDetection({ ignoreChangesOutsideZone: true }),
  ],
});

Więcej informacji o nowym Change Detection pojawi się niedługo na blogu, a tymczasem zapraszam do lektury artykułu Matthieu Rieglera.

Inne warte uwagi zmiany w Angular 18

  • Najniższa wspierana wersja TypeScript to 5.4
  • Składnia Control Flow nie ma już statusu Developer Preview, teraz jest stabilna (stable)
  • HttpClientModule i jemu podobne mają teraz status depracated. Developerzy powinni używać provideHttpClient()

 

 

 

 

 

 

O autorze

Marcin Stelmaszczyk

Angular Developer w House of Angular. W świecie tworzenia aplikacji wiele już widziałem, jednak to właśnie Angular najbardziej przypadł mi do gustu. W wolnym czasie jeżdżę na rowerze, podróżuję z rodzinką lub odpalam PSa.

Zapisz się do naszego newslettera. Bądź na bieżąco z najnowszymi trendami, poradami, meetupami i stań się częścią społeczności Angulara w Polsce. Rynek pracy docenia członków społeczności.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *