# Angular standalone components, directives, pipes

The standalone components are getting more and more attention in the Angular world. Even the creators encourage people to use them by default. In this article, I'll show how to use standalone components, directives and pipes.

# Setup

If unsure how to set up Angular with Jest please refer to the article: https://barcioch.pro/angular-with-jest-setup.

# The difference

Before Angular@14 everything had to be defined in the `NgModule`s. They are contextual containers for the application features. All external modules have to be imported in `imports` section, providers in `providers` section and modules' components in `declarations` section. If any of the module's component is to be used in the different place of the application, it has to be exported in `exports` section.

```typescript
@NgModule({
  declarations: [
    AppComponent,
    InternalComponent,
  ],
  imports: [
    ButtonsModule
  ],
  providers: [
    MyService
  ],
  exports: [
    AppComponent
  ]
})
export class AppModule { }
```

With the introduction of standalone features (components, directives, pipes) they became obsolete. To define the feature as standalone, you have to add `standalone: true` property to the `@Component`, `@Pipe` or `@Directive` decorator. Now, each feature is self-contained and can have its own `imports` section.

```typescript
@Component({
  selector: 'app-test',
  standalone: true,
  imports: [MyDirective, MyPipe, MyComponent],
  providers: [MyService]
})
export class TestComponent {

}
```

# Basic examples

## Pipe

This example pipe reverses the order of the passed string.

```typescript
import { Pipe, PipeTransform } from "@angular/core";

@Pipe({
  name: "reverseText",
  standalone: true,
})
export class ReverseTextPipe implements PipeTransform {
  transform(value: string | undefined | null): string {
    if (value === null || value === undefined) {
      return '';
    }

    return value.split('').reverse().join('');
  }
}
```

### Test it

The standard pipe test.

```typescript
import { ReverseTextPipe } from './reverse-text.pipe';

describe('ReverseTextPipe', () => {
  describe('when pipe is created', () => {
    let pipe: ReverseTextPipe;

    beforeAll(() => {
      pipe = new ReverseTextPipe();
    });

    it.each([
      { input: undefined, text: 'undefined' },
      { input: null, text: 'null' },
      { input: '', text: 'empty string' },
    ])('should transform $text to empty string', ({ input, text }) => {
      expect(pipe.transform(input)).toBe('');
    });

    it('should reverse the "reverse me" text to "em esrever"', () => {
      expect(pipe.transform('reverse me')).toBe('em esrever');
    });
  });
});
```

## Directive

This example directive adds `data-test-id` attribute with static `aaa-bb-1` value.

```typescript
import { Directive, HostBinding, HostListener, inject } from '@angular/core';
import { Logger } from '../services/logger';

@Directive({
  selector: '[test-id]',
  standalone: true,
})
export class TestIdDirective {
  @HostBinding("attr.data-test-id") testId = 'aaa-bb-1';
}
```

### Test it

The test uses a standalone `TestComponent` as a wrapper. There is no `TestBed.configureTestingModule()` call since there is no `NgModule`.

```typescript
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Component, DebugElement } from '@angular/core';
import { By } from '@angular/platform-browser';
import { TestIdDirective } from './test-id.directive';

describe('TestIdDirective', () => {
  let fixture: ComponentFixture<TestComponent>;

  beforeEach(() => {
    fixture = TestBed.createComponent(TestComponent);
    fixture.detectChanges();
  });

  describe('when component with directive is initialized', () => {
    it('should add data-test-id attribute to the element', () => {
      expect(getElement().nativeElement.getAttribute('data-test-id')).toBe('aaa-bb-1');
    });
  });

  const getElement = (): DebugElement => {
    return fixture.debugElement.query(By.css('[data-test="element-with-id"]'));
  }
});


@Component({
  selector: 'app-test',
  standalone: true,
  imports: [TestIdDirective],
  template: `
    <div data-test="element-with-id" test-id></div>`,
})
export class TestComponent {

}
```

## Component

A simple component that wraps passed content in `<h1>` tags.

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

@Component({
  selector: 'app-h1',
  standalone: true,
  template: `
    <h1 data-test="header-component">
      <ng-content></ng-content>
    </h1>`,
})
export class HeaderComponent {

}
```

### Test it

The test uses a standalone `TestComponent` as a wrapper. There is no `TestBed.configureTestingModule()` call since there is no `NgModule`.

```typescript
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Component, DebugElement } from '@angular/core';
import { By } from '@angular/platform-browser';
import { HeaderComponent } from './header.component';

describe('HeaderComponent', () => {
  let fixture: ComponentFixture<HeaderComponent>;

  beforeEach(() => {
    fixture = TestBed.createComponent(TestComponent);
    fixture.detectChanges();
  });

  describe('when component is initialized', () => {
    it('should display header component', () => {
      expect(getElement()).toBeTruthy();
    });

    it('should display header component content', () => {
      expect(getElement().nativeElement.textContent).toBe('the content');
    });
  });

  const getElement = (): DebugElement => {
    return fixture.debugElement.query(By.css('[data-test="header"]'));
  }
});


@Component({
  selector: 'app-test',
  standalone: true,
  imports: [HeaderComponent],
  template: `
    <app-h1 data-test="header">the content</app-h1>`,
})
export class TestComponent {

}
```

# Advanced example

With the `NgModule` is gone (feature module), we no longer have its context. So where to place the providers, store and lazy loading? In the `Route` or in the `Component`. Personally, I'm against this approach, but right now, there is no alternative.

In the next examples, I'll be using the standalone `DummyComonent`.

```typescript
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';

@Component({
  selector: 'app-dummy',
  standalone: true,
  imports: [RouterOutlet],
  template: `<router-outlet></router-outlet>`,
})
export class DummyComponent {

}
```

# Bootstrapping standalone component

## Bootstrapping with `NgModule`

Bootstrapping with `NgModule` requires putting a component in `bootstrap` property. It takes an array, but there is no point in having multiple application entry points.

```typescript
// app.module.ts

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

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

In the `imports` property there is an imported `AppRoutingModule`, which contains definitions of initial app routes.

```typescript
// app-routing.module.ts

import { RouterModule, Routes } from '@angular/router';
import { NgModule } from '@angular/core';
import { DummyComponent } from './components/dummy.component';

export const routes: Routes = [
  {
    path: `dummy`,
    component: DummyComponent,
  },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule],
})
export class AppRoutingModule {
}
```

The `main.ts` file bootstraps the application using the `AppModule`.

```typescript
// main.ts

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';

platformBrowserDynamic().bootstrapModule(AppModule)
  .catch(err => console.error(err));
```

## Bootstrapping with standalone component

The bootstrapped component has to be declared as standalone. I'll also add `router-outlet` and import `RouterOutlet`.

```typescript
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';

@Component({
  selector: 'app-root',
  standalone: true,
  template: '<router-outlet></router-outlet>',
  imports: [RouterOutlet]
})
export class AppComponent {
}
```

To achieve the same bootstrapping result with the standalone component, the Angular team provided the new `bootstrapApplication` function.

```typescript
// main.ts

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

bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(routes),
  ]
});
```

The first argument is our main standalone component. The second is a `ApplicationConfig` with a `providers` property. You can provide `Router` or any other global dependencies that you might need.

The `provideRouter` function enables `Router` functionality and tree-shaking, which was not possible before.

## Lazy-loaded standalone components

To lazy load a standalone component there's a new route property `loadComponent` that is very similar to the `loadModule`.

```typescript
import { Routes } from '@angular/router';
import { DummyComponent } from './components/dummy.component';

export const routes: Routes = [
  {
    path: `main`,
    component: DummyComponent,
  },
  {
    path: 'lazy-route',
    loadComponent: () => import('./components/dummy.component').then(c => c.DummyComponent)
  }
];
```

## Lazy-loaded routes

The following examples will be achieving the `/feature1/feature2` routing. Both `features` are lazy-loaded.

### Nested `NgModule` routing

When using the `feature module` we import the `feature routing module` that imports `RouterModule` with passed routes. Lazy loading is achieved by utilizing `loadChildren` property.

#### Application structure using the `NgModules`

The `AppModule` imports the `AppRoutingModule`.

```typescript
// app.module.ts

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { DummyComponent } from './components/dummy.component';

@NgModule({
  declarations: [
    AppComponent,
    DummyComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }
```

The `AppRoutingModule` defines the lazy route `feature1` that points to `Feature1Module`.

```typescript
// app-routing.module.ts

import { RouterModule, Routes } from '@angular/router';
import { NgModule } from '@angular/core';
import { DummyComponent } from './components/dummy.component';

export const routes: Routes = [
  {
    path: `main`,
    component: DummyComponent,
  },
  {
    path: 'feature1',
    loadChildren: () => import('./feature1/feature1.module').then(m => m.Feature1Module)
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule],
})
export class AppRoutingModule {
}
```

The `Feature1Module` imports `Feature1RoutingModule`.

```typescript
// feature1.module.ts

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Feature1Component } from './feature1.component';
import { Feature1RoutingModule } from './feature1-routing.module';

@NgModule({
  declarations: [Feature1Component],
  imports: [
    CommonModule,
    Feature1RoutingModule
  ],
})
export class Feature1Module {
}
```

The `Feature1RoutingModule` points directly to `Feature1Component` and defines a lazy child route `feature2` that points to `Feature2Module`.

```typescript
// feature1-routing.module

import { RouterModule, Routes } from '@angular/router';
import { NgModule } from '@angular/core';
import { Feature1Component } from './feature1.component';

export const routes: Routes = [
  {
    path: ``,
    component: Feature1Component,
    children: [
      {
        path: 'feature2',
        loadChildren: () => import('./feature2/feature2.module').then(m => m.Feature2Module)
      }
    ]
  },
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule],
})
export class Feature1RoutingModule {
}
```

The `Feature2Module` imports `Feature2RoutingModule`.

```typescript
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Feature2Component } from './feature2.component';
import { Feature2RoutingModule } from './feature2-routing.module';

@NgModule({
  declarations: [Feature2Component],
  imports: [
    CommonModule,
    Feature2RoutingModule
  ],
})
export class Feature2Module {
}
```

The `Feature2RoutingModule` points directly to `Feature2Component`

```typescript
import { RouterModule, Routes } from '@angular/router';
import { NgModule } from '@angular/core';
import { Feature2Component } from './feature2.component';

export const routes: Routes = [
  {
    path: ``,
    component: Feature2Component,
  },
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule],
})
export class Feature2RoutingModule {
}
```

### Nested standalone component routing

With the `NgModule` is gone all we need to do is import the routes with the old `loadChildren` property.

#### Application structure using the `standalone components`

The `main.ts` file bootstraps the application with the main routes.

```typescript
// main.ts

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


bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(routes),
  ]
});
```

The main app routes file defines lazy route `feature1` that points to `feature1-routes` file.

```typescript
// app-routes.ts

import { Routes } from '@angular/router';
import { DummyComponent } from './components/dummy.component';

export const routes: Routes = [
  {
    path: `main`,
    component: DummyComponent,
  },
  {
    path: 'feature1',
    loadChildren: () => import('./feature1/feature1-routes').then(r => r.routes)
  }
];
```

The `feature1` points directly to `Feature1Component`. Also, it defines the lazy child route `feature2` that points to `feature2-routes` file.

```typescript
// feature1-routes.ts

import { Routes } from '@angular/router';
import { Feature1Component } from './feature1.component';

export const routes: Routes = [
  {
    path: ``,
    component: Feature1Component,
    children: [
      {
        path: 'feature2',
        loadChildren: () => import('./feature2/feature2-routes').then(r => r.routes)
      }
    ]
  },
];
```

The `feature2` points directly to `Feature2Component`.

```typescript
// feature2-routes.ts

import { RouterModule, Routes } from '@angular/router';
import { NgModule } from '@angular/core';
import { Feature2Component } from './feature2.component';

export const routes: Routes = [
  {
    path: ``,
    component: Feature2Component,
  },
];
```

And that's all in terms of routing. The `NgModules` are gone.

### Test it

To test the routing you still need to use the `TestBed`. I'm using `configureTestingModule` method without calling `compileComponents()` since the latter is not needed. Also, the `RouterTestingHarness` will come in handy while testing the routes. The `routes` are imported directly from the main app route file.

```typescript
import { TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { RouterTestingHarness } from '@angular/router/testing';
import { provideRouter } from '@angular/router';
import { routes } from './app-routes';


describe('App Routing', () => {
  let harness: RouterTestingHarness;

  beforeEach(async () => {
    TestBed.configureTestingModule({
      providers: [provideRouter(routes)]
    });

    harness = await RouterTestingHarness.create();
  });

  describe('when testing module is initialized', () => {
    describe('and user enters "/main" route', () => {
      beforeEach(async () => {
        await harness.navigateByUrl('/main');
      });

      it('should render DummyComponent', () => {
        const element = harness.fixture.debugElement.query(By.css('app-dummy'));
        expect(element).toBeTruthy()
      });
    });

    describe('and user enters "/feature1" route', () => {
      beforeEach(async () => {
        await harness.navigateByUrl('/feature1');
      });

      it('should render Feature1Component', () => {
        const element = harness.fixture.debugElement.query(By.css('app-feature1'));
        expect(element).toBeTruthy()
      });
    });

    describe('and user enters "/feature1/feature2" route', () => {
      beforeEach(async () => {
        await harness.navigateByUrl('/feature1/feature2');
      });

      it('should render Feature1Component', () => {
        const element = harness.fixture.debugElement.query(By.css('app-feature1'));
        expect(element).toBeTruthy()
      });

      it('should render Feature2Component', () => {
        const element = harness.fixture.debugElement.query(By.css('app-feature2'));
        expect(element).toBeTruthy()
      });
    });
  });
});
```

# Providers

Without the `NgModule` `providers` property, there are two places to declare them unless `providedIn: 'root'` is used.

1 - Component providers - the component and its all descendants have access to the provider (through `Component Injector`). Just use the `providers` property of `@Component` decorator.


```typescript
  import { Component } from '@angular/core';
  import { RouterOutlet } from '@angular/router';
  
  @Component({
    selector: 'app-dummy',
    standalone: true,
    imports: [RouterOutlet],
    providers: [MyService],
    template: `<router-outlet></router-outlet>`,
  })
  export class DummyComponent {
  
  }
```

2 - Route providers - all the route descendants have access to the provider (through `Route Injector`). Just use the `providers` property of `Route` interface.


```typescript
export const routes: Routes = [
  {
    providers: [MyService],
    path: 'feature1',
    loadChildren: () => import('./feature1/feature1-routes').then(r => r.routes)
  }
]
```

# Source code

https://gitlab.com/barcioch-blog-examples/015-angular-standalone-elements

