interviewDeck

Your one-stop platform to prepare, practice and ace your interviews.

Loading your questions…

All Questions

Filters & tools

Angular Coding Interview Questions and Answers

21 hand-picked Angular Coding interview questions with detailed answers. Open the interactive version above to search, filter by difficulty, run code, bookmark questions and track your progress.

Build a search box that calls an API while typing without making unnecessary requests.

Create a FormControl for the search input and listen to its valueChanges. Use debounceTime() to wait until the user pauses typing, distinctUntilChanged() to ignore duplicate searches, and switchMap() to cancel previous API requests and send only the latest one. Display the results using the async pipe.

search = new FormControl('');

results$ = this.search.valueChanges.pipe(
  debounceTime(300),
  distinctUntilChanged(),
  switchMap(term => this.api.search(term ?? ''))
);

Display a list with client-side pagination (10 items per page).

Store the complete list in memory and maintain the current page number and page size. Calculate the items for the current page using slice(). Generate page numbers dynamically and provide Previous and Next buttons while preventing navigation beyond the first or last page. Never modify the original data; always derive the visible items from it.

page = 1;
pageSize = 10;

get totalPages() {
  return Math.ceil(this.items.length / this.pageSize);
}

get pagedItems() {
  const start = (this.page - 1) * this.pageSize;
  return this.items.slice(start, start + this.pageSize);
}

next() {
  if (this.page < this.totalPages) {
    this.page++;
  }
}

previous() {
  if (this.page > 1) {
    this.page--;
  }
}

Load more data automatically as the user scrolls to the bottom of the page.

Implement infinite scrolling using an IntersectionObserver. Place a sentinel element at the end of the list and observe when it enters the viewport. When triggered, fetch the next page of data, append it to the existing list, and continue observing until all records have been loaded. Disconnect the observer in ngOnDestroy() to prevent memory leaks.

@ViewChild('sentinel') sentinel!: ElementRef;

ngAfterViewInit() {
  this.observer = new IntersectionObserver(([entry]) => {
    if (entry.isIntersecting && !this.loading) {
      this.loadMore();
    }
  });

  this.observer.observe(this.sentinel.nativeElement);
}

ngOnDestroy() {
  this.observer.disconnect();
}

Create a reusable 5-star rating component.

Create a reusable component that renders five clickable stars. Accept the current rating using @Input() and notify the parent of changes using @Output(). Highlight the selected stars based on the current rating. For Reactive Forms support, implement ControlValueAccessor so the component behaves like a standard Angular form control.

stars = [1, 2, 3, 4, 5];

@Input() value = 0;
@Output() valueChange = new EventEmitter<number>();

setRating(rating: number) {
  this.value = rating;
  this.valueChange.emit(rating);
}

Create a reusable modal/dialog component.

Create a reusable modal component that can be opened and closed by its parent. Accept the open state using @Input(), notify the parent when it closes using @Output(), and display custom content with ng-content. Close the modal when the user clicks the backdrop or presses the Esc key.

@Input() open = false;
@Output() closed = new EventEmitter<void>();

@HostListener('document:keydown.escape')
onEscape() {
  if (this.open) {
    this.close();
  }
}

close() {
  this.open = false;
  this.closed.emit();
}

Build a todo list (add / toggle / delete / filter).

Create a todo array where each item contains id, text, and done. Implement CRUD operations using immutable array updates: add with a new object, toggle using map(), delete using filter(), and derive filtered views (All, Active, Completed) without modifying the original array. Use trackBy with *ngFor to avoid unnecessary DOM updates. Immutable updates also work well with OnPush change detection.

interface Todo {
  id: number;
  text: string;
  done: boolean;
}

todos: Todo[] = [];
filter: 'all' | 'active' | 'completed' = 'all';
private seq = 0;

add(text: string) {
  this.todos = [
    ...this.todos,
    { id: ++this.seq, text, done: false }
  ];
}

toggle(id: number) {
  this.todos = this.todos.map(todo =>
    todo.id === id
      ? { ...todo, done: !todo.done }
      : todo
  );
}

remove(id: number) {
  this.todos = this.todos.filter(todo => todo.id !== id);
}

trackById(index: number, todo: Todo) {
  return todo.id;
}

get visibleTodos() {
  switch (this.filter) {
    case 'active':
      return this.todos.filter(t => !t.done);
    case 'completed':
      return this.todos.filter(t => t.done);
    default:
      return this.todos;
  }
}

Build a reusable data table with dynamic columns and sorting.

Create a reusable table component that accepts column definitions and row data as inputs. Generate headers and cells dynamically, support sorting when a column header is clicked, and allow custom cell rendering using ng-template and ngTemplateOutlet. Perform sorting on an immutable copy of the data and use trackBy for efficient rendering.

@Input() columns!: Column[];
@Input() rows: any[] = [];

sortKey = '';
dir: 'asc' | 'desc' = 'asc';

sort(column: string) {
  this.dir =
    this.sortKey === column && this.dir === 'asc'
      ? 'desc'
      : 'asc';

  this.sortKey = column;

  this.rows = [...this.rows].sort((a, b) => {
    const result = a[column] > b[column] ? 1 : -1;
    return this.dir === 'asc' ? result : -result;
  });
}

Create a reusable tabs component using content projection.

Create a TabComponent that accepts a title and projects its content using ng-content. Build a parent TabsComponent that discovers all child tabs using @ContentChildren, renders the tab headers, tracks the active tab, and displays only the selected tab's content.

@ContentChildren(TabComponent)
tabs!: QueryList<TabComponent>;

ngAfterContentInit() {
  this.select(this.tabs.first);
}

select(tab: TabComponent) {
  this.tabs.forEach(t => t.active = t === tab);
}

Build a countdown timer that updates every second.

Use RxJS timer() or interval() to emit a value every second. Calculate the remaining time, stop the stream when it reaches zero using takeWhile(), and display the value with the async pipe. This avoids manual subscription management and works well with Angular change detection.

duration = 60;

remaining$ = timer(0, 1000).pipe(
  map(elapsed => this.duration - elapsed),
  takeWhile(time => time >= 0)
);

Build a dynamic form from a JSON configuration.

Create a form dynamically from a configuration object. Generate FormControls and FormGroups based on the field definitions, apply validators from the configuration, and render the appropriate input component using *ngFor and ngSwitch. This approach allows forms to be generated without changing the component code.

fields = [
  {
    name: 'firstName',
    type: 'text',
    required: true
  },
  {
    name: 'email',
    type: 'email',
    required: true
  }
];

form = this.fb.group({});

ngOnInit() {
  this.fields.forEach(field => {
    this.form.addControl(
      field.name,
      this.fb.control(
        '',
        field.required ? Validators.required : []
      )
    );
  });
}

Display different content based on a radio button selection.

Bind all radio buttons to a single property using [(ngModel)]. When the selected value changes, Angular automatically updates the model. Use @if, *ngIf, or @switch to conditionally render the appropriate content based on the selected option.

selected = 'A';

Fetch employees from an API and display only their id and name.

Call the employee API and use the RxJS map() operator to transform the response before it reaches the component. Inside the RxJS map(), use JavaScript's Array.map() to create a new array containing only the required fields.

employees$ = this.employeeService.getEmployees().pipe(
  map(employees =>
    employees.map(employee => ({
      id: employee.id,
      name: employee.name
    }))
  )
);

Fetch user details, then load the user's orders.

When one API depends on the result of another, use switchMap(). The first request retrieves the user details, and the returned user ID is passed to the second request to fetch the user's orders. switchMap() automatically cancels any previous in-progress request if a new one starts.

this.userService.getUser(userId).pipe(
  switchMap(user =>
    this.orderService.getOrders(user.id)
  )
).subscribe(orders => {
  this.orders = orders;
});

Load data from multiple APIs in parallel and display the page after all responses are received.

Use forkJoin() to execute multiple independent API calls at the same time. It waits until every Observable completes, then emits a single object or array containing all responses. This is ideal for loading dashboard or page data that depends on multiple APIs.

forkJoin({
  profile: this.userService.getProfile(),
  orders: this.orderService.getOrders(),
  notifications: this.notificationService.getNotifications()
}).subscribe(({ profile, orders, notifications }) => {
  this.profile = profile;
  this.orders = orders;
  this.notifications = notifications;
});

Load data once and share it across multiple components without making duplicate API calls.

Create a shared service that fetches the data only once and exposes it as an Observable. Cache the result using shareReplay(1) or store it in a BehaviorSubject. Multiple components subscribe to the same stream, ensuring that the HTTP request is executed only once.

@Injectable({ providedIn: 'root' })
export class UserService {

  private users$?: Observable<User[]>;

  getUsers() {
    if (!this.users$) {
      this.users$ = this.http
        .get<User[]>('/api/users')
        .pipe(shareReplay(1));
    }

    return this.users$;
  }
}

Build an autocomplete search component without using Angular Material.

Create an input backed by a FormControl and listen to valueChanges. Use debounceTime(), distinctUntilChanged(), and switchMap() to fetch matching suggestions from an API. Display the suggestions in a dropdown, allow the user to select an item, and hide the dropdown when an item is selected or the input loses focus.

search = new FormControl('');

options$ = this.search.valueChanges.pipe(
  debounceTime(300),
  distinctUntilChanged(),
  switchMap(value => this.api.search(value ?? ''))
);

select(option: User) {
  this.search.setValue(option.name, { emitEvent: false });
  this.selected = option;
}

Reorder a list using Angular CDK Drag & Drop.

Use Angular CDK's cdkDropList and cdkDrag directives to make list items draggable. Handle the cdkDropListDropped event and reorder the array using moveItemInArray(). For dragging items between different lists, use transferArrayItem().

drop(event: CdkDragDrop<string[]>) {
  if (event.previousContainer === event.container) {
    moveItemInArray(
      this.items,
      event.previousIndex,
      event.currentIndex
    );
  } else {
    transferArrayItem(
      event.previousContainer.data,
      event.container.data,
      event.previousIndex,
      event.currentIndex
    );
  }
}

Build a recursive Tree View component to display nested comments or folders.

Create a reusable recursive component that accepts a node as an input. Display the current node and recursively render its child nodes using the same component. Use *ngFor to iterate through the children and stop recursion when a node has no children.

export interface TreeNode {
  id: number;
  text: string;
  children?: TreeNode[];
}

@Input() node!: TreeNode;

<ul>
  <li>
    {{ node.text }}

    <app-tree-node
      *ngFor="let child of node.children"
      [node]="child">
    </app-tree-node>
  </li>
</ul>

Build a Kanban board where users can drag tasks between columns.

Create multiple cdkDropList containers (Todo, In Progress, Done) and render tasks inside each column using cdkDrag. Allow users to reorder tasks within a column using moveItemInArray() and move tasks between columns using transferArrayItem(). Update the underlying state whenever a task is moved.

drop(event: CdkDragDrop<Task[]>) {
  if (event.previousContainer === event.container) {
    moveItemInArray(
      event.container.data,
      event.previousIndex,
      event.currentIndex
    );
  } else {
    transferArrayItem(
      event.previousContainer.data,
      event.container.data,
      event.previousIndex,
      event.currentIndex
    );
  }
}

Implement a Light / Dark theme switcher with persistent user preference.

Create a ThemeService to manage the application's current theme. Toggle a CSS class on the document body or root element, save the selected theme in localStorage, and restore the saved preference when the application starts.

@Injectable({ providedIn: 'root' })
export class ThemeService {
  toggleTheme() {
    const dark = document.body.classList.toggle('dark-theme');
    localStorage.setItem('theme', dark ? 'dark' : 'light');
  }

  initializeTheme() {
    if (localStorage.getItem('theme') === 'dark') {
      document.body.classList.add('dark-theme');
    }
  }
}

Build a Login Page with username and password validation.

Create a Reactive Form with username and password controls. Apply validators such as required, email, and minLength. Display validation errors only after the user interacts with a field or submits the form. On successful validation, process the login request.

loginForm = this.fb.group({
  username: ['', [Validators.required, Validators.email]],
  password: ['', [Validators.required, Validators.minLength(6)]]
});

submitted = false;

submit() {
  this.submitted = true;

  if (this.loginForm.invalid) {
    return;
  }

  console.log(this.loginForm.value);
}