Bước 2: Lập trình
B. Services
Đầu tiên ta sẽ tách phần gọi API lấy dữ liệu sang 1 service riêng, để có thể gọi đến ở bất cứ chỗ nào mà không phải viết lại nữa.
Dùng CLI để tự động generate luôn
- Weather Service
1 2 | ng g s weather |
Service này gọi đến API của OpenWeatherMap để lấy thông tin thời tiết.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | import {Injectable} from '@angular/core'; import {HttpClient} from '@angular/common/http'; import {Observable} from 'rxjs'; import {environment} from '../../../environments/environment'; import {first, map} from 'rxjs/operators'; @Injectable({ providedIn: 'root' }) export class WeatherService { private readonly baseURL = 'https://api.openweathermap.org/data/2.5/weather?q='; private readonly forcastURL = 'https://api.openweathermap.org/data/2.5/forecast?q='; private readonly appID = environment.appID; constructor(public http: HttpClient) { } getWeather(city: string, metric: 'metric' | 'imperial' = 'metric'): Observable<any> { return this.http.get( `${this.baseURL}${city}&units=${metric}&APPID=${this.appID}`).pipe((first())); } getForecast(city: string, metric: 'metric' | 'imperial' = 'metric'): Observable<any> { return this.http.get( `${this.forcastURL}${city}&units=${metric}&APPID=${this.appID}`) .pipe(first(), map((weather) => weather['list'])); } } |
Ở đây ta có 2 function:
getWeather(): Lấy thời tiết hiện tại của thành phố mà ta truyền vào params
getForecast(): Lấy thời tiết 5 ngày tiếp theo của thành phố mà ta truyền vào params.
- UI service
Đây là 1 service nhỏ để chứa các function dùng để chia sẻ trạng thái UI, như kiểu người dùng đang chọn chế độ dark mode hay light mode
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | import { Injectable } from '@angular/core'; import { BehaviorSubject } from 'rxjs'; @Injectable() export class UiService { darkModeState: BehaviorSubject<boolean>; constructor() { // TODO: if the user is signed in get the default value from Firebase this.darkModeState = new BehaviorSubject<boolean>(false); } } |
C. Routing
Về cơ bản khi chúng ta generate app bằng CLI thì nó đã có sẵn file routing rồi, nhưng vẫn phải sửa lại để thêm url các trang và nó liên kết vs component nào
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | import {NgModule} from '@angular/core'; import {Routes, RouterModule} from '@angular/router'; import {HomeComponent} from './pages/home/home.component'; import {DetailsComponent} from './pages/details/details.component'; import {AddComponent} from './pages/add/add.component'; import {LoginComponent} from './pages/login/login.component'; import {SignupComponent} from './pages/signup/signup.component'; const routes: Routes = [ {path: '', component: HomeComponent}, {path: 'details/:city', component: DetailsComponent}, {path: 'add', component: AddComponent}, {path: 'login', component: LoginComponent}, {path: 'signup', component: SignupComponent}, ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { } |
Rồi, xong 2 phần còn sót lại của bài trước, giờ tiếp phần 2 nhé!
D. Refactor
Update Angular mới nhất
Nếu bạn đã follow bài viết từ phần 1 thi chắc hẳn project của bạn vẫn đang là bản Angular 7, vì vậy mình sẽ hướng dẫn các bạn cách update lên version mới nhất. Cũng khá dễ thôi vì từ bản 7 lên bản 8 không có phần nào gây ảnh hưởng đến code cũ.
Chỉ cần chạy lệnh sau và đợi nó update:
1 2 | ng update @angular/cli @angular/core |
Refactor weather card để dùng lại trong trang thêm thành phố
Chúng ta sẽ thêm 1 trang để người dùng có thể thêm thành phố mới vào trang homepage của mình, nên có thể dùng lại weather card làm card kết quả tìm kiếm và thêm 1 biến boolean để component cha truyền xuống để phân biệt.
1 2 3 4 5 6 7 8 9 10 11 12 | <button class="add-city-btn" *ngIf="addMode" (click)="addCity()">ADD CITY +</button> <div *ngIf="cityAdded" class="city-added-note"> <h5 class="add-success-text">City has been successfully added!</h5> <svg viewBox="0 0 50 50" height="5rem"> <circle cx="25" cy="25" r="25" fill="#25ae88"/> <path fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M38 15L22 33l-10-8"/> </svg> </div> |
Để biết được addMode
là biến truyền sang, chúng ta sẽ thêm @input
vào phía trước nó khi khai báo. Biến city
cũng tương tự vậy, có điều, ta muốn lấy điều kiện thời tiết của thành phố từ component cha dưới dạng 1 string, và để làm được điều này thì ta thêm @input
cho 1 hàm thay vì 1 biến, để được gọi đến mỗi khi có thành phố được tạo thành công.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | import {Component, EventEmitter, Input, OnDestroy, OnInit, Output} from '@angular/core'; import {Router} from '@angular/router'; import {WeatherService} from '../../services/weather/weather.service'; import {UiService} from '../../services/ui/ui.service'; import {Subscription} from 'rxjs'; import {first} from 'rxjs/operators'; import {FbService} from '../../services/fb/fb.service'; @Component({ selector: 'app-weather-card', templateUrl: './weather-card.component.html', styleUrls: ['./weather-card.component.css'] }) export class WeatherCardComponent implements OnInit, OnDestroy { @Input() set city(city: string) { this.cityName = city; this.weather.getWeather(city) .pipe(first()) .subscribe((payload) => { this.state = payload.weather[0].main; this.temp = Math.ceil(payload.main.temp); }, (err) => { this.errorMessage = err.error.message; setTimeout(() => { this.errorMessage = ''; }, 3000); }); this.weather.getForecast(city) .pipe(first()) .subscribe((payload) => { this.maxTemp = Math.round(payload[0].main.temp); this.minTemp = Math.round(payload[0].main.temp); for (const res of payload) { if (new Date().toLocaleDateString('en-GB') === new Date(res.dt_txt).toLocaleDateString('en-GB')) { this.maxTemp = res.main.temp > this.maxTemp ? Math.round(res.main.temp) : this.maxTemp; this.minTemp = res.main.temp < this.minTemp ? Math.round(res.main.temp) : this.minTemp; } } }, (err) => { this.errorMessage = err.error.message; setTimeout(() => { this.errorMessage = ''; }, 3000); }); } @Input() addMode; @Output() cityStored = new EventEmitter(); citesWeather: Object; darkMode: boolean; sub1: Subscription; state: string; temp: number; maxTemp: number; minTemp: number; errorMessage: string; cityName; cityAdded = false; constructor(public weather: WeatherService, public router: Router, public ui: UiService, public fb: FbService) { } ngOnInit() { this.sub1 = this.ui.darkModeState.subscribe((isDark) => { this.darkMode = isDark; }); } ngOnDestroy() { this.sub1.unsubscribe(); } openDetails() { if (!this.addMode) { this.router.navigateByUrl('/details/' + this.cityName); } } addCity() { this.fb.addCity(this.cityName).subscribe(() => { this.cityName = null; this.maxTemp = null; this.minTemp = null; this.state = null; this.temp = null; this.cityAdded = true; this.cityStored.emit(); setTimeout(() => this.cityAdded = false, 2000); }); } } |
Bên trong mỗi phương thức subcribe
, có sử dụng 1 số phương thức Javascript thông thường như Math.round
để gán các giá trị hiển thị ngoài HTML, nó giúp cho WeatherService đơn giản hết mức có thể, chỉ trả về đúng dữ liệu API, đề phòng sau này ta sẽ muốn dùng dữ liệu API trả về làm cái gì khác.
Trang home cũng phải sửa lại 1 chút
1 2 | <app-weather-card *ngFor="let city of cities | async;" [city]="city?.name"></app-weather-card> |
E: Bổ sung và thêm tính năng mới
CSS animation