Now that we have our User interface and UserService set up, it’s time to build the UI to display our list of users. In this chapter, we’ll create the UserListComponent, which will:
- Inject
UserService. - Subscribe to the
users$Observable from the service. - Display the users using Angular’s new
@forcontrol flow. - Show loading and error states using signals and
@ifcontrol flow.
This chapter will highlight how signals (within a BehaviorSubject in our service, then mapped to a component signal) and zoneless change detection (via the async pipe or direct signal access) streamline UI updates.
Step 1: Generate UserListComponent
Let’s generate the component inside our features/users/components directory.
ng generate component features/users/components/user-list --standalone --skip-tests
Step 2: Implement UserListComponent Logic
Open src/app/features/users/components/user-list/user-list.component.ts and add the following:
// src/app/features/users/components/user-list/user-list.component.ts
import { Component, inject, OnInit, OnDestroy, signal } from '@angular/core';
import { CommonModule } from '@angular/common'; // For NgIf, NgFor, AsyncPipe
import { UserService } from '../../../../core/services/user.service'; // Adjust path
import { User } from '../../../../shared/models/user.interface'; // Adjust path
import { Subscription } from 'rxjs';
@Component({
selector: 'app-user-list',
standalone: true,
imports: [CommonModule], // Import CommonModule for directives
templateUrl: './user-list.component.html',
styleUrls: ['./user-list.component.css'],
})
export class UserListComponent implements OnInit, OnDestroy {
private userService = inject(UserService);
private subscriptions: Subscription = new Subscription();
// Component-local signals for users, loading, and error states
users = signal<User[]>([]);
loading = signal(false);
error = signal<string | null>(null);
ngOnInit(): void {
// Subscribe to the users$ observable from the service
this.subscriptions.add(
this.userService.users$.subscribe((latestUsers) => {
this.users.set(latestUsers); // Update the component's user signal
})
);
// Subscribe to the loadingUsers observable from the service
this.subscriptions.add(
this.userService.loadingUsers.subscribe((isLoading) => {
this.loading.set(isLoading); // Update the component's loading signal
})
);
// Subscribe to the errorLoadingUsers observable from the service
this.subscriptions.add(
this.userService.errorLoadingUsers.subscribe((err) => {
this.error.set(err); // Update the component's error signal
})
);
// Optionally, if you want to explicitly re-fetch from the component:
// this.userService.fetchUsers();
}
// Method to trigger a refresh of users from the service
refreshUsers(): void {
this.userService.fetchUsers();
}
ngOnDestroy(): void {
this.subscriptions.unsubscribe(); // Clean up all subscriptions
}
}
Explanation:
private userService = inject(UserService);: Injects ourUserService.users = signal<User[]>([]);: We create a local signal in the component to hold the user data. This is a common pattern: services manage the global state (e.g., viaBehaviorSubject), and components map that state to their local signals for efficient reactivity.- Subscriptions in
ngOnInit: We subscribe touserService.users$,userService.loadingUsers, anduserService.errorLoadingUsers. Whenever the service’sBehaviorSubjectemits a new value, we update our component’s corresponding signals using.set(). subscriptions.add()andngOnDestroy: Best practice for managing RxJS subscriptions. We add all subscriptions to aSubscriptionobject and thenunsubscribe()inngOnDestroyto prevent memory leaks.refreshUsers(): A method that simply calls the service’sfetchUsers()to reload the data.
Step 3: Implement UserListComponent Template
Now, let’s create src/app/features/users/components/user-list/user-list.component.html to display the users, loading states, and error messages using Angular v21’s new control flow syntax.
<!-- src/app/features/users/components/user-list/user-list.component.html -->
<div class="user-list-container">
<h3>Registered Users</h3>
<!-- Loading State -->
@if (loading()) {
<div class="loading-message">
<p>Loading users...</p>
<div class="spinner"></div>
</div>
}
<!-- Error State -->
@if (!loading() && error()) {
<div class="error-message">
<p>{{ error() }}</p>
<button (click)="refreshUsers()">Try Again</button>
</div>
}
<!-- User List -->
@if (!loading() && !error() && users().length > 0) {
<ul class="user-cards">
@for (user of users(); track user.id) {
<li class="user-card">
<div class="user-info">
<h4>{{ user.name }}</h4>
<p>Email: {{ user.email }}</p>
<p>Role: <span [class]="'role-' + user.role">{{ user.role }}</span></p>
</div>
<!-- Add action buttons later if needed -->
</li>
}
</ul>
} @else if (!loading() && !error() && users().length === 0) {
<div class="no-users-message">
<p>No users found. Perhaps add some?</p>
<button (click)="refreshUsers()">Reload Users</button>
</div>
}
<div class="actions">
<button (click)="refreshUsers()" [disabled]="loading()">Refresh All</button>
</div>
</div>
Explanation:
@if (loading()) { ... }: Displays a loading indicator whenloading()signal is true.@if (!loading() && error()) { ... }: Shows an error message and a “Try Again” button if an error occurs.@for (user of users(); track user.id) { ... }: Iterates over theusers()signal.track user.idis important for performance with lists, helping Angular optimize DOM updates.[class]="'role-' + user.role": Dynamically applies a CSS class based on the user’s role (e.g.,role-admin,role-user). This uses direct class binding.@else ifblock: Handles the case where no users are found after loading.refreshUsers()button: Allows users to manually trigger a data reload.[disabled]="loading()"prevents multiple clicks while fetching.
Step 4: Add Basic Styling
Create src/app/features/users/components/user-list/user-list.component.css:
/* src/app/features/users/components/user-list/user-list.component.css */
.user-list-container {
max-width: 800px;
margin: 20px auto;
padding: 25px;
border-radius: 10px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
background-color: #fcfcfc;
font-family: Arial, sans-serif;
}
h3 {
text-align: center;
color: #333;
margin-bottom: 25px;
font-size: 1.8em;
}
.loading-message, .error-message, .no-users-message {
text-align: center;
padding: 20px;
margin: 20px 0;
border-radius: 8px;
font-size: 1.1em;
}
.loading-message {
background-color: #e6f7ff;
color: #0056b3;
border: 1px solid #91d5ff;
}
.error-message {
background-color: #fff0f6;
color: #c90a16;
border: 1px solid #ffadd2;
}
.error-message button {
margin-top: 10px;
padding: 8px 15px;
background-color: #dc3545;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
.no-users-message {
background-color: #fffbe6;
color: #d46b08;
border: 1px solid #ffe58f;
}
.no-users-message button {
margin-top: 10px;
padding: 8px 15px;
background-color: #ffc107;
color: #333;
border: none;
border-radius: 5px;
cursor: pointer;
}
.user-cards {
list-style-type: none;
padding: 0;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
}
.user-card {
background-color: #ffffff;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
transition: transform 0.2s ease-in-out;
}
.user-card:hover {
transform: translateY(-3px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.user-info h4 {
margin-top: 0;
color: #007bff;
font-size: 1.4em;
margin-bottom: 8px;
}
.user-info p {
margin: 5px 0;
color: #666;
}
.user-info p span {
font-weight: bold;
padding: 3px 8px;
border-radius: 4px;
font-size: 0.9em;
}
.role-admin {
background-color: #ffe0b2; /* Light orange */
color: #e65100; /* Dark orange */
}
.role-user {
background-color: #c8e6c9; /* Light green */
color: #2e7d32; /* Dark green */
}
.role-guest {
background-color: #e0f2f7; /* Light blue */
color: #01579b; /* Dark blue */
}
.actions {
text-align: center;
margin-top: 30px;
}
.actions button {
padding: 10px 20px;
background-color: #007bff;
color: white;
border: none;
border-radius: 5px;
font-size: 1em;
cursor: pointer;
transition: background-color 0.3s ease;
}
.actions button:hover:not(:disabled) {
background-color: #0056b3;
}
.actions button:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
.spinner {
border: 4px solid rgba(0, 0, 0, 0.1);
width: 36px;
height: 36px;
border-radius: 50%;
border-left-color: #007bff;
animation: spin 1s ease infinite;
margin: 10px auto;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
Step 5: Integrate UserListComponent into AppComponent
For now, let’s directly add UserListComponent to app.component.ts so we can see it in action. Later, we’ll create a dedicated UsersPageComponent and set up routing.
Open src/app/app.component.ts:
// src/app/app.component.ts
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterOutlet } from '@angular/router';
import { UserListComponent } from './features/users/components/user-list/user-list.component'; // Import it!
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, RouterOutlet, UserListComponent], // Add UserListComponent to imports
template: `
<header class="app-header">
<h1>User Management Dashboard</h1>
</header>
<main>
<app-user-list></app-user-list> <!-- Add our user list component -->
</main>
<router-outlet></router-outlet>
`,
styles: [`
.app-header {
background-color: #007bff;
color: white;
padding: 20px;
text-align: center;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
h1 {
margin: 0;
font-size: 2em;
}
main {
padding: 20px;
background-color: #f8f9fa;
min-height: calc(100vh - 80px); /* Adjust based on header height */
}
`]
})
export class AppComponent {
title = 'user-management-app';
}
Step 6: Run the Application
Ensure your json-server is running in one terminal:
npm run serve:json-api
And your Angular app in another:
ng serve
Open your browser to http://localhost:4200. You should now see the “Registered Users” heading, a loading message (briefly), and then your list of users fetched from db.json!
Mini-Challenge: Simulate an API Error
To test your error handling:
- Temporarily stop your
json-server(e.g.,Ctrl+Cin its terminal). - Refresh your Angular application in the browser.
- What do you observe? Does the error message appear?
- Restart your
json-serverand click the “Try Again” button in your app. Does it recover?
This helps verify your error and loading state logic.
Summary/Key Takeaways
- We’ve successfully created
UserListComponentas a standalone component. - The component injects
UserServiceand subscribes to itsusers$,loadingUsers, anderrorLoadingUsersobservables. - We use local signals (
users,loading,error) in the component to manage UI state, updating them from the service’sBehaviorSubjects. - Angular v21’s new
@ifand@forcontrol flow syntax provides a clean and performant way to render lists and conditional content. - Direct class binding (
[class]) is used to dynamically style user roles. - Proper subscription cleanup is handled in
ngOnDestroy.
Our user list is now functional! In the next chapter, we’ll build a form to add new users, giving us a chance to explore Signal Forms (experimentally).