A comprehensive example demonstrating separation of concerns in Angular applications using Google Maps with polygon drawing capabilities. This project showcases modern Angular patterns with signal-based reactive programming, proper architectural boundaries, and form integration.
This application demonstrates clean separation between three distinct responsibilities:
- Responsibility: Core Google Maps functionality (zoom, center, map instance)
- Boundaries: Does not know about polygons or form controls
- Scope: Pure map rendering and basic state management
- Responsibility: Angular Reactive Forms integration
- Boundaries: Does not know about Google Maps implementation details
- Scope: Form control value accessor, validation, form state
- Responsibility: Polygon creation, editing, and user interaction
- Boundaries: Bridges map component and form control without tight coupling
- Scope: Drawing logic, event handling, coordinate management
- Each component has a single, well-defined responsibility
- Components can be tested independently
- Easy to modify or replace individual parts without affecting others
GoogleMapComponent
can be used for any map-based featurePolygonFormControlComponent
can work with any drawing implementationPolygonDrawingDirective
can be applied to any map component
- Changes to Google Maps API only affect the map component
- Form validation logic is isolated in the form control
- Drawing behavior can be enhanced without touching other components
This project follows the Angular Development Constitution principles:
- ✅ All components use
standalone: true
- ✅ No NgModules for feature components
- ✅ Direct imports in component decorators
- ✅ Signal-based state management:
signal<T>()
- ✅ Signal inputs:
input<T>()
- ✅ Signal outputs:
output<T>()
- ✅ Effects for side effects:
effect(() => ...)
- ✅ Computed signals for derived state
- ✅ Component-specific types in namespaces:
GoogleMapComponent.Coordinates
- ✅ Clear interface definitions:
PolygonDrawingDirective.PolygonChangeEvent
- ✅ Proper use of utility types:
Omit<google.maps.MapOptions, ...>
- ✅ Modern
inject()
function usage - ✅ Host injection:
inject(GoogleMapComponent, { host: true })
- ✅ Injection tokens for configuration:
GOOGLE_MAPS_API_KEY
- ✅
ControlValueAccessor
implementation - ✅ Signal-based form state management
- ✅ Proper change detection handling
// Clean provider configuration
provideGoogleMapsApiKey(environment.googleMaps.apiKey),
provideGoogleMapsLoader(),
provideGoogleMapsInitializer(environment.googleMaps.libraries)
// Modern signal-based ControlValueAccessor
public value = signal<Coordinates[] | null>(null, { equal: jsonEquals });
public onValueChange = effect(() => {
queueMicrotask(() => {
this.onChange(this.value());
});
});
// Directive accessing host component
private readonly mapComponent = inject(GoogleMapComponent, { host: true });
- Node.js 18+
- Angular CLI 20+
- Google Maps API Key
npm install
- Copy
.secret.json.example
to.secret.json
- Add your Google Maps API Key:
{
"googleMaps": {
"apiKey": "YOUR_API_KEY_HERE"
}
}
npm start
src/app/
├── components/
│ ├── google-map/ # Map component (zoom, center, instance)
│ │ ├── google-map.component.ts
│ │ └── google-map.component.scss
│ └── polygon-form-control/ # Form integration component
│ └── polygon-form-control.component.ts
├── directives/
│ └── polygon-drawing/ # Drawing behavior directive
│ └── polygon-drawing.directive.ts
├── providers/
│ └── google-map.provider.ts # Google Maps configuration
├── environments/
│ ├── environment.ts
│ ├── environment.prod.ts
│ └── environment.model.ts
└── app.ts # Main application component
// Pure map functionality
public center = input<Coordinates>({ lat: 40.7128, lng: -74.0060 });
public zoom = input<number>(8);
public map = signal<google.maps.Map | null>(null);
// Form integration only
implements ControlValueAccessor {
value = signal<Coordinates[] | null>(null);
writeValue(value: Coordinates[] | null): void { /* ... */ }
registerOnChange(fn: Function): void { /* ... */ }
}
// Drawing behavior bridge
coordinates = input<Coordinates[] | null>(null);
polygonChange = output<PolygonChangeEvent>();
// Bridges map and form without tight coupling
- User draws polygon →
PolygonDrawingDirective
captures interaction - Directive emits event →
polygonChange.emit(coordinates)
- Form control updates →
polygonControl.setValue(coordinates)
- Form reflects changes → Template shows validation state
The application handles Angular change detection challenges with:
// Proper timing for form updates
queueMicrotask(() => {
this.polygonChange.emit({ coordinates, action: 'edit' });
});
This ensures form state changes happen after change detection completes, preventing timing conflicts.
- ✅ Polygon Drawing: Click to create, drag to edit
- ✅ Form Integration: Reactive forms with validation
- ✅ Real-time Updates: Bidirectional data binding
- ✅ Change Detection: Proper signal-based reactivity
- ✅ Error Handling: Graceful Google Maps API failures
- ✅ TypeScript: Strong typing throughout
- ✅ Responsive Design: Mobile-friendly interface
- Multiple Polygon Support: Extend to handle polygon arrays
- Drawing Tools: Add circle, rectangle, and line drawing
- Persistence: Save/load polygon data from backend
- Advanced Validation: Custom polygon validation rules
- Performance: Implement virtual scrolling for large datasets
- GoogleMapComponent: Mock Google Maps API, test signal reactivity
- PolygonFormControlComponent: Test ControlValueAccessor implementation
- PolygonDrawingDirective: Test host injection and event emission
- Form Integration: Test complete drawing → form → validation flow
- Error Scenarios: Test Google Maps API failures
- Change Detection: Verify no timing conflicts
This example teaches:
- Architectural Patterns: How to separate concerns effectively
- Angular Signals: Modern reactive programming patterns
- Form Integration: Custom form controls with proper change detection
- Google Maps: Professional integration patterns
- TypeScript: Advanced typing with namespaces and utility types
- Dependency Injection: Modern patterns with
inject()
function
- Single Responsibility: Each component has one clear purpose
- Composition over Inheritance: Directive composition for behavior
- Signal-First: Modern Angular reactivity patterns
- Type Safety: Comprehensive TypeScript usage
- Clean Code: Self-documenting code with clear naming
- Performance: Efficient change detection and memory management
- Maintainability: Easy to extend and modify
ng serve
ng build
ng test
This project serves as a template for building complex Angular applications with proper architectural boundaries and modern Angular patterns.