Skip to content

Commit 318bc23

Browse files
authored
Merge pull request #15 from nigrosimone/ng-21
feat: upgrade angular to version 21
2 parents 530d489 + 6a69acf commit 318bc23

15 files changed

Lines changed: 9561 additions & 13464 deletions

apps/angular/angular.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,7 @@
3030
"outputPath": "dist/weather-app-angular",
3131
"index": "src/index.html",
3232
"main": "src/main.ts",
33-
"polyfills": [
34-
"zone.js"
35-
],
33+
"polyfills": [],
3634
"tsConfig": "tsconfig.app.json",
3735
"assets": [
3836
"src/favicon.ico",

apps/angular/package-lock.json

Lines changed: 9345 additions & 13267 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/angular/package.json

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,24 +10,23 @@
1010
"test:ui": "playwright test --ui"
1111
},
1212
"dependencies": {
13-
"@angular/animations": "^17.0.0",
14-
"@angular/common": "^17.0.0",
15-
"@angular/compiler": "^17.0.0",
16-
"@angular/core": "^17.0.0",
17-
"@angular/forms": "^17.0.0",
18-
"@angular/platform-browser": "^17.0.0",
19-
"@angular/platform-browser-dynamic": "^17.0.0",
20-
"@angular/router": "^17.0.0",
13+
"@angular/animations": "^21.2.11",
14+
"@angular/common": "^21.2.11",
15+
"@angular/compiler": "^21.2.11",
16+
"@angular/core": "^21.2.11",
17+
"@angular/forms": "^21.2.11",
18+
"@angular/platform-browser": "^21.2.11",
19+
"@angular/platform-browser-dynamic": "^21.2.11",
20+
"@angular/router": "^21.2.11",
2121
"rxjs": "~7.8.0",
22-
"tslib": "^2.3.0",
23-
"zone.js": "~0.14.0"
22+
"tslib": "^2.8.1"
2423
},
2524
"devDependencies": {
26-
"@angular-devkit/build-angular": "^17.0.0",
27-
"@angular/cli": "^17.0.0",
28-
"@angular/compiler-cli": "^17.0.0",
25+
"@angular-devkit/build-angular": "^21.2.9",
26+
"@angular/cli": "^21.2.9",
27+
"@angular/compiler-cli": "^21.2.11",
2928
"@playwright/test": "^1.40.0",
30-
"typescript": "~5.2.0"
29+
"typescript": "~5.9.3"
3130
},
3231
"keywords": [
3332
"weather",

apps/angular/src/app/app.component.ts

Lines changed: 13 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
1-
import { Component, OnInit, OnDestroy } from '@angular/core';
2-
import { CommonModule } from '@angular/common';
3-
import { Subject } from 'rxjs';
4-
import { takeUntil } from 'rxjs/operators';
1+
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
52

63
import { WeatherStateService } from './services/weather-state.service';
7-
import { AppState } from './types/weather.types';
84

95
import { SearchFormComponent } from './components/search-form.component';
106
import { LoadingStateComponent } from './components/loading-state.component';
@@ -15,12 +11,12 @@ import { WeatherContentComponent } from './components/weather-content.component'
1511
selector: 'app-root',
1612
standalone: true,
1713
imports: [
18-
CommonModule,
1914
SearchFormComponent,
2015
LoadingStateComponent,
2116
ErrorStateComponent,
2217
WeatherContentComponent
2318
],
19+
changeDetection: ChangeDetectionStrategy.OnPush,
2420
template: `
2521
<header class="header">
2622
<div class="container">
@@ -30,22 +26,24 @@ import { WeatherContentComponent } from './components/weather-content.component'
3026
3127
<main class="main">
3228
<div class="container">
29+
@let currentState = state();
30+
3331
<app-search-form
34-
[isLoading]="state.isLoading"
32+
[isLoading]="currentState.isLoading"
3533
(search)="onSearch($event)"
3634
></app-search-form>
3735
3836
<div class="weather-container" data-testid="weather-container">
39-
<app-loading-state [isVisible]="state.isLoading"></app-loading-state>
37+
<app-loading-state [isVisible]="currentState.isLoading"></app-loading-state>
4038
4139
<app-error-state
42-
[isVisible]="!!state.error && !state.isLoading"
43-
[message]="state.error"
40+
[isVisible]="!!currentState.error && !currentState.isLoading"
41+
[message]="currentState.error"
4442
></app-error-state>
4543
4644
<app-weather-content
47-
[isVisible]="!!state.weatherData && !state.isLoading && !state.error"
48-
[weatherData]="state.weatherData"
45+
[isVisible]="!!currentState.weatherData && !currentState.isLoading && !currentState.error"
46+
[weatherData]="currentState.weatherData"
4947
></app-weather-content>
5048
</div>
5149
</div>
@@ -63,29 +61,10 @@ import { WeatherContentComponent } from './components/weather-content.component'
6361
</footer>
6462
`
6563
})
66-
export class AppComponent implements OnInit, OnDestroy {
67-
state: AppState = {
68-
weatherData: null,
69-
isLoading: false,
70-
error: null
71-
};
72-
73-
private destroy$ = new Subject<void>();
74-
75-
constructor(private weatherStateService: WeatherStateService) {}
64+
export class AppComponent {
65+
private readonly weatherStateService = inject(WeatherStateService);
7666

77-
ngOnInit(): void {
78-
this.weatherStateService.state$
79-
.pipe(takeUntil(this.destroy$))
80-
.subscribe(state => {
81-
this.state = state;
82-
});
83-
}
84-
85-
ngOnDestroy(): void {
86-
this.destroy$.next();
87-
this.destroy$.complete();
88-
}
67+
protected readonly state = this.weatherStateService.state;
8968

9069
onSearch(city: string): void {
9170
this.weatherStateService.loadWeather(city);
Lines changed: 79 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,34 @@
1-
import { Component, Input } from '@angular/core';
2-
import { CommonModule } from '@angular/common';
1+
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
32
import { WeatherData } from '../types/weather.types';
43
import { WeatherUtils } from '../utils/weather.utils';
54

65
@Component({
76
selector: 'app-current-weather',
87
standalone: true,
9-
imports: [CommonModule],
8+
imports: [],
9+
changeDetection: ChangeDetectionStrategy.OnPush,
1010
template: `
11-
<section class="current-section" *ngIf="weatherData">
11+
@if (weatherData(); as weatherData) {
12+
<section class="current-section">
1213
<h2 class="section-title">Current Weather</h2>
1314
<div class="weather-card" data-testid="current-weather">
1415
<div class="current-weather">
1516
<h3 class="current-weather__location" data-testid="current-location">
16-
{{ weatherData.locationName }}{{ weatherData.country ? ', ' + weatherData.country : '' }}
17+
{{ locationLabel() }}
1718
</h3>
1819
<div class="current-weather__main">
1920
<div class="current-weather__icon" data-testid="current-icon">
20-
{{ getWeatherIcon(weatherData.current.weather_code, weatherData.current.is_day) }}
21+
{{ weatherIcon() }}
2122
</div>
2223
<div class="current-weather__temp-group">
2324
<div class="current-weather__temp" data-testid="current-temperature">
24-
{{ formatTemperature(weatherData.current.temperature_2m) }}
25+
{{ currentTemperature() }}
2526
</div>
2627
<div
27-
class="current-weather__condition {{ getConditionClass(weatherData.current.weather_code) }}"
28+
class="current-weather__condition {{ conditionClass() }}"
2829
data-testid="current-condition"
2930
>
30-
{{ getWeatherDescription(weatherData.current.weather_code) }}
31+
{{ weatherDescription() }}
3132
</div>
3233
</div>
3334
</div>
@@ -36,54 +37,107 @@ import { WeatherUtils } from '../utils/weather.utils';
3637
<div class="weather-detail">
3738
<div class="weather-detail__label">Feels like</div>
3839
<div class="weather-detail__value" data-testid="feels-like">
39-
{{ formatTemperature(weatherData.current.apparent_temperature) }}
40+
{{ apparentTemperature() }}
4041
</div>
4142
</div>
4243
<div class="weather-detail">
4344
<div class="weather-detail__label">Humidity</div>
4445
<div class="weather-detail__value" data-testid="humidity">
45-
{{ formatPercentage(weatherData.current.relative_humidity_2m) }}
46+
{{ humidity() }}
4647
</div>
4748
</div>
4849
<div class="weather-detail">
4950
<div class="weather-detail__label">Wind Speed</div>
5051
<div class="weather-detail__value" data-testid="wind-speed">
51-
{{ formatWindSpeed(weatherData.current.wind_speed_10m) }}
52+
{{ windSpeed() }}
5253
</div>
5354
</div>
5455
<div class="weather-detail">
5556
<div class="weather-detail__label">Pressure</div>
5657
<div class="weather-detail__value" data-testid="pressure">
57-
{{ formatPressure(weatherData.current.pressure_msl) }}
58+
{{ pressure() }}
5859
</div>
5960
</div>
6061
<div class="weather-detail">
6162
<div class="weather-detail__label">Cloud Cover</div>
6263
<div class="weather-detail__value" data-testid="cloud-cover">
63-
{{ formatPercentage(weatherData.current.cloud_cover) }}
64+
{{ cloudCover() }}
6465
</div>
6566
</div>
6667
<div class="weather-detail">
6768
<div class="weather-detail__label">Wind Direction</div>
6869
<div class="weather-detail__value" data-testid="wind-direction">
69-
{{ getWindDirection(weatherData.current.wind_direction_10m) }}
70+
{{ windDirection() }}
7071
</div>
7172
</div>
7273
</div>
7374
</div>
7475
</div>
75-
</section>
76+
</section>
77+
}
7678
`
7779
})
7880
export class CurrentWeatherComponent {
79-
@Input() weatherData: WeatherData | null = null;
81+
readonly weatherData = input<WeatherData | null>(null);
8082

81-
formatTemperature = WeatherUtils.formatTemperature;
82-
formatPercentage = WeatherUtils.formatPercentage;
83-
formatWindSpeed = WeatherUtils.formatWindSpeed;
84-
formatPressure = WeatherUtils.formatPressure;
85-
getWindDirection = WeatherUtils.getWindDirection;
86-
getWeatherDescription = WeatherUtils.getWeatherDescription;
87-
getWeatherIcon = WeatherUtils.getWeatherIcon;
88-
getConditionClass = WeatherUtils.getConditionClass;
83+
readonly locationLabel = computed(() => {
84+
const weatherData = this.weatherData();
85+
if (!weatherData) {
86+
return '';
87+
}
88+
89+
return weatherData.country
90+
? `${weatherData.locationName}, ${weatherData.country}`
91+
: (weatherData.locationName ?? '');
92+
});
93+
94+
readonly weatherIcon = computed(() => {
95+
const current = this.weatherData()?.current;
96+
return current ? WeatherUtils.getWeatherIcon(current.weather_code, current.is_day) : '';
97+
});
98+
99+
readonly currentTemperature = computed(() => {
100+
const current = this.weatherData()?.current;
101+
return current ? WeatherUtils.formatTemperature(current.temperature_2m) : '';
102+
});
103+
104+
readonly conditionClass = computed(() => {
105+
const current = this.weatherData()?.current;
106+
return current ? WeatherUtils.getConditionClass(current.weather_code) : '';
107+
});
108+
109+
readonly weatherDescription = computed(() => {
110+
const current = this.weatherData()?.current;
111+
return current ? WeatherUtils.getWeatherDescription(current.weather_code) : '';
112+
});
113+
114+
readonly apparentTemperature = computed(() => {
115+
const current = this.weatherData()?.current;
116+
return current ? WeatherUtils.formatTemperature(current.apparent_temperature) : '';
117+
});
118+
119+
readonly humidity = computed(() => {
120+
const current = this.weatherData()?.current;
121+
return current ? WeatherUtils.formatPercentage(current.relative_humidity_2m) : '';
122+
});
123+
124+
readonly windSpeed = computed(() => {
125+
const current = this.weatherData()?.current;
126+
return current ? WeatherUtils.formatWindSpeed(current.wind_speed_10m) : '';
127+
});
128+
129+
readonly pressure = computed(() => {
130+
const current = this.weatherData()?.current;
131+
return current ? WeatherUtils.formatPressure(current.pressure_msl) : '';
132+
});
133+
134+
readonly cloudCover = computed(() => {
135+
const current = this.weatherData()?.current;
136+
return current ? WeatherUtils.formatPercentage(current.cloud_cover) : '';
137+
});
138+
139+
readonly windDirection = computed(() => {
140+
const current = this.weatherData()?.current;
141+
return current ? WeatherUtils.getWindDirection(current.wind_direction_10m) : '';
142+
});
89143
}
Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,24 @@
1-
import { Component, Input } from '@angular/core';
2-
import { CommonModule } from '@angular/common';
1+
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
32

43
@Component({
54
selector: 'app-error-state',
65
standalone: true,
7-
imports: [CommonModule],
6+
imports: [],
7+
changeDetection: ChangeDetectionStrategy.OnPush,
88
template: `
99
<div
1010
class="error"
1111
data-testid="error"
12-
[hidden]="!isVisible"
12+
[hidden]="!isVisible()"
1313
>
1414
<h2 class="error__title">Unable to load weather data</h2>
1515
<p class="error__message">
16-
{{ message || 'Please check the city name and try again.' }}
16+
{{ message() || 'Please check the city name and try again.' }}
1717
</p>
1818
</div>
1919
`
2020
})
2121
export class ErrorStateComponent {
22-
@Input() isVisible = false;
23-
@Input() message: string | null = null;
22+
readonly isVisible = input(false);
23+
readonly message = input<string | null>(null);
2424
}

0 commit comments

Comments
 (0)