<!DOCTYPE html>
<html>
<head>
<base href="." />
<script type="text/javascript" charset="utf-8">
window.AngularVersionForThisPlunker = 'latest'
</script>
<title>angular playground</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="style.css" />
<!-- Polyfills -->
<script src="https://unpkg.com/core-js/client/shim.min.js"></script>
<script src="https://unpkg.com/zone.js@0.7.4?main=browser"></script>
<script src="https://unpkg.com/systemjs@0.19.39/dist/system.src.js"></script>
<script src="systemjs.config.js"></script>
<script>System.import('main.js').catch(function(err){ console.error(err); });</script>
</head>
<body>
<my-app>
loading...
</my-app>
</body>
</html>
/* Styles go here */
# Weather App Angular
* Search city by name.
* Add city to the list.
* Retrieve weather data from public API service http://openweathermap.org/
* Display city weather data.
* Save city list as profile. Delete profiles.
* Load city weather data from saved profiles.
//main entry point
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {AppModule} from './app/app.module';
platformBrowserDynamic().bootstrapModule(AppModule);
import { Component, NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { WeatherListComponent } from "./weather/list/weather-list.component";
import { WeatherItemComponent } from "./weather/item/weather-item.component";
import { WeatherSearchComponent } from "./weather/search/weather-search.component";
import { WeatherService } from "./weather/weather.service";
import { TemperatureDirective } from "./weather/temperature.directive";
import { SidebarComponent } from "./sidebar/sidebar.component";
import { ProfileService } from "./profile/profile.service";
@NgModule({
declarations: [
AppComponent,
WeatherListComponent,
WeatherItemComponent,
WeatherSearchComponent,
SidebarComponent,
TemperatureDirective
],
imports: [
BrowserModule,
FormsModule,
HttpModule
],
providers: [
ProfileService,
WeatherService
],
bootstrap: [AppComponent]
})
export class AppModule { }
import { Component, VERSION } from '@angular/core';
import { NgForm } from '@angular/forms';
import { WeatherListComponent } from "./weather/list/weather-list.component";
import { WeatherItemComponent } from "./weather/item/weather-item.component";
import { WeatherSearchComponent } from "./weather/search/weather-search.component";
import { SidebarComponent } from "./sidebar/sidebar.component";
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
public title:string;
constructor () {
this.title = `Weather App Angular v${VERSION.full}`;
}
}
<section class="main container">
<header>
<h1>{{title}}</h1>
</header>
<weather-sidebar></weather-sidebar>
<weather-search></weather-search>
<weather-list></weather-list>
</section>
header{
background-color:#3498db;
padding:30px;
margin-bottom: 20px;
text-align:center;
font-size:30px;
color:#f1c40f;
}
.main.container {
max-width: 960px;
min-width: 600px;
margin: auto;
}
import { City } from "./city";
export class Profile {
public profileName: string;
public cities: City[]
constructor(profileName: string, cities: City[]) {
this.profileName = profileName;
this.cities = cities;
}
}
import { Injectable } from "@angular/core";
import { Observable } from "rxjs/Observable";
import { Profile } from "./profile";
import { City } from "./city";
@Injectable()
export class ProfileService {
private profiles: Profile[] = [
new Profile(
'Default Profile',
[
{
cityName: 'New York',
countryCode: 'US'
},
{
cityName: 'London',
countryCode: 'GB'
},
{
cityName: 'Berlin',
countryCode: 'DE'
}
]
)
];
saveNewProfile(cities: City[]): Observable<any> {
const profileName = 'Profile ' + (this.profiles.length);
const profile = new Profile(profileName, cities);
this.profiles.push(profile);
return null;
}
getProfiles() {
return this.profiles;
}
deleteProfile(profile: Profile): Observable<any> {
this.profiles.splice(this.profiles.indexOf(profile), 1);
return null;
}
}
export class City {
public cityName: string;
public countryCode: string;
constructor(cityName: string, countryCode?: string) {
this.cityName = cityName;
this.countryCode = countryCode;
}
}
import { Component, OnInit } from "@angular/core";
import { Observable } from "rxjs/Rx";
import { Profile } from "../profile/profile";
import { ProfileService } from "../profile/profile.service";
import { WeatherService } from "../weather/weather.service";
import { WeatherItem } from "../weather/item/weather-item";
@Component({
selector: 'weather-sidebar',
templateUrl: './sidebar.component.html',
styleUrls: ['./sidebar.component.css']
})
export class SidebarComponent implements OnInit {
profiles:Profile[];
constructor(private _profileService:ProfileService, private _weatherService:WeatherService) {}
onSaveNewProfile() {
const cities = this._weatherService.getWeatherItems().map(function (element) {
return {
cityName: element.city,
countryCode: element.countryCode
};
});
if (cities.length) {
this._profileService.saveNewProfile(cities);
}
}
onLoadProfile(profile: Profile):void {
this._weatherService.clearWeatherItems();
for (let i = 0; i < profile.cities.length; i++) {
this._weatherService.searchWeatherInfo(profile.cities[i].cityName)
.retry()
.subscribe(
data => {
let cityName: string = data.name;
let cityDescription: string = data.weather[0].main;
let cityTemperature: number = +data.main.temp_min;
let countryCode = data.sys.country;
const weatherItem = new WeatherItem(cityName, cityDescription, cityTemperature, countryCode);
this._weatherService.addWeatherItem(weatherItem);
}
);
}
}
onDeleteProfile(event: any, profile: Profile):void {
event.stopPropagation();
this._profileService.deleteProfile(profile);
}
ngOnInit():any {
this.profiles = this._profileService.getProfiles();
}
}
<h3>Your Profiles</h3>
<button (click)="onSaveNewProfile()">Save List as Profile</button>
<article *ngFor="let profile of profiles" class="profile" (click)="onLoadProfile(profile)">
<div class="inner">
<h4>{{ profile.profileName }}</h4>
<div>
<em>Cities:</em>
<ul>
<li *ngFor="let item of profile.cities">{{item.cityName}} <span *ngIf="item.countryCode">({{item.countryCode}})</span></li>
</ul>
</div>
</div>
<span class="delete" (click)="onDeleteProfile($event, profile)">[x]</span>
</article>
:host{
float:left;
width:25%;
padding:0 10px 0 0;
}
h3{
margin:0 0 10px;
padding:0;
font-size:18px;
}
button{
display:block;
width:100%;
font-size:16px;
font-family:inherit;
background-color:#3498db;
box-shadow:2px 2px 6px #95a5a6;
border:none;
padding:8px;
cursor:pointer;
color:#fff;
transition: all 0.1s;
}
button:active,button:focus{
outline: none;
}
button:active{
transform: scale(0.95);
}
button:hover{
background-color:#2980b9
}
.profile{
position:relative;
cursor:pointer;
}
.profile h4{
margin:0;
padding:0;
}
.profile p{
margin:0;
padding:0;
}
.profile .delete{
position:absolute;
top:5px;
right:10px;
font-size:1em;
line-height:1em;
color: rgba(255, 0, 0, 0.5);
text-align: center;
display:block;
width:1em;
}
.profile .delete:hover{
color: rgb(170, 15, 0);
}
.profile .inner{
padding:5px;
margin-top:10px;
background-color:#b9d5e8;
transition: all 0.1s;
}
.profile,
.profile:hover,
.profile:hover .inner,
.profile .inner:hover{
background-color:#3498db;
}
.profile .inner:active{
transform: scale(1.05);
}
import { Injectable } from "@angular/core";
import { Http } from "@angular/http";
import { Observable } from "rxjs/Rx";
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
import 'rxjs/add/operator/switchMap';
import {WeatherItem} from "./item/weather-item";
import {WEATHER_ITEMS} from "./mock-weather";
@Injectable()
export class WeatherService {
constructor(private _http: Http) {}
getWeatherItems() {
return WEATHER_ITEMS;
}
addWeatherItem(item: WeatherItem) {
WEATHER_ITEMS.push(item);
}
clearWeatherItems() {
WEATHER_ITEMS.splice(0);
}
deleteWeatherItem(item: WeatherItem): Observable<any> {
WEATHER_ITEMS.splice(WEATHER_ITEMS.indexOf(item), 1);
return null;
}
isExistWeatherItem(item: WeatherItem): any {
return WEATHER_ITEMS.some(elem => (elem.city == item.city && elem.countryCode == item.countryCode));
}
searchWeatherInfo(city: string): Observable<any> {
const APPID = '7a211c68435846ab04153a9d815b09f3';
let url = 'https://api.openweathermap.org/data/2.5/weather?q=' + city + '&APPID=' + APPID + '&units=metric';
return this._http.get(url)
.map(
response => response.json()
)
.catch(
error => {
return Observable.of<any>(error.json());
}
);
}
}
import { Directive, ElementRef, OnInit, HostListener, Renderer2, Attribute, Input } from "@angular/core";
@Directive({
selector: '.temperature'
})
export class TemperatureDirective {
private tooltip: HTMLElement = null;
@Input() temperatureCelsius: number;
constructor(private _elRef: ElementRef, private _renderer: Renderer2) {}
@HostListener('mousemove', ['$event']) onMouseOver(event: MouseEvent) {
if (this.tooltip === null) {
this.tooltip = this._renderer.createElement('div');
const text = this._renderer.createText('Fahrenheit: ' + (this.temperatureCelsius * 1.8 + 32));
this._renderer.appendChild(this.tooltip, text);
this._renderer.addClass(this.tooltip, 'tooltip');
}
this._renderer.setStyle(this.tooltip, 'top', '' + (event.clientY + 3) + 'px');
this._renderer.setStyle(this.tooltip, 'left', '' + (event.clientX + 10) + 'px');
this._renderer.setProperty(this.tooltip, 'hidden', '');
this._renderer.appendChild(this._elRef.nativeElement, this.tooltip);
}
@HostListener('mouseleave') onMouseLeave() {
this._renderer.setProperty(this.tooltip, 'hidden', 'true');
}
}
import {WeatherItem} from "./item/weather-item";
export const WEATHER_ITEMS: WeatherItem[] = [];
export class WeatherItem {
public city: string;
public description: string;
public temperature: number;
public countryCode: string;
constructor(city: string, description: string, temperature: number, countryCode?:string) {
this.city = city;
this.description = description;
this.temperature = temperature;
this.countryCode = countryCode;
}
}
import { Component, Input } from "@angular/core";
import { WeatherService } from "../weather.service";
import { WeatherItem } from "../item/weather-item";
@Component({
selector: 'weather-item',
templateUrl: './weather-item.component.html',
styleUrls: ['./weather-item.component.css']
})
export class WeatherItemComponent {
@Input('weatherItem') item: WeatherItem;
constructor(private _weatherService: WeatherService) {}
onDeleteItem(event: any, item: WeatherItem):void {
event.stopPropagation();
this._weatherService.deleteWeatherItem(item);
}
}
<article class="weather-element">
<div class="col-1">
<h3>{{ item.city }} <span *ngIf="item.countryCode">({{item.countryCode}})</span></h3>
<p class="info">{{ item.description | uppercase }}</p>
</div>
<div class="col-2">
<span class="temperature" [temperatureCelsius]="item.temperature">{{ item.temperature }}°C</span>
</div>
<span class="delete" (click)="onDeleteItem($event, item)">[x]</span>
</article>
.col-1{display:inline-block;width:40%;}
.col-1 h3{margin:0 0 10px;padding:0;font-size:22px;}
.col-2{display:inline-block;width:50%;vertical-align:top;text-align:right;}
.tooltip{position:fixed;padding:6px;font-size:14px;font-style:italic;background-color:#c3c3c3;border-radius:3px;border:1px solid #999;text-align:left;}
.temperature{font-size:22px;cursor:pointer;}
.weather-element{position:relative;box-shadow:1px 1px 6px #ccc;padding:10px;margin:10px 0;}
.weather-element .delete{
position: absolute;
top: 5px;
right: 10px;
font-size: 1em;
color: rgba(255, 0, 0, 0.5);
cursor: pointer;
}
.weather-element .delete:hover{
color: rgb(170, 15, 0);
}
import { Component, OnInit } from "@angular/core";
import { WeatherService } from "../weather.service";
import { WeatherItem } from "../item/weather-item";
@Component({
selector: 'weather-list',
templateUrl: './weather-list.component.html',
styleUrls: ['./weather-list.component.css']
})
export class WeatherListComponent implements OnInit {
weatherItems: WeatherItem[];
constructor(private _weatherService: WeatherService) {}
ngOnInit():any {
this.weatherItems = this._weatherService.getWeatherItems();
}
}
<section class="weather-list">
<weather-item *ngFor="let item of weatherItems" [weatherItem]="item"></weather-item>
</section>
:host{
width: 72%;
float: left;
}
.weather-list{}
import { Component, OnInit } from "@angular/core";
import {NgForm} from '@angular/forms';
import { Observable } from "rxjs/Rx";
import { Subject } from "rxjs/Subject";
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/switchMap';
import 'rxjs/add/operator/catch';
import { WeatherService } from "../weather.service";
import { WeatherItem } from "../item/weather-item";
@Component({
selector: 'weather-search',
templateUrl: './weather-search.component.html',
styleUrls: ['./weather-search.component.css']
})
export class WeatherSearchComponent implements OnInit {
private searchStream = new Subject<string>();
data:any = {};
isCityFound:boolean = false;
isSearching:boolean = false;
constructor(private _weatherService:WeatherService) {}
onSearchLocation(value:string): void {
console.log(value);
this.isSearching = true;
this.searchStream.next(value);
}
onSubmit(f: NgForm) {
let cityName: string = this.data.name;
let cityDescription: string = this.data.weather ? this.data.weather[0].main : '';
let cityTemperature: number = this.data.main ? 1*this.data.main.temp_min : null;
let countryCode = this.data.sys ? this.data.sys.country : '';
const newItem = new WeatherItem(cityName, cityDescription, cityTemperature, countryCode);
if (cityName && !this._weatherService.isExistWeatherItem(newItem)) {
this._weatherService.addWeatherItem(newItem);
f.resetForm();
}
}
ngOnInit():any {
this.searchStream
.debounceTime(500)
.distinctUntilChanged()
.switchMap((term:string) => this._weatherService.searchWeatherInfo(term))
.subscribe(
data => {
if (data.name) {
this.isCityFound = true;
}
else {
this.isCityFound = false;
}
this.isSearching = false;
return this.data = data;
},
error => console.warn(error)
);
}
}
<section class="weather-search">
<div class="result">
<span class="info" *ngIf="!isCityFound">Search city here</span>
<span class="info" *ngIf="isCityFound">City found:</span> {{ data?.name }} {{ data?.sys?.country }}
</div>
<form #f="ngForm" (ngSubmit)="onSubmit(f)">
<label for="city">
<input ngControl="location" type="text" id="city" value="" (input)="onSearchLocation(input.value)" #input required placeholder="search ...">
<div id="mdev-overlay" *ngIf="isSearching"><div class="mdev-loader rotateLinear">●●</div></div>
</label>
<button type="submit">Add City</button>
</form>
</section>
:host{
width: 72%;
float: left;
}
.weather-search{margin-top: -6px}
.weather-search form{position: relative;}
.weather-search label{position:relative;font-weight:700;margin-right:20px;}
.weather-search button, .weather-search input{font-size:inherit;font-family:inherit;}
.weather-search input{padding:6px 30px 6px 6px;outline:none;width:50%;}
.weather-search button{background-color:#2ecc71;border:none;padding:8px;box-shadow:2px 2px 6px #95a5a6;cursor:pointer;color:#fff;transition: all 0.1s;}
.weather-search button:hover{background-color:#27ae60;}
.weather-search button:active,.weather-search button:focus{outline:none;}
.weather-search button:active{transform: scale(1.05);}
.result{font-size:2em;}
.info{color:rgba(95,95,95,.51);font-weight:700;}
/*
* HTML: <div id="mdev-overlay"><div class="mdev-loader rotateLinear">●●</div></div>
*/
#mdev-overlay{
/*display: none;*/
position: absolute;
z-index: 9999;
top: 0;
right: 30px;
/*width:100%;*/
/*height:100%;*/
/*background: rgba(0,0,0,0.5);*/
margin:auto;
color: rgba(0,0,0,0.5);
font-weight: bold;
font-size: 1em;
text-align: center;
text-transform: lowercase;
}
#mdev-overlay .mdev-loader{
position: absolute;
top: 50%;
left: 50%;
/*margin: -0.5em 0 0 -0.6em;*/
/*font-size: 5em;*/
letter-spacing: 0.1em;
}
#mdev-overlay .rotateLinear {
-webkit-animation: rotateLinear 1s infinite linear;
-moz-animation: rotateLinear 1s infinite linear;
-o-animation: rotateLinear 1s infinite linear;
}
@-webkit-keyframes rotateLinear {
from { -webkit-transform: rotate(0deg) scale(1) skew(0deg) translate(0px); }
to { -webkit-transform: rotate(360deg) scale(1) skew(0deg) translate(0px); }
}
@-moz-keyframes rotateLinear {
from { -moz-transform: rotate(0deg) scale(1) skew(0deg) translate(0px); }
to { -moz-transform: rotate(360deg) scale(1) skew(0deg) translate(0px); }
}
@-o-keyframes rotateLinear {
from { -o-transform: rotate(0deg) scale(1) skew(0deg) translate(0px); }
to { -o-transform: rotate(360deg) scale(1) skew(0deg) translate(0px); }
}
var templateUrlRegex = /templateUrl\s*:(\s*['"`](.*?)['"`]\s*)/gm;
var stylesRegex = /styleUrls *:(\s*\[[^\]]*?\])/g;
var stringRegex = /(['`"])((?:[^\\]\\\1|.)*?)\1/g;
module.exports.translate = function(load){
var url = document.createElement('a');
url.href = load.address;
var basePathParts = url.pathname.split('/');
basePathParts.pop();
var basePath = basePathParts.join('/');
var baseHref = document.createElement('a');
baseHref.href = this.baseURL;
baseHref = baseHref.pathname;
basePath = basePath.replace(baseHref, '');
load.source = load.source
.replace(templateUrlRegex, function(match, quote, url){
let resolvedUrl = url;
if (url.startsWith('.')) {
resolvedUrl = basePath + url.substr(1);
}
return 'templateUrl: "' + resolvedUrl + '"';
})
.replace(stylesRegex, function(match, relativeUrls) {
var urls = [];
while ((match = stringRegex.exec(relativeUrls)) !== null) {
if (match[2].startsWith('.')) {
urls.push('"' + basePath + match[2].substr(1) + '"');
} else {
urls.push('"' + match[2] + '"');
}
}
return "styleUrls: [" + urls.join(', ') + "]";
});
return load;
};
/**
* WEB ANGULAR VERSION
* (based on systemjs.config.js in angular.io)
* System configuration for Angular samples
* Adjust as necessary for your application needs.
*/
(function (global) {
System.config({
// DEMO ONLY! REAL CODE SHOULD NOT TRANSPILE IN THE BROWSER
transpiler: 'ts',
typescriptOptions: {
// Copy of compiler options in standard tsconfig.json
"target": "es5",
"module": "commonjs",
"moduleResolution": "node",
"sourceMap": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"lib": ["es2015", "dom"],
"noImplicitAny": true,
"suppressImplicitAnyIndexErrors": true
},
meta: {
'typescript': {
"exports": "ts"
}
},
paths: {
// paths serve as alias
'npm:': 'https://unpkg.com/'
},
// map tells the System loader where to look for things
map: {
// our app is within the app folder
'app': 'app',
// angular bundles
'@angular/core': 'npm:@angular/core/bundles/core.umd.js',
'@angular/common': 'npm:@angular/common/bundles/common.umd.js',
'@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
'@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
'@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
'@angular/http': 'npm:@angular/http/bundles/http.umd.js',
'@angular/router': 'npm:@angular/router/bundles/router.umd.js',
'@angular/router/upgrade': 'npm:@angular/router/bundles/router-upgrade.umd.js',
'@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js',
'@angular/upgrade': 'npm:@angular/upgrade/bundles/upgrade.umd.js',
'@angular/upgrade/static': 'npm:@angular/upgrade/bundles/upgrade-static.umd.js',
// other libraries
'rxjs': 'npm:rxjs@5.0.1',
'angular-in-memory-web-api': 'npm:angular-in-memory-web-api/bundles/in-memory-web-api.umd.js',
'ts': 'npm:plugin-typescript@5.2.7/lib/plugin.js',
'typescript': 'npm:typescript@2.0.10/lib/typescript.js',
},
// packages tells the System loader how to load when no filename and/or no extension
packages: {
app: {
main: './main.ts',
defaultExtension: 'ts',
meta: {
'./*.ts': {
loader: 'systemjs-angular-loader.js'
}
}
},
rxjs: {
defaultExtension: 'js'
}
}
});
})(this);
/*
Copyright 2016 Google Inc. All Rights Reserved.
Use of this source code is governed by an MIT-style license that
can be found in the LICENSE file at http://v2.angular.io/license
*/