DEV Community 👩‍💻👨‍💻

Michael Muscat
Michael Muscat

Posted on

Angular 14 dependency injection unlocked

Quietly released with Angular 14 is a fix for this handy little inject utility that allows us to create and retrieve dependencies from the injector tree without relying on parameterized class constructors (read docs)

A small change with big ramifications?

Angular changelog

Until now it's only been possible to use it in places such as an InjectionToken or Injectable constructor context. Angular 14 now allows inject to be used in Directive and Component constructor contexts as well.

This makes it a little nicer when injecting non class-based tokens.

const API = new InjectionToken<Api>("API")

// Angular <=13
@Directive()
export class MyDirective {
  constructor(@Inject(API) private api: Api) {}
}

// Angular <=13
@Component()
export class MyDirective {
  constructor(@Inject(API) private api: Api) {}
}

// Angular 14+
@Directive()
export class MyDirective {
  private api = inject(API) // type inferred
}

// Angular 14+
@Component()
export class MyDirective {
  private api = inject(API) // type inferred
}
Enter fullscreen mode Exit fullscreen mode

Higher Order Services

Another change in Angular 14 loosens the rules around abstract class constructors so we can safely use them without running afoul of the Angular compiler with strictInjectionParameters.

Angular changelog

Thanks to this change it's now much easier to compose higher order services using mixins. For example, we can replace the ResourceManager in this example with service composition.

// generate higher order service, mixing plain value
// parameters with dependency injection
export function createResource<T extends Fetchable>(
  fetchable: Type<T>
): Type<Resource<T>> {
  @Injectable()
  class ResourceImpl extends Resource<T> {
    constructor() {
      super(inject(fetchable));
    }
  }
  return ResourceImpl;
}

@Injectable()
export abstract class Resource<T extends Fetchable> {
  // this value is injected
  private changeDetectorRef = inject(ChangeDetectorRef);
  private subscription = Subscription.EMPTY

  ...

  // Angular behaviors require the `Injectable` decorator
  ngOnDestroy() {
    this.subscription.unsubscribe()
  }

  // this value is passed in through `super()`
  constructor(private fetchable: Fetchable) {}
}
Enter fullscreen mode Exit fullscreen mode

This example shows how we can now easily mix dependency injection with explicit parameter constructors while preserving Angular behaviors like ngOnDestroy.

Let's see it in action.

const endpoint = 'https://jsonplaceholder.typicode.com/todos'

@Injectable({ providedIn: 'root' })
export class FetchTodosByUserId implements Fetchable<Todo[]> {
  private http = inject(HttpClient);

  fetch(userId: string) {
    return this.http.get<Todo[]>(endpoint, {
      params: {
        userId,
      },
    });
  }
}
Enter fullscreen mode Exit fullscreen mode
<!-- todos.component.html -->
<div *ngFor="let todo of todos.value">
  <div>id: {{ todo.id }}</div>
  <div>title: {{ todo.title }}</div>
  <input disabled type="checkbox" [checked]="todo.completed" />
</div>
Enter fullscreen mode Exit fullscreen mode
const TodosByUserId = createResource(FetchTodosByUserId);

@Component({
  selector: 'app-todos',
  templateUrl: './todos.component.html',
  providers: [TodosByUserId],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TodosComponent {
  protected todos = inject(TodosByUserId);

  @Input()
  userId: string;

  ngOnChanges() {
    this.todos.fetch(this.userId);
  }
}
Enter fullscreen mode Exit fullscreen mode

A working example can be seen here 👉 view on Stackblitz

A small step forward

Angular 14 has many long needed and welcomed improvements to the core developer experience. Be sure to check the change log so you don't miss anything.

Happy Coding!

Top comments (0)

🌚 Friends don't let friends browse without dark mode.

Sorry, it's true.