Skip to content

Instantly share code, notes, and snippets.

@jelbourn
Created October 28, 2024 22:23
Show Gist options
  • Save jelbourn/0158b02cfb426e69c172db4ec92e3c0c to your computer and use it in GitHub Desktop.
Save jelbourn/0158b02cfb426e69c172db4ec92e3c0c to your computer and use it in GitHub Desktop.
Draft of the 2024 Revised Angular Style Guide

Introduction

This guide covers a range of style conventions for Angular application code. These recommendations are not required for Angular to work, but instead establish a set of coding practices that promote consistency across the Angular ecosystem. A consistent set of practices makes it easier to share code and move between projects.

This guide does not cover TypeScript or general coding practices unrelated to Angular. For TypeScript, check out Google's TypeScript style guide.

When in doubt, prefer consistency

Whenever you encounter a situation in which these rules contradict the style of a particular file, prioritize maintaining consistency within a file. Mixing different style conventions in a single file creates more confusion than diverging from the recommendations in this guide.

Naming

Separate words in file names with hyphens

Separate words within a file name with hyphens (-). For example, a component named UserProfile has a file name user-profile.ts.

Use the same name for a file's tests with .spec at the end

For unit tests, end file names with .spec.ts. For example, the unit test file for the UserProfile component has the file name user-profile.spec.ts.

Avoid suffixing components, directives, and services with their type

In TypeScript code, the presence of an Angular decorator (Component, Directive, and Injectable) clearly indicates the type of a class. Suffixing the type onto the class names is redundant and generally makes the code feel unnecessarily verbose. For example, a component named UserListComponent does not communicate any more useful information than the shorter UserList.

Service class names should generally describe what that class does. The "Service" suffix doesn't communicate anything meaningful about the class's behavior or responsibilities. Prefer names that more specifically communicate the class's role. For example, a class that requests user data from the server could be UserDataClient rather than UserService.

If you run into naming collisions between different classes that would have the same name with different types, consider choosing more specific or detailed names.

Include a type suffix for NgModule and Pipe classes

NgModule classes should end with the word "Module". An NgModule is primarily a configuration construct and typically does not have any meaningful behaviors or responsibilities.

Pipe classes should end with the word Pipe. A pipe's behavior is intrinsic to it being a pipe; a pipe transforms a single value within an Angular template expression. Given this, including "Pipe" in the name communicates meaningful information about the behavior of the class.

Match file names to the TypeScript identifier within

File names should generally describe the contents of the code in the file. When the file contains a TypeScript class, the file name should reflect that class name. For example, a file containing a component named UserProfile has the name user-profile.ts.

If the file contains more than one primary namable identifier, choose a name that describes the common theme to the code within. If the code in a file does not fit within a common theme or feature area, consider breaking the code up into different files. Avoid overly generic file names like helpers.ts, utils.ts, or common.ts.

Use the same file name for a component's TypeScript, template, and styles

Components typically consist of one TypeScript file, one template file, and one style file. These files should share the same name with different file extensions. For example, a UserProfile component can have the files user-profile.ts, user-profile.html, and user-profile.css.

If a component has more than one style file, append the name with additional words that describe the styles specific to that file. For example, UserProfile might have style files user-profile-settings.css and user-profile-subscription.css.

Project structure

All of the application's code goes in a directory named src

All of your Angular UI code (TypeScript, HTML, and styles) should live inside of a directory named src. Code that's not related to UI, such as configuration files or scripts, should live outside the src directory.

This keeps the root application directory consistent between different Angular projects and creates a clear separation between UI code and other code in your project.

Bootstrap your application in a file named main.ts directly inside src

The code to start up, or bootstrap, an Angular application should always live in a file named main.ts. This represents the primary entry point to the application.

Group closely related files together in the same directory

Angular components consist of a TypeScript file and, optionally, a template and one or more style files. You should group these together in the same directory.

Unit tests should live in the same directory as the code-under-test. Avoid collecting unrelated tests into a single tests directory.

Organize your project by feature areas

Organize your project into subdirectories based on the features or your application or common themes to the code in those directories. For example, the project structure for a movie theater site, MovieReel, might look like this:

src/
├─ movie-reel/
│ ├─ show-times/
│ │ ├─ film-calendar/
│ │ ├─ film-details/
│ ├─ reserve-tickets/
│ │ ├─ payment-info/
│ │ ├─ purchase-confirmation/

Avoid creating subdirectories based on the type of code that lives in those directories. For example, avoid creating directories like components, directives, and services.

Avoid putting so many files into one directory that it becomes hard to read or navigate. As the number files in a directory grows, consider splitting further into additional sub-directories.

One concept per file

Prefer focusing source files on a single concept. For Angular classes specifically, this usually means one component, directive, or service per file. However, it's okay if a file contains more than one component or directive if your classes are relatively small and they tie together as part of a single concept.

When in doubt, go with the approach that leads to smaller files.

Components and directives

Choosing component selectors

See the Components guide for details on choosing component selectors.

Naming component and directive members

See the Components guide for details on naming input properties and naming output properties.

Choosing directive selectors

Directives should use the same application-specific prefix as your components.

When using an attribute selector for a directive, use a camelCase attribute name. For example, if your application is named "MovieReel" and you build a directive that adds a tooltip to an element, you might use the selector [mrTooltip].

Group Angular-specific properties before methods

Components and directives should group Angular-specific properties together, typically near the top of the class declaration. This includes inputs, outputs, queries, and injected dependencies. Define these and other properties before the class's methods.

This practice makes it easier to find the class's template APIs and dependencies.

Keep components and directives focused on presentation

Code inside your components and directives should generally relate to the UI shown on the page. For code that makes sense on its own, decoupled from the UI, prefer refactoring to other files. For example, you can factor form validation rules or data transformations into separate functions or classes.

Avoid overly complex logic in templates

Angular templates are designed to accommodate JavaScript-like expressions. You should take advantage of these expressions to capture relatively straightforward logic directly in template expressions.

When the code in a template gets too complex, though, refactor logic into the TypeScript code ( typically with a computed).

There's no one hard-and-fast rule that determines what constitutes "complex". Use your best judgement.

Use protected on class members that are only used by a component's template

A component class's public members intrinsically define a public API that's accessible via dependency injection and queries. Prefer protected access for any members that are meant to be read from the component's template.

@Component({
  ...,
  template: `<p>{{ fullName() }}</p>`,
})
export class UserProfile {
  firstName = input();
  lastName = input();

// `fullName` is not part of the component's public API, but is used in the template.
  protected fullName = computed(() => `${this.firstName()} ${this.lastName()}`);
}

Use readonly on properties that are initialized by Angular

Mark component and directive properties initialized by Angular as readonly. This includes properties initialized by input, model, output, and queries. The readonly access modifier ensures that the value set by Angular is not overwritten.

@Component({/* ... */})
export class UserProfile {
  readonly userId = input();
  readonly userSaved = output();
}

For components and directives that use the decorator-based @Input, @Output, and query APIs, this advice applies to output properties and queries, but not input properties.

@Component({/* ... */})
export class UserProfile {
  @Output() readonly userSaved = new EventEmitter<void>();
  @ViewChildren(PaymentMethod) readonly paymentMethods?: QueryList<PaymentMethod>;
}

Prefer class and style over ngClass and ngStyle

Prefer class and style bindings over using the NgClass and NgStyle directives.

<!-- PREFER -->
<div [class.admin]="isAdmin" [class.dense]="density === 'high'">

<!-- AVOID -->
<div [ngClass]="{admin: isAdmin, dense: density === 'high'}">

Both class and style bindings use a more straightforward syntax that aligns closely with standard HTML attributes. This makes your templates easier to read and understand, especially for developers familiar with basic HTML.

Additionally, the NgClass and NgStyle directives incur an additional performance cost compared to the built-in class and style binding syntax.

Name event handlers for what they do, not for the triggering event

Prefer naming event handlers for the action they perform rather than for the triggering event:

<!-- PREFER -->
<button (click)="saveUserData()">Save</button>

<!-- AVOID -->
<button (click)="handleClick()">Save</button>

Using meaningful names like this makes it easier to tell what an event does from reading the template.

For keyboard events, you can use Angular's key event modifiers with specific handler names:

<textarea (keydown.control.enter)="commitNotes()" (keydown.control.space)="showSuggestions()">

Sometimes, event handling logic is especially long or complex, making it impractical to declare a single well-named handler. In these cases, it's fine to fall back to a name like 'handleKeydown' and then delegate to more specific behaviors based on the event details:

@Component({/* ... */})
class RichText {
  handleKeydown(event: KeyboardEvent) {
    if (event.ctrlKey) {
      if (event.key === 'B') {
        this.activateBold();
      } else if (event.key === 'I') {
        this.activateItalic();
      }
// ...
    }
  }
}

Keep lifecycle methods simple

Avoid putting long or complex logic inside lifecycle hooks like ngOnInit. Instead, prefer creating well-named methods to contain that logic and then call those methods in your lifecycle hooks. Lifecycle hook names describe when they run, meaning that the code inside doesn't have a meaningful name that describes what the code inside is doing.

// PREFER
ngOnInit() {
  this.startLogging();
  this.runBackgroundTask();
}

// AVOID
ngOnInit() {
  this.logger.setMode('info');
  this.logger.monitorErrors();
  // ...and all the rest of the code that would be unrolled from these methods.
}

Use lifecycle hook interfaces

Angular provides a TypeScript interface for each lifecycle method. When adding a lifecycle hook to your class, import and implement these interfaces to ensure that the methods are named correctly.

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

@Component({/* ... */})
export class UserProfile implements OnInit {

  // The `OnInit` interface ensures this method is named correctly.
  ngOnInit() { /* ... */ }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment