diff --git a/public/docs/_examples/server-communication/ts/app/main.ts b/public/docs/_examples/server-communication/ts/app/main.ts index 7709586e67..c992809065 100644 --- a/public/docs/_examples/server-communication/ts/app/main.ts +++ b/public/docs/_examples/server-communication/ts/app/main.ts @@ -6,10 +6,10 @@ import {bootstrap} from 'angular2/platform/browser'; import 'rxjs/Rx'; // #enddocregion import-rxjs -import {WikiComponent} from './wiki/wiki.component'; -import {WikiFormComponent} from './wiki/wiki-form.component'; -import {TohComponent} from './toh/toh.component'; +import {WikiComponent} from './wiki/wiki.component'; +import {WikiSmartComponent} from './wiki/wiki-smart.component'; +import {TohComponent} from './toh/toh.component'; bootstrap(WikiComponent); -bootstrap(WikiFormComponent); +bootstrap(WikiSmartComponent); bootstrap(TohComponent); \ No newline at end of file diff --git a/public/docs/_examples/server-communication/ts/app/toh/hero-list.component.1.ts b/public/docs/_examples/server-communication/ts/app/toh/hero-list.component.1.ts index c1e62db7ab..2a96ce2df3 100644 --- a/public/docs/_examples/server-communication/ts/app/toh/hero-list.component.1.ts +++ b/public/docs/_examples/server-communication/ts/app/toh/hero-list.component.1.ts @@ -47,7 +47,6 @@ export class HeroListComponent implements OnInit { .then( hero => this.heroes.push(hero), error => this.errorMessage = error); - } // #enddocregion methods } diff --git a/public/docs/_examples/server-communication/ts/app/toh/hero-list.component.ts b/public/docs/_examples/server-communication/ts/app/toh/hero-list.component.ts index 30de6d5537..a55cf35a72 100644 --- a/public/docs/_examples/server-communication/ts/app/toh/hero-list.component.ts +++ b/public/docs/_examples/server-communication/ts/app/toh/hero-list.component.ts @@ -48,7 +48,6 @@ export class HeroListComponent implements OnInit { .subscribe( hero => this.heroes.push(hero), error => this.errorMessage = error); - } // #enddocregion addHero // #enddocregion methods diff --git a/public/docs/_examples/server-communication/ts/app/wiki/wiki-form.component.ts b/public/docs/_examples/server-communication/ts/app/wiki/wiki-form.component.ts deleted file mode 100644 index 14e47bb0b8..0000000000 --- a/public/docs/_examples/server-communication/ts/app/wiki/wiki-form.component.ts +++ /dev/null @@ -1,37 +0,0 @@ -// #docregion -import {Component, OnInit} from 'angular2/core'; -import {Control} from 'angular2/common'; -import {Observable} from 'rxjs/Observable'; -import {JSONP_PROVIDERS, Jsonp, URLSearchParams} from 'angular2/http'; -import {WikipediaService} from './wikipedia.service'; - -@Component({ - selector: 'my-wiki-form', - template: ` -

Wikipedia Form Demo

-

Fetches when typing stops

- - - `, - providers:[JSONP_PROVIDERS, WikipediaService] -}) -export class WikiFormComponent implements OnInit { - - constructor (private _wikipediaService: WikipediaService) {} - - items: Observable; - // #docregion control - inputs = new Control(); - // #enddocregion control - - ngOnInit() { - // #docregion distinctdebounce - this.items = this.inputs.valueChanges - .debounceTime(300) - .distinctUntilChanged() - // #enddocregion distinctdebounce - .switchMap((term:string) => this._wikipediaService.search(term)); - } -} diff --git a/public/docs/_examples/server-communication/ts/app/wiki/wiki-smart.component.ts b/public/docs/_examples/server-communication/ts/app/wiki/wiki-smart.component.ts new file mode 100644 index 0000000000..6062e5d861 --- /dev/null +++ b/public/docs/_examples/server-communication/ts/app/wiki/wiki-smart.component.ts @@ -0,0 +1,43 @@ +// #docregion +import {Component} from 'angular2/core'; +import {JSONP_PROVIDERS} from 'angular2/http'; +import {Observable} from 'rxjs/Observable'; +// #docregion import-subject +import {Subject} from 'rxjs/Subject'; +// #enddocregion import-subject +import {WikipediaService} from './wikipedia.service'; + +@Component({ + selector: 'my-wiki-smart', + template: ` +

Smarter Wikipedia Demo

+

Fetches when typing stops

+ + + +
    +
  • {{item}}
  • +
+ `, + providers:[JSONP_PROVIDERS, WikipediaService] +}) +export class WikiSmartComponent { + + constructor (private _wikipediaService: WikipediaService) { } + +// #docregion subject + private _searchTermStream = new Subject(); + + search(value: string){ + this._searchTermStream.next(value); + } +// #enddocregion subject + +// #docregion observable + items = this._searchTermStream + .debounceTime(300) + .distinctUntilChanged() + .switchMap((term:string) => this._wikipediaService.search(term)); + +// #enddocregion observable +} diff --git a/public/docs/_examples/server-communication/ts/app/wiki/wiki.component.ts b/public/docs/_examples/server-communication/ts/app/wiki/wiki.component.ts index 3330861882..a7e0fe9f01 100644 --- a/public/docs/_examples/server-communication/ts/app/wiki/wiki.component.ts +++ b/public/docs/_examples/server-communication/ts/app/wiki/wiki.component.ts @@ -9,7 +9,9 @@ import {WikipediaService} from './wikipedia.service'; template: `

Wikipedia Demo

Fetches after each keystroke

+ +
  • {{item}}
@@ -20,7 +22,7 @@ export class WikiComponent { constructor (private _wikipediaService: WikipediaService) {} - items: Observable; + items: Observable; search (term: string) { this.items = this._wikipediaService.search(term); diff --git a/public/docs/_examples/server-communication/ts/app/wiki/wikipedia.service.1.ts b/public/docs/_examples/server-communication/ts/app/wiki/wikipedia.service.1.ts new file mode 100644 index 0000000000..b88038f2f2 --- /dev/null +++ b/public/docs/_examples/server-communication/ts/app/wiki/wikipedia.service.1.ts @@ -0,0 +1,24 @@ +// Create the query string by hand +// #docregion +import {Injectable} from 'angular2/core'; +import {Jsonp} from 'angular2/http'; + +@Injectable() +export class WikipediaService { + constructor(private jsonp: Jsonp) { } + + // TODO: Add error handling + search(term: string) { + + let wikiUrl = 'http://en.wikipedia.org/w/api.php'; + + // #docregion query-string + let queryString = + `?search=${term}&action=opensearch&format=json&callback=JSONP_CALLBACK` + + return this.jsonp + .get(wikiUrl + queryString) + .map(request => request.json()[1]); + // #enddocregion query-string + } +} diff --git a/public/docs/_examples/server-communication/ts/app/wiki/wikipedia.service.ts b/public/docs/_examples/server-communication/ts/app/wiki/wikipedia.service.ts index e0eeaa22a9..01b328c3b3 100644 --- a/public/docs/_examples/server-communication/ts/app/wiki/wikipedia.service.ts +++ b/public/docs/_examples/server-communication/ts/app/wiki/wikipedia.service.ts @@ -8,16 +8,21 @@ export class WikipediaService { search (term: string) { + let wikiUrl = 'http://en.wikipedia.org/w/api.php'; + + // #docregion search-parameters var params = new URLSearchParams(); - params.set('search', term); + params.set('search', term); // the user's search value params.set('action', 'opensearch'); params.set('format', 'json'); + params.set('callback', 'JSONP_CALLBACK'); + // #enddocregion search-parameters - let wikiUrl = 'http://en.wikipedia.org/w/api.php?callback=JSONP_CALLBACK'; - - // TODO: Error handling + // #docregion call-jsonp + // TODO: Add error handling return this.jsonp .get(wikiUrl, { search: params }) - .map(request => request.json()[1]); + .map(request => request.json()[1]); + // #enddocregion call-jsonp } } diff --git a/public/docs/_examples/server-communication/ts/index.html b/public/docs/_examples/server-communication/ts/index.html index c831586a74..0df1448380 100644 --- a/public/docs/_examples/server-communication/ts/index.html +++ b/public/docs/_examples/server-communication/ts/index.html @@ -40,7 +40,7 @@ ToH Loading... Wiki Loading... - Wikiform Loading... + WikiSmart Loading... diff --git a/public/docs/ts/latest/guide/server-communication.jade b/public/docs/ts/latest/guide/server-communication.jade index 2d64bd4f77..387d4fee54 100644 --- a/public/docs/ts/latest/guide/server-communication.jade +++ b/public/docs/ts/latest/guide/server-communication.jade @@ -14,6 +14,7 @@ include ../../../../_includes/_util-fns [Add headers](#headers)
[Promises instead of observables](#promises)
[JSONP](#jsonp)
+ [Set query string parameters](#search-parameters)
[Debounce search term input](#more-observables)
[Appendix: the in-memory web api service](#in-mem-web-api)
@@ -273,18 +274,18 @@ code-example(format="." language="javascript"). { "name": "Windstorm" } :marked The server will generate the `id` and return the entire `JSON` representation - of the new hero including its generated id for our convenience, tucked inside an object - with a `data` property. + of the new hero including its generated id. The hero arrives tucked inside a response object + with its own `data` property. Now that we know how the API works, we implement `addHero`like this: +makeExample('server-communication/ts/app/toh/hero.service.ts', 'import-request-options', 'app/toh/hero.service.ts (additional imports)')(format=".") +makeExample('server-communication/ts/app/toh/hero.service.ts', 'addhero', 'app/toh/hero.service.ts (addHero)')(format=".") :marked The second *body* parameter of the `post` method requires a JSON ***string*** - so we have to `JSON.stringify` the hero content first. + so we have to `JSON.stringify` the hero content before sending. .l-sub-section :marked - We may be able to skip the body stringify in the near future. + We may be able to skip the `stringify` step in the near future. :marked @@ -359,125 +360,163 @@ code-example(format="." language="javascript"). :marked ## Get data with `JSONP` - We just learned how to make `XMLHttpRequests` using Angulars built-in `Http` service. This is the most common approach for - server communication. It doesn't work in all scenarios though. + We just learned how to make `XMLHttpRequests` using Angulars built-in `Http` service. + This is the most common approach for server communication. + It doesn't work in all scenarios. - For security reasons web browser do not permit to make `XHR` calls if the origin of the remote server is different from the one the web page runs in. - The origin is defined as the combination of URI scheme, hostname and port number. The policy that prevents such `XHR` requests is called [Same-origin Policy](https://en.wikipedia.org/wiki/Same-origin_policy) accordingly. + For security reasons, web browsers block `XHR` calls to a remote server whose origin is different from the origin of the web page. + The *origin* is the combination of URI scheme, hostname and port number. + This is called the [Same-origin Policy](https://en.wikipedia.org/wiki/Same-origin_policy). .l-sub-section :marked - That's not entirely true. Modern browsers do allow `XHR` requests against foreign origins if the server sends the appropriate [CORS](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing) headers. - In such cases there's nothing specific to do for the client unless we want our `XHR` request to include credentials (e.g Cookies) which we then have to explicitly enable for the request. + Modern browsers do allow `XHR` requests to servers from a different origin if the server supports the + [CORS](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing) protocol. + If the server requires user credentials, we'll enable them in the [request headers](#headers). :marked - But let's not go too deep into the rabbit hole of `JSONP`. If you like to learn how this technique works under the cover, you may like to read up on this [answer](http://stackoverflow.com/questions/2067472/what-is-jsonp-all-about/2067584#2067584) on StackOverflow. - For now, let's focus on how we can use `JSONP` in Angular. - + Some servers do not support CORS but do support an older, read-only alternative called [JSONP](https://en.wikipedia.org/wiki/JSONP). + Wikipedia is one such server. +.l-sub-section + :marked + This [StackOverflow answer](http://stackoverflow.com/questions/2067472/what-is-jsonp-all-about/2067584#2067584) covers many details of JSONP. +:marked ### Search wikipedia Wikipedia offers a `JSONP` search api. Let's build a simple search that shows suggestions from wikipedia as we type in a text box. figure.image-display img(src='/resources/images/devguide/server-communication/wiki-1.gif' alt="Wikipedia search app (v.1)" width="250") :marked - Angular provides us with a `Jsonp` services which has the same API surface as the `Http` service with the only difference that it restricts us to use `GET` requests only. This is - because the nature of `JSONP` does not allow any other kind of requests. In order to use the `Jsonp` service we have to specify the `JSONP_PROVIDERS`. + The Angular `Jsonp` service both extends the `Http` service for JSONP and restricts us to `GET` requests. + All other HTTP methods throw an error because JSONP is a read-only facility. - Again, we'll make sure to do the server communication in a dedicated service that we call `WikipediaService`. + As always, we wrap our interaction with an Angular data access client service inside a dedicated service, here called `WikipediaService`. +makeExample('server-communication/ts/app/wiki/wikipedia.service.ts',null,'app/wiki/wikipedia.service.ts') - :marked - We use the `URLSearchParams` helper to define a `params` object with the key/value pairs that define the wikipedia query. - The keys are `search`, `action`, and `format`. - The value of the `search` key is the user-supplied search term that we'll lookup in wikipedia. + The constructor expects Angular to inject its `jsonp` service. + We register that service with `JSONP_PROVIDERS` in the [component below](#wikicomponent) that calls our `WikipediaService`. + + + +:marked + ### Search parameters + The [Wikipedia 'opensearch' API](https://www.mediawiki.org/wiki/API:Opensearch) + expects four parameters (key/value pairs) to arrive in the request URL's query string. + The keys are `search`, `action`, `format`, and `callback`. + The value of the `search` key is the user-supplied search term to find in Wikipedia. + The other three are the fixed values "opensearch", "json", and "JSONP_CALLBACK" respectively. +.l-sub-section + :marked + The `JSONP` technique requires that we pass a callback function name to the server in the query string: `callback=JSONP_CALLBACK`. + The server uses that name to build a JavaScript wrapper function in its response which Angular ultimately calls to extract the data. + All of this happens under the hood. +:marked + If we're looking for articles with the word "Angular", we could construct the query string by hand and call `jsonp` like this: ++makeExample('server-communication/ts/app/wiki/wikipedia.service.1.ts','query-string')(format='.') +:marked + In more parameterized examples we might prefer to build the query string with the Angular `URLSearchParams` helper as shown here: ++makeExample('server-communication/ts/app/wiki/wikipedia.service.ts','search-parameters','app/wiki/wikipedia.service.ts (search parameters)')(format=".") +:marked + This time we call `jsonp` with *two* arguments: the `wikiUrl` and an options object whose `search` property is the `params` object. ++makeExample('server-communication/ts/app/wiki/wikipedia.service.ts','call-jsonp','app/wiki/wikipedia.service.ts (call jsonp)')(format=".") +:marked + `Jsonp` flattens the `params` object into the same query string we saw earlier before putting the request on the wire. + + +:marked + ### The WikiComponent - We call `Jsonp` with two arguments: the `wikiUrl` and an options object with a `search` property whose value is the `params` object. - `Jsonp` flattens the `params` object into a query string such as -code-example. - &search=foo&action=opensearch&format=json` -:marked - and appends it to the `wikiUrl`. Notice that the `wikiUrl` contains `callback=JSONP_CALLBACK`. The nature of `JSONP` requires that - we pass a unique function name to the server so that the server can pick up that name in the response. Many JSONP APIs pass this function name - through an URL parameter called they call `callback`. But that's more common sense than a strict rule. Since Angular doesn't assume a fixed name - for that parameter it requires *us* to put it in the URL and asign the placeholder `JSONP_CALLBACK` to it. Angular will make sure to replace - the placeholder with a unique function name for each request for us. - - Now that we have the service ready to query the Wikipedia API, let's look at the actual component that should accept the user input - and shows the search results. + Now that we have a service that can query the Wikipedia API, + we turn to the component that takes user input and displays search results. +makeExample('server-communication/ts/app/wiki/wiki.component.ts', null, 'app/wiki/wiki.component.ts') :marked - There shouldn't be much of a surprise here. Our component defines an `` and calls a `search(term)` method for each `keyup` event. - The `search(term)` method delegates to our `WikipediaService` which returns an `Observable>` to us. Instead of subscribing to it - manually we use the [async pipe](pipes.html#async-pipe) in the `ngFor` to let the view subscribe to the Observable directly. + The `providers` array in the component metadata specifies the Angular `JSONP_PROVIDERS` collection that supports the `Jsonp` service. + We register that collection at the component level to make `Jsonp` injectable in the `WikipediaService`. + The component presents an `` element *search box* to gather search terms from the user. + and calls a `search(term)` method after each `keyup` event. + + The `search(term)` method delegates to our `WikipediaService` which returns an observable array of string results (`Observable - ### More fun with Observables + ## Our wasteful app - If you ever wrote a search-as-you-type control yourself before, you are probably aware of some typical corner cases that arise with this task. - - ### 1. Don't hit the search endpoint on every key stroke - - Treat the search endpoint as if you pay for it on a per-request basis. No matter if it's your own hardware or not. We shouldn't be hammering - the search enpoint more often than needed. - What we want is to hit the search endpoint as soon as the user *stops typing* instead of after every keystroke. - + Our wikipedia search makes too many calls to the server. + It is inefficient and potentially expensive on mobile devices with limited data plans. + + ### 1. Wait for the user to stop typing + At the moment we call the server after every key stroke. + The app should only make requests when the user *stops typing* . Here's how it *should* work — and *will* work — when we're done refactoring: figure.image-display img(src='/resources/images/devguide/server-communication/wiki-2.gif' alt="Wikipedia search app (v.2)" width="250") :marked - ### 2. Don't hit the search endpoint for the same term again + ### 2. Search when the search term changes - Consider you type *foo*, stop, type another *o*, hit return and stop back at *foo*. That should be just one request with the term *foo* - and not two even if we technically stopped twice after we had *foo* in the search box. + Suppose the user enters the work *angular* in the search box and pauses for a while. + The application issues a search request for *Angular*. + + Then the user backspaces over the last three letters, *lar*, and immediately re-types *lar* before pausing once more. + The search term is still "angular". The app shouldn't make another request. ### 3. Cope with out-of-order responses - When we have multiple requests in-flight at the same time we must account for cases where they come back in unexpected order. Consider we first typed - *computer*, stop, a request goes out, we type *car*, stop, a request goes out but then the request that carries the results for *computer* comes back - after the request that carries the results for *car*. If we don't deal with such cases properly we can get a buggy application that shows - results for *computer* even if the search box reads *car*. - - Now that we identified the problems that need to be solved, let's make a couple of trivial changes to our code to fix them in a *functional reactive* way. Here is the - entire example with all changes. + The user enters *angular*, pauses, clears the search box, and enters *http*. + The application issues two search requests, one for *angular* and one for *http*. - There are no changes for our `WikipediaService` so we can skip that one. We'll go over each change to briefly describe what - it does. - -+makeExample('server-communication/ts/app/wiki/wiki-form.component.ts', null, 'app/wiki/wiki-form.component.ts') - -:marked - The first thing we need to do to unveil the full magic of a observable-based solution is to get an `Observable` for our `` control. - The best way to do that is to change `` into `` and to create an `inputs` `Control` accordingly. - -+makeExample('server-communication/ts/app/wiki/wiki-form.component.ts', 'control') - -:marked - We now have an `Observable` at `this.inputs.valueChanges`. We can simply use the `debounceTime(ms)` and `distinctUntilChanged()` operators to fix the first two problems. We basically get a new `Observable` that emits - new values exactly the way we want them to be emitted. - -+makeExample('server-communication/ts/app/wiki/wiki-form.component.ts', 'distinctdebounce')(format=".") -:marked - With the previous change we tamed the input but we still need to deal with the out-of-order cases. At this point we have an `Observable` and a - `search(term)` method on the `WikipediaService` that returns an `Observable>`. What we want is an `Observable>` that - carries the results of the *last* term that was emitted from the `Observable`. + Which response will arrive first? We can't be sure. + A load balancer could dispatch the requests to two different servers with different response times. + The results from the first *angular* request might arrive after the later *http* results. + The user will be confused if we display the *angular* results to the *http* query. - If we would just map our current `Observable` like `.map(term => wikipediaService.search(term))` we would transform it into an `Observable>` - which isn't quite what we want. Enter [`switchMap`](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/flatmaplatest.md) to the rescue. Basically `switchMap` flattens from an `Observable` into an `Observable` by - emitting values only from the most recent `Observable` that was produced from the outer `Observable`. + When there are multiple requests in-flight, the app should present the responses + in the original request order. That won't happen if *angular* results arrive last. + + + ## More fun with Observables + We can address these problems and improve our app with the help of some nifty observable operators. + + We could make our changes to the `WikipediaService`. + But we sense that our concerns are driven by the user experience so we update the component class instead. + ++makeExample('server-communication/ts/app/wiki/wiki-smart.component.ts', null, 'app/wiki/wiki-smart.component.ts') +:marked + We made no changes to the template or metadata, confining them all to the component class. - This may sound a lot like black magic for people unfamiliar with Observables but as soon as the coin sinks in it's starting to make a whole world of difficult programming tasks appear much simpler. + The first step turns the user's search box entries into the *subject* of an observable. + We import the `Subject` class from the RxJS observable library: ++makeExample('server-communication/ts/app/wiki/wiki-smart.component.ts', 'import-subject') +:marked + After every keystroke we pump the search box value into the private `_searchTermStream` subject, creating a stream of search term strings. ++makeExample('server-communication/ts/app/wiki/wiki-smart.component.ts', 'subject')(format='.') +:marked + Earlier, we passed each search term directly to the service and bound the template to the service results. + Now we listen to the *stream of terms*, manipulating the stream before it reaches the `WikipediaService`. ++makeExample('server-communication/ts/app/wiki/wiki-smart.component.ts', 'observable')(format='.') +:marked + We wait for the user to stop typing for at least 300 milliseconds + ([debounce](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/debounce.md)). + Only changed search values make it through to the service + ([distinctUntilChanged](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/distinctuntilchanged.md)). + + The `WikipediaService` returns a separate observable of strings (`Observable`) for each request. + We could have multiple requests "in flight", all awaiting the server's reply, + which means multiple *observables-of-strings* could arrive at any moment in any order. + + We rely on [switchMap](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/flatmaplatest.md) + (formerly known as `flatMapLatest`) to re-arrange these observables in their original-request order. + + The `switchmap` operator ensures that the component's `items` property is always set to the truly latest `WikipediaService` observable. + Consequently, the displayed list of search results stays in sync with the user's sequence of search terms. .l-main-section