Overview
This guide will walk you through creating a working Angular component using the patterns and architecture from the Angular PWA Demo application. You’ll learn about modern Angular features including signals, dependency injection, and service integration.
This tutorial assumes you have completed the installation steps and have the development server running.
Create your first component
Let’s create a simple dashboard component that demonstrates the application’s core patterns.
Generate the component
Use Angular CLI to generate a new component: ng generate component components/dashboard --standalone=false
This creates:
dashboard.component.ts - Component logic
dashboard.component.html - Template
dashboard.component.css - Styles
dashboard.component.spec.ts - Tests
Implement component logic
Open dashboard.component.ts and implement the component using Angular 21 patterns: import { Component , OnInit , inject , signal } from '@angular/core' ;
import { ConfigService } from '../../_services/__Utils/ConfigService/config.service' ;
import { BackendService } from '../../_services/BackendService/backend.service' ;
@ Component ({
selector: 'app-dashboard' ,
templateUrl: './dashboard.component.html' ,
styleUrls: [ './dashboard.component.css' ],
standalone: false
})
export class DashboardComponent implements OnInit {
// Modern dependency injection using inject()
private readonly configService = inject ( ConfigService );
private readonly backendService = inject ( BackendService );
// Reactive state using Signals (Angular 21)
public readonly appBrand = signal < string >( '' );
public readonly appVersion = signal < string >( '' );
public readonly isLoading = signal < boolean >( false );
ngOnInit () : void {
this . loadConfiguration ();
}
private loadConfiguration () : void {
// Get configuration values
const brand = this . configService . getConfigValue ( 'appBrand' ) ?? 'Angular PWA' ;
const version = this . configService . getConfigValue ( 'appVersion' ) ?? '1.0.0' ;
// Update signals
this . appBrand . set ( brand );
this . appVersion . set ( version );
}
public refreshData () : void {
this . isLoading . set ( true );
// Simulate data refresh
setTimeout (() => {
this . isLoading . set ( false );
console . log ( 'Data refreshed' );
}, 1000 );
}
}
Create the template
Add the HTML template in dashboard.component.html: < div class = "container mt-4" >
< div class = "card" >
< div class = "card-header bg-primary text-white" >
< h2 > {{ appBrand() }} Dashboard </ h2 >
< small > Version: {{ appVersion() }} </ small >
</ div >
< div class = "card-body" >
< div class = "row" >
< div class = "col-md-6" >
< h4 > Welcome </ h4 >
< p > This is a dashboard component built with Angular 21 patterns. </ p >
< h5 > Key Features: </ h5 >
< ul >
< li > Reactive state with Signals </ li >
< li > Modern inject() function for DI </ li >
< li > Service integration </ li >
< li > Bootstrap styling </ li >
</ ul >
</ div >
< div class = "col-md-6" >
< h4 > Actions </ h4 >
< button
class = "btn btn-primary"
(click) = "refreshData()"
[disabled] = "isLoading()" >
@if (isLoading()) {
< span class = "spinner-border spinner-border-sm me-2" ></ span >
Loading...
} @else {
Refresh Data
}
</ button >
</ div >
</ div >
</ div >
</ div >
</ div >
Add component styling
Style your component in dashboard.component.css: .card {
box-shadow : 0 4 px 6 px rgba ( 0 , 0 , 0 , 0.1 );
border-radius : 8 px ;
}
.card-header {
border-top-left-radius : 8 px ;
border-top-right-radius : 8 px ;
}
.card-header h2 {
margin : 0 ;
font-size : 1.5 rem ;
}
.card-header small {
opacity : 0.9 ;
}
.btn {
transition : all 0.3 s ease ;
}
.btn:hover:not ( :disabled ) {
transform : translateY ( -2 px );
box-shadow : 0 4 px 8 px rgba ( 0 , 0 , 0 , 0.2 );
}
Register in module
Add your component to the appropriate module. For example, in app.module.ts: import { DashboardComponent } from './components/dashboard/dashboard.component' ;
@ NgModule ({
declarations: [
AppComponent ,
DashboardComponent , // Add here
// ... other components
],
// ... rest of module configuration
})
export class AppModule { }
Add routing
Add a route in app-routing.module.ts: import { DashboardComponent } from './components/dashboard/dashboard.component' ;
const routes : Routes = [
{ path: 'dashboard' , component: DashboardComponent },
// ... other routes
];
Understanding the architecture
Signals for reactive state
Angular 21 introduces Signals for fine-grained reactivity. Here’s how they’re used in the application:
// Create a signal
public readonly title = signal < string >( '' );
// Update a signal
this . title . set ( 'New Value' );
// Read a signal in template
{{ title () }}
From app.component.ts:20-38:
// Properties using Signals
public readonly title = signal < string >( '' );
public readonly appBrand = signal < string >( '' );
public readonly appVersion = signal < string >( '' );
private initializeConfig (): void {
// Get configuration values
const brand = this . _configService . getConfigValue ( 'appBrand' ) ?? 'App' ;
const version = this . _configService . getConfigValue ( 'appVersion' ) ?? '1.0.0' ;
// Update Signals
this . appBrand . set ( brand );
this . appVersion . set ( version );
this . title . set ( brand );
// Update browser tab title
this . titleService . setTitle ( ` ${ brand } - ${ version } ` );
}
Modern dependency injection
Use the inject() function instead of constructor injection:
import { Component , inject } from '@angular/core' ;
import { ConfigService } from './services/config.service' ;
export class MyComponent {
// Modern style - cleaner and more readable
private readonly configService = inject ( ConfigService );
// Old style (still supported)
// constructor(private configService: ConfigService) {}
}
Configuration service
The application uses a centralized configuration service that loads settings at startup. From config.service.ts:34-43:
loadConfig () {
return this . http . get ( './assets/config/config.json' ). toPromise ()
. then (( data : any ) => {
_environment . externalConfig = data ;
})
. catch ( error => {
console . error ( 'Error loading configuration:' , error );
});
}
Access configuration values anywhere:
const apiUrl = this . configService . getConfigValue ( 'baseUrlNetCore' );
const appName = this . configService . getConfigValue ( 'appBrand' );
Advanced patterns
HTTP interceptor
The application includes a global HTTP interceptor that logs all requests. From app.module.ts:39-60:
export const loggingInterceptor : HttpInterceptorFn = (
req : HttpRequest < unknown >,
next : HttpHandlerFn
) => {
const started = Date . now ();
const backend = inject ( BackendService );
let status : string = 'pending' ;
return next ( req ). pipe (
tap ({
next : ( event ) => {
if ( event instanceof HttpResponse ) status = 'succeeded' ;
},
error : ( error : HttpErrorResponse ) => {
status = 'failed' ;
backend . SetLog (
"[HTTP ERROR]" ,
`URL: ${ req . url } - Status: ${ error . status } ` ,
LogType . Error
);
}
}),
finalize (() => {
const elapsed = Date . now () - started ;
console . warn ( `[HTTP LOG]: ${ req . method } " ${ req . urlWithParams } " ${ status } in ${ elapsed } ms.` );
})
);
};
Custom error handler
Global error handling captures runtime exceptions. From app.module.ts:66-74:
@ Injectable ({ providedIn: 'root' })
export class CustomErrorHandler implements ErrorHandler {
private backendService = inject ( BackendService );
handleError ( _error : Error ) : void {
console . error ( "[RUNTIME ERROR]: \n " , _error );
this . backendService . SetLog (
"[RUNTIME ERROR]" ,
_error . message ,
LogType . Error
);
}
}
Service worker registration
PWA capabilities are enabled through service workers. From app.module.ts:103-106:
ServiceWorkerModule . register ( 'ngsw-worker.js' , {
enabled: ! isDevMode (),
registrationStrategy: 'registerWhenStable:30000'
})
Working with services
Creating a custom service
Generate a new service:
ng generate service services/data/data
Implement the service:
import { Injectable , inject } from '@angular/core' ;
import { HttpClient } from '@angular/common/http' ;
import { Observable } from 'rxjs' ;
import { ConfigService } from '../__Utils/ConfigService/config.service' ;
@ Injectable ({
providedIn: 'root'
})
export class DataService {
private readonly http = inject ( HttpClient );
private readonly config = inject ( ConfigService );
private get baseUrl () : string {
return this . config . getConfigValue ( 'baseUrlNetCore' ) || '' ;
}
getData () : Observable < any > {
return this . http . get ( ` ${ this . baseUrl } /api/data` );
}
postData ( payload : any ) : Observable < any > {
return this . http . post ( ` ${ this . baseUrl } /api/data` , payload );
}
}
Testing your component
Run the development server if not already running:
Navigate to http://localhost:4200/dashboard to see your component in action.
Run unit tests
The default test suite will verify your component is created successfully.
Common patterns
Use signals to manage loading states: public readonly isLoading = signal < boolean >( false );
async loadData () {
this . isLoading . set ( true );
try {
const data = await this . dataService . getData ();
// Process data
} finally {
this . isLoading . set ( false );
}
}
Access route parameters: import { ActivatedRoute } from '@angular/router' ;
export class DetailComponent {
private route = inject ( ActivatedRoute );
public readonly itemId = signal < string >( '' );
ngOnInit () {
this . route . params . subscribe ( params => {
this . itemId . set ( params [ 'id' ]);
});
}
}
Next steps
Explore demo modules Check out the algorithm, game, and file generation demos in the application
Add Material components Integrate Angular Material components for rich UI elements
Build a feature module Create a complete feature module with routing and lazy loading
Deploy as PWA Build and deploy your application with offline capabilities
All code examples in this guide are based on real patterns from the Angular PWA Demo source code. The application follows Angular 21 best practices including standalone components support, signals, and modern dependency injection.