Frontend

Angular 15 – the same logic, less code | Frontend Weekly vol. 113

Standalone Components is one of the functionalities that the Angular community has been waiting for for a long time. However, Angular 15 is not just Standalone Components, but a whole bunch of long-awaited functionality.

Article cover

1. Angular v15 

Angular is released every 6 months with the precision of a Swiss watch. Today Angular 15 saw the light of day. Does it turn the status quo upside down like Svelte or at least present new concepts like React Server Components? Unfortunately, no. However, it is a release full of small incremental changes addressing the real pain points of Angular developers. For me, this is the most interesting Angular release in at least two years.

Angular Standalone Components

Before we get to the point, I would like to make a small digression for all those who don’t use Angular on daily basis. Angular modules have little in common with modules known from JavaScript. Native modules allow you to split your application into multiple files and manage the APIs you provide. Angular modules are intended to provide configuration for Dependency Injection. In Angular, every component or directive must be part of some module, making them the most atomic element of the framework. Interestingly, modules didn’t make their way into Angular until version 2.0.0-rc.5, and were a response to problems with publishing Angular libraries in npm. Due to the fact that the framework was already at the Release Candidate stage, the whole solution was created at an accelerated pace. As it happens with such solutions, it stayed with us for longer.

Over the years, good practices around Angular have evolved. Right now the SCAM (Single Component Angular Module) scheme is the most common. There probably wouldn’t be anything wrong with this if it weren’t for the amount of generated boilerplate.

@Component({
  selector: 'app-my-component',
  template: `
    <h2>Today is {{today | date}}</h2>'
    <app-other-component></app-other-component>
  `
})
export class MyComponent {
  readonly today = new Date();
}

@NgModule({
  imports: [CommonModule, OtherComponentModule],
  declarations: [MyComponent],
  exports: [MyComponent],
})
export class MyComponentModule {}

Angular Standalone Components, or module-less components, were first presented in the form of an RFC in late 2021. The community actually immediately loved them. In mid-2022, along with Angular 14, we received their first unstable implementation. This week, with Angular 15, we lived to see their first stable version. 

In a nutshell, Standalone Components are a SCAM architecture done right. If you attach the right metadata to a component, Angular will automatically start treating the component as a module.

@Component({
  selector: 'app-my-component',
  standalone: true,
  imports: [CommonModule, OtherComponent]
  template: `
    <h2>Today is {{today | date}}</h2>'
    <app-other-component></app-other-component>
  `
})
export class MyComponent {
  readonly today = new Date();
}

The Angular team didn’t stop with the API for creating Standalone Components and set a goal to prepare a fully functional API without modules. Angular 15 is the culmination of these efforts. Functional API for bootstrapping applications (added in Angular 14) is joined by a functional router and a functional http module.

// This is how you bootstrap application with modules
@NgModule({
  declarations: [AppComponent],
  imports: [
    RouterModule.forRoot(
      [
        { path: '/home', component: HomeComponent },
        { path: '**', redirectTo: '/home' },
      ],
      {
        preloadingStrategy: PreloadAllModules,
      }
    ),
  ],
  providers: [
    { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
  ],
  bootstrap: [AppComponent],
})
export class AppModule {}

platformBrowserDynamic().bootstrapModule(AppModule)
// This is how you bootstrap application without modules
bootstrapApplication(AppComponent, {
  providers: [
    provideRouter([
      { path: '/home', component: HomeComponent },
      { path: '**', redirectTo: '/home' },
    ], withPreloading(PreloadAllModules)),
    provideHttpClient(withInterceptors([AuthInterceptor]))
  ]
});

In addition to the obvious advantages of more readable code and getting rid of complicated NgModules, the functional approach has meant that all APIs are now fully tree-shakable. As the Angular team boasts, in the case of the router API, this slims down the package by an average of 11%!

In conclusion, it is worth noting that Standalone Components are not an attempt to completely remove Angular modules. Their concept is deeply rooted in Angular’s architecture, and in many cases, their use will still be necessary to achieve the desired result. Modules also remain a key aspect of encapsulating many external libraries.

Functional Router Guards

In a fair amount of simplification, Routing in Angular is based on an array of objects defining the path, the component to be rendered, and a list of Guards. The last one contains logic that conditions access to a path and allows you to perform the appropriate redirection.

@NgModule({
  imports: [
    RouterModule.forRoot(
      [
        { path: '/login', component: LoginComponent },
        { path: '/home', component: HomeComponet, canActivate: [LoggedInGuard] },
        { path: '**', redirectTo: '/home' },
      ]
    ),
  ],
  exports: [RouterModule],
})
export class AppRoutingModule {}

@Injectable({ providedIn: 'root' })
export class LoggedInGuard implements CanActivate {
  constructor(
    private readonly authService: AuthService,
    private readonly router: Router
  ) {}

  async canActivate(route: ActivatedRouteSnapshot): Promise<boolean> {
    if (!await this.authService.isLoggedIn()) {
      return true;
    }
    return this.router.navigateByUrl('/home');
  }
}

If I had to come up with a catchy advertising slogan for Angular 15, it would be “Less boilerplate, more fun.” Another new feature in Angular 15 is the ability to replace the complicated class Gurads with a simple function that returns a boolean. I’d venture to say that using the inject() (added in Angular 14), it’s possible to get rid of class Guards from the code altogether.

provideRouter([
  { path: '/login', component: LoginComponent },
  { path: '/home', component: HomeComponet, canActivate: [isLoggedIn] },
  { path: '**', redirectTo: '/home' },
])

function isLoggedIn() { 
  if (!await inject(AuthService).isLoggedIn()) {
    return true;
  }
  return inject(Router).navigateByUrl('/home');
}

An undoubted advantage of the new architecture is the simplicity of parameterization. Instead of passing and reading data from ActivatedRouteSnapshot, we can simply pass parameters to a function.

// This is how you parametarize Guards with class approach
@NgModule({
  imports: [
    RouterModule.forRoot(
      [
        { path: '/login', component: LoginComponent },
        { 
          path: '/home',
          component: HomeComponet, 
          canActivate: [AuthStateGuard],
          data: { states: ['LOGGED_IN'] } 
        },
        { path: '**', redirectTo: '/home' },
      ]
    ),
  ],
  exports: [RouterModule],
})
export class AppRoutingModule {}

@Injectable({ providedIn: 'root' })
export class AuthStateGuard implements CanActivate {
  constructor(
    private readonly authService: AuthService,
    private readonly router: Router
  ) {}

  async canActivate(route: ActivatedRouteSnapshot): Promise<boolean> {
    if (
      route.data.states.includes('LOGGED_IN') &&
      await this.authService.isLoggedIn()
    ) {
      return true;
    }
    return this.router.navigateByUrl('/login');
  }
}
// This is how you parametarize Guards with functional approach
provideRouter([
  { path: '/login', component: LoginComponent },
  { 
    path: '/home', 
    component: HomeComponet, 
    canActivate: [authStatesGuard(['LOGGED_IN'])] 
  } ,
  { path: '**', redirectTo: '/home' },
])

function authStatesGuard(states: string[]) {
  return async function(): Promise<boolean> {
    if (
      states.includes('LOGGED_IN') &&
      await inject(AuthService).isLoggedIn()
    ) {
      return true;
    }
    return inject(Router).navigateByUrl('/login');
  };
}

Finally, it is still worth mentioning that analogous functional APIs also got an HTTP interceptor 

provideHttpClient(
  withInterceptors([
    (req, next) => {
      // We can use the inject() function inside this function
      // For example: inject(AuthService)
      return next(req);
    },
  ])
)

Directive Composition API

Unlike React or Vue, which are based on functions, Angular is built mainly around classes. For this reason, the framework from Google cannot use a simple composition of functions. Also, inheritance is not that easy with decorators. Because of that, Angular introduced Directives, which are basically mixins for components.

@Directive({
  selector: '[appHighlight]'
})
export class HighlightDirective {
  constructor(private el: ElementRef) {
    this.el.nativeElement.style.backgroundColor = 'yellow';
  }
}

@Component({
  selector: 'app-my-component',
  standalone: true,
  imports: [HighlightDirective, OtherComponent]
  // 👇 This is how you enrich component behaviour with Directive
  template: '<app-other-component appHighlight></app-other-component>';  
})
export class MyComponent {}

One of the biggest issues with directives so far has been the inability to assign them directly in the component definition. To better illustrate the problem, imagine that you implement a button that lights up yellow when hovered over. There is already a directive in your code that allows any component to light up yellow. However, Angular does not allow you to assign a directive to a component when you define it. As a result, you are doomed to either duplicate your code or manually tack on a directive every time you use the component. 

Angular 15 solves this problem by adding the hostDirective property to the @Component decorator. We pass to it a list of directives to be applied to our component. The base component automatically inherits all the properties of the directives assigned to it.

@Component({
  selector: 'app-my-component',
  standalone: true,
  imports: [HighlightDirective],
  // 👇 This is how you assign directive to the component in Angular 15
  hostDirectives: [HighlightDirective],
  template: '<button>Hello World</button>';
})
export class MyComponent {
  readonly today = new Date();
}

Interestingly, the newly added property also allows the composition of the directives themselves. 

@Directive({
  selector: '[appHighlightAndUnderline]',
  hostDirectives: [HighlightDirective, UnderlineDirective]
})
export class HighlightAndUnderlineDirective {}

Optimised Image Directive

Project Aurora is an initiative set up by the Google Chrome team. Its main goal is to work with developers of popular libraries and frameworks to create the best possible experience for web users. Less than a few weeks ago, we informed you about next/image, a component for presenting images so as to optimize Core Web Vitals metrics. The component was a collaboration between the Project Aurora team and Next.js. An analogous directive has made its way into Angular 15, which is also the result of a collaboration with Project Aurora.

@Component({
  standalone: true,
  // 👇 Notice how src is replaced with ngSrc
  template: '<img [ngSrc]="src" [alt]="alt"/>'
  imports: [NgOptimizedImage],
})
class MyStandaloneComponent {
  @Input() src: string;
  @Input() alt: string;
}

Improved Stack Trace

The results of an annual survey conducted by the Angular team clearly showed that the biggest problem in debugging is complicated error messages. To address this problem, in collaboration with the Chrome DevTools team, an annotation mechanism was created to remove lines from the stack trace that point to node_modules. This should make it easier to understand where in the application the error actually occurred.

// Error from Angular 14
ERROR Error: Uncaught (in promise): Error
Error
    at app.component.ts:18:11
    at Generator.next (<anonymous>)
    at asyncGeneratorStep (asyncToGenerator.js:3:1)
    at _next (asyncToGenerator.js:25:1)
    at _ZoneDelegate.invoke (zone.js:372:26)
    at Object.onInvoke (core.mjs:26378:33)
    at _ZoneDelegate.invoke (zone.js:371:52)
    at Zone.run (zone.js:134:43)
    at zone.js:1275:36
    at _ZoneDelegate.invokeTask (zone.js:406:31)
    at resolvePromise (zone.js:1211:31)
    at zone.js:1118:17
    at zone.js:1134:33
// The same Error from Angular 15
ERROR Error: Uncaught (in promise): Error
Error
    at app.component.ts:18:11
    at fetch (async)  
    at (anonymous) (app.component.ts:4)
    at request (app.component.ts:4)
    at (anonymous) (app.component.ts:17)
    at submit (app.component.ts:15)
    at AppComponent_click_3_listener (app.component.html:4)

Sources

https://blog.angular.io/angular-v15-is-now-available-df7be7f2f4c8

Discover more IT content selected for you
In Vived, you will find articles handpicked by devs. Download the app and read the good stuff!

phone newsletter image

2. TypeScript 4.9

The last few versions of TypeScript have been mostly boring optimizations and support for more edge cases. TypeScript 4.9, released this week, is quite different, as it introduces as many as two new keywords. Without further ado, let’s take a look at what’s new to the language from Microsoft and why this is its most interesting version in a long time.

Satisfies operator

To explain how this new operator works I will use an example from a Microsoft beta note. Suppose we need to add types the following code:

// Each property can be a string or an RGB tuple.
const palette = {
    red: [255, 0, 0],
    green: "#00ff00",
    blue: [0, 0, 255]
};

// We want to be able to use array methods on 'red'...
const redComponent = palette.red.at(0);

// or string methods on 'green'...
const greenNormalized = palette.green.toUpperCase();

The first thing that may come to mind is defining the Color type and using the Record Utility Type. Unfortunately, if we want the code to compile we are forced to perform a dangerous cast operation:

type Color = "red" | "green" | "blue";
type RGB = [red: number, green: number, blue: number];

type Palette = Record<Color, string | RGB>

const palette: Palette = {
    red: [255, 0, 0],
    green: "#00ff00",
    blue: [0, 0, 255]
};

// We want to be able to use array methods on 'red'...
const redComponent = (palette.red as RGB).at(0);

// or string methods on 'green'...
const greenNormalized = (palette.green as string).toUpperCase();

To remove casting, in the type definition we can explicitly define how each color will be initialised. In our case, this may not be the worst idea, but see for yourself how much additional code we have to generate. Not to mention much less flexibility we get at the end…

type StringColor =  "green";
type RGBColor = "red" | "blue";
type RGB = [red: number, green: number, blue: number];

type StringColorPalette = Record<StringColor, string>;
type RGBColorPalette = Record<RGBColor, RGB>;
type Palette = StringColorPalette & RGBColorPalette;

const palette: Palette = {
    red: [255, 0, 0],
    green: "#00ff00",
    blue: [0, 0, 255]
};

const redComponent = palette.red.at(0);
const greenNormalized = palette.green.toUpperCase();

A workaround for this problem is a new satisfies operator that will validate the type at the time of assignment, but will not affect the type being evaluated by TypeScript. It sounds complicated, but a simple example gives a good idea of how it works:


type Color = "red" | "green" | "blue";
type RGB = [red: number, green: number, blue: number];

type Palette = Record<Color, string | RGB>

const palette = {
    red: [255, 0, 0],
    green: "#00ff00",
    blue: [0, 0, 255]
} satisfies Palette;

// Both of these methods are still accessible!
const redComponent = palette.red.at(0);
const greenNormalized = palette.green.toUpperCase();

// —-----------------------------------
// Example errors caught by satisfies
//  —-----------------------------------

const spelloPalette = {
    red: [255, 0, 0],
    green: "#00ff00",
    bleu: [0, 0, 255] // Such typos are now caught
} satisfies Palette;

// Missing properties are now caught
const missingColorPalette = {
    red: [255, 0, 0],
    bleu: [0, 0, 255]
} satisfies Palette;

const wrongColorTypePalette = {
    red: [255, 0], // Such typos are now also caught
    green: "#00ff00",
    bleu: [0, 0, 255]
} satisfies Palette;

Auto-Accessors

TypeScript 4.9 introduces yet another new keyword `accessor`. It is used to define a field in a class, which underneath will be a private variable packed with a getter and setter. What is all this for? As it turns out, it is a preparation for decorators, which are currently in phase 3 of the TC39 process.

class Person {
  accessor name: string;

  constructor(name: string) {
    this.name = name;
  }
}

class Person {
  #__name: string;

  get name() {
    return this.#__name;
  }
  set name(value: string) {
    this.#__name = name;
  }

  constructor(name: string) {
    this.name = name;
  }
}

Improved NaN comparison

I think we can all agree that JavaScript is a crazy language. After all, in what other language could something as this happen:

Today, we will not talk about JavaScript being crazy, but about comparisons with NaN (Not a Number). In most languages that support floating-point variables, it is assumed that nothing can be equal to NaN – not even another instance of NaN

NaN == 0    // false
NaN == Nan  // false
NaN === Nan // false

A fairly common error that is easily overlooked is a direct comparison with NaN. TypeScript 4.9 will scrupulously protect us from this by returning an appropriate error.

function validate(someValue: number) {
  return someValue !== NaN;
  //     ~~~~~~~~~~~~~~~~~
  // error: This condition will always return 'true'.
  //        Did you mean '!Number.isNaN(someValue)'?
}

TypeScript 5.0

The work on TypeScript 5.0 has already begun. However, it’s worth to remember, that TypeScript does not use the Semantic Versioning convention. This means that the 5.0 version might not contain any breaking changes. Browsing through the project repository, we can already see what the folks at Microsoft are preparing for us. In short, the compiler has been rewritten from archaic namespaces to modules. As a result, the compiler runs between 10-25% faster, and the package has been slimmed down by almost 50%.

Sources

https://devblogs.microsoft.com/typescript/announcing-typescript-4-9/

3. Deno 1.28 with npm support

Hell has officially frozen! Deno, the project that was supposed to fix all Node.js mistakes, got npm support. After years of trying to wean themselves off the most popular JavaScript modules repository, Deno has accepted it’s hard to live without npm. 

Don’t be fooled, though – npm in combination with Deno will work quite differently than in combination with Node.js. First, Deno will not require running npm install or any other npm command. Second, Deno will not create a node_modules directory – downloaded packages will be stored globally. Third, npm imports will be marked with a special prefix npm:.

import { chalk } from "npm:chalk@5";

Why was the decision to add support for npm made now? In my opinion, mainly because it is the right decision. However, if you like movies with yellow subtitles, there is another theory.

I hope outside Poland all your videos about conspiracy theories also have yellow subtitles

A pretty sizable company has been built around Deno. It has raised more than $25M from a number of investors. It’s hard to make money on JavaScript runtime engine development alone, so the company has focused on expanding its cloud infrastructure. The result of the work is the Deno Deploy cloud. It has already found its first commercial customers in the form of Supabase (an open-source alternative to Firebase) and Netlify. Both companies have decided to base their Edge Functions on Deno Deploy. Along with outside investors come growth expectations. Deno has really built a lot around the idea of better Node.js. However, it seems that without meaningful integration with npm, further development was not possible.

If you really like movies with yellow subtitles, there is another theory. At the end of this year’s vacation, a new player appeared on the market in the form of Bun.js. It’s a drop-in alternative to Node.js, written in the fast Zig language and based on the JavaScriptCore engine. While Deno focuses its marketing on solving Node.js problems, Bun puts the emphasis on performance. 

At first, Bun seemed like a temporary curiosity. However, within a few weeks, it turned into a full-fledged startup that raised $7M. Oven (that is how the startup is named) has adopted a strategy twinned with Deno Company. The company intends to prepare a Bun-based cloud architecture, which they will sell to customers.

Information about work on adding npm support in Deno completely coincidentally coincided with the creation of Oven, and the fact that Bun.js supports npm certainly did not influence the decisions made

Sources

https://deno.com/blog/v1.28

Discover more IT content selected for you
In Vived, you will find articles handpicked by devs. Download the app and read the good stuff!

phone newsletter image

Bonus: Opera is the first browser with built-in TikTok support

You haven’t heard wrong – Opera is the first browser to integrate with TikTok. It has become part of the bar of integrations, among which we find Twitter or Messenger. According to the browser’s developers – people who started using integrations built into the browser are no longer going back to their web versions. So, what do we do now? Install Opera to unscrupulously watch TikTok at work!