HTTP

2.2

Send feedback

Our stakeholders appreciate our progress. Now they want to get the hero data from a server, let users add, edit, and delete heroes, and save these changes back to the server.

In this chapter we teach our application to make the corresponding HTTP calls to a remote server's web API.

Run the for this part.

Where We Left Off

In the previous chapter, we learned to navigate between the dashboard and the fixed heroes list, editing a selected hero along the way. That's our starting point for this chapter.

Keep the app compiling and running

Open a terminal/console window. Start the Dart compiler, watch for changes, and start our server by entering the command:

pub serve

The application runs and updates automatically as we continue to build the Tour of Heroes.

Providing HTTP Services

We'll be using the Dart http package's BrowserClient class to communicate with a server.

Pubspec updates

Update package dependencies by adding the stream_transformers and Dart http packages.

We also need to add a resolved_identifiers entry, to inform the angular2 transformer that we'll be using BrowserClient. (For an explanation of why this extra configuration is needed, see the HTTP client page.) We'll also need to use Client from http, so let's add that now as well.

Update pubspec.yaml to look like this (additions are highlighted):

pubspec.yaml (additions)

name: angular_tour_of_heroes # . . . dependencies: angular2: ^2.2.0 http: ^0.11.0 stream_transformers: ^0.3.0 # . . . transformers: - angular2: # . . . entry_points: web/main.dart resolved_identifiers: BrowserClient: 'package:http/browser_client.dart' Client: 'package:http/http.dart' - dart_to_js_script_rewriter

Register for HTTP services

Before our app can use BrowserClient, we have to register it as a service provider.

We should be able to access BrowserClient services from anywhere in the application. So we register it in the bootstrap call where we launch the application and its root AppComponent.

web/main.dart (v1)

import 'package:angular2/core.dart'; import 'package:angular2/platform/browser.dart'; import 'package:angular_tour_of_heroes/app_component.dart'; import 'package:http/browser_client.dart'; void main() { bootstrap(AppComponent, [ provide(BrowserClient, useFactory: () => new BrowserClient(), deps: []) ]); }

Notice that we supply BrowserClient in a list, as the second parameter to the bootstrap method. This has the same effect as the providers list in @Component annotation.

Simulating the web API

We recommend registering application-wide services in the root AppComponent providers. Here we're registering in main for a special reason.

Our application is in the early stages of development and far from ready for production. We don't even have a web server that can handle requests for heroes. Until we do, we'll have to fake it.

We're going to trick the HTTP client into fetching and saving data from a mock service, the in-memory web API. The application itself doesn't need to know and shouldn't know about this. So we'll slip the in-memory web API into the configuration above the AppComponent.

Here is a version of web/main.dart that performs this trick:

web/main.dart (v2)

import 'package:angular2/core.dart'; import 'package:angular2/platform/browser.dart'; import 'package:angular_tour_of_heroes/app_component.dart'; import 'package:angular_tour_of_heroes/in_memory_data_service.dart'; import 'package:http/http.dart'; void main() { bootstrap(AppComponent, [provide(Client, useClass: InMemoryDataService)] // Using a real back end? // Import browser_client.dart and change the above to: // [provide(Client, useFactory: () => new BrowserClient(), deps: [])] ); }

We want to replace BrowserClient, the service that talks to the remote server, with the in-memory web API service. Our in-memory web API service, shown below, is implemented using the http library MockClient class. All http client implementations share a common Client interface, so we'll have our app use the Client type so that we can freely switch between implementations.

lib/in_memory_data_service.dart

import 'dart:async'; import 'dart:convert'; import 'dart:math'; import 'package:angular2/core.dart'; import 'package:http/http.dart'; import 'package:http/testing.dart'; import 'hero.dart'; @Injectable() class InMemoryDataService extends MockClient { static final _initialHeroes = [ {'id': 11, 'name': 'Mr. Nice'}, {'id': 12, 'name': 'Narco'}, {'id': 13, 'name': 'Bombasto'}, {'id': 14, 'name': 'Celeritas'}, {'id': 15, 'name': 'Magneta'}, {'id': 16, 'name': 'RubberMan'}, {'id': 17, 'name': 'Dynama2'}, {'id': 18, 'name': 'Dr IQ'}, {'id': 19, 'name': 'Magma'}, {'id': 20, 'name': 'Tornado'} ]; static final List<Hero> _heroesDb = _initialHeroes.map((json) => new Hero.fromJson(json)).toList(); static int _nextId = _heroesDb.map((hero) => hero.id).fold(0, max) + 1; static Future<Response> _handler(Request request) async { var data; switch (request.method) { case 'GET': final id = int.parse(request.url.pathSegments.last, onError: (_) => null); if (id != null) { data = _heroesDb.firstWhere((hero) => hero.id == id); // throws if no match } else { String prefix = request.url.queryParameters['name'] ?? ''; final regExp = new RegExp(prefix, caseSensitive: false); data = _heroesDb.where((hero) => hero.name.contains(regExp)).toList(); } break; case 'POST': var name = JSON.decode(request.body)['name']; var newHero = new Hero(_nextId++, name); _heroesDb.add(newHero); data = newHero; break; case 'PUT': var heroChanges = new Hero.fromJson(JSON.decode(request.body)); var targetHero = _heroesDb.firstWhere((h) => h.id == heroChanges.id); targetHero.name = heroChanges.name; data = targetHero; break; case 'DELETE': var id = int.parse(request.url.pathSegments.last); _heroesDb.removeWhere((hero) => hero.id == id); // No data, so leave it as null. break; default: throw 'Unimplemented HTTP method ${request.method}'; } return new Response(JSON.encode({'data': data}), 200, headers: {'content-type': 'application/json'}); } InMemoryDataService() : super(_handler); }

This file replaces the mock_heroes.dart which is now safe to delete.

As is common for web API services, our mock in-memory service will be encoding and decoding heroes in JSON format, so we enhance the Hero class with these capabilities:

lib/hero.dart

class Hero { final int id; String name; Hero(this.id, this.name); factory Hero.fromJson(Map<String, dynamic> hero) => new Hero(_toInt(hero['id']), hero['name']); Map toJson() => {'id': id, 'name': name}; } int _toInt(id) => id is int ? id : int.parse(id);

Heroes and HTTP

Look at our current HeroService implementation

Future<List<Hero>> getHeroes() async => mockHeroes;

We returned a Future resolved with mock heroes. It may have seemed like overkill at the time, but we were anticipating the day when we fetched heroes with an HTTP client and we knew that would have to be an asynchronous operation.

That day has arrived! Let's convert getHeroes() to use HTTP.

lib/hero_service.dart (updated getHeroes and new class members)

static const _heroesUrl = 'api/heroes'; // URL to web API final Client _http; HeroService(this._http); Future<List<Hero>> getHeroes() async { try { final response = await _http.get(_heroesUrl); final heroes = _extractData(response) .map((value) => new Hero.fromJson(value)) .toList(); return heroes; } catch (e) { throw _handleError(e); } } dynamic _extractData(Response resp) => JSON.decode(resp.body)['data']; Exception _handleError(dynamic e) { print(e); // for demo purposes only return new Exception('Server error; cause: $e'); }

Our updated import statements are now:

lib/hero_service.dart (updated imports)

import 'dart:async'; import 'dart:convert'; import 'package:angular2/core.dart'; import 'package:http/http.dart'; import 'hero.dart';

Refresh the browser, and the hero data should be successfully loaded from the mock server.

HTTP Future

We're still returning a Future but we're creating it differently.

To get the list of heroes, we first make an asynchronous call to http.get(). Then we use the _extractData helper method to decode the response body.

That response JSON has a single data property. The data property holds the list of heroes that the caller really wants. So we grab that list and return it as the resolved Future value.

Pay close attention to the shape of the data returned by the server. This particular in-memory web API example happens to return an object with a data property. Your API might return something else. Adjust the code to match your web API.

The caller is unaware of these machinations. It receives a Future of heroes just as it did before. It has no idea that we fetched the heroes from the (mock) server. It knows nothing of the twists and turns required to convert the HTTP response into heroes. Such is the beauty and purpose of delegating data access to a service like this HeroService.

Error Handling

At the end of getHeroes() we catch server failures and pass them to an error handler:

} catch (e) { throw _handleError(e); }

This is a critical step! We must anticipate HTTP failures as they happen frequently for reasons beyond our control.

Exception _handleError(dynamic e) { print(e); // for demo purposes only return new Exception('Server error; cause: $e'); }

In this demo service we log the error to the console; we would do better in real life.

We've also decided to return a user friendly form of the error to the caller in a propagated exception so that the caller can display a proper error message to the user.

Get hero by id

The HeroDetailComponent asks the HeroService to fetch a single hero to edit.

The HeroService currently fetches all heroes and then finds the desired hero by filtering for the one with the matching id. That's fine in a simulation. It's wasteful to ask a real server for all heroes when we only want one. Most web APIs support a get-by-id request in the form api/hero/:id (e.g., api/hero/11).

Update the HeroService.getHero method to make a get-by-id request, applying what we just learned to write getHeroes:

Future<Hero> getHero(int id) async { try { final response = await _http.get('$_heroesUrl/$id'); return new Hero.fromJson(_extractData(response)); } catch (e) { throw _handleError(e); } }

It's almost the same as getHeroes. The URL identifies which hero the server should update by encoding the hero id into the URL to match the api/hero/:id pattern.

We also adjust to the fact that the data in the response is a single hero object rather than a list.

Unchanged getHeroes API

Although we made significant internal changes to getHeroes() and getHero(), the public signatures did not change. We still return a Future from both methods. We won't have to update any of the components that call them.

Our stakeholders are thrilled with the web API integration so far. Now they want the ability to create and delete heroes.

Let's see first what happens when we try to update a hero's details.

Update hero details

We can edit a hero's name already in the hero detail view. Go ahead and try it. As we type, the hero name is updated in the view heading. But when we hit the Back button, the changes are lost!

Updates weren't lost before. What changed? When the app used a list of mock heroes, updates were applied directly to the hero objects within the single, app-wide, shared list. Now that we are fetching data from a server, if we want changes to persist, we'll need to write them back to the server.

Save hero details

Let's ensure that edits to a hero's name aren't lost. Start by adding, to the end of the hero detail template, a save button with a click event binding that invokes a new component method named save:

lib/hero_detail_component.html (save)

<button (click)="save()">Save</button>

The save method persists hero name changes using the hero service update method and then navigates back to the previous view:

lib/hero_detail_component.dart (save)

Future<Null> save() async { await _heroService.update(hero); goBack(); }

Hero service update method

The overall structure of the update method is similar to that of getHeroes, although we'll use an HTTP put to persist changes server-side:

lib/hero_service.dart (update)

static final _headers = {'Content-Type': 'application/json'}; Future<Hero> update(Hero hero) async { try { final url = '$_heroesUrl/${hero.id}'; final response = await _http.put(url, headers: _headers, body: JSON.encode(hero)); return new Hero.fromJson(_extractData(response)); } catch (e) { throw _handleError(e); } }

We identify which hero the server should update by encoding the hero id in the URL. The put body is the JSON string encoding of the hero, obtained by calling JSON.encode. We identify the body content type (application/json) in the request header.

Refresh the browser and give it a try. Changes to hero names should now persist.

Add a hero

To add a new hero we need to know the hero's name. Let's use an input element for that, paired with an add button.

Insert the following into the heroes component HTML, first thing after the heading:

lib/heroes_component.html (add)

<div> <label>Hero name:</label> <input #heroName /> <button (click)="add(heroName.value); heroName.value=''"> Add </button> </div>

In response to a click event, we call the component's click handler and then clear the input field so that it will be ready to use for another name.

lib/heroes_component.dart (add)

Future<Null> add(String name) async { name = name.trim(); if (name.isEmpty) return; heroes.add(await _heroService.create(name)); selectedHero = null; }

When the given name is non-blank, the handler delegates creation of the named hero to the hero service, and then adds the new hero to our list.

Finally, we implement the create method in the HeroService class.

lib/hero_service.dart (create)

Future<Hero> create(String name) async { try { final response = await _http.post(_heroesUrl, headers: _headers, body: JSON.encode({'name': name})); return new Hero.fromJson(_extractData(response)); } catch (e) { throw _handleError(e); } }

Refresh the browser and create some new heroes!

Delete a hero

Too many heroes? Let's add a delete button to each hero in the heroes view.

Add this button element to the heroes component HTML, right after the hero name in the repeated <li> tag:

<button class="delete" (click)="delete(hero); $event.stopPropagation()">x</button>

The <li> element should now look like this:

lib/heroes_component.html (li-element)

<li *ngFor="let hero of heroes" (click)="onSelect(hero)" [class.selected]="hero === selectedHero"> <span class="badge">{{hero.id}}</span> <span>{{hero.name}}</span> <button class="delete" (click)="delete(hero); $event.stopPropagation()">x</button> </li>

In addition to calling the component's delete method, the delete button click handling code stops the propagation of the click event — we don't want the <li> click handler to be triggered because that would select the hero that we are going to delete!

The logic of the delete handler is a bit trickier:

lib/heroes_component.dart (delete)

Future<Null> delete(Hero hero) async { await _heroService.delete(hero.id); heroes.remove(hero); if (selectedHero == hero) selectedHero = null; }

Of course, we delegate hero deletion to the hero service, but the component is still responsible for updating the display: it removes the deleted hero from the list and resets the selected hero if necessary.

We want our delete button to be placed at the far right of the hero entry. This extra CSS accomplishes that:

lib/heroes_component.css (additions)

button.delete { float:right; margin-top: 2px; margin-right: .8em; background-color: gray !important; color:white; }

Hero service delete method

The hero service's delete method uses the delete HTTP method to remove the hero from the server:

lib/hero_service.dart (delete)

Future<Null> delete(int id) async { try { final url = '$_heroesUrl/$id'; await _http.delete(url, headers: _headers); } catch (e) { throw _handleError(e); } }

Refresh the browser and try the new delete functionality.

Streams

Recall that HeroService.getHeroes() awaits for an http.get() response and yields a Future List<Hero>, which is fine when we are only interested in a single result.

But requests aren't always "one and done". We may start one request, then cancel it, and make a different request before the server has responded to the first request. Such a request-cancel-new-request sequence is difficult to implement with Futures. It's easy with Streams as we'll see.

Search-by-name

We're going to add a hero search feature to the Tour of Heroes. As the user types a name into a search box, we'll make repeated HTTP requests for heroes filtered by that name.

We start by creating HeroSearchService that sends search queries to our server's web api.

lib/hero_search_service.dart

import 'dart:async'; import 'dart:convert'; import 'package:angular2/core.dart'; import 'package:http/http.dart'; import 'hero.dart'; @Injectable() class HeroSearchService { final Client _http; HeroSearchService(this._http); Future<List<Hero>> search(String term) async { try { final response = await _http.get('app/heroes/?name=$term'); return _extractData(response) .map((json) => new Hero.fromJson(json)) .toList(); } catch (e) { throw _handleError(e); } } dynamic _extractData(Response resp) => JSON.decode(resp.body)['data']; Exception _handleError(dynamic e) { print(e); // for demo purposes only return new Exception('Server error; cause: $e'); } }

The _http.get() call in HeroSearchService is similar to the one in the HeroService, although the URL now has a query string.

HeroSearchComponent

Let's create a new HeroSearchComponent that calls this new HeroSearchService.

The component template is simple — just a text box and a list of matching search results.

lib/hero_search_component.html

<div id="search-component"> <h4>Hero Search</h4> <input #searchBox id="search-box" (keyup)="search(searchBox.value)" /> <div> <div *ngFor="let hero of heroes | async" (click)="gotoDetail(hero)" class="search-result" > {{hero.name}} </div> </div> </div>

We'll also want to add styles for the new component.

lib/hero_search_component.css

.search-result { border-bottom: 1px solid gray; border-left: 1px solid gray; border-right: 1px solid gray; width:195px; height: 20px; padding: 5px; background-color: white; cursor: pointer; } #search-box { width: 200px; height: 20px; }

As the user types in the search box, a keyup event binding calls the component's search method with the new search box value.

The *ngFor repeats hero objects from the component's heroes property. No surprise there.

But, as we'll soon see, the heroes property is now a Stream of hero lists, rather than just a hero list. The *ngFor can't do anything with a Stream until we flow it through the async pipe (AsyncPipe). The async pipe subscribes to the Stream and produces the list of heroes to *ngFor.

Time to create the HeroSearchComponent class and metadata.

lib/hero_search_component.dart

import 'dart:async'; import 'package:angular2/core.dart'; import 'package:angular2/router.dart'; import 'package:stream_transformers/stream_transformers.dart'; import 'hero_search_service.dart'; import 'hero.dart'; @Component( selector: 'hero-search', templateUrl: 'hero_search_component.html', styleUrls: const ['hero_search_component.css'], providers: const [HeroSearchService]) class HeroSearchComponent implements OnInit { HeroSearchService _heroSearchService; Router _router; Stream<List<Hero>> heroes; StreamController<String> _searchTerms = new StreamController<String>.broadcast(); HeroSearchComponent(this._heroSearchService, this._router) {} // Push a search term into the stream. void search(String term) => _searchTerms.add(term); Future<Null> ngOnInit() async { heroes = _searchTerms.stream .transform(new Debounce(new Duration(milliseconds: 300))) .distinct() .transform(new FlatMapLatest((term) => term.isEmpty ? new Stream<List<Hero>>.fromIterable([<Hero>[]]) : _heroSearchService.search(term).asStream())) .handleError((e) { print(e); // for demo purposes only }); } void gotoDetail(Hero hero) { var link = [ 'HeroDetail', {'id': hero.id.toString()} ]; _router.navigate(link); } }

Search terms

Let's focus on the _searchTerms:

StreamController<String> _searchTerms = new StreamController<String>.broadcast(); // Push a search term into the stream. void search(String term) => _searchTerms.add(term);

A StreamController, as its name implies, is a controller for a Stream that allows us to manipulate the underlying stream by adding data to it, for example.

In our sample, the underlying stream of strings (_searchTerms.stream) represents the hero name search patterns entered by the user. Each call to search puts a new string into the stream by calling add over the controller.

Initialize the heroes property (ngOnInit)

A Subject is also an Observable. We're going to turn the stream of search terms into a stream of Hero lists and assign the result to the heroes property.

Stream<List<Hero>> heroes; Future<Null> ngOnInit() async { heroes = _searchTerms.stream .transform(new Debounce(new Duration(milliseconds: 300))) .distinct() .transform(new FlatMapLatest((term) => term.isEmpty ? new Stream<List<Hero>>.fromIterable([<Hero>[]]) : _heroSearchService.search(term).asStream())) .handleError((e) { print(e); // for demo purposes only }); }

If we passed every user keystroke directly to the HeroSearchService, we'd unleash a storm of HTTP requests. Bad idea. We don't want to tax our server resources and burn through our cellular network data plan.

Fortunately, there are stream transformers that will help us reduce the request flow. We'll make fewer calls to the HeroSearchService and still get timely results. Here's how:

  • transform(new Debounce(... 300))) waits until the flow of search terms pauses for 300 milliseconds before passing along the latest string. We'll never make requests more frequently than 300ms.

  • distinct() ensures that we only send a request if a search term has changed. There's no point in repeating a request for the same search term.

  • transform(new FlatMapLatest(...)) applies a map-like transformer that (1) calls our search service for each search term that makes it through the debounce and distinct gauntlet and (2) returns only the most recent search service result, discarding any previous results.

  • handleError() handles errors. Our simple example prints the error to the console; a real life application should do better.

Add the search component to the dashboard

We add the hero search HTML element to the bottom of the DashboardComponent template.

lib/dashboard_component.html

<h3>Top Heroes</h3> <div class="grid grid-pad"> <a *ngFor="let hero of heroes" [routerLink]="['HeroDetail', {id: hero.id.toString()}]" class="col-1-4"> <div class="module hero"> <h4>{{hero.name}}</h4> </div> </a> </div> <hero-search></hero-search>

Finally, we import HeroSearchComponent from hero_search_component.dart and add it to the directives list:

lib/dashboard_component.dart (search)

import 'hero_search_component.dart'; @Component( selector: 'my-dashboard', templateUrl: 'dashboard_component.html', styleUrls: const ['dashboard_component.css'], directives: const [HeroSearchComponent, ROUTER_DIRECTIVES])

Run the app again, go to the Dashboard, and enter some text in the search box. At some point it might look like this.

Hero Search Component

Application structure and code

Review the sample source code in the for this chapter. Verify that we have the following structure:

angular_tour_of_heroes
lib
app_component.css
app_component.dart
dashboard_component.css
dashboard_component.dart
dashboard_component.html
hero.dart
hero_detail_component.css
hero_detail_component.dart
hero_detail_component.html
hero_search_component.css (new)
hero_search_component.dart (new)
hero_search_component.html (new)
hero_search_service.dart (new)
hero_service.dart
heroes_component.css
heroes_component.dart
heroes_component.html
in_memory_data_service.dart (new)
web
main.dart
index.html
styles.css
pubspec.yaml

Home Stretch

We are at the end of our journey for now, but we have accomplished a lot.

  • We added the necessary dependencies to use HTTP in our application.
  • We refactored HeroService to load heroes from a web API.
  • We extended HeroService to support post, put and delete methods.
  • We updated our components to allow adding, editing and deleting of heroes.
  • We configured an in-memory web API.
  • We learned how to use Streams.

Here are the files we added or changed in this chapter.

import 'dart:async'; import 'package:angular2/core.dart'; import 'package:angular2/router.dart'; import 'hero.dart'; import 'hero_service.dart'; import 'hero_search_component.dart'; @Component( selector: 'my-dashboard', templateUrl: 'dashboard_component.html', styleUrls: const ['dashboard_component.css'], directives: const [HeroSearchComponent, ROUTER_DIRECTIVES]) class DashboardComponent implements OnInit { List<Hero> heroes; final HeroService _heroService; DashboardComponent(this._heroService); Future<Null> ngOnInit() async { heroes = (await _heroService.getHeroes()).skip(1).take(4).toList(); } } <h3>Top Heroes</h3> <div class="grid grid-pad"> <a *ngFor="let hero of heroes" [routerLink]="['HeroDetail', {id: hero.id.toString()}]" class="col-1-4"> <div class="module hero"> <h4>{{hero.name}}</h4> </div> </a> </div> <hero-search></hero-search> class Hero { final int id; String name; Hero(this.id, this.name); factory Hero.fromJson(Map<String, dynamic> hero) => new Hero(_toInt(hero['id']), hero['name']); Map toJson() => {'id': id, 'name': name}; } int _toInt(id) => id is int ? id : int.parse(id); import 'dart:async'; import 'package:angular2/core.dart'; import 'package:angular2/router.dart'; import 'package:angular2/platform/common.dart'; import 'hero.dart'; import 'hero_service.dart'; @Component( selector: 'my-hero-detail', templateUrl: 'hero_detail_component.html', styleUrls: const ['hero_detail_component.css'] ) class HeroDetailComponent implements OnInit { Hero hero; final HeroService _heroService; final RouteParams _routeParams; final Location _location; HeroDetailComponent(this._heroService, this._routeParams, this._location); Future<Null> ngOnInit() async { var _id = _routeParams.get('id'); var id = int.parse(_id ?? '', onError: (_) => null); if (id != null) hero = await (_heroService.getHero(id)); } Future<Null> save() async { await _heroService.update(hero); goBack(); } void goBack() => _location.back(); } <div *ngIf="hero != null"> <h2>{{hero.name}} details!</h2> <div> <label>id: </label>{{hero.id}}</div> <div> <label>name: </label> <input [(ngModel)]="hero.name" placeholder="name" /> </div> <button (click)="goBack()">Back</button> <button (click)="save()">Save</button> </div> import 'dart:async'; import 'dart:convert'; import 'package:angular2/core.dart'; import 'package:http/http.dart'; import 'hero.dart'; @Injectable() class HeroService { static final _headers = {'Content-Type': 'application/json'}; static const _heroesUrl = 'api/heroes'; // URL to web API final Client _http; HeroService(this._http); Future<List<Hero>> getHeroes() async { try { final response = await _http.get(_heroesUrl); final heroes = _extractData(response) .map((value) => new Hero.fromJson(value)) .toList(); return heroes; } catch (e) { throw _handleError(e); } } dynamic _extractData(Response resp) => JSON.decode(resp.body)['data']; Exception _handleError(dynamic e) { print(e); // for demo purposes only return new Exception('Server error; cause: $e'); } Future<Hero> getHero(int id) async { try { final response = await _http.get('$_heroesUrl/$id'); return new Hero.fromJson(_extractData(response)); } catch (e) { throw _handleError(e); } } Future<Hero> create(String name) async { try { final response = await _http.post(_heroesUrl, headers: _headers, body: JSON.encode({'name': name})); return new Hero.fromJson(_extractData(response)); } catch (e) { throw _handleError(e); } } Future<Hero> update(Hero hero) async { try { final url = '$_heroesUrl/${hero.id}'; final response = await _http.put(url, headers: _headers, body: JSON.encode(hero)); return new Hero.fromJson(_extractData(response)); } catch (e) { throw _handleError(e); } } Future<Null> delete(int id) async { try { final url = '$_heroesUrl/$id'; await _http.delete(url, headers: _headers); } catch (e) { throw _handleError(e); } } } .selected { background-color: #CFD8DC !important; color: white; } .heroes { margin: 0 0 2em 0; list-style-type: none; padding: 0; width: 15em; } .heroes li { cursor: pointer; position: relative; left: 0; background-color: #EEE; margin: .5em; padding: .3em 0; height: 1.6em; border-radius: 4px; } .heroes li:hover { color: #607D8B; background-color: #DDD; left: .1em; } .heroes li.selected:hover { background-color: #BBD8DC !important; color: white; } .heroes .text { position: relative; top: -3px; } .heroes .badge { display: inline-block; font-size: small; color: white; padding: 0.8em 0.7em 0 0.7em; background-color: #607D8B; line-height: 1em; position: relative; left: -1px; top: -4px; height: 1.8em; margin-right: .8em; border-radius: 4px 0 0 4px; } button { font-family: Arial; background-color: #eee; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer; cursor: hand; } button:hover { background-color: #cfd8dc; } button.delete { float:right; margin-top: 2px; margin-right: .8em; background-color: gray !important; color:white; } import 'dart:async'; import 'package:angular2/core.dart'; import 'package:angular2/router.dart'; import 'hero.dart'; import 'hero_detail_component.dart'; import 'hero_service.dart'; @Component( selector: 'my-heroes', templateUrl: 'heroes_component.html', styleUrls: const ['heroes_component.css'], directives: const [HeroDetailComponent]) class HeroesComponent implements OnInit { List<Hero> heroes; Hero selectedHero; final HeroService _heroService; final Router _router; HeroesComponent(this._heroService, this._router); Future<Null> getHeroes() async { heroes = await _heroService.getHeroes(); } Future<Null> add(String name) async { name = name.trim(); if (name.isEmpty) return; heroes.add(await _heroService.create(name)); selectedHero = null; } Future<Null> delete(Hero hero) async { await _heroService.delete(hero.id); heroes.remove(hero); if (selectedHero == hero) selectedHero = null; } void ngOnInit() { getHeroes(); } void onSelect(Hero hero) { selectedHero = hero; } Future<Null> gotoDetail() => _router.navigate([ 'HeroDetail', {'id': selectedHero.id.toString()} ]); } import 'dart:async'; import 'dart:convert'; import 'dart:math'; import 'package:angular2/core.dart'; import 'package:http/http.dart'; import 'package:http/testing.dart'; import 'hero.dart'; @Injectable() class InMemoryDataService extends MockClient { static final _initialHeroes = [ {'id': 11, 'name': 'Mr. Nice'}, {'id': 12, 'name': 'Narco'}, {'id': 13, 'name': 'Bombasto'}, {'id': 14, 'name': 'Celeritas'}, {'id': 15, 'name': 'Magneta'}, {'id': 16, 'name': 'RubberMan'}, {'id': 17, 'name': 'Dynama2'}, {'id': 18, 'name': 'Dr IQ'}, {'id': 19, 'name': 'Magma'}, {'id': 20, 'name': 'Tornado'} ]; static final List<Hero> _heroesDb = _initialHeroes.map((json) => new Hero.fromJson(json)).toList(); static int _nextId = _heroesDb.map((hero) => hero.id).fold(0, max) + 1; static Future<Response> _handler(Request request) async { var data; switch (request.method) { case 'GET': final id = int.parse(request.url.pathSegments.last, onError: (_) => null); if (id != null) { data = _heroesDb.firstWhere((hero) => hero.id == id); // throws if no match } else { String prefix = request.url.queryParameters['name'] ?? ''; final regExp = new RegExp(prefix, caseSensitive: false); data = _heroesDb.where((hero) => hero.name.contains(regExp)).toList(); } break; case 'POST': var name = JSON.decode(request.body)['name']; var newHero = new Hero(_nextId++, name); _heroesDb.add(newHero); data = newHero; break; case 'PUT': var heroChanges = new Hero.fromJson(JSON.decode(request.body)); var targetHero = _heroesDb.firstWhere((h) => h.id == heroChanges.id); targetHero.name = heroChanges.name; data = targetHero; break; case 'DELETE': var id = int.parse(request.url.pathSegments.last); _heroesDb.removeWhere((hero) => hero.id == id); // No data, so leave it as null. break; default: throw 'Unimplemented HTTP method ${request.method}'; } return new Response(JSON.encode({'data': data}), 200, headers: {'content-type': 'application/json'}); } InMemoryDataService() : super(_handler); } .search-result { border-bottom: 1px solid gray; border-left: 1px solid gray; border-right: 1px solid gray; width:195px; height: 20px; padding: 5px; background-color: white; cursor: pointer; } #search-box { width: 200px; height: 20px; } import 'dart:async'; import 'package:angular2/core.dart'; import 'package:angular2/router.dart'; import 'package:stream_transformers/stream_transformers.dart'; import 'hero_search_service.dart'; import 'hero.dart'; @Component( selector: 'hero-search', templateUrl: 'hero_search_component.html', styleUrls: const ['hero_search_component.css'], providers: const [HeroSearchService]) class HeroSearchComponent implements OnInit { HeroSearchService _heroSearchService; Router _router; Stream<List<Hero>> heroes; StreamController<String> _searchTerms = new StreamController<String>.broadcast(); HeroSearchComponent(this._heroSearchService, this._router) {} // Push a search term into the stream. void search(String term) => _searchTerms.add(term); Future<Null> ngOnInit() async { heroes = _searchTerms.stream .transform(new Debounce(new Duration(milliseconds: 300))) .distinct() .transform(new FlatMapLatest((term) => term.isEmpty ? new Stream<List<Hero>>.fromIterable([<Hero>[]]) : _heroSearchService.search(term).asStream())) .handleError((e) { print(e); // for demo purposes only }); } void gotoDetail(Hero hero) { var link = [ 'HeroDetail', {'id': hero.id.toString()} ]; _router.navigate(link); } } <div id="search-component"> <h4>Hero Search</h4> <input #searchBox id="search-box" (keyup)="search(searchBox.value)" /> <div> <div *ngFor="let hero of heroes | async" (click)="gotoDetail(hero)" class="search-result" > {{hero.name}} </div> </div> </div> import 'dart:async'; import 'dart:convert'; import 'package:angular2/core.dart'; import 'package:http/http.dart'; import 'hero.dart'; @Injectable() class HeroSearchService { final Client _http; HeroSearchService(this._http); Future<List<Hero>> search(String term) async { try { final response = await _http.get('app/heroes/?name=$term'); return _extractData(response) .map((json) => new Hero.fromJson(json)) .toList(); } catch (e) { throw _handleError(e); } } dynamic _extractData(Response resp) => JSON.decode(resp.body)['data']; Exception _handleError(dynamic e) { print(e); // for demo purposes only return new Exception('Server error; cause: $e'); } } name: angular_tour_of_heroes description: Tour of Heroes version: 0.0.1 environment: sdk: '>=1.19.0 <2.0.0' dependencies: angular2: ^2.2.0 http: ^0.11.0 stream_transformers: ^0.3.0 dev_dependencies: browser: ^0.10.0 dart_to_js_script_rewriter: ^1.0.1 transformers: - angular2: platform_directives: - 'package:angular2/common.dart#COMMON_DIRECTIVES' platform_pipes: - 'package:angular2/common.dart#COMMON_PIPES' entry_points: web/main.dart resolved_identifiers: BrowserClient: 'package:http/browser_client.dart' Client: 'package:http/http.dart' - dart_to_js_script_rewriter import 'package:angular2/core.dart'; import 'package:angular2/platform/browser.dart'; import 'package:angular_tour_of_heroes/app_component.dart'; import 'package:angular_tour_of_heroes/in_memory_data_service.dart'; import 'package:http/http.dart'; void main() { bootstrap(AppComponent, [provide(Client, useClass: InMemoryDataService)] // Using a real back end? // Import browser_client.dart and change the above to: // [provide(Client, useFactory: () => new BrowserClient(), deps: [])] ); }

Next Step

Return to the learning path where you can read about the concepts and practices you discovered in this tutorial.