- TypeScript Blueprints
- Ivo Gabe de Wolff
- 1769字
- 2021-07-14 10:59:29
Creating the forecast component
As a quick recap, the forecast widget will look like this:

What properties does the class need? The template will need forecast data of the current day or the next day. The component can show the weather of Today and Tomorrow, so we will also need a property for that. For fetching the forecast, we also need the location. To show the loading state in the template, we will also store that in the class. This will result in the following class, in lib/forecast.ts
:
import { Component, Input } from "angular2/core"; import { ForecastResponse } from "./api"; export interface ForecastData { date: string; temperature: number; main: string; description: string; } enum State { Loading, Refreshing, Loaded, Error } @Component({ selector: "weather-forecast", template: `...` }) export class Forecast { temperatureUnit = "degrees Celsius"; @Input() tomorrow = false; @Input() location = "Utrecht"; data: ForecastData[] = []; state = State.Loading; }
Tip
Testing
You can test this component by adjusting the tag in index.html
and bootstrapping the right component in index.ts
. Run gulp
to compile the sources and open the web browser.
Templates
The template uses the ngFor
directive to iterate over the data
array:
import { Component, Input } from "angular2/core"; import { ForecastResponse } from "./api"; ... @Component({ selector: "weather-forecast", template: ` <span *ngIf="loading" class="state">Loading...</span> <span *ngIf="refreshing" class="state">Refreshing...</span> <a *ngIf="loaded || error" href="javascript:;" (click)="load()" class="state">Refresh</a> <h2>{{ tomorrow ? 'Tomorrow' : 'Today' }}'s weather in {{ location }}</h2> <div *ngIf="error">Failed to load data.</div> <ul> <li *ngFor="#item of data"> <div class="item-date">{{ item.date }}</div> <div class="item-main">{{ item.main }}</div> <div class="item-description">{{ item.description }}</div> <div class="item-temperature"> {{ item.temperature }} {{ temperatureUnit }} </div> </li> </ul> <div class="clearfix"></div> `,
Using the styles
property, we can add nice CSS
styles, as shown here:
styles: [ `.state { float: right; margin-top: 6px; } ul { margin: 0; padding: 0 0 15px; list-style: none; width: 100%; overflow-x: scroll; white-space: nowrap; } li { display: inline-block; margin-right: 15px; width: 170px; white-space: initial; } .item-date { font-size: 15pt; color: #165366; margin-right: 10px; display: inline-block; } .item-main { font-size: 15pt; display: inline-block; } .item-description { border-top: 1px solid #44A4C2; width: 100%; font-size: 11pt; } .item-temperature { font-size: 11pt; }` ] })
In the class
body, we add the getters which we used in the template:
export class Forecast { ... state = State.Loading; get loading() { return this.state === State.Loading; } get refreshing() { return this.state === State.Refreshing; } get loaded() { return this.state === State.Loaded; } get error() { return this.state === State.Error; } ... }
Tip
Enums
Enums are just numbers with names attached to them. It's more readable to write State.Loaded
than 2
, but they mean the same in this context.
As you can see, the syntax of ngFor
is *ngFor="#variable of array"
. The enum
cannot be referenced from the template, so we need to add getters in the body of the class.
Downloading the forecast
To download data from the Internet in Angular, we need to get the HTTP service. We need to set the viewProviders
section for that:
import { Component, Input } from "angular2/core"; import { Http, Response, HTTP_PROVIDERS } from "angular2/http"; import { getUrl, ForecastResponse } from "./api"; ... @Component({ selector: "weather-forecast", viewProviders: [HTTP_PROVIDERS], template: `...`, styles: [...] }) export class Forecast { constructor(private http: Http) { } ...
Angular will inject the Http
service into the constructor.
Tip
By including private
or public
before an argument of the constructor, that argument will become a property of the class, initialized by the value of the argument.
We will now implement the load
function, which will try to download the forecast on the specified location. The function can also use coordinates as a location, written as Coordinates lat lon
, where lat
and lon
are the coordinates as shown here:
private load() { let path = "forecast?mode=json&"; const start = "coordinate "; if (this.location && this.location.substring(0, start.length).toLowerCase() === start) { const coordinate = this.location.split(" "); path += `lat=${ parseFloat(coordinate[1]) }&lon=${ parseFloat(coordinate[2]) }`; } else { path += "q=" + this.location; } this.state = this.state === State.Loaded ? State.Refreshing : State.Loading; this.http.get(getUrl(path)) .map(response => response.json()) .subscribe(res => this.update(<ForecastResponse> res), () =>this.showError()); };
Tip
Three kinds of variables
You can define variables with const
, let
, and var
. A variable declared with const
cannot be modified. Variables declared with const
or let
are block-scoped and cannot be used before their definition. A variable declared with var
is function scoped and can be used before its definition. Such variable can give unexpected behavior, so it's advised to use const
or let
.
The function will first calculate the URL, then set the state and finally fetch the data and get returns an observable. An observable, comparable to a promise, is something that contains a value that can change later on. Like with arrays, you can map an observable to a different observable. Subscribe registers a callback, which is called when the observable is changed. This observable changes only once, when the data is loaded. If something goes wrong, the second callback will be called.
Tip
Lambda expressions (inline functions)
The fat arrow (=>
) creates a new function. It's almost equal to a function defined with the function keyword (function () { return ... })
, but it is scoped lexically, which means that this
refers to the value of this
outside the function. x => expression
is a shorthand for (x) => expression
, which is a shorthand for (x) => { return expression; }
. TypeScript will automatically infer the type of the argument based on the signature of map
and subscribe
.
As you can see, this function uses the update
and showError
functions. The update function stores the results of the open weather map API, and showError is a small function that sets the state to State.Error
. Since temperatures of the API are expressed in Kelvin, we must substract 273 to get the value in Celsius:
fullData: ForecastData[] = []; data: ForecastData[] = []; private formatDate(date: Date) { return date.getHours() + ":" + date.getMinutes() + ":" + date.getSeconds(); } private update(data: ForecastResponse) { if (!data.list) { this.showError(); return; } this.fullData = data.list.map(item => ({ date: this.formatDate(new Date(item.dt * 1000)), temperature: Math.round(item.main.temp - 273), main: item.weather[0].main, description: item.weather[0].description })); this.filterData(); this.state = State.Loaded; } private showError() { this.data = []; this.state = State.Error; } private filterData() { const start = this.tomorrow ? 8 : 0; this.data = this.fullData.slice(start, start + 8); }
The filterData
method will filter the forecast based on whether we want to see the forecast of today or tomorrow. Open weather map has one forecast per 3 hours, so 8 per day. The slice
function will return a section of the array. fullData
will contain the full forecast, so we can easily show the forecast of tomorrow, if we have already shown today.
Tip
Change detection
Angular will automatically reload the template when some property is changed, there's no need to invalidate anything (as C# developers might expect). This is called change detection.
We also want to refresh data when the location is changed. If tomorrow is changed, we do not need to download any data, because we can just use a different section of the fullData
array. To do that, we will use getters and setters. In the setter, we can detect changes:
private _tomorrow = false; @Input() set tomorrow(value) { if (this._tomorrow === value) return; this._tomorrow = value; this.filterData(); } get tomorrow() { return this._tomorrow; } private _location: string; @Input() set location(value) { if (this._location === value) return; this._location = value; this.state = State.Loading; this.data = []; this.load(); } get location() { return this._location; }
Adding @Output
The response of Open weather map contains the name of the city. We can use this to simulate completion later on. We will create an event emitter. Other components can listen to the event and update the location when the event is triggered. The whole code will look like this with final changes highlighted:
import { Component, Input, Output, EventEmitter } from "angular2/core"; import { Http, Response, HTTP_PROVIDERS } from "angular2/http"; import { getUrl, ForecastResponse } from "./api"; interface ForecastData { date: string; temperature: number; main: string; description: string; } enum State { Loading, Refreshing, Loaded, Error } @Component({ selector: "weather-forecast", viewProviders: [HTTP_PROVIDERS], template: ` <span *ngIf="loading" class="state">Loading...</span> <span *ngIf="refreshing" class="state">Refreshing...</span> <a *ngIf="loaded || error" href="javascript:;" (click)="load()" class="state">Refresh</a> <h2>{{ tomorrow ? 'Tomorrow' : 'Today' }}'s weather in {{ location }}</h2> <div *ngIf="error">Failed to load data.</div> <ul> <li *ngFor="#item of data"> <div class="item-date">{{ item.date }}</div> <div class="item-main">{{ item.main }}</div> <div class="item-description">{{ item.description }}</div> <div class="item-temperature"> {{ item.temperature }} {{ temperatureUnit }} </div> </li> </ul> <div class="clearfix;"></div> `, styles: [ `.state { float: right; margin-top: 6px; } ul { margin: 0; padding: 0 0 15px; list-style: none; width: 100%; overflow-x: scroll; white-space: nowrap; } li { display: inline-block; margin-right: 15px; width: 170px; white-space: initial; } .item-date { font-size: 15pt; color: #165366; margin-right: 10px; display: inline-block; } .item-main { font-size: 15pt; display: inline-block; } .item-description { border-top: 1px solid #44A4C2; width: 100%; font-size: 11pt; } .item-temperature { font-size: 11pt; }` ] }) export class Forecast { constructor(private http: Http) { } temperatureUnit = "degrees Celsius"; private _tomorrow = false; @Input() set tomorrow(value) { if (this._tomorrow === value) return; this._tomorrow = value; this.filterData(); } get tomorrow() { return this._tomorrow; } private _location: string; @Input() set location(value) { if (this._location === value) return; this._location = value; this.state = State.Loading; this.data = []; this.load(); } get location() { return this._location; } fullData: ForecastData[] = []; data: ForecastData[] = []; state = State.Loading; get loading() { return this.state === State.Loading; } get refreshing() { return this.state === State.Refreshing; } get loaded() { return this.state === State.Loaded; } get error() { return this.state === State.Error; } @Output() correctLocation = new EventEmitter<string>(true); private formatDate(date: Date) { return date.getHours() + ":" + date.getMinutes() + date.getSeconds(); } private update(data: ForecastResponse) { if (!data.list) { this.showError(); return; } const location = data.city.name + ", " + data.city.country; if (this._location !== location) { this._location = location; this.correctLocation.next(location); } this.fullData = data.list.map(item => ({ date: this.formatDate(new Date(item.dt * 1000)), temperature: Math.round(item.main.temp - 273), main: item.weather[0].main, description: item.weather[0].description })); this.filterData(); this.state = State.Loaded; } private showError() { this.data = []; this.state = State.Error; } private filterData() { const start = this.tomorrow ? 8 : 0; this.data = this.fullData.slice(start, start + 8); } private load() { let path = "forecast?mode=json&"; const start = "coordinate "; if (this.location&&this.location.substring(0, start.length).toLowerCase() === start) { const coordinate = this.location.split(" "); path += `lat=${ parseFloat(coordinate[1]) }&lon=${ parseFloat(coordinate[2]) }`; } else { path += "q=" + this.location; } this.state = this.state === State.Loaded ? State.Refreshing : State.Loading; this.http.get(getUrl(path)) .map(response => response.json())) .subscribe(res => this.update(<ForecastResponse> res), () => this.showError()); } }
Tip
The generic (type argument) in new EventEmitter<string>()
means that the contents of an event will be a string. If the generic is not specified, it defaults to {}
, an empty object type, which means that there is no content. In this case, we want to send the new location, which is a string.
- LaTeX Cookbook
- Progressive Web Apps with React
- Objective-C Memory Management Essentials
- 自制編譯器
- Java入門經(jīng)典(第6版)
- Learning Spring 5.0
- 無代碼編程:用云表搭建企業(yè)數(shù)字化管理平臺(tái)
- 架構(gòu)不再難(全5冊)
- MongoDB for Java Developers
- Software Testing using Visual Studio 2012
- Learning SQLite for iOS
- Julia Cookbook
- 琢石成器:Windows環(huán)境下32位匯編語言程序設(shè)計(jì)
- Unity 2D Game Development Cookbook
- IBM Cognos Business Intelligence 10.1 Dashboarding cookbook