<!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">&#9679;&#9679;</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">&#9679;&#9679;</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
*/