diff --git a/public/docs/_examples/heroes-graphql-starter/e2e-spec.ts b/public/docs/_examples/heroes-graphql-starter/e2e-spec.ts new file mode 100644 index 0000000000..9440778496 --- /dev/null +++ b/public/docs/_examples/heroes-graphql-starter/e2e-spec.ts @@ -0,0 +1,283 @@ +'use strict'; // necessary for es6 output in node + +import { browser, element, by, ElementFinder, ElementArrayFinder } from 'protractor'; +import { promise } from 'selenium-webdriver'; + +const expectedH1 = 'Tour of Heroes - GraphQL Starter'; +const expectedTitle = `Angular ${expectedH1}`; +const targetHero = { id: 15, name: 'Magneta' }; +const targetHeroDashboardIndex = 3; +const nameSuffix = 'X'; +const newHeroName = targetHero.name + nameSuffix; + +class Hero { + id: number; + name: string; + + // Factory methods + + // Hero from string formatted as ' '. + static fromString(s: string): Hero { + return { + id: +s.substr(0, s.indexOf(' ')), + name: s.substr(s.indexOf(' ') + 1), + }; + } + + // Hero from hero list
  • element. + static async fromLi(li: ElementFinder): Promise { + let strings = await li.all(by.xpath('span')).getText(); + return { id: +strings[0], name: strings[1] }; + } + + // Hero id and name from the given detail element. + static async fromDetail(detail: ElementFinder): Promise { + // Get hero id from the first
    + let _id = await detail.all(by.css('div')).first().getText(); + // Get name from the h2 + let _name = await detail.element(by.css('h2')).getText(); + return { + id: +_id.substr(_id.indexOf(' ') + 1), + name: _name.substr(0, _name.lastIndexOf(' ')) + }; + } +} + +describe('Heroes GraphQL Starter', () => { + + beforeAll(() => browser.get('')); + + function getPageElts() { + let navElts = element.all(by.css('my-app nav a')); + + return { + navElts: navElts, + + myDashboardHref: navElts.get(0), + myDashboard: element(by.css('my-app my-dashboard')), + topHeroes: element.all(by.css('my-app my-dashboard > div h4')), + + myHeroesHref: navElts.get(1), + myHeroes: element(by.css('my-app my-heroes')), + allHeroes: element.all(by.css('my-app my-heroes li')), + selectedHero: element(by.css('my-app li.selected')), + selectedHeroSubview: element(by.css('my-app my-heroes > div:last-child')), + + heroDetail: element(by.css('my-app my-hero-detail > div')), + + searchBox: element(by.css('#search-box')), + searchResults: element.all(by.css('.search-result')) + }; + } + + describe('Initial page', () => { + + it(`has title '${expectedTitle}'`, () => { + expect(browser.getTitle()).toEqual(expectedTitle); + }); + + it(`has h1 '${expectedH1}'`, () => { + expectHeading(1, expectedH1); + }); + + const expectedViewNames = ['Dashboard', 'Heroes']; + it(`has views ${expectedViewNames}`, () => { + let viewNames = getPageElts().navElts.map((el: ElementFinder) => el.getText()); + expect(viewNames).toEqual(expectedViewNames); + }); + + it('has dashboard as the active view', () => { + let page = getPageElts(); + expect(page.myDashboard.isPresent()).toBeTruthy(); + }); + + }); + + describe('Dashboard tests', () => { + + beforeAll(() => browser.get('')); + + it('has top heroes', () => { + let page = getPageElts(); + expect(page.topHeroes.count()).toEqual(4); + }); + + it(`selects and routes to ${targetHero.name} details`, dashboardSelectTargetHero); + + it(`updates hero name (${newHeroName}) in details view`, updateHeroNameInDetailView); + + it(`cancels and shows ${targetHero.name} in Dashboard`, () => { + element(by.buttonText('Back')).click(); + browser.waitForAngular(); // seems necessary to gets tests to past for toh-6 + + let targetHeroElt = getPageElts().topHeroes.get(targetHeroDashboardIndex); + expect(targetHeroElt.getText()).toEqual(targetHero.name); + }); + + it(`selects and routes to ${targetHero.name} details`, dashboardSelectTargetHero); + + it(`updates hero name (${newHeroName}) in details view`, updateHeroNameInDetailView); + + it(`saves and shows ${newHeroName} in Dashboard`, () => { + element(by.buttonText('Save')).click(); + browser.waitForAngular(); // seems necessary to gets tests to past for toh-6 + + let targetHeroElt = getPageElts().topHeroes.get(targetHeroDashboardIndex); + expect(targetHeroElt.getText()).toEqual(newHeroName); + }); + + }); + + describe('Heroes tests', () => { + + beforeAll(() => browser.get('')); + + it('can switch to Heroes view', () => { + getPageElts().myHeroesHref.click(); + let page = getPageElts(); + expect(page.myHeroes.isPresent()).toBeTruthy(); + expect(page.allHeroes.count()).toEqual(10, 'number of heroes'); + }); + + it(`selects and shows ${targetHero.name} as selected in list`, () => { + getHeroLiEltById(targetHero.id).click(); + expect(Hero.fromLi(getPageElts().selectedHero)).toEqual(targetHero); + }); + + it('shows selected hero subview', () => { + let page = getPageElts(); + let title = page.selectedHeroSubview.element(by.css('h2')).getText(); + let expectedTitle = `${targetHero.name.toUpperCase()} is my hero`; + expect(title).toEqual(expectedTitle); + }); + + it('can route to hero details', () => { + element(by.buttonText('View Details')).click(); + + let page = getPageElts(); + expect(page.heroDetail.isPresent()).toBeTruthy('shows hero detail'); + let hero = Hero.fromDetail(page.heroDetail); + expect(hero).toEqual(targetHero); + }); + + it(`updates hero name (${newHeroName}) in details view`, updateHeroNameInDetailView); + + it(`shows ${newHeroName} in Heroes list`, () => { + element(by.buttonText('Save')).click(); + browser.waitForAngular(); // seems necessary to gets tests to past for toh-6 + let expectedHero = {id: targetHero.id, name: newHeroName}; + expect(Hero.fromLi(getHeroLiEltById(targetHero.id))).toEqual(expectedHero); + }); + + it(`deletes ${newHeroName} from Heroes list`, async () => { + const heroesBefore = await toHeroArray(getPageElts().allHeroes); + const li = getHeroLiEltById(targetHero.id); + li.element(by.buttonText('x')).click(); + + const page = getPageElts(); + expect(page.myHeroes.isPresent()).toBeTruthy(); + expect(page.allHeroes.count()).toEqual(9, 'number of heroes'); + const heroesAfter = await toHeroArray(page.allHeroes); + const expectedHeroes = heroesBefore.filter(h => h.name !== newHeroName); + expect(heroesAfter).toEqual(expectedHeroes); + expect(page.selectedHeroSubview.isPresent()).toBeFalsy(); + }); + + it(`adds back ${targetHero.name}`, async () => { + const newHeroName = 'Alice'; + const heroesBefore = await toHeroArray(getPageElts().allHeroes); + const numHeroes = heroesBefore.length; + + element(by.css('input')).sendKeys(newHeroName); + element(by.buttonText('Add')).click(); + + let page = getPageElts(); + let heroesAfter = await toHeroArray(page.allHeroes); + expect(heroesAfter.length).toEqual(numHeroes + 1, 'number of heroes'); + + expect(heroesAfter.slice(0, numHeroes)).toEqual(heroesBefore, 'Old heroes are still there'); + + const maxId = heroesBefore[heroesBefore.length - 1].id; + expect(heroesAfter[numHeroes]).toEqual({id: maxId + 1, name: newHeroName}); + }); + }); + + describe('Progressive hero search', () => { + + beforeAll(() => browser.get('')); + + it(`searches for 'Ma'`, async () => { + getPageElts().searchBox.sendKeys('Ma'); + browser.sleep(1000); + expect(getPageElts().searchResults.count()).toBe(4); + }); + + it(`continues search with 'g'`, async () => { + getPageElts().searchBox.sendKeys('g'); + browser.sleep(1000); + expect(getPageElts().searchResults.count()).toBe(2); + }); + + it(`continues search with 'n' and gets ${targetHero.name}`, async () => { + getPageElts().searchBox.sendKeys('n'); + browser.sleep(1000); + let page = getPageElts(); + expect(page.searchResults.count()).toBe(1); + let hero = page.searchResults.get(0); + expect(hero.getText()).toEqual(targetHero.name); + }); + + it(`navigates to ${targetHero.name} details view`, async () => { + let hero = getPageElts().searchResults.get(0); + expect(hero.getText()).toEqual(targetHero.name); + hero.click(); + + let page = getPageElts(); + expect(page.heroDetail.isPresent()).toBeTruthy('shows hero detail'); + expect(Hero.fromDetail(page.heroDetail)).toEqual(targetHero); + }); + }); + + function dashboardSelectTargetHero() { + let targetHeroElt = getPageElts().topHeroes.get(targetHeroDashboardIndex); + expect(targetHeroElt.getText()).toEqual(targetHero.name); + targetHeroElt.click(); + browser.waitForAngular(); // seems necessary to gets tests to past for toh-6 + + let page = getPageElts(); + expect(page.heroDetail.isPresent()).toBeTruthy('shows hero detail'); + let hero = Hero.fromDetail(page.heroDetail); + expect(hero).toEqual(targetHero); + } + + async function updateHeroNameInDetailView() { + // Assumes that the current view is the hero details view. + addToHeroName(nameSuffix); + + let hero = await Hero.fromDetail(getPageElts().heroDetail); + expect(hero).toEqual({id: targetHero.id, name: newHeroName}); + } + +}); + +function addToHeroName(text: string): promise.Promise { + let input = element(by.css('input')); + return input.sendKeys(text); +} + +function expectHeading(hLevel: number, expectedText: string): void { + let hTag = `h${hLevel}`; + let hText = element(by.css(hTag)).getText(); + expect(hText).toEqual(expectedText, hTag); +}; + +function getHeroLiEltById(id: number): ElementFinder { + let spanForId = element(by.cssContainingText('li span.badge', id.toString())); + return spanForId.element(by.xpath('..')); +} + +async function toHeroArray(allHeroes: ElementArrayFinder): Promise { + let promisedHeroes = await allHeroes.map(Hero.fromLi); + // The cast is necessary to get around issuing with the signature of Promise.all() + return > Promise.all(promisedHeroes); +} diff --git a/public/docs/_examples/heroes-graphql-starter/ts/.gitignore b/public/docs/_examples/heroes-graphql-starter/ts/.gitignore new file mode 100644 index 0000000000..c66c617d1f --- /dev/null +++ b/public/docs/_examples/heroes-graphql-starter/ts/.gitignore @@ -0,0 +1,10 @@ +aot/**/*.ts +**/*.ngfactory.ts +**/*.ngsummary.json +**/*.metadata.json +**/*.js +dist +!app/tsconfig.json +!rollup-config.js +!copy-dist-files.js +!systemjs.config.extras.js diff --git a/public/docs/_examples/heroes-graphql-starter/ts/aot/index.html b/public/docs/_examples/heroes-graphql-starter/ts/aot/index.html new file mode 100644 index 0000000000..b80b8d4d4c --- /dev/null +++ b/public/docs/_examples/heroes-graphql-starter/ts/aot/index.html @@ -0,0 +1,19 @@ + + + + + + Angular Tour of Heroes - GraphQL Starter + + + + + + + + + + Loading... + + + diff --git a/public/docs/_examples/heroes-graphql-starter/ts/aot/styles.css b/public/docs/_examples/heroes-graphql-starter/ts/aot/styles.css new file mode 100644 index 0000000000..d81835d0cd --- /dev/null +++ b/public/docs/_examples/heroes-graphql-starter/ts/aot/styles.css @@ -0,0 +1,116 @@ +/* #docregion , quickstart, toh */ +/* Master Styles */ +h1 { + color: #369; + font-family: Arial, Helvetica, sans-serif; + font-size: 250%; +} +h2, h3 { + color: #444; + font-family: Arial, Helvetica, sans-serif; + font-weight: lighter; +} +body { + margin: 2em; +} +/* #enddocregion quickstart */ +body, input[text], button { + color: #888; + font-family: Cambria, Georgia; +} +/* #enddocregion toh */ +a { + cursor: pointer; + cursor: hand; +} +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:disabled { + background-color: #eee; + color: #aaa; + cursor: auto; +} + +/* Navigation link styles */ +nav a { + padding: 5px 10px; + text-decoration: none; + margin-right: 10px; + margin-top: 10px; + display: inline-block; + background-color: #eee; + border-radius: 4px; +} +nav a:visited, a:link { + color: #607D8B; +} +nav a:hover { + color: #039be5; + background-color: #CFD8DC; +} +nav a.active { + color: #039be5; +} + +/* items class */ +.items { + margin: 0 0 2em 0; + list-style-type: none; + padding: 0; + width: 24em; +} +.items li { + cursor: pointer; + position: relative; + left: 0; + background-color: #EEE; + margin: .5em; + padding: .3em 0; + height: 1.6em; + border-radius: 4px; +} +.items li:hover { + color: #607D8B; + background-color: #DDD; + left: .1em; +} +.items li.selected { + background-color: #CFD8DC; + color: white; +} +.items li.selected:hover { + background-color: #BBD8DC; +} +.items .text { + position: relative; + top: -3px; +} +.items .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; +} +/* #docregion toh */ +/* everywhere else */ +* { + font-family: Arial, Helvetica, sans-serif; +} diff --git a/public/docs/_examples/heroes-graphql-starter/ts/bs-config.aot.json b/public/docs/_examples/heroes-graphql-starter/ts/bs-config.aot.json new file mode 100644 index 0000000000..e59a7403a0 --- /dev/null +++ b/public/docs/_examples/heroes-graphql-starter/ts/bs-config.aot.json @@ -0,0 +1,14 @@ +{ + "open": false, + "logLevel": "silent", + "port": 8080, + "server": { + "baseDir": "aot", + "routes": { + "/node_modules": "node_modules" + }, + "middleware": { + "0": null + } + } +} diff --git a/public/docs/_examples/heroes-graphql-starter/ts/copy-dist-files.js b/public/docs/_examples/heroes-graphql-starter/ts/copy-dist-files.js new file mode 100644 index 0000000000..a451cbd633 --- /dev/null +++ b/public/docs/_examples/heroes-graphql-starter/ts/copy-dist-files.js @@ -0,0 +1,12 @@ +// #docregion +var fs = require('fs'); +var resources = [ + 'node_modules/core-js/client/shim.min.js', + 'node_modules/zone.js/dist/zone.min.js', + 'src/styles.css' +]; +resources.map(function(f) { + var path = f.split('/'); + var t = 'aot/' + path[path.length-1]; + fs.createReadStream(f).pipe(fs.createWriteStream(t)); +}); \ No newline at end of file diff --git a/public/docs/_examples/heroes-graphql-starter/ts/example-config.json b/public/docs/_examples/heroes-graphql-starter/ts/example-config.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/public/docs/_examples/heroes-graphql-starter/ts/plnkr.json b/public/docs/_examples/heroes-graphql-starter/ts/plnkr.json new file mode 100644 index 0000000000..037d93c3f3 --- /dev/null +++ b/public/docs/_examples/heroes-graphql-starter/ts/plnkr.json @@ -0,0 +1,10 @@ +{ + "description": "Tour of Heroes - GraphQL Starter", + "basePath": "src/", + "files":[ + "!**/*.d.ts", + "!**/*.js", + "!**/*.[1,2].*" + ], + "tags": ["tutorial", "tour", "heroes", "http", "GraphQL"] +} diff --git a/public/docs/_examples/heroes-graphql-starter/ts/rollup-config.js b/public/docs/_examples/heroes-graphql-starter/ts/rollup-config.js new file mode 100755 index 0000000000..3591eb4e2a --- /dev/null +++ b/public/docs/_examples/heroes-graphql-starter/ts/rollup-config.js @@ -0,0 +1,53 @@ +// #docregion +import rollup from 'rollup' +import nodeResolve from 'rollup-plugin-node-resolve' +import commonjs from 'rollup-plugin-commonjs'; +import uglify from 'rollup-plugin-uglify' + +import builtins from 'rollup-plugin-node-builtins'; +import globals from 'rollup-plugin-node-globals'; +//paths are relative to the execution path +export default { + entry: 'src/main-aot.js', + dest: 'aot/dist/build.js', // output a single application bundle + sourceMap: true, + sourceMapFile: 'aot/dist/build.js.map', + format: 'iife', + onwarn: function(warning) { + // Skip certain warnings + + // should intercept ... but doesn't in some rollup versions + if ( warning.code === 'THIS_IS_UNDEFINED' ) { return; } + // intercepts in some rollup versions + if ( warning.indexOf("The 'this' keyword is equivalent to 'undefined'") > -1 ) { return; } + + // console.warn everything else + console.warn( warning.message ); + }, + plugins: [ + nodeResolve({jsnext: true, module: true, browser: true}), + commonjs({ + include: [ + 'node_modules/rxjs/**', + 'node_modules/graphql-tag/**', + 'node_modules/apollo-client/**', + 'node_modules/lodash/**', + 'node_modules/graphql-tools/dist/**', + 'node_modules/graphql/**', + 'node_modules/graphql-anywhere/**', + 'node_modules/iterall/**', + 'node_modules/deprecated-decorator/**', + 'node_modules/uuid/**' + ], + namedExports: { + 'node_modules/graphql-tools/dist/index.js': ['makeExecutableSchema' ], + 'node_modules/graphql/index.js': ['execute' ], + 'node_modules/graphql-tag/bundledPrinter.js': ['print'], + 'node_modules/lodash/lodash.js': ['find', 'omit', 'assign', 'isFunction'], + } + }), + globals(), + builtins(), + uglify() + ] +} \ No newline at end of file diff --git a/public/docs/_examples/heroes-graphql-starter/ts/src/app/app-routing.module.ts b/public/docs/_examples/heroes-graphql-starter/ts/src/app/app-routing.module.ts new file mode 100644 index 0000000000..bc070f6c31 --- /dev/null +++ b/public/docs/_examples/heroes-graphql-starter/ts/src/app/app-routing.module.ts @@ -0,0 +1,19 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +import { DashboardComponent } from './dashboard.component'; +import { HeroesComponent } from './heroes.component'; +import { HeroDetailComponent } from './hero-detail.component'; + +const routes: Routes = [ + { path: '', redirectTo: '/dashboard', pathMatch: 'full' }, + { path: 'dashboard', component: DashboardComponent }, + { path: 'detail/:id', component: HeroDetailComponent }, + { path: 'heroes', component: HeroesComponent } +]; + +@NgModule({ + imports: [ RouterModule.forRoot(routes) ], + exports: [ RouterModule ] +}) +export class AppRoutingModule {} diff --git a/public/docs/_examples/heroes-graphql-starter/ts/src/app/app.component.css b/public/docs/_examples/heroes-graphql-starter/ts/src/app/app.component.css new file mode 100644 index 0000000000..071e665767 --- /dev/null +++ b/public/docs/_examples/heroes-graphql-starter/ts/src/app/app.component.css @@ -0,0 +1,29 @@ +/* #docregion */ +h1 { + font-size: 1.2em; + color: #999; + margin-bottom: 0; +} +h2 { + font-size: 2em; + margin-top: 0; + padding-top: 0; +} +nav a { + padding: 5px 10px; + text-decoration: none; + margin-top: 10px; + display: inline-block; + background-color: #eee; + border-radius: 4px; +} +nav a:visited, a:link { + color: #607D8B; +} +nav a:hover { + color: #039be5; + background-color: #CFD8DC; +} +nav a.active { + color: #039be5; +} diff --git a/public/docs/_examples/heroes-graphql-starter/ts/src/app/app.component.ts b/public/docs/_examples/heroes-graphql-starter/ts/src/app/app.component.ts new file mode 100644 index 0000000000..f947f02698 --- /dev/null +++ b/public/docs/_examples/heroes-graphql-starter/ts/src/app/app.component.ts @@ -0,0 +1,19 @@ +// #docplaster +// #docregion +import { Component } from '@angular/core'; + +@Component({ + selector: 'my-app', + template: ` +

    {{title}}

    + + + `, + styleUrls: ['./app.component.css'] +}) +export class AppComponent { + title = 'Tour of Heroes - GraphQL Starter'; +} diff --git a/public/docs/_examples/heroes-graphql-starter/ts/src/app/app.module.ts b/public/docs/_examples/heroes-graphql-starter/ts/src/app/app.module.ts new file mode 100644 index 0000000000..58eeb10c54 --- /dev/null +++ b/public/docs/_examples/heroes-graphql-starter/ts/src/app/app.module.ts @@ -0,0 +1,52 @@ +// #docplaster +// #docregion +// #docregion v1, v2 +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { FormsModule } from '@angular/forms'; +import { HttpModule } from '@angular/http'; + +import { AppRoutingModule } from './app-routing.module'; + +// #enddocregion v1 +// Imports for loading & configuring the in-memory web api +import { InMemoryWebApiModule } from 'angular-in-memory-web-api'; +import { InMemoryDataService } from './in-memory-data.service'; + +// #docregion v1 +import { AppComponent } from './app.component'; +import { DashboardComponent } from './dashboard.component'; +import { HeroesComponent } from './heroes.component'; +import { HeroDetailComponent } from './hero-detail.component'; +import { HeroService } from './hero.service'; +// #enddocregion v1, v2 +import { HeroSearchComponent } from './hero-search.component'; +// #docregion v1, v2 + +@NgModule({ + imports: [ + BrowserModule, + FormsModule, + HttpModule, + // #enddocregion v1 + // #docregion in-mem-web-api + InMemoryWebApiModule.forRoot(InMemoryDataService), + // #enddocregion in-mem-web-api + // #docregion v1 + AppRoutingModule + ], + // #docregion search + declarations: [ + AppComponent, + DashboardComponent, + HeroDetailComponent, + HeroesComponent, + // #enddocregion v1, v2 + HeroSearchComponent + // #docregion v1, v2 + ], + // #enddocregion search + providers: [ HeroService ], + bootstrap: [ AppComponent ] +}) +export class AppModule { } diff --git a/public/docs/_examples/heroes-graphql-starter/ts/src/app/dashboard.component.css b/public/docs/_examples/heroes-graphql-starter/ts/src/app/dashboard.component.css new file mode 100644 index 0000000000..dc7fb7ce06 --- /dev/null +++ b/public/docs/_examples/heroes-graphql-starter/ts/src/app/dashboard.component.css @@ -0,0 +1,62 @@ +/* #docregion */ +[class*='col-'] { + float: left; + padding-right: 20px; + padding-bottom: 20px; +} +[class*='col-']:last-of-type { + padding-right: 0; +} +a { + text-decoration: none; +} +*, *:after, *:before { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +h3 { + text-align: center; margin-bottom: 0; +} +h4 { + position: relative; +} +.grid { + margin: 0; +} +.col-1-4 { + width: 25%; +} +.module { + padding: 20px; + text-align: center; + color: #eee; + max-height: 120px; + min-width: 120px; + background-color: #607D8B; + border-radius: 2px; +} +.module:hover { + background-color: #EEE; + cursor: pointer; + color: #607d8b; +} +.grid-pad { + padding: 10px 0; +} +.grid-pad > [class*='col-']:last-of-type { + padding-right: 20px; +} +@media (max-width: 600px) { + .module { + font-size: 10px; + max-height: 75px; } +} +@media (max-width: 1024px) { + .grid { + margin: 0; + } + .module { + min-width: 60px; + } +} diff --git a/public/docs/_examples/heroes-graphql-starter/ts/src/app/dashboard.component.html b/public/docs/_examples/heroes-graphql-starter/ts/src/app/dashboard.component.html new file mode 100644 index 0000000000..db8546ccd2 --- /dev/null +++ b/public/docs/_examples/heroes-graphql-starter/ts/src/app/dashboard.component.html @@ -0,0 +1,10 @@ + +

    Top Heroes

    + + diff --git a/public/docs/_examples/heroes-graphql-starter/ts/src/app/dashboard.component.ts b/public/docs/_examples/heroes-graphql-starter/ts/src/app/dashboard.component.ts new file mode 100644 index 0000000000..9960aa77d4 --- /dev/null +++ b/public/docs/_examples/heroes-graphql-starter/ts/src/app/dashboard.component.ts @@ -0,0 +1,22 @@ +// #docregion , search +import { Component, OnInit } from '@angular/core'; + +import { Hero } from './hero'; +import { HeroService } from './hero.service'; + +@Component({ + selector: 'my-dashboard', + templateUrl: './dashboard.component.html', + styleUrls: [ './dashboard.component.css' ] +}) +// #enddocregion search +export class DashboardComponent implements OnInit { + heroes: Hero[] = []; + + constructor(private heroService: HeroService) { } + + ngOnInit(): void { + this.heroService.getHeroes() + .then(heroes => this.heroes = heroes.slice(1, 5)); + } +} diff --git a/public/docs/_examples/heroes-graphql-starter/ts/src/app/hero-detail.component.css b/public/docs/_examples/heroes-graphql-starter/ts/src/app/hero-detail.component.css new file mode 100644 index 0000000000..ab2437efd8 --- /dev/null +++ b/public/docs/_examples/heroes-graphql-starter/ts/src/app/hero-detail.component.css @@ -0,0 +1,30 @@ +/* #docregion */ +label { + display: inline-block; + width: 3em; + margin: .5em 0; + color: #607D8B; + font-weight: bold; +} +input { + height: 2em; + font-size: 1em; + padding-left: .4em; +} +button { + margin-top: 20px; + font-family: Arial; + background-color: #eee; + border: none; + padding: 5px 10px; + border-radius: 4px; + cursor: pointer; cursor: hand; +} +button:hover { + background-color: #cfd8dc; +} +button:disabled { + background-color: #eee; + color: #ccc; + cursor: auto; +} diff --git a/public/docs/_examples/heroes-graphql-starter/ts/src/app/hero-detail.component.html b/public/docs/_examples/heroes-graphql-starter/ts/src/app/hero-detail.component.html new file mode 100644 index 0000000000..32fe6d4391 --- /dev/null +++ b/public/docs/_examples/heroes-graphql-starter/ts/src/app/hero-detail.component.html @@ -0,0 +1,14 @@ + +
    +

    {{hero.name}} details!

    +
    + {{hero.id}}
    +
    + + +
    + + + + +
    diff --git a/public/docs/_examples/heroes-graphql-starter/ts/src/app/hero-detail.component.ts b/public/docs/_examples/heroes-graphql-starter/ts/src/app/hero-detail.component.ts new file mode 100644 index 0000000000..54423cea7f --- /dev/null +++ b/public/docs/_examples/heroes-graphql-starter/ts/src/app/hero-detail.component.ts @@ -0,0 +1,43 @@ +// #docregion +import 'rxjs/add/operator/switchMap'; +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Params } from '@angular/router'; +import { Location } from '@angular/common'; + +import { Hero } from './hero'; +import { HeroService } from './hero.service'; + +@Component({ + selector: 'my-hero-detail', + templateUrl: './hero-detail.component.html', + styleUrls: [ './hero-detail.component.css' ] +}) +export class HeroDetailComponent implements OnInit { + hero: Hero; + + constructor( + private heroService: HeroService, + private route: ActivatedRoute, + private location: Location + ) {} + + // #docregion service-fetch-by-id + ngOnInit(): void { + this.route.params + .switchMap((params: Params) => this.heroService.getHero(+params['id'])) + .subscribe(hero => this.hero = hero); + } + // #enddocregion service-fetch-by-id + + + // #docregion save + save(): void { + this.heroService.update(this.hero) + .then(() => this.goBack()); + } + // #enddocregion save + + goBack(): void { + this.location.back(); + } +} diff --git a/public/docs/_examples/heroes-graphql-starter/ts/src/app/hero-search.component.css b/public/docs/_examples/heroes-graphql-starter/ts/src/app/hero-search.component.css new file mode 100644 index 0000000000..9bf8d13457 --- /dev/null +++ b/public/docs/_examples/heroes-graphql-starter/ts/src/app/hero-search.component.css @@ -0,0 +1,21 @@ +/* #docregion */ +.search-result{ + border-bottom: 1px solid gray; + border-left: 1px solid gray; + border-right: 1px solid gray; + width:195px; + height: 16px; + padding: 5px; + background-color: white; + cursor: pointer; +} + +.search-result:hover { + color: #eee; + background-color: #607D8B; +} + +#search-box{ + width: 200px; + height: 20px; +} diff --git a/public/docs/_examples/heroes-graphql-starter/ts/src/app/hero-search.component.html b/public/docs/_examples/heroes-graphql-starter/ts/src/app/hero-search.component.html new file mode 100644 index 0000000000..08c0560c5b --- /dev/null +++ b/public/docs/_examples/heroes-graphql-starter/ts/src/app/hero-search.component.html @@ -0,0 +1,11 @@ + +
    +

    Hero Search

    + +
    +
    + {{hero.name}} +
    +
    +
    diff --git a/public/docs/_examples/heroes-graphql-starter/ts/src/app/hero-search.component.ts b/public/docs/_examples/heroes-graphql-starter/ts/src/app/hero-search.component.ts new file mode 100644 index 0000000000..8b2d32f06b --- /dev/null +++ b/public/docs/_examples/heroes-graphql-starter/ts/src/app/hero-search.component.ts @@ -0,0 +1,69 @@ +// #docplaster +// #docregion +import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; + +// #docregion rxjs-imports +import { Observable } from 'rxjs/Observable'; +import { Subject } from 'rxjs/Subject'; + +// Observable class extensions +import 'rxjs/add/observable/of'; + +// Observable operators +import 'rxjs/add/operator/catch'; +import 'rxjs/add/operator/debounceTime'; +import 'rxjs/add/operator/distinctUntilChanged'; +// #enddocregion rxjs-imports + +import { HeroSearchService } from './hero-search.service'; +import { Hero } from './hero'; + +@Component({ + selector: 'hero-search', + templateUrl: './hero-search.component.html', + styleUrls: [ './hero-search.component.css' ], + providers: [HeroSearchService] +}) +export class HeroSearchComponent implements OnInit { + // #docregion search + heroes: Observable; + // #enddocregion search + // #docregion searchTerms + private searchTerms = new Subject(); + // #enddocregion searchTerms + + constructor( + private heroSearchService: HeroSearchService, + private router: Router) {} + // #docregion searchTerms + + // Push a search term into the observable stream. + search(term: string): void { + this.searchTerms.next(term); + } + // #enddocregion searchTerms + // #docregion search + + ngOnInit(): void { + this.heroes = this.searchTerms + .debounceTime(300) // wait 300ms after each keystroke before considering the term + .distinctUntilChanged() // ignore if next search term is same as previous + .switchMap(term => term // switch to new observable each time the term changes + // return the http search observable + ? this.heroSearchService.search(term) + // or the observable of empty heroes if there was no search term + : Observable.of([])) + .catch(error => { + // TODO: add real error handling + console.log(error); + return Observable.of([]); + }); + } + // #enddocregion search + + gotoDetail(hero: Hero): void { + let link = ['/detail', hero.id]; + this.router.navigate(link); + } +} diff --git a/public/docs/_examples/heroes-graphql-starter/ts/src/app/hero-search.service.ts b/public/docs/_examples/heroes-graphql-starter/ts/src/app/hero-search.service.ts new file mode 100644 index 0000000000..d24e0fba41 --- /dev/null +++ b/public/docs/_examples/heroes-graphql-starter/ts/src/app/hero-search.service.ts @@ -0,0 +1,20 @@ +// #docregion +import { Injectable } from '@angular/core'; +import { Http } from '@angular/http'; + +import { Observable } from 'rxjs/Observable'; +import 'rxjs/add/operator/map'; + +import { Hero } from './hero'; + +@Injectable() +export class HeroSearchService { + + constructor(private http: Http) {} + + search(term: string): Observable { + return this.http + .get(`app/heroes/?name=${term}`) + .map(response => response.json().data as Hero[]); + } +} diff --git a/public/docs/_examples/heroes-graphql-starter/ts/src/app/hero.service.ts b/public/docs/_examples/heroes-graphql-starter/ts/src/app/hero.service.ts new file mode 100644 index 0000000000..18af476123 --- /dev/null +++ b/public/docs/_examples/heroes-graphql-starter/ts/src/app/hero.service.ts @@ -0,0 +1,87 @@ +// #docplaster +// #docregion , imports +import { Injectable } from '@angular/core'; +import { Headers, Http } from '@angular/http'; + +// #docregion rxjs +import 'rxjs/add/operator/toPromise'; +// #enddocregion rxjs + +import { Hero } from './hero'; +// #enddocregion imports + +@Injectable() +export class HeroService { + + // #docregion update + private headers = new Headers({'Content-Type': 'application/json'}); + // #enddocregion update + // #docregion getHeroes + private heroesUrl = 'api/heroes'; // URL to web api + + constructor(private http: Http) { } + + getHeroes(): Promise { + return this.http.get(this.heroesUrl) + // #docregion to-promise + .toPromise() + // #enddocregion to-promise + // #docregion to-data + .then(response => response.json().data as Hero[]) + // #enddocregion to-data + // #docregion catch + .catch(this.handleError); + // #enddocregion catch + } + + // #enddocregion getHeroes + + // #docregion getHero + getHero(id: number): Promise { + const url = `${this.heroesUrl}/${id}`; + return this.http.get(url) + .toPromise() + .then(response => response.json().data as Hero) + .catch(this.handleError); + } + // #enddocregion getHero + + // #docregion delete + delete(id: number): Promise { + const url = `${this.heroesUrl}/${id}`; + return this.http.delete(url, {headers: this.headers}) + .toPromise() + .then(() => null) + .catch(this.handleError); + } + // #enddocregion delete + + // #docregion create + create(name: string): Promise { + return this.http + .post(this.heroesUrl, JSON.stringify({name: name}), {headers: this.headers}) + .toPromise() + .then(res => res.json().data) + .catch(this.handleError); + } + // #enddocregion create + // #docregion update + + update(hero: Hero): Promise { + const url = `${this.heroesUrl}/${hero.id}`; + return this.http + .put(url, JSON.stringify(hero), {headers: this.headers}) + .toPromise() + .then(() => hero) + .catch(this.handleError); + } + // #enddocregion update + + // #docregion getHeroes, handleError + private handleError(error: any): Promise { + console.error('An error occurred', error); // for demo purposes only + return Promise.reject(error.message || error); + } + // #enddocregion getHeroes, handleError +} + diff --git a/public/docs/_examples/heroes-graphql-starter/ts/src/app/hero.ts b/public/docs/_examples/heroes-graphql-starter/ts/src/app/hero.ts new file mode 100644 index 0000000000..e3eac516da --- /dev/null +++ b/public/docs/_examples/heroes-graphql-starter/ts/src/app/hero.ts @@ -0,0 +1,4 @@ +export class Hero { + id: number; + name: string; +} diff --git a/public/docs/_examples/heroes-graphql-starter/ts/src/app/heroes.component.1.ts b/public/docs/_examples/heroes-graphql-starter/ts/src/app/heroes.component.1.ts new file mode 100644 index 0000000000..1ce69b7f4c --- /dev/null +++ b/public/docs/_examples/heroes-graphql-starter/ts/src/app/heroes.component.1.ts @@ -0,0 +1,48 @@ +// #docregion +import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; + +import { Hero } from './hero'; + +@Component({ + selector: 'my-heroes', + templateUrl: './heroes.component.html', + styleUrls: [ './heroes.component.css' ] +}) +export class HeroesComponent implements OnInit { + heroes: Hero[]; + selectedHero: Hero; + + constructor( + private router: Router) { } + + getHeroes(): void { + + } + + // #docregion add + add(name: string): void { + name = name.trim(); + if (!name) { return; } + + } + // #enddocregion add + + // #docregion delete + delete(hero: Hero): void { + + } + // #enddocregion delete + + ngOnInit(): void { + this.getHeroes(); + } + + onSelect(hero: Hero): void { + this.selectedHero = hero; + } + + gotoDetail(): void { + this.router.navigate(['/detail', this.selectedHero.id]); + } +} diff --git a/public/docs/_examples/heroes-graphql-starter/ts/src/app/heroes.component.css b/public/docs/_examples/heroes-graphql-starter/ts/src/app/heroes.component.css new file mode 100644 index 0000000000..d2c958a911 --- /dev/null +++ b/public/docs/_examples/heroes-graphql-starter/ts/src/app/heroes.component.css @@ -0,0 +1,68 @@ +/* #docregion */ +.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; +} +/* #docregion additions */ +button.delete { + float:right; + margin-top: 2px; + margin-right: .8em; + background-color: gray !important; + color:white; +} diff --git a/public/docs/_examples/heroes-graphql-starter/ts/src/app/heroes.component.html b/public/docs/_examples/heroes-graphql-starter/ts/src/app/heroes.component.html new file mode 100644 index 0000000000..392d241d52 --- /dev/null +++ b/public/docs/_examples/heroes-graphql-starter/ts/src/app/heroes.component.html @@ -0,0 +1,29 @@ + +

    My Heroes

    + +
    + + +
    + +
      + +
    • + {{hero.id}} + {{hero.name}} + + + +
    • + +
    +
    +

    + {{selectedHero.name | uppercase}} is my hero +

    + +
    diff --git a/public/docs/_examples/heroes-graphql-starter/ts/src/app/heroes.component.ts b/public/docs/_examples/heroes-graphql-starter/ts/src/app/heroes.component.ts new file mode 100644 index 0000000000..6350b803c4 --- /dev/null +++ b/public/docs/_examples/heroes-graphql-starter/ts/src/app/heroes.component.ts @@ -0,0 +1,61 @@ +// #docregion +import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; + +import { Hero } from './hero'; +import { HeroService } from './hero.service'; + +@Component({ + selector: 'my-heroes', + templateUrl: './heroes.component.html', + styleUrls: [ './heroes.component.css' ] +}) +export class HeroesComponent implements OnInit { + heroes: Hero[]; + selectedHero: Hero; + + constructor( + private heroService: HeroService, + private router: Router) { } + + getHeroes(): void { + this.heroService + .getHeroes() + .then(heroes => this.heroes = heroes); + } + + // #docregion add + add(name: string): void { + name = name.trim(); + if (!name) { return; } + this.heroService.create(name) + .then(hero => { + this.heroes.push(hero); + this.selectedHero = null; + }); + } + // #enddocregion add + + // #docregion delete + delete(hero: Hero): void { + this.heroService + .delete(hero.id) + .then(() => { + this.heroes = this.heroes.filter(h => h !== hero); + if (this.selectedHero === hero) { this.selectedHero = null; } + }); + } + // #enddocregion delete + + ngOnInit(): void { + this.getHeroes(); + } + + onSelect(hero: Hero): void { + this.selectedHero = hero; + } + + gotoDetail(): void { + this.router.navigate(['/detail', this.selectedHero.id]); + } +} diff --git a/public/docs/_examples/heroes-graphql-starter/ts/src/app/in-memory-data.service.ts b/public/docs/_examples/heroes-graphql-starter/ts/src/app/in-memory-data.service.ts new file mode 100644 index 0000000000..c915955e22 --- /dev/null +++ b/public/docs/_examples/heroes-graphql-starter/ts/src/app/in-memory-data.service.ts @@ -0,0 +1,19 @@ +// #docregion , init +import { InMemoryDbService } from 'angular-in-memory-web-api'; +export class InMemoryDataService implements InMemoryDbService { + createDb() { + let heroes = [ + {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: 'Dynama'}, + {id: 18, name: 'Dr IQ'}, + {id: 19, name: 'Magma'}, + {id: 20, name: 'Tornado'} + ]; + return {heroes}; + } +} diff --git a/public/docs/_examples/heroes-graphql-starter/ts/src/app/in-memory-graphql.ts b/public/docs/_examples/heroes-graphql-starter/ts/src/app/in-memory-graphql.ts new file mode 100755 index 0000000000..4a8156fc42 --- /dev/null +++ b/public/docs/_examples/heroes-graphql-starter/ts/src/app/in-memory-graphql.ts @@ -0,0 +1,103 @@ +// #docregion +// #docregion import-lodash +import { find as lodashFind } from 'lodash'; +// #enddocregion import-lodash +// #docregion import-graphql-tools +import { makeExecutableSchema } from 'graphql-tools'; +// #enddocregion import-graphql-tools +// #docregion import-graphql +import { execute } from 'graphql'; +// #enddocregion import-graphql +// #docregion graphql-schema +import { typeDefinitions } from './schema'; +// #enddocregion graphql-schema +// #docregion heroes-array +let heroes = [ + {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: 'Dynama'}, + {id: 18, name: 'Dr IQ'}, + {id: 19, name: 'Magma'}, + {id: 20, name: 'Tornado'} +]; +// #enddocregion heroes-array + +// #docregion resolvers +const resolveFunctions = { + Query: { + heroes(obj: any, args: any) { + if (args.search) { + return heroes.filter(function (currentHero){ + return currentHero.name.toLowerCase().search(args.search.toLowerCase()) !== -1; + }); + } else { + return heroes; + } + }, + hero(obj: any, args: any, context: any) { + return lodashFind(heroes, { id: args.heroId }); + } + }, + Mutation: { + updateHero(root: any, args: any) { + let hero = lodashFind(heroes, { id: args.id }); + if (!hero) { + throw new Error(`Couldn't find post with id ${args.id}`); + } + hero.name = args.name; + return hero; + }, + addHero(root: any, args: any) { + const maxId = Math.max(...heroes.map((hero) => {return hero.id; })); + const newHero = { + name: args.heroName, + id: maxId + 1 + }; + heroes.push(newHero); + return newHero; + }, + deleteHero(root: any, args: any) { + let hero = lodashFind(heroes, { id: args.id }); + if (!hero) { + throw new Error(`Couldn't find post with id ${args.id}`); + } + heroes = heroes.filter(function (currentHero) { return currentHero.id !== args.id; }); + return hero; + }, + } +}; +// #enddocregion resolvers +// #docregion make-executable-schema +const schema = makeExecutableSchema({ + typeDefs: typeDefinitions, + resolvers: resolveFunctions, +}); +// #enddocregion make-executable-schema +// #docregion execute-and-export +class InBrowserNetworkInterface { + schema: any = {}; + constructor(params: any) { + this.schema = params.schema; + } + + query(request: any) { + return execute( + this.schema, + request.query, + {}, + {}, + request.variables, + request.operationName); + } +} + +const networkInterface = new InBrowserNetworkInterface({ schema }); +export { + networkInterface +} +// #enddocregion execute-and-export +// #enddocregion diff --git a/public/docs/_examples/heroes-graphql-starter/ts/src/app/schema.ts b/public/docs/_examples/heroes-graphql-starter/ts/src/app/schema.ts new file mode 100644 index 0000000000..0baf8da308 --- /dev/null +++ b/public/docs/_examples/heroes-graphql-starter/ts/src/app/schema.ts @@ -0,0 +1,38 @@ +// #docregion +export const typeDefinitions = ` +# the model +type Hero { + id: Int! + name: String! +} + +# The schema allows the following queries: +type Query { + heroes(search: String): [Hero] + + hero(heroId: Int!): Hero +} + +# This schema allows the following mutation: +type Mutation { + updateHero ( + id: Int! + name: String! + ): Hero + + addHero ( + heroName: String! + ): Hero + + deleteHero ( + id: Int! + ): Hero +} + +# Tell the server which types represent the root query and root mutation types. +# By convention, they are called RootQuery and RootMutation. +schema { + query: Query + mutation: Mutation +} +`; diff --git a/public/docs/_examples/heroes-graphql-starter/ts/src/index.html b/public/docs/_examples/heroes-graphql-starter/ts/src/index.html new file mode 100644 index 0000000000..144c9e105c --- /dev/null +++ b/public/docs/_examples/heroes-graphql-starter/ts/src/index.html @@ -0,0 +1,27 @@ + + + + + + Angular Tour of Heroes - GraphQL Starter + + + + + + + + + + + + + + + + + Loading... + + diff --git a/public/docs/_examples/heroes-graphql-starter/ts/src/main-aot.ts b/public/docs/_examples/heroes-graphql-starter/ts/src/main-aot.ts new file mode 100644 index 0000000000..bd2ca604a3 --- /dev/null +++ b/public/docs/_examples/heroes-graphql-starter/ts/src/main-aot.ts @@ -0,0 +1,6 @@ +// #docregion +import { platformBrowser } from '@angular/platform-browser'; + +import { AppModuleNgFactory } from '../aot/src/app/app.module.ngfactory'; + +platformBrowser().bootstrapModuleFactory(AppModuleNgFactory); diff --git a/public/docs/_examples/heroes-graphql-starter/ts/src/main.ts b/public/docs/_examples/heroes-graphql-starter/ts/src/main.ts new file mode 100644 index 0000000000..f332d1d245 --- /dev/null +++ b/public/docs/_examples/heroes-graphql-starter/ts/src/main.ts @@ -0,0 +1,6 @@ +// #docregion +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +import { AppModule } from './app/app.module'; + +platformBrowserDynamic().bootstrapModule(AppModule); diff --git a/public/docs/_examples/heroes-graphql-starter/ts/src/systemjs.config.extras.js b/public/docs/_examples/heroes-graphql-starter/ts/src/systemjs.config.extras.js new file mode 100644 index 0000000000..0cb9d58fdb --- /dev/null +++ b/public/docs/_examples/heroes-graphql-starter/ts/src/systemjs.config.extras.js @@ -0,0 +1,76 @@ + +/** App specific SystemJS configuration */ +System.config({ + + map: { + + // #docregion systemjs-apollo-client-map + 'apollo-client': 'npm:apollo-client/apollo.umd.js', + 'apollo-client-rxjs': 'npm:apollo-client-rxjs/build/bundles/apollo-rxjs.umd.js', + 'apollo-angular': 'npm:apollo-angular/build/bundles/apollo.umd.js', + + 'whatwg-fetch': 'npm:whatwg-fetch', + + 'graphql-anywhere': 'npm:graphql-anywhere', + + 'graphql-tag': 'npm:graphql-tag', + 'symbol-observable': 'npm:symbol-observable', + 'redux': 'npm:redux/dist/redux.min.js', + // #enddocregion systemjs-apollo-client-map + + // #docregion systemjs-apollo-test-utils-map + 'apollo-test-utils': 'npm:apollo-test-utils', + + // #docregion systemjs-graphql-server-map + 'graphql': 'npm:graphql', + 'graphql-tools': 'npm:graphql-tools', + 'deprecated-decorator': 'npm:deprecated-decorator', + 'node-uuid': 'npm:node-uuid', + 'uuid': 'npm:uuid', + 'iterall': 'npm:iterall', + 'lodash': 'npm:lodash' + // #enddocregion systemjs-graphql-server-map + // #enddocregion systemjs-apollo-test-utils-map + }, + packages: { + + // #docregion systemjs-apollo-client-packages + 'whatwg-fetch': { main: './fetch.js', defaultExtension: 'js' }, + 'redux': { format: 'cjs', defaultExtension: 'js' }, + 'graphql-tag': { main: './index.js', defaultExtension: 'js' }, + 'symbol-observable': { main: './index.js', defaultExtension: 'js' }, + 'graphql-anywhere': { + main: '/lib/src/index.js', + defaultExtension: 'js' + }, + // #enddocregion systemjs-apollo-client-packages + + // #docregion systemjs-apollo-test-utils-packages + 'apollo-test-utils': { main: '/dist/src/index.js', defaultExtension: 'js' }, + + // #docregion systemjs-graphql-server-packages + 'graphql': { + main: './index.js', + defaultExtension: 'js', + map: { + './type': './type/index.js', + './language': './language/index.js', + './execution': './execution/index.js', + './validation': './validation/index.js', + './error': './error/index.js', + './utilities': './utilities/index.js' + }, + }, + 'graphql-tools': { + main: '/dist/index.js', + defaultExtension: 'js' + }, + 'deprecated-decorator': { main: '/bld/index.js', defaultExtension: 'js' }, + 'node-uuid': { main: './uuid.js', defaultExtension: 'js' }, + 'uuid': { main: './lib/rng-browser.js', defaultExtension: 'js' }, + 'iterall': { main: './index.js', defaultExtension: 'js' }, + 'lodash': { main: './index.js', defaultExtension: 'js' } + // #enddocregion systemjs-graphql-server-packages + // #enddocregion systemjs-apollo-test-utils-packages + } +}); diff --git a/public/docs/_examples/heroes-graphql-starter/ts/tsconfig-aot.json b/public/docs/_examples/heroes-graphql-starter/ts/tsconfig-aot.json new file mode 100644 index 0000000000..fe1e6df520 --- /dev/null +++ b/public/docs/_examples/heroes-graphql-starter/ts/tsconfig-aot.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "es5", + "module": "es2015", + "moduleResolution": "node", + "sourceMap": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "lib": ["es2015", "dom"], + "noImplicitAny": true, + "suppressImplicitAnyIndexErrors": true, + "typeRoots": [ + "../../node_modules/@types/" + ] + }, + + "files": [ + "src/app/app.module.ts", + "src/main-aot.ts" + ], + + "angularCompilerOptions": { + "genDir": "aot", + "skipMetadataEmit" : true + } +} diff --git a/public/docs/_examples/heroes-graphql/e2e-spec.ts b/public/docs/_examples/heroes-graphql/e2e-spec.ts new file mode 100755 index 0000000000..302f396db4 --- /dev/null +++ b/public/docs/_examples/heroes-graphql/e2e-spec.ts @@ -0,0 +1,283 @@ +'use strict'; // necessary for es6 output in node + +import { browser, element, by, ElementFinder, ElementArrayFinder } from 'protractor'; +import { promise } from 'selenium-webdriver'; + +const expectedH1 = 'Tour of Heroes - GraphQL'; +const expectedTitle = `Angular ${expectedH1}`; +const targetHero = { id: 15, name: 'Magneta' }; +const targetHeroDashboardIndex = 3; +const nameSuffix = 'X'; +const newHeroName = targetHero.name + nameSuffix; + +class Hero { + id: number; + name: string; + + // Factory methods + + // Hero from string formatted as ' '. + static fromString(s: string): Hero { + return { + id: +s.substr(0, s.indexOf(' ')), + name: s.substr(s.indexOf(' ') + 1), + }; + } + + // Hero from hero list
  • element. + static async fromLi(li: ElementFinder): Promise { + let strings = await li.all(by.xpath('span')).getText(); + return { id: +strings[0], name: strings[1] }; + } + + // Hero id and name from the given detail element. + static async fromDetail(detail: ElementFinder): Promise { + // Get hero id from the first
    + let _id = await detail.all(by.css('div')).first().getText(); + // Get name from the h2 + let _name = await detail.element(by.css('h2')).getText(); + return { + id: +_id.substr(_id.indexOf(' ') + 1), + name: _name.substr(0, _name.lastIndexOf(' ')) + }; + } +} + +describe('Heroes GraphQL', () => { + + beforeAll(() => browser.get('')); + + function getPageElts() { + let navElts = element.all(by.css('my-app nav a')); + + return { + navElts: navElts, + + myDashboardHref: navElts.get(0), + myDashboard: element(by.css('my-app my-dashboard')), + topHeroes: element.all(by.css('my-app my-dashboard > div h4')), + + myHeroesHref: navElts.get(1), + myHeroes: element(by.css('my-app my-heroes')), + allHeroes: element.all(by.css('my-app my-heroes li')), + selectedHero: element(by.css('my-app li.selected')), + selectedHeroSubview: element(by.css('my-app my-heroes > div:last-child')), + + heroDetail: element(by.css('my-app my-hero-detail > div')), + + searchBox: element(by.css('#search-box')), + searchResults: element.all(by.css('.search-result')) + }; + } + + describe('Initial page', () => { + + it(`has title '${expectedTitle}'`, () => { + expect(browser.getTitle()).toEqual(expectedTitle); + }); + + it(`has h1 '${expectedH1}'`, () => { + expectHeading(1, expectedH1); + }); + + const expectedViewNames = ['Dashboard', 'Heroes']; + it(`has views ${expectedViewNames}`, () => { + let viewNames = getPageElts().navElts.map((el: ElementFinder) => el.getText()); + expect(viewNames).toEqual(expectedViewNames); + }); + + it('has dashboard as the active view', () => { + let page = getPageElts(); + expect(page.myDashboard.isPresent()).toBeTruthy(); + }); + + }); + + describe('Dashboard tests', () => { + + beforeAll(() => browser.get('')); + + it('has top heroes', () => { + let page = getPageElts(); + expect(page.topHeroes.count()).toEqual(4); + }); + + it(`selects and routes to ${targetHero.name} details`, dashboardSelectTargetHero); + + it(`updates hero name (${newHeroName}) in details view`, updateHeroNameInDetailView); + + it(`cancels and shows ${targetHero.name} in Dashboard`, () => { + element(by.buttonText('Back')).click(); + browser.waitForAngular(); // seems necessary to gets tests to past for toh-6 + + let targetHeroElt = getPageElts().topHeroes.get(targetHeroDashboardIndex); + expect(targetHeroElt.getText()).toEqual(targetHero.name); + }); + + it(`selects and routes to ${targetHero.name} details`, dashboardSelectTargetHero); + + it(`updates hero name (${newHeroName}) in details view`, updateHeroNameInDetailView); + + it(`saves and shows ${newHeroName} in Dashboard`, () => { + element(by.buttonText('Save')).click(); + browser.waitForAngular(); // seems necessary to gets tests to past for toh-6 + + let targetHeroElt = getPageElts().topHeroes.get(targetHeroDashboardIndex); + expect(targetHeroElt.getText()).toEqual(newHeroName); + }); + + }); + + describe('Heroes tests', () => { + + beforeAll(() => browser.get('')); + + it('can switch to Heroes view', () => { + getPageElts().myHeroesHref.click(); + let page = getPageElts(); + expect(page.myHeroes.isPresent()).toBeTruthy(); + expect(page.allHeroes.count()).toEqual(10, 'number of heroes'); + }); + + it(`selects and shows ${targetHero.name} as selected in list`, () => { + getHeroLiEltById(targetHero.id).click(); + expect(Hero.fromLi(getPageElts().selectedHero)).toEqual(targetHero); + }); + + it('shows selected hero subview', () => { + let page = getPageElts(); + let title = page.selectedHeroSubview.element(by.css('h2')).getText(); + let expectedTitle = `${targetHero.name.toUpperCase()} is my hero`; + expect(title).toEqual(expectedTitle); + }); + + it('can route to hero details', () => { + element(by.buttonText('View Details')).click(); + + let page = getPageElts(); + expect(page.heroDetail.isPresent()).toBeTruthy('shows hero detail'); + let hero = Hero.fromDetail(page.heroDetail); + expect(hero).toEqual(targetHero); + }); + + it(`updates hero name (${newHeroName}) in details view`, updateHeroNameInDetailView); + + it(`shows ${newHeroName} in Heroes list`, () => { + element(by.buttonText('Save')).click(); + browser.waitForAngular(); // seems necessary to gets tests to past for toh-6 + let expectedHero = {id: targetHero.id, name: newHeroName}; + expect(Hero.fromLi(getHeroLiEltById(targetHero.id))).toEqual(expectedHero); + }); + + it(`deletes ${newHeroName} from Heroes list`, async () => { + const heroesBefore = await toHeroArray(getPageElts().allHeroes); + const li = getHeroLiEltById(targetHero.id); + li.element(by.buttonText('x')).click(); + + const page = getPageElts(); + expect(page.myHeroes.isPresent()).toBeTruthy(); + expect(page.allHeroes.count()).toEqual(9, 'number of heroes'); + const heroesAfter = await toHeroArray(page.allHeroes); + const expectedHeroes = heroesBefore.filter(h => h.name !== newHeroName); + expect(heroesAfter).toEqual(expectedHeroes); + expect(page.selectedHeroSubview.isPresent()).toBeFalsy(); + }); + + it(`adds back ${targetHero.name}`, async () => { + const newHeroName = 'Alice'; + const heroesBefore = await toHeroArray(getPageElts().allHeroes); + const numHeroes = heroesBefore.length; + + element(by.css('input')).sendKeys(newHeroName); + element(by.buttonText('Add')).click(); + + let page = getPageElts(); + let heroesAfter = await toHeroArray(page.allHeroes); + expect(heroesAfter.length).toEqual(numHeroes + 1, 'number of heroes'); + + expect(heroesAfter.slice(0, numHeroes)).toEqual(heroesBefore, 'Old heroes are still there'); + + const maxId = heroesBefore[heroesBefore.length - 1].id; + expect(heroesAfter[numHeroes]).toEqual({id: maxId + 1, name: newHeroName}); + }); + }); + + describe('Progressive hero search', () => { + + beforeAll(() => browser.get('')); + + it(`searches for 'Ma'`, async () => { + getPageElts().searchBox.sendKeys('Ma'); + browser.sleep(1000); + expect(getPageElts().searchResults.count()).toBe(4); + }); + + it(`continues search with 'g'`, async () => { + getPageElts().searchBox.sendKeys('g'); + browser.sleep(1000); + expect(getPageElts().searchResults.count()).toBe(2); + }); + + it(`continues search with 'n' and gets ${targetHero.name}`, async () => { + getPageElts().searchBox.sendKeys('n'); + browser.sleep(1000); + let page = getPageElts(); + expect(page.searchResults.count()).toBe(1); + let hero = page.searchResults.get(0); + expect(hero.getText()).toEqual(targetHero.name); + }); + + it(`navigates to ${targetHero.name} details view`, async () => { + let hero = getPageElts().searchResults.get(0); + expect(hero.getText()).toEqual(targetHero.name); + hero.click(); + + let page = getPageElts(); + expect(page.heroDetail.isPresent()).toBeTruthy('shows hero detail'); + expect(Hero.fromDetail(page.heroDetail)).toEqual(targetHero); + }); + }); + + function dashboardSelectTargetHero() { + let targetHeroElt = getPageElts().topHeroes.get(targetHeroDashboardIndex); + expect(targetHeroElt.getText()).toEqual(targetHero.name); + targetHeroElt.click(); + browser.waitForAngular(); // seems necessary to gets tests to past for toh-6 + + let page = getPageElts(); + expect(page.heroDetail.isPresent()).toBeTruthy('shows hero detail'); + let hero = Hero.fromDetail(page.heroDetail); + expect(hero).toEqual(targetHero); + } + + async function updateHeroNameInDetailView() { + // Assumes that the current view is the hero details view. + addToHeroName(nameSuffix); + + let hero = await Hero.fromDetail(getPageElts().heroDetail); + expect(hero).toEqual({id: targetHero.id, name: newHeroName}); + } + +}); + +function addToHeroName(text: string): promise.Promise { + let input = element(by.css('input')); + return input.sendKeys(text); +} + +function expectHeading(hLevel: number, expectedText: string): void { + let hTag = `h${hLevel}`; + let hText = element(by.css(hTag)).getText(); + expect(hText).toEqual(expectedText, hTag); +}; + +function getHeroLiEltById(id: number): ElementFinder { + let spanForId = element(by.cssContainingText('li span.badge', id.toString())); + return spanForId.element(by.xpath('..')); +} + +async function toHeroArray(allHeroes: ElementArrayFinder): Promise { + let promisedHeroes = await allHeroes.map(Hero.fromLi); + // The cast is necessary to get around issuing with the signature of Promise.all() + return > Promise.all(promisedHeroes); +} diff --git a/public/docs/_examples/heroes-graphql/ts/.gitignore b/public/docs/_examples/heroes-graphql/ts/.gitignore new file mode 100755 index 0000000000..c66c617d1f --- /dev/null +++ b/public/docs/_examples/heroes-graphql/ts/.gitignore @@ -0,0 +1,10 @@ +aot/**/*.ts +**/*.ngfactory.ts +**/*.ngsummary.json +**/*.metadata.json +**/*.js +dist +!app/tsconfig.json +!rollup-config.js +!copy-dist-files.js +!systemjs.config.extras.js diff --git a/public/docs/_examples/heroes-graphql/ts/aot/index.html b/public/docs/_examples/heroes-graphql/ts/aot/index.html new file mode 100755 index 0000000000..6aea787999 --- /dev/null +++ b/public/docs/_examples/heroes-graphql/ts/aot/index.html @@ -0,0 +1,19 @@ + + + + + + Angular Tour of Heroes - GraphQL + + + + + + + + + + Loading... + + + \ No newline at end of file diff --git a/public/docs/_examples/heroes-graphql/ts/aot/styles.css b/public/docs/_examples/heroes-graphql/ts/aot/styles.css new file mode 100755 index 0000000000..d81835d0cd --- /dev/null +++ b/public/docs/_examples/heroes-graphql/ts/aot/styles.css @@ -0,0 +1,116 @@ +/* #docregion , quickstart, toh */ +/* Master Styles */ +h1 { + color: #369; + font-family: Arial, Helvetica, sans-serif; + font-size: 250%; +} +h2, h3 { + color: #444; + font-family: Arial, Helvetica, sans-serif; + font-weight: lighter; +} +body { + margin: 2em; +} +/* #enddocregion quickstart */ +body, input[text], button { + color: #888; + font-family: Cambria, Georgia; +} +/* #enddocregion toh */ +a { + cursor: pointer; + cursor: hand; +} +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:disabled { + background-color: #eee; + color: #aaa; + cursor: auto; +} + +/* Navigation link styles */ +nav a { + padding: 5px 10px; + text-decoration: none; + margin-right: 10px; + margin-top: 10px; + display: inline-block; + background-color: #eee; + border-radius: 4px; +} +nav a:visited, a:link { + color: #607D8B; +} +nav a:hover { + color: #039be5; + background-color: #CFD8DC; +} +nav a.active { + color: #039be5; +} + +/* items class */ +.items { + margin: 0 0 2em 0; + list-style-type: none; + padding: 0; + width: 24em; +} +.items li { + cursor: pointer; + position: relative; + left: 0; + background-color: #EEE; + margin: .5em; + padding: .3em 0; + height: 1.6em; + border-radius: 4px; +} +.items li:hover { + color: #607D8B; + background-color: #DDD; + left: .1em; +} +.items li.selected { + background-color: #CFD8DC; + color: white; +} +.items li.selected:hover { + background-color: #BBD8DC; +} +.items .text { + position: relative; + top: -3px; +} +.items .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; +} +/* #docregion toh */ +/* everywhere else */ +* { + font-family: Arial, Helvetica, sans-serif; +} diff --git a/public/docs/_examples/heroes-graphql/ts/bs-config.aot.json b/public/docs/_examples/heroes-graphql/ts/bs-config.aot.json new file mode 100755 index 0000000000..b219464d0e --- /dev/null +++ b/public/docs/_examples/heroes-graphql/ts/bs-config.aot.json @@ -0,0 +1,14 @@ +{ + "open": false, + "logLevel": "silent", + "port": 8080, + "server": { + "baseDir": "aot", + "routes": { + "/node_modules": "node_modules" + }, + "middleware": { + "0": null + } + } +} \ No newline at end of file diff --git a/public/docs/_examples/heroes-graphql/ts/copy-dist-files.js b/public/docs/_examples/heroes-graphql/ts/copy-dist-files.js new file mode 100755 index 0000000000..a451cbd633 --- /dev/null +++ b/public/docs/_examples/heroes-graphql/ts/copy-dist-files.js @@ -0,0 +1,12 @@ +// #docregion +var fs = require('fs'); +var resources = [ + 'node_modules/core-js/client/shim.min.js', + 'node_modules/zone.js/dist/zone.min.js', + 'src/styles.css' +]; +resources.map(function(f) { + var path = f.split('/'); + var t = 'aot/' + path[path.length-1]; + fs.createReadStream(f).pipe(fs.createWriteStream(t)); +}); \ No newline at end of file diff --git a/public/docs/_examples/heroes-graphql/ts/example-config.json b/public/docs/_examples/heroes-graphql/ts/example-config.json new file mode 100755 index 0000000000..e69de29bb2 diff --git a/public/docs/_examples/heroes-graphql/ts/plnkr.json b/public/docs/_examples/heroes-graphql/ts/plnkr.json new file mode 100755 index 0000000000..fe0f72aa24 --- /dev/null +++ b/public/docs/_examples/heroes-graphql/ts/plnkr.json @@ -0,0 +1,11 @@ +{ + "description": "Tour of Heroes: GraphQL", + "basePath": "src/", + "files":[ + "!**/*.d.ts", + "!**/*.js", + "!**/*.[1,2].*", + "systemjs.config.extras.js" + ], + "tags": ["tutorial", "tour", "heroes", "http", "GraphQL"] +} \ No newline at end of file diff --git a/public/docs/_examples/heroes-graphql/ts/rollup-config.js b/public/docs/_examples/heroes-graphql/ts/rollup-config.js new file mode 100755 index 0000000000..3591eb4e2a --- /dev/null +++ b/public/docs/_examples/heroes-graphql/ts/rollup-config.js @@ -0,0 +1,53 @@ +// #docregion +import rollup from 'rollup' +import nodeResolve from 'rollup-plugin-node-resolve' +import commonjs from 'rollup-plugin-commonjs'; +import uglify from 'rollup-plugin-uglify' + +import builtins from 'rollup-plugin-node-builtins'; +import globals from 'rollup-plugin-node-globals'; +//paths are relative to the execution path +export default { + entry: 'src/main-aot.js', + dest: 'aot/dist/build.js', // output a single application bundle + sourceMap: true, + sourceMapFile: 'aot/dist/build.js.map', + format: 'iife', + onwarn: function(warning) { + // Skip certain warnings + + // should intercept ... but doesn't in some rollup versions + if ( warning.code === 'THIS_IS_UNDEFINED' ) { return; } + // intercepts in some rollup versions + if ( warning.indexOf("The 'this' keyword is equivalent to 'undefined'") > -1 ) { return; } + + // console.warn everything else + console.warn( warning.message ); + }, + plugins: [ + nodeResolve({jsnext: true, module: true, browser: true}), + commonjs({ + include: [ + 'node_modules/rxjs/**', + 'node_modules/graphql-tag/**', + 'node_modules/apollo-client/**', + 'node_modules/lodash/**', + 'node_modules/graphql-tools/dist/**', + 'node_modules/graphql/**', + 'node_modules/graphql-anywhere/**', + 'node_modules/iterall/**', + 'node_modules/deprecated-decorator/**', + 'node_modules/uuid/**' + ], + namedExports: { + 'node_modules/graphql-tools/dist/index.js': ['makeExecutableSchema' ], + 'node_modules/graphql/index.js': ['execute' ], + 'node_modules/graphql-tag/bundledPrinter.js': ['print'], + 'node_modules/lodash/lodash.js': ['find', 'omit', 'assign', 'isFunction'], + } + }), + globals(), + builtins(), + uglify() + ] +} \ No newline at end of file diff --git a/public/docs/_examples/heroes-graphql/ts/src/app/app-routing.module.ts b/public/docs/_examples/heroes-graphql/ts/src/app/app-routing.module.ts new file mode 100755 index 0000000000..dfd957782b --- /dev/null +++ b/public/docs/_examples/heroes-graphql/ts/src/app/app-routing.module.ts @@ -0,0 +1,20 @@ +// #docregion +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +import { DashboardComponent } from './dashboard.component'; +import { HeroesComponent } from './heroes.component'; +import { HeroDetailComponent } from './hero-detail.component'; + +const routes: Routes = [ + { path: '', redirectTo: '/dashboard', pathMatch: 'full' }, + { path: 'dashboard', component: DashboardComponent }, + { path: 'detail/:id', component: HeroDetailComponent }, + { path: 'heroes', component: HeroesComponent } +]; + +@NgModule({ + imports: [ RouterModule.forRoot(routes) ], + exports: [ RouterModule ] +}) +export class AppRoutingModule {} diff --git a/public/docs/_examples/heroes-graphql/ts/src/app/app.component.css b/public/docs/_examples/heroes-graphql/ts/src/app/app.component.css new file mode 100755 index 0000000000..071e665767 --- /dev/null +++ b/public/docs/_examples/heroes-graphql/ts/src/app/app.component.css @@ -0,0 +1,29 @@ +/* #docregion */ +h1 { + font-size: 1.2em; + color: #999; + margin-bottom: 0; +} +h2 { + font-size: 2em; + margin-top: 0; + padding-top: 0; +} +nav a { + padding: 5px 10px; + text-decoration: none; + margin-top: 10px; + display: inline-block; + background-color: #eee; + border-radius: 4px; +} +nav a:visited, a:link { + color: #607D8B; +} +nav a:hover { + color: #039be5; + background-color: #CFD8DC; +} +nav a.active { + color: #039be5; +} diff --git a/public/docs/_examples/heroes-graphql/ts/src/app/app.component.ts b/public/docs/_examples/heroes-graphql/ts/src/app/app.component.ts new file mode 100755 index 0000000000..18a45c6dad --- /dev/null +++ b/public/docs/_examples/heroes-graphql/ts/src/app/app.component.ts @@ -0,0 +1,19 @@ +// #docplaster +// #docregion +import { Component } from '@angular/core'; + +@Component({ + selector: 'my-app', + template: ` +

    {{title}}

    + + + `, + styleUrls: ['./app.component.css'] +}) +export class AppComponent { + title = 'Tour of Heroes - GraphQL'; +} diff --git a/public/docs/_examples/heroes-graphql/ts/src/app/app.module.1.ts b/public/docs/_examples/heroes-graphql/ts/src/app/app.module.1.ts new file mode 100644 index 0000000000..68cd5a9284 --- /dev/null +++ b/public/docs/_examples/heroes-graphql/ts/src/app/app.module.1.ts @@ -0,0 +1,56 @@ +// #docplaster +// #docregion + +// #docregion v1, v2 +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { FormsModule } from '@angular/forms'; +import { HttpModule } from '@angular/http'; + +import { AppRoutingModule } from './app-routing.module'; + +// #enddocregion v1 +import { InMemoryWebApiModule } from 'angular-in-memory-web-api'; +import { InMemoryDataService } from './in-memory-data.service'; + +// #docregion import-apollo +import { ApolloModule } from 'apollo-angular'; +import { getClient } from './client'; +// #enddocregion import-apollo + +// #docregion v1 +import { AppComponent } from './app.component'; +import { DashboardComponent } from './dashboard.component'; +import { HeroesComponent } from './heroes.component'; +import { HeroDetailComponent } from './hero-detail.component'; +// #enddocregion v1, v2 +import { HeroSearchComponent } from './hero-search.component'; +// #docregion v1, v2 + +// #docregion apollo-ngmodule +@NgModule({ + imports: [ + BrowserModule, + FormsModule, + HttpModule, + InMemoryWebApiModule.forRoot(InMemoryDataService), + // #enddocregion v1 + // #docregion v1 + AppRoutingModule, + ApolloModule.forRoot(getClient) + ], + // #docregion search + declarations: [ +// #enddocregion apollo-ngmodule + AppComponent, + DashboardComponent, + HeroDetailComponent, + HeroesComponent, + // #enddocregion v1, v2 + HeroSearchComponent + // #docregion v1, v2 + ], + // #enddocregion search + bootstrap: [ AppComponent ] +}) +export class AppModule { } diff --git a/public/docs/_examples/heroes-graphql/ts/src/app/app.module.ts b/public/docs/_examples/heroes-graphql/ts/src/app/app.module.ts new file mode 100755 index 0000000000..a783800749 --- /dev/null +++ b/public/docs/_examples/heroes-graphql/ts/src/app/app.module.ts @@ -0,0 +1,53 @@ +// #docplaster +// #docregion + +// #docregion v1, v2 +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { FormsModule } from '@angular/forms'; +import { HttpModule } from '@angular/http'; + +import { AppRoutingModule } from './app-routing.module'; + +// #enddocregion v1 + +// #docregion import-apollo +import { ApolloModule } from 'apollo-angular'; +import { getClient } from './client'; +// #enddocregion import-apollo + +// #docregion v1 +import { AppComponent } from './app.component'; +import { DashboardComponent } from './dashboard.component'; +import { HeroesComponent } from './heroes.component'; +import { HeroDetailComponent } from './hero-detail.component'; +// #enddocregion v1, v2 +import { HeroSearchComponent } from './hero-search.component'; +// #docregion v1, v2 + +// #docregion apollo-ngmodule +@NgModule({ + imports: [ + BrowserModule, + FormsModule, + HttpModule, + // #enddocregion v1 + // #docregion v1 + AppRoutingModule, + ApolloModule.forRoot(getClient) + ], + // #docregion search + declarations: [ +// #enddocregion apollo-ngmodule + AppComponent, + DashboardComponent, + HeroDetailComponent, + HeroesComponent, + // #enddocregion v1, v2 + HeroSearchComponent + // #docregion v1, v2 + ], + // #enddocregion search + bootstrap: [ AppComponent ] +}) +export class AppModule { } diff --git a/public/docs/_examples/heroes-graphql/ts/src/app/client.1.ts b/public/docs/_examples/heroes-graphql/ts/src/app/client.1.ts new file mode 100755 index 0000000000..388789b10a --- /dev/null +++ b/public/docs/_examples/heroes-graphql/ts/src/app/client.1.ts @@ -0,0 +1,8 @@ +// #docregion , default-initialization +import ApolloClient from 'apollo-client'; + +const client = new ApolloClient(); +export function getClient(): ApolloClient { + return client; +} +// #enddocregion default-initialization diff --git a/public/docs/_examples/heroes-graphql/ts/src/app/client.2.ts b/public/docs/_examples/heroes-graphql/ts/src/app/client.2.ts new file mode 100755 index 0000000000..7c0c442e3f --- /dev/null +++ b/public/docs/_examples/heroes-graphql/ts/src/app/client.2.ts @@ -0,0 +1,13 @@ +// #docregion , network-initialization +import ApolloClient, { createNetworkInterface } from 'apollo-client'; + +const client = new ApolloClient({ + networkInterface: createNetworkInterface({ + uri: 'http://my-api.graphql.com' + }) +}); + +export function getClient(): ApolloClient { + return client; +} +// #enddocregion network-initialization diff --git a/public/docs/_examples/heroes-graphql/ts/src/app/client.3.ts b/public/docs/_examples/heroes-graphql/ts/src/app/client.3.ts new file mode 100644 index 0000000000..b39e743367 --- /dev/null +++ b/public/docs/_examples/heroes-graphql/ts/src/app/client.3.ts @@ -0,0 +1,13 @@ +// #docregion +import { ApolloClient } from 'apollo-client'; +// #docregion import-and-use +import { mockNetworkInterface } from './mockedNetworkInterface'; + +const client = new ApolloClient({ + networkInterface: mockNetworkInterface +}); +// #enddocregion import-and-use +export function getClient(): ApolloClient { + return client; +} +// #enddocregion diff --git a/public/docs/_examples/heroes-graphql/ts/src/app/client.ts b/public/docs/_examples/heroes-graphql/ts/src/app/client.ts new file mode 100755 index 0000000000..555a090f9b --- /dev/null +++ b/public/docs/_examples/heroes-graphql/ts/src/app/client.ts @@ -0,0 +1,12 @@ +// #docregion +import { ApolloClient } from 'apollo-client'; +import { networkInterface } from './in-memory-graphql'; + +const client = new ApolloClient({ + networkInterface, + dataIdFromObject: (object: any) => object.id, +}); +export function getClient(): ApolloClient { + return client; +} +// #enddocregion diff --git a/public/docs/_examples/heroes-graphql/ts/src/app/dashboard.component.css b/public/docs/_examples/heroes-graphql/ts/src/app/dashboard.component.css new file mode 100755 index 0000000000..dc7fb7ce06 --- /dev/null +++ b/public/docs/_examples/heroes-graphql/ts/src/app/dashboard.component.css @@ -0,0 +1,62 @@ +/* #docregion */ +[class*='col-'] { + float: left; + padding-right: 20px; + padding-bottom: 20px; +} +[class*='col-']:last-of-type { + padding-right: 0; +} +a { + text-decoration: none; +} +*, *:after, *:before { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +h3 { + text-align: center; margin-bottom: 0; +} +h4 { + position: relative; +} +.grid { + margin: 0; +} +.col-1-4 { + width: 25%; +} +.module { + padding: 20px; + text-align: center; + color: #eee; + max-height: 120px; + min-width: 120px; + background-color: #607D8B; + border-radius: 2px; +} +.module:hover { + background-color: #EEE; + cursor: pointer; + color: #607d8b; +} +.grid-pad { + padding: 10px 0; +} +.grid-pad > [class*='col-']:last-of-type { + padding-right: 20px; +} +@media (max-width: 600px) { + .module { + font-size: 10px; + max-height: 75px; } +} +@media (max-width: 1024px) { + .grid { + margin: 0; + } + .module { + min-width: 60px; + } +} diff --git a/public/docs/_examples/heroes-graphql/ts/src/app/dashboard.component.html b/public/docs/_examples/heroes-graphql/ts/src/app/dashboard.component.html new file mode 100755 index 0000000000..04962576c1 --- /dev/null +++ b/public/docs/_examples/heroes-graphql/ts/src/app/dashboard.component.html @@ -0,0 +1,12 @@ + +

    Top Heroes

    + + diff --git a/public/docs/_examples/heroes-graphql/ts/src/app/dashboard.component.ts b/public/docs/_examples/heroes-graphql/ts/src/app/dashboard.component.ts new file mode 100755 index 0000000000..6732904c29 --- /dev/null +++ b/public/docs/_examples/heroes-graphql/ts/src/app/dashboard.component.ts @@ -0,0 +1,44 @@ +// #docregion , search +import { Component, OnInit } from '@angular/core'; + +// #docregion import-apollo +import { Apollo } from 'apollo-angular'; +// #enddocregion import-apollo +// #docregion import-graphql-tag +import gql from 'graphql-tag'; +// #enddocregion import-graphql-tag +import { ApolloQueryResult } from 'apollo-client'; +import { Hero } from './hero'; + +@Component({ + selector: 'my-dashboard', + templateUrl: './dashboard.component.html', + styleUrls: [ './dashboard.component.css' ] +}) +// #enddocregion search +export class DashboardComponent implements OnInit { + // #docregion this-heroes + heroes: Hero[]; + // #enddocregion this-heroes + // #docregion inject-apollo + constructor(private apollo: Apollo) { } + // #enddocregion inject-apollo + + // #docregion query-heroes + ngOnInit(): void { + this.apollo.watchQuery({ + query: gql` + query allHeroes { + heroes { + id + name + } + } + ` + }).subscribe((queryResult: ApolloQueryResult<{ heroes: Hero[] }>) => { + this.heroes = queryResult.data.heroes.slice(1, 5); + }); + } + // #enddocregion query-heroes +} +// #enddocregion diff --git a/public/docs/_examples/heroes-graphql/ts/src/app/hero-detail.component.1.html b/public/docs/_examples/heroes-graphql/ts/src/app/hero-detail.component.1.html new file mode 100755 index 0000000000..7c08aaf22d --- /dev/null +++ b/public/docs/_examples/heroes-graphql/ts/src/app/hero-detail.component.1.html @@ -0,0 +1,17 @@ + +
    +

    {{hero.name}} details!

    +
    + {{hero.id}}
    +
    + + + + + +
    + + + + +
    diff --git a/public/docs/_examples/heroes-graphql/ts/src/app/hero-detail.component.1.ts b/public/docs/_examples/heroes-graphql/ts/src/app/hero-detail.component.1.ts new file mode 100755 index 0000000000..830a11e129 --- /dev/null +++ b/public/docs/_examples/heroes-graphql/ts/src/app/hero-detail.component.1.ts @@ -0,0 +1,62 @@ +// #docregion +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { Location } from '@angular/common'; + +import { Apollo, ApolloQueryObservable } from 'apollo-angular'; +import { ISubscription } from 'rxjs/Subscription'; +import gql from 'graphql-tag'; + +import { Hero } from './hero'; + +@Component({ + selector: 'my-hero-detail', + templateUrl: './hero-detail.component.html', + styleUrls: [ './hero-detail.component.css' ] +}) +export class HeroDetailComponent implements OnInit { + hero: Hero; + + private heroSubscription: ISubscription; + private heroObservable: ApolloQueryObservable; + + constructor( + private route: ActivatedRoute, + private location: Location, + private apollo: Apollo + ) {} + + ngOnInit(): void { + this.route.params.subscribe(params => { + const heroId = params['id']; + + // #docregion graphql-query-new-field + this.heroObservable = this.apollo.watchQuery({ + query: gql` + query Hero($heroId: Int!) { + hero(heroId: $heroId) { + id + name + age + } + } + `, + variables: { heroId: heroId } + }); + // #enddocregion graphql-query-new-field + + this.heroSubscription = this.heroObservable.subscribe(({data}) => { + this.hero = Object.assign({}, data.hero); + }); + }); + } + + // #docregion save + save(): void { + } + // #enddocregion save + + goBack(): void { + this.location.back(); + } +} diff --git a/public/docs/_examples/heroes-graphql/ts/src/app/hero-detail.component.css b/public/docs/_examples/heroes-graphql/ts/src/app/hero-detail.component.css new file mode 100755 index 0000000000..ab2437efd8 --- /dev/null +++ b/public/docs/_examples/heroes-graphql/ts/src/app/hero-detail.component.css @@ -0,0 +1,30 @@ +/* #docregion */ +label { + display: inline-block; + width: 3em; + margin: .5em 0; + color: #607D8B; + font-weight: bold; +} +input { + height: 2em; + font-size: 1em; + padding-left: .4em; +} +button { + margin-top: 20px; + font-family: Arial; + background-color: #eee; + border: none; + padding: 5px 10px; + border-radius: 4px; + cursor: pointer; cursor: hand; +} +button:hover { + background-color: #cfd8dc; +} +button:disabled { + background-color: #eee; + color: #ccc; + cursor: auto; +} diff --git a/public/docs/_examples/heroes-graphql/ts/src/app/hero-detail.component.html b/public/docs/_examples/heroes-graphql/ts/src/app/hero-detail.component.html new file mode 100755 index 0000000000..32fe6d4391 --- /dev/null +++ b/public/docs/_examples/heroes-graphql/ts/src/app/hero-detail.component.html @@ -0,0 +1,14 @@ + +
    +

    {{hero.name}} details!

    +
    + {{hero.id}}
    +
    + + +
    + + + + +
    diff --git a/public/docs/_examples/heroes-graphql/ts/src/app/hero-detail.component.ts b/public/docs/_examples/heroes-graphql/ts/src/app/hero-detail.component.ts new file mode 100755 index 0000000000..0f9eeb15fb --- /dev/null +++ b/public/docs/_examples/heroes-graphql/ts/src/app/hero-detail.component.ts @@ -0,0 +1,81 @@ +// #docregion +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { Location } from '@angular/common'; + +import { Apollo, ApolloQueryObservable } from 'apollo-angular'; +import { ISubscription } from 'rxjs/Subscription'; +import gql from 'graphql-tag'; + +import { Hero } from './hero'; +import { ApolloQueryResult } from 'apollo-client'; + +@Component({ + selector: 'my-hero-detail', + templateUrl: './hero-detail.component.html', + styleUrls: [ './hero-detail.component.css' ] +}) +export class HeroDetailComponent implements OnInit { + hero: Hero; + + private heroSubscription: ISubscription; + private heroObservable: ApolloQueryObservable; + + constructor( + private route: ActivatedRoute, + private location: Location, + private apollo: Apollo + ) {} + + // #docregion service-fetch-by-id + ngOnInit(): void { + this.route.params.subscribe(params => { + const heroId = params['id']; + + // #docregion graphql-query + this.heroObservable = this.apollo.watchQuery({ + query: gql` + query Hero($heroId: Int!) { + hero(heroId: $heroId) { + id + name + } + } + `, + variables: { heroId: heroId } + }); + + this.heroSubscription = this.heroObservable.subscribe(({data}) => { + this.hero = Object.assign({}, data.hero); + }); + // #enddocregion graphql-query + }); + } + // #enddocregion service-fetch-by-id + + // #docregion save + save(): void { + + this.apollo.mutate({ + mutation: gql` + mutation updateHero($id: Int!, $name: String!) { + updateHero(id: $id, name: $name) { + id + name + } + } + `, + variables: { + id: this.hero.id, + name: this.hero.name + } + }).subscribe((mutationResult: ApolloQueryResult<{ updateHero: Hero }>) => { + this.goBack(); + }); + } + // #enddocregion save + + goBack(): void { + this.location.back(); + } +} diff --git a/public/docs/_examples/heroes-graphql/ts/src/app/hero-search.component.css b/public/docs/_examples/heroes-graphql/ts/src/app/hero-search.component.css new file mode 100755 index 0000000000..9bf8d13457 --- /dev/null +++ b/public/docs/_examples/heroes-graphql/ts/src/app/hero-search.component.css @@ -0,0 +1,21 @@ +/* #docregion */ +.search-result{ + border-bottom: 1px solid gray; + border-left: 1px solid gray; + border-right: 1px solid gray; + width:195px; + height: 16px; + padding: 5px; + background-color: white; + cursor: pointer; +} + +.search-result:hover { + color: #eee; + background-color: #607D8B; +} + +#search-box{ + width: 200px; + height: 20px; +} diff --git a/public/docs/_examples/heroes-graphql/ts/src/app/hero-search.component.html b/public/docs/_examples/heroes-graphql/ts/src/app/hero-search.component.html new file mode 100755 index 0000000000..c6cd10894c --- /dev/null +++ b/public/docs/_examples/heroes-graphql/ts/src/app/hero-search.component.html @@ -0,0 +1,11 @@ + +
    +

    Hero Search

    + +
    +
    + {{hero.name}} +
    +
    +
    diff --git a/public/docs/_examples/heroes-graphql/ts/src/app/hero-search.component.ts b/public/docs/_examples/heroes-graphql/ts/src/app/hero-search.component.ts new file mode 100755 index 0000000000..b02face7ff --- /dev/null +++ b/public/docs/_examples/heroes-graphql/ts/src/app/hero-search.component.ts @@ -0,0 +1,71 @@ +// #docplaster +// #docregion +import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; + +// #docregion rxjs-imports +import { Subject } from 'rxjs/Subject'; + +// Observable class extensions +import 'rxjs/add/observable/of'; + +// Observable operators +import 'rxjs/add/operator/catch'; +import 'rxjs/add/operator/debounceTime'; +import 'rxjs/add/operator/distinctUntilChanged'; +// #enddocregion rxjs-imports + +import { Hero } from './hero'; + +import { Apollo, ApolloQueryObservable } from 'apollo-angular'; +import gql from 'graphql-tag'; + +@Component({ + selector: 'hero-search', + templateUrl: './hero-search.component.html', + styleUrls: [ './hero-search.component.css' ] +}) +export class HeroSearchComponent implements OnInit { + // #docregion search + heroes: ApolloQueryObservable; + // #enddocregion search + // #docregion searchTerms + private searchTerms = new Subject(); + // #enddocregion searchTerms + + constructor( + private apollo: Apollo, + private router: Router) {} + // #docregion searchTerms + + // Push a search term into the observable stream. + search(term: string): void { + this.searchTerms.next(term); + } + // #enddocregion searchTerms + // #docregion search + + ngOnInit(): void { + + this.heroes = this.apollo.watchQuery({ + query: gql` + query searchHeroes ($search: String) { + heroes (search: $search) { + id + name + } + }`, + variables: { + search: this.searchTerms + .debounceTime(300) // wait 300ms after each keystroke before considering the term + .distinctUntilChanged() // ignore if next search term is same as previous + } + }); + } + // #enddocregion search + + gotoDetail(hero: Hero): void { + let link = ['/detail', hero.id]; + this.router.navigate(link); + } +} diff --git a/public/docs/_examples/heroes-graphql/ts/src/app/hero.ts b/public/docs/_examples/heroes-graphql/ts/src/app/hero.ts new file mode 100755 index 0000000000..e3eac516da --- /dev/null +++ b/public/docs/_examples/heroes-graphql/ts/src/app/hero.ts @@ -0,0 +1,4 @@ +export class Hero { + id: number; + name: string; +} diff --git a/public/docs/_examples/heroes-graphql/ts/src/app/heroes.component.1.html b/public/docs/_examples/heroes-graphql/ts/src/app/heroes.component.1.html new file mode 100755 index 0000000000..4fbf37a8bc --- /dev/null +++ b/public/docs/_examples/heroes-graphql/ts/src/app/heroes.component.1.html @@ -0,0 +1,24 @@ + +

    My Heroes

    + +
    + + +
    + + +
      +
    • + {{hero.id}} + {{hero.name}} +
    • +
    + +
    +

    + {{selectedHero.name | uppercase}} is my hero +

    + +
    diff --git a/public/docs/_examples/heroes-graphql/ts/src/app/heroes.component.1.ts b/public/docs/_examples/heroes-graphql/ts/src/app/heroes.component.1.ts new file mode 100755 index 0000000000..e4e278b257 --- /dev/null +++ b/public/docs/_examples/heroes-graphql/ts/src/app/heroes.component.1.ts @@ -0,0 +1,69 @@ +// #docregion +import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; + +// #docregion import-apollo +import { Apollo } from 'apollo-angular'; +// #enddocregion import-apollo +// #docregion import-graphql-tag +import gql from 'graphql-tag'; +// #enddocregion import-graphql-tag +import { ApolloQueryResult } from 'apollo-client'; +import { Hero } from './hero'; + +@Component({ + selector: 'my-heroes', + templateUrl: './heroes.component.html', + styleUrls: [ './heroes.component.css' ] +}) +export class HeroesComponent implements OnInit { + // #docregion this-heroes + heroes: Hero[]; + selectedHero: Hero; + // #enddocregion this-heroes + +// #docregion inject-apollo + constructor( + private apollo: Apollo, + private router: Router) { } + // #enddocregion inject-apollo + + // #docregion query-heroes + getHeroes(): void { + this.apollo.watchQuery({ + query: gql` + query allHeroes { + heroes { + id + name + } + } + `, + }).subscribe((queryResult: ApolloQueryResult<{ heroes: Hero[] }>) => { + this.heroes = queryResult.data.heroes; + }); + } + // #enddocregion query-heroes + + // #docregion add + add(name: string): void { + } + // #enddocregion add + + // #docregion delete + delete(hero: Hero): void { + } + // #enddocregion delete + + ngOnInit(): void { + this.getHeroes(); + } + + onSelect(hero: Hero): void { + this.selectedHero = hero; + } + + gotoDetail(): void { + this.router.navigate(['/detail', this.selectedHero.id]); + } +} diff --git a/public/docs/_examples/heroes-graphql/ts/src/app/heroes.component.2.ts b/public/docs/_examples/heroes-graphql/ts/src/app/heroes.component.2.ts new file mode 100755 index 0000000000..a45a24f6e2 --- /dev/null +++ b/public/docs/_examples/heroes-graphql/ts/src/app/heroes.component.2.ts @@ -0,0 +1,111 @@ +// #docregion +import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; + +// #docregion import-apollo +import { Apollo } from 'apollo-angular'; +// #enddocregion import-apollo +// #docregion import-graphql-tag +import gql from 'graphql-tag'; +// #enddocregion import-graphql-tag +import { ApolloQueryResult } from 'apollo-client'; +import { Hero } from './hero'; + +@Component({ + selector: 'my-heroes', + templateUrl: './heroes.component.html', + styleUrls: [ './heroes.component.css' ] +}) +export class HeroesComponent implements OnInit { + // #docregion this-heroes + heroes: Hero[]; + selectedHero: Hero; + // #enddocregion this-heroes + +// #docregion inject-apollo + constructor( + private apollo: Apollo, + private router: Router) { } + // #enddocregion inject-apollo + + // #docregion query-heroes + getHeroes(): void { + this.apollo.watchQuery({ + query: gql` + query allHeroes { + heroes { + id + name + } + } + ` + }).subscribe((queryResult: ApolloQueryResult<{ heroes: Hero[] }>) => { + this.heroes = queryResult.data.heroes; + }); + // #enddocregion query-heroes + } + + // #docregion add + add(name: string): void { + name = name.trim(); + if (!name) { return; } + + // #docregion add-mutation + this.apollo.mutate({ + mutation: gql` + mutation addHero($heroName: String!) { + addHero(heroName: $heroName) { + id + name + } + } + `, + variables: { + heroName: name + } + }).subscribe((mutationResult: ApolloQueryResult<{ addHero: Hero }>) => { + this.heroes.push({ + // #docregion access-mutation-result + id: mutationResult.data.addHero.id, + name: mutationResult.data.addHero.name + // #enddocregion access-mutation-result + }); + }); + // #enddocregion add-mutation + } + // #enddocregion add + + // #docregion delete + delete(hero: Hero): void { + + this.apollo.mutate({ + mutation: gql` + mutation deleteHero($id: Int!) { + deleteHero(id: $id) { + id + name + } + } + `, + variables: { + id: hero.id + } + }).subscribe((mutationResult: ApolloQueryResult<{ deleteHero: Hero }>) => { + this.heroes = this.heroes.filter(h => h !== hero); + if (this.selectedHero === hero) { this.selectedHero = null; } + }); + } + // #enddocregion delete + + ngOnInit(): void { + this.getHeroes(); + } + + onSelect(hero: Hero): void { + this.selectedHero = hero; + } + + gotoDetail(): void { + this.router.navigate(['/detail', this.selectedHero.id]); + } +} diff --git a/public/docs/_examples/heroes-graphql/ts/src/app/heroes.component.css b/public/docs/_examples/heroes-graphql/ts/src/app/heroes.component.css new file mode 100755 index 0000000000..d2c958a911 --- /dev/null +++ b/public/docs/_examples/heroes-graphql/ts/src/app/heroes.component.css @@ -0,0 +1,68 @@ +/* #docregion */ +.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; +} +/* #docregion additions */ +button.delete { + float:right; + margin-top: 2px; + margin-right: .8em; + background-color: gray !important; + color:white; +} diff --git a/public/docs/_examples/heroes-graphql/ts/src/app/heroes.component.html b/public/docs/_examples/heroes-graphql/ts/src/app/heroes.component.html new file mode 100755 index 0000000000..19e155241b --- /dev/null +++ b/public/docs/_examples/heroes-graphql/ts/src/app/heroes.component.html @@ -0,0 +1,31 @@ + +

    My Heroes

    + +
    + + +
    + +
      + + +
    • + {{hero.id}} + {{hero.name}} + + + +
    • + + +
    +
    +

    + {{selectedHero.name | uppercase}} is my hero +

    + +
    diff --git a/public/docs/_examples/heroes-graphql/ts/src/app/heroes.component.ts b/public/docs/_examples/heroes-graphql/ts/src/app/heroes.component.ts new file mode 100755 index 0000000000..f49a60e7b4 --- /dev/null +++ b/public/docs/_examples/heroes-graphql/ts/src/app/heroes.component.ts @@ -0,0 +1,114 @@ +// #docregion +import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; + +// #docregion import-apollo +import { Apollo } from 'apollo-angular'; +// #enddocregion import-apollo +// #docregion import-graphql-tag +import gql from 'graphql-tag'; +// #enddocregion import-graphql-tag +// #docregion import-apollo-query-result +import { ApolloQueryResult } from 'apollo-client'; +// #enddocregion import-apollo-query-result +import { Hero } from './hero'; + +@Component({ + selector: 'my-heroes', + templateUrl: './heroes.component.html', + styleUrls: [ './heroes.component.css' ] +}) +export class HeroesComponent implements OnInit { + // #docregion this-heroes + heroes: Hero[]; + selectedHero: Hero; + // #enddocregion this-heroes + +// #docregion inject-apollo + constructor( + private apollo: Apollo, + private router: Router) { } + // #enddocregion inject-apollo + + // #docregion query-heroes + getHeroes(): void { + this.apollo.watchQuery({ + query: gql` + query allHeroes { + heroes { + id + name + } + } + ` + }).subscribe((queryResult: ApolloQueryResult<{ heroes: Hero[] }>) => { + this.heroes = queryResult.data.heroes.slice(); + }); + } + // #enddocregion query-heroes + + // #docregion add + add(name: string): void { + name = name.trim(); + if (!name) { return; } + + // #docregion add-mutation + this.apollo.mutate({ + mutation: gql` + mutation addHero($heroName: String!) { + addHero(heroName: $heroName) { + id + name + } + } + `, + variables: { + heroName: name + } + }).subscribe((mutationResult: ApolloQueryResult<{ addHero: Hero }>) => { + // #docregion access-mutation-result + this.heroes.push({ + id: mutationResult.data.addHero.id, + name: mutationResult.data.addHero.name + }); + // #enddocregion access-mutation-result + this.selectedHero = null; + }); + // #enddocregion add-mutation + } + // #enddocregion add + + // #docregion delete + delete(hero: Hero): void { + + this.apollo.mutate({ + mutation: gql` + mutation deleteHero($id: Int!) { + deleteHero(id: $id) { + id + name + } + } + `, + variables: { + id: hero.id + } + }).subscribe((mutationResult: ApolloQueryResult<{ deleteHero: Hero }>) => { + this.heroes = this.heroes.filter(h => h !== hero); + if (this.selectedHero === hero) { this.selectedHero = null; } + }); + } + // #enddocregion delete + + ngOnInit(): void { + this.getHeroes(); + } + + onSelect(hero: Hero): void { + this.selectedHero = hero; + } + + gotoDetail(): void { + this.router.navigate(['/detail', this.selectedHero.id]); + } +} diff --git a/public/docs/_examples/heroes-graphql/ts/src/app/in-memory-data.service.ts b/public/docs/_examples/heroes-graphql/ts/src/app/in-memory-data.service.ts new file mode 100644 index 0000000000..c915955e22 --- /dev/null +++ b/public/docs/_examples/heroes-graphql/ts/src/app/in-memory-data.service.ts @@ -0,0 +1,19 @@ +// #docregion , init +import { InMemoryDbService } from 'angular-in-memory-web-api'; +export class InMemoryDataService implements InMemoryDbService { + createDb() { + let heroes = [ + {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: 'Dynama'}, + {id: 18, name: 'Dr IQ'}, + {id: 19, name: 'Magma'}, + {id: 20, name: 'Tornado'} + ]; + return {heroes}; + } +} diff --git a/public/docs/_examples/heroes-graphql/ts/src/app/in-memory-graphql.ts b/public/docs/_examples/heroes-graphql/ts/src/app/in-memory-graphql.ts new file mode 100755 index 0000000000..4a8156fc42 --- /dev/null +++ b/public/docs/_examples/heroes-graphql/ts/src/app/in-memory-graphql.ts @@ -0,0 +1,103 @@ +// #docregion +// #docregion import-lodash +import { find as lodashFind } from 'lodash'; +// #enddocregion import-lodash +// #docregion import-graphql-tools +import { makeExecutableSchema } from 'graphql-tools'; +// #enddocregion import-graphql-tools +// #docregion import-graphql +import { execute } from 'graphql'; +// #enddocregion import-graphql +// #docregion graphql-schema +import { typeDefinitions } from './schema'; +// #enddocregion graphql-schema +// #docregion heroes-array +let heroes = [ + {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: 'Dynama'}, + {id: 18, name: 'Dr IQ'}, + {id: 19, name: 'Magma'}, + {id: 20, name: 'Tornado'} +]; +// #enddocregion heroes-array + +// #docregion resolvers +const resolveFunctions = { + Query: { + heroes(obj: any, args: any) { + if (args.search) { + return heroes.filter(function (currentHero){ + return currentHero.name.toLowerCase().search(args.search.toLowerCase()) !== -1; + }); + } else { + return heroes; + } + }, + hero(obj: any, args: any, context: any) { + return lodashFind(heroes, { id: args.heroId }); + } + }, + Mutation: { + updateHero(root: any, args: any) { + let hero = lodashFind(heroes, { id: args.id }); + if (!hero) { + throw new Error(`Couldn't find post with id ${args.id}`); + } + hero.name = args.name; + return hero; + }, + addHero(root: any, args: any) { + const maxId = Math.max(...heroes.map((hero) => {return hero.id; })); + const newHero = { + name: args.heroName, + id: maxId + 1 + }; + heroes.push(newHero); + return newHero; + }, + deleteHero(root: any, args: any) { + let hero = lodashFind(heroes, { id: args.id }); + if (!hero) { + throw new Error(`Couldn't find post with id ${args.id}`); + } + heroes = heroes.filter(function (currentHero) { return currentHero.id !== args.id; }); + return hero; + }, + } +}; +// #enddocregion resolvers +// #docregion make-executable-schema +const schema = makeExecutableSchema({ + typeDefs: typeDefinitions, + resolvers: resolveFunctions, +}); +// #enddocregion make-executable-schema +// #docregion execute-and-export +class InBrowserNetworkInterface { + schema: any = {}; + constructor(params: any) { + this.schema = params.schema; + } + + query(request: any) { + return execute( + this.schema, + request.query, + {}, + {}, + request.variables, + request.operationName); + } +} + +const networkInterface = new InBrowserNetworkInterface({ schema }); +export { + networkInterface +} +// #enddocregion execute-and-export +// #enddocregion diff --git a/public/docs/_examples/heroes-graphql/ts/src/app/mockedNetworkInterface.ts b/public/docs/_examples/heroes-graphql/ts/src/app/mockedNetworkInterface.ts new file mode 100644 index 0000000000..291328e919 --- /dev/null +++ b/public/docs/_examples/heroes-graphql/ts/src/app/mockedNetworkInterface.ts @@ -0,0 +1,17 @@ +// #docregion +// #docregion imports +import { + makeExecutableSchema, + addMockFunctionsToSchema +} from 'graphql-tools'; +import { mockNetworkInterfaceWithSchema } from 'apollo-test-utils'; +import { typeDefinitions } from './schema'; +// #enddocregion imports + +// #docregion create-interface +const schema = makeExecutableSchema({ + typeDefs: typeDefinitions +}); +addMockFunctionsToSchema({ schema }); +export const mockNetworkInterface = mockNetworkInterfaceWithSchema({ schema }); +// #enddocregion create-interface diff --git a/public/docs/_examples/heroes-graphql/ts/src/app/schema.ts b/public/docs/_examples/heroes-graphql/ts/src/app/schema.ts new file mode 100644 index 0000000000..0baf8da308 --- /dev/null +++ b/public/docs/_examples/heroes-graphql/ts/src/app/schema.ts @@ -0,0 +1,38 @@ +// #docregion +export const typeDefinitions = ` +# the model +type Hero { + id: Int! + name: String! +} + +# The schema allows the following queries: +type Query { + heroes(search: String): [Hero] + + hero(heroId: Int!): Hero +} + +# This schema allows the following mutation: +type Mutation { + updateHero ( + id: Int! + name: String! + ): Hero + + addHero ( + heroName: String! + ): Hero + + deleteHero ( + id: Int! + ): Hero +} + +# Tell the server which types represent the root query and root mutation types. +# By convention, they are called RootQuery and RootMutation. +schema { + query: Query + mutation: Mutation +} +`; diff --git a/public/docs/_examples/heroes-graphql/ts/src/index.html b/public/docs/_examples/heroes-graphql/ts/src/index.html new file mode 100755 index 0000000000..29cfb3785e --- /dev/null +++ b/public/docs/_examples/heroes-graphql/ts/src/index.html @@ -0,0 +1,27 @@ + + + + + + Angular Tour of Heroes - GraphQL + + + + + + + + + + + + + + + + + Loading... + + \ No newline at end of file diff --git a/public/docs/_examples/heroes-graphql/ts/src/main-aot.ts b/public/docs/_examples/heroes-graphql/ts/src/main-aot.ts new file mode 100755 index 0000000000..bd2ca604a3 --- /dev/null +++ b/public/docs/_examples/heroes-graphql/ts/src/main-aot.ts @@ -0,0 +1,6 @@ +// #docregion +import { platformBrowser } from '@angular/platform-browser'; + +import { AppModuleNgFactory } from '../aot/src/app/app.module.ngfactory'; + +platformBrowser().bootstrapModuleFactory(AppModuleNgFactory); diff --git a/public/docs/_examples/heroes-graphql/ts/src/main.ts b/public/docs/_examples/heroes-graphql/ts/src/main.ts new file mode 100755 index 0000000000..f332d1d245 --- /dev/null +++ b/public/docs/_examples/heroes-graphql/ts/src/main.ts @@ -0,0 +1,6 @@ +// #docregion +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +import { AppModule } from './app/app.module'; + +platformBrowserDynamic().bootstrapModule(AppModule); diff --git a/public/docs/_examples/heroes-graphql/ts/src/systemjs.config.extras.js b/public/docs/_examples/heroes-graphql/ts/src/systemjs.config.extras.js new file mode 100755 index 0000000000..0cb9d58fdb --- /dev/null +++ b/public/docs/_examples/heroes-graphql/ts/src/systemjs.config.extras.js @@ -0,0 +1,76 @@ + +/** App specific SystemJS configuration */ +System.config({ + + map: { + + // #docregion systemjs-apollo-client-map + 'apollo-client': 'npm:apollo-client/apollo.umd.js', + 'apollo-client-rxjs': 'npm:apollo-client-rxjs/build/bundles/apollo-rxjs.umd.js', + 'apollo-angular': 'npm:apollo-angular/build/bundles/apollo.umd.js', + + 'whatwg-fetch': 'npm:whatwg-fetch', + + 'graphql-anywhere': 'npm:graphql-anywhere', + + 'graphql-tag': 'npm:graphql-tag', + 'symbol-observable': 'npm:symbol-observable', + 'redux': 'npm:redux/dist/redux.min.js', + // #enddocregion systemjs-apollo-client-map + + // #docregion systemjs-apollo-test-utils-map + 'apollo-test-utils': 'npm:apollo-test-utils', + + // #docregion systemjs-graphql-server-map + 'graphql': 'npm:graphql', + 'graphql-tools': 'npm:graphql-tools', + 'deprecated-decorator': 'npm:deprecated-decorator', + 'node-uuid': 'npm:node-uuid', + 'uuid': 'npm:uuid', + 'iterall': 'npm:iterall', + 'lodash': 'npm:lodash' + // #enddocregion systemjs-graphql-server-map + // #enddocregion systemjs-apollo-test-utils-map + }, + packages: { + + // #docregion systemjs-apollo-client-packages + 'whatwg-fetch': { main: './fetch.js', defaultExtension: 'js' }, + 'redux': { format: 'cjs', defaultExtension: 'js' }, + 'graphql-tag': { main: './index.js', defaultExtension: 'js' }, + 'symbol-observable': { main: './index.js', defaultExtension: 'js' }, + 'graphql-anywhere': { + main: '/lib/src/index.js', + defaultExtension: 'js' + }, + // #enddocregion systemjs-apollo-client-packages + + // #docregion systemjs-apollo-test-utils-packages + 'apollo-test-utils': { main: '/dist/src/index.js', defaultExtension: 'js' }, + + // #docregion systemjs-graphql-server-packages + 'graphql': { + main: './index.js', + defaultExtension: 'js', + map: { + './type': './type/index.js', + './language': './language/index.js', + './execution': './execution/index.js', + './validation': './validation/index.js', + './error': './error/index.js', + './utilities': './utilities/index.js' + }, + }, + 'graphql-tools': { + main: '/dist/index.js', + defaultExtension: 'js' + }, + 'deprecated-decorator': { main: '/bld/index.js', defaultExtension: 'js' }, + 'node-uuid': { main: './uuid.js', defaultExtension: 'js' }, + 'uuid': { main: './lib/rng-browser.js', defaultExtension: 'js' }, + 'iterall': { main: './index.js', defaultExtension: 'js' }, + 'lodash': { main: './index.js', defaultExtension: 'js' } + // #enddocregion systemjs-graphql-server-packages + // #enddocregion systemjs-apollo-test-utils-packages + } +}); diff --git a/public/docs/_examples/heroes-graphql/ts/tsconfig-aot.json b/public/docs/_examples/heroes-graphql/ts/tsconfig-aot.json new file mode 100755 index 0000000000..646b63f6d2 --- /dev/null +++ b/public/docs/_examples/heroes-graphql/ts/tsconfig-aot.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "es5", + "module": "es2015", + "moduleResolution": "node", + "sourceMap": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "lib": ["es2015", "dom"], + "noImplicitAny": true, + "suppressImplicitAnyIndexErrors": true, + "typeRoots": [ + "../../node_modules/@types/" + ] + }, + + "files": [ + "src/app/app.module.ts", + "src/main-aot.ts" + ], + + "angularCompilerOptions": { + "genDir": "aot", + "skipMetadataEmit" : true + } +} \ No newline at end of file diff --git a/public/docs/_examples/package.json b/public/docs/_examples/package.json index 41dc5e5b56..c323ed5811 100644 --- a/public/docs/_examples/package.json +++ b/public/docs/_examples/package.json @@ -26,7 +26,15 @@ "@angular/tsc-wrapped": "~4.0.0", "@angular/upgrade": "~4.0.0", "angular-in-memory-web-api": "~0.3.1", + "apollo-angular": "^0.12.0", + "apollo-client": "^1.0.0-rc.7", + "apollo-test-utils": "^0.2.2", "core-js": "^2.4.1", + "graphql": "^0.9.1", + "graphql-subscriptions": "^0.3.1", + "graphql-tag": "^1.3.2", + "graphql-tools": "^0.10.1", + "lodash": "^4.17.4", "rxjs": "5.0.1", "systemjs": "0.19.39", "zone.js": "^0.8.4" @@ -40,7 +48,11 @@ "@types/angular-resource": "^1.5.6", "@types/angular-route": "^1.3.2", "@types/angular-sanitize": "^1.3.3", + "@types/chai": "^3.4.35", + "@types/graphql": "^0.8.6", + "@types/isomorphic-fetch": "0.0.33", "@types/jasmine": "2.5.36", + "@types/lodash": "^4.14.58", "@types/node": "^6.0.45", "angular2-template-loader": "^0.6.0", "awesome-typescript-loader": "^3.0.4", @@ -74,6 +86,8 @@ "rimraf": "^2.5.4", "rollup": "^0.41.6", "rollup-plugin-commonjs": "^8.0.2", + "rollup-plugin-node-builtins": "^2.1.0", + "rollup-plugin-node-globals": "^1.1.0", "rollup-plugin-node-resolve": "2.0.0", "rollup-plugin-uglify": "^1.0.1", "source-map-explorer": "^1.3.2", diff --git a/public/docs/_examples/toh-4/ts/src/app/app.component.ts b/public/docs/_examples/toh-4/ts/src/app/app.component.ts index 0d57acb5b0..e22b81ae70 100644 --- a/public/docs/_examples/toh-4/ts/src/app/app.component.ts +++ b/public/docs/_examples/toh-4/ts/src/app/app.component.ts @@ -20,7 +20,9 @@ import { HeroService } from './hero.service'; {{hero.id}} {{hero.name}}
  • + // #docregion calling-component + // #enddocregion calling-component `, // #enddocregion template styles: [` diff --git a/public/docs/_examples/toh-4/ts/src/app/hero-detail.component.ts b/public/docs/_examples/toh-4/ts/src/app/hero-detail.component.ts index 865fb98da7..487c7f3592 100644 --- a/public/docs/_examples/toh-4/ts/src/app/hero-detail.component.ts +++ b/public/docs/_examples/toh-4/ts/src/app/hero-detail.component.ts @@ -18,5 +18,7 @@ import { Hero } from './hero'; ` }) export class HeroDetailComponent { + // #docregion declaring-conponent-input @Input() hero: Hero; + // #enddocregion declaring-conponent-input } diff --git a/public/docs/_examples/toh-4/ts/src/app/hero.ts b/public/docs/_examples/toh-4/ts/src/app/hero.ts index e3eac516da..8f7cc205c8 100644 --- a/public/docs/_examples/toh-4/ts/src/app/hero.ts +++ b/public/docs/_examples/toh-4/ts/src/app/hero.ts @@ -1,3 +1,4 @@ +// #docregion export class Hero { id: number; name: string; diff --git a/public/docs/_examples/toh-6/ts/src/app/hero-detail.component.ts b/public/docs/_examples/toh-6/ts/src/app/hero-detail.component.ts index 6224f10ac1..697a28c753 100644 --- a/public/docs/_examples/toh-6/ts/src/app/hero-detail.component.ts +++ b/public/docs/_examples/toh-6/ts/src/app/hero-detail.component.ts @@ -21,11 +21,14 @@ export class HeroDetailComponent implements OnInit { private location: Location ) {} + // #docregion service-fetch-by-id ngOnInit(): void { this.route.params .switchMap((params: Params) => this.heroService.getHero(+params['id'])) .subscribe(hero => this.hero = hero); } + // #enddocregion service-fetch-by-id + // #docregion save save(): void { diff --git a/public/docs/ts/latest/cookbook/_data.json b/public/docs/ts/latest/cookbook/_data.json index 1aed135fa8..86dc4f1faf 100644 --- a/public/docs/ts/latest/cookbook/_data.json +++ b/public/docs/ts/latest/cookbook/_data.json @@ -41,6 +41,11 @@ "intro": "Validate user's form entries." }, + "graphql": { + "title": "GraphQL", + "intro": "Use GraphQL to talk to a remote server" + }, + "i18n": { "title": "Internationalization (i18n)", "intro": "Translate the app's template text into multiple languages." diff --git a/public/docs/ts/latest/cookbook/graphql.jade b/public/docs/ts/latest/cookbook/graphql.jade new file mode 100644 index 0000000000..546fa88ec6 --- /dev/null +++ b/public/docs/ts/latest/cookbook/graphql.jade @@ -0,0 +1,760 @@ +include ../_util-fns + + +:marked + GraphQL is a network protocol, a query language for your API, and a runtime for fulfilling those queries with your existing data. + +.l-sub-section + :marked + The GraphQL interface is a replacement or enhancement for REST and can be used in conjunction with it. + +:marked + It provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools. + + + +:marked + ## Contents + + - [What is GraphQL?](#what-is-graphql) + + - [The benefits of GraphQL with Angular](#benefits-of-graphql) + + - [How to use GraphQL in an Angular app with app demo:](#how-to) + - [Installation](#installation) + + - [Performing a query](#querying) + + - [Performing a mutation](#mutation) + + - [Appendix 1: Mocking a GraphQL server](#mock-server) + + - [Appendix 2: Setting up a GraphQL server](#server) + + - [Further resources](#resources) + + - [Full Example](#example) + +:marked + **See the **. + +.l-main-section + +:marked + ## What is GraphQL? + + GraphQL is an API query language, that helps your Angular app do the following: + + - Fetch exactly the information it needs from the server. + - Add type safety to your API. + - Merge multiple dependencies into one single response from the server. + - Handle server data dependency in a component structure. + + It’s also important to understand these key points: + + - **GraphQL is not a data source**. The GraphQL runtime works on top of any data source—SQL, + NoSql, REST, Queues, .NET servers, Java servers, or any other technology or data source. + - GraphQL solves the need of sending multiple requests to the server for different resources and + then running complex joins on the client—without the need to create a custom endpoint like REST does. + - The GraphQL specification also includes protocols for real-time push updates from the server to the client. + + See the official [GraphQL](http://graphql.org/) site for a more in-depth look. + +.l-main-section + +:marked + ## The benefits of GraphQL with Angular + +.l-sub-section + :marked + For a summary of this section, see the following [video](https://www.youtube.com/watch?v=Xx39bv-5ojA&t=1s) + by [Jeff Cross](https://twitter.com/jeffbcross) and [Uri Goldshtein](https://twitter.com/UriGoldshtein). + +iframe(type='text/html' width='560' height='315' + src='https://www.youtube.com/embed/Xx39bv-5ojA' + frameborder='0') + +:marked + ### Component based API + + Angular components are composable, reusable, and allow encapsulation of behaviour + and state. So how does one keep these benefits when fetching data from the server? + Without GraphQL, there are three possible solutions: + + 1. Using the HTTP service inside components. + 2. Calling a service from a component. + 3. Fetching data at the parent component and passing it down the component tree. + + While these solutions are valid, they have their limitations. + + The following three sections cover each of these in turn. + + #### Using the HTTP service in the Component + + There are two potential issues with this approach: + + 1. Multiple redundant requests; when you render multiple components, + such as an `ngFor` with many components, each sends its own + HTTP call. + 2. Inconsistent data; if two components fetch the same data but in different requests, + the data might change and not be consistent across the app. + +a#http-heroes +:marked + #### Using a service + + Consider the following service and component: + ++makeExample('toh-6/ts/src/app/hero.service.ts', 'getHero','hero.service.ts (Fetch by Id)') ++makeExample('toh-6/ts/src/app/hero-detail.component.ts', 'service-fetch-by-id','hero-detail.component.ts (Fetch by Id)') + +:marked + There are two potential issues here. + + 1. A dependency between the service and all of the components that use it + make the component no longer reusable. If you change something in the service, + it might break other components that use that service. + 2. There is a main, complex point to handle batching, caching, and join logic. + Because the logic is shared between components, the potential of things breaking increases. + +:marked + #### Fetching data at the parent component and passing it down the component tree + + Consider an example of the third possibility: + ++makeExample('toh-4/ts/src/app/hero.ts','','Declare the API type') ++makeExample('toh-4/ts/src/app/hero-detail.component.ts','declaring-conponent-input','Declare the input') ++makeExample('toh-4/ts/src/app/app.component.ts','calling-component','Using from parent') + +:marked + This works until you change the API of the component, which means you need to change + its parent components _all the way to the top_. + Again, this creates a dependency. This time the dependency is with the child component + all the way up to the fetching component + and _all the components in between_. + + #### Solution - Component based API + + +:marked + Here, the data dependency is inside the component. The query reflects just + what the single component needs and is included as part of the + component or a wrapper component. That means that when the + data dependency changes, the component is the only thing + impacted. You don't have to touch any services or parent components. + +:marked + Now, a single component contains all of its own data dependency changes. + +:marked + The `watchQuery` function tells the Apollo Client, the GraphQL client that lets you use GraphQL in your Angular app, + what data this component needs. The Apollo Client then returns the + necessary data to the component as an Observable. + For example, adding an `age` field to the app is + simple because you only change the component (you cover + this syntax [later](#querying) in the cookbook) and then modify the template accordingly: + ++makeExample('heroes-graphql/ts/src/app/hero-detail.component.1.ts','graphql-query-new-field','Adding an `age` field to the component') ++makeExample('heroes-graphql/ts/src/app/hero-detail.component.1.html','template-new-field','Adding an `age` field to the template') + +:marked + So far, you've seen how to fetch data with GraphQL for a component while + keeping the component isolated from the rest of the app. + That solves the most time-consuming and bug provoking code + that one usually writes in order to fetch data from the server. + You can also use GraphQL to improve efficiency. + +:marked + ### Network Performance + + The [Tour of Heroes HTTP guide](latest/tutorial/toh-pt6.html) + calls `getHeroes` to fetch all heroes and their information. + +:marked + That might work for simple cases but but as your app grows `getHeroes` might fetch + more information than the app really needs for each hero. + This approach also creates a dependency between the server endpoint and the UI + component—if you change or limit the amount of information you send on the + server, you might break the components that use that endpoint. + + The other approach would be to call `getHeroes`, get the ids of the heroes and call `getHero` for each id. + That might result in _multiple requests to the server_ for one single render of the page. + + With a REST API, you _always have to choose between those two options_ and + their respective problems. + +:marked + With GraphQL, you just specify the dependency of each component and a + GraphQL client library, like the [Apollo Client](http://dev.apollodata.com/), + to merge those into **one single network request**. GraphQL sends back the information + in a single response, with exactly the information you need—no more, no + less—in exactly the structure and shape you want the data to be with + no need to do complex joins or wait for responses. + +.l-sub-section + :marked + You can work with the Apollo Client + while still keep your existing REST services in your Angular app + and migrate gradually from them. + +:marked + ### Typed API, tooling and, auto documentation + + Just as TypeScript provides tooling to increase productivity and best practices, + GraphQL provides a similar solution for working with APIs. + + Often, APIs are written by teams you don't have access to and + can change without notice. + + With GraphQL, the schema is typed and shared between the client + and the server. As a result, just as with Angular and TypeScript, + you get the same development experience when calling calling a remote + API—validation and autocompletion inside the IDE at development time. + +.l-main-section + +:marked + ## How to use GraphQL in an Angular app + + This guide uses [Apollo Client](http://dev.apollodata.com/) as the GraphQL client for Angular. + Apollo helps you query GraphQL and provides a caching layer + with common features you need for querying a server such as + caching, mutations, optimistic UI, real-time subscriptions, + pagination, server-side rendering, and prefetching. + +.l-sub-section + :marked + This cookbook touches on the main points of using GraphQL with Angular. + The full documentation can be found on the [Apollo Client website](http://dev.apollodata.com/). + +:marked + The starting point for the app is the Tour of Heroes tutorial app at its end state. + + **Download the **. + + This guide shows you how to migrate that app from REST to GraphQL. + +.l-main-section + +:marked + ## Installation + + First, install Apollo Client and the integration libraries from npm: + +code-example(language="sh" class="code-shell"). + npm install apollo-client apollo-angular graphql-tag --save + +.l-sub-section + :marked + This example uses `system.js` so you need to also add the configuration to it. + With other build systems, the following process will be different and maybe easier. + +:marked + Add the following configuration to your `systemjs.config.js` file under the `map` key: + ++makeExample('heroes-graphql/ts/src/systemjs.config.extras.js', 'systemjs-apollo-client-map', 'under map: { (excerpt)') + +:marked + and the following configuration to your `systemjs.config.js` file under the `packages` key: + ++makeExample('heroes-graphql/ts/src/systemjs.config.extras.js', 'systemjs-apollo-client-packages', 'under packages: { (excerpt)') + +:marked + Because you also have a running server on your app with more dependencies, you will need to add additional configurations to + `systemjs.config.js` as well. + + Add the following configuration to your `systemjs.config.js` file under the `map` key: + ++makeExample('heroes-graphql/ts/src/systemjs.config.extras.js', 'systemjs-graphql-server-map', 'under map: { (excerpt)') + +:marked + and the following configuration to your `systemjs.config.js` file under the `packages` key: + ++makeExample('heroes-graphql/ts/src/systemjs.config.extras.js', 'systemjs-graphql-server-packages', 'under packages: { (excerpt)') + +:marked + Next, initialize the client by creating a new file called `client.ts` and + pasting in the following code: + ++makeExample('heroes-graphql/ts/src/app/client.1.ts', '', 'app/client.ts') +:marked + So what's happening here? + + This is how to use the default initialization of Apollo which calls the `/graphql` endpoint. + First, you import `ApolloClient`. then you create a constant for the new instance of the client, + and finally export it so that it is available to the app. + +.l-sub-section + :marked + ### To use a different URI for the Apollo Client + For this cookbook you would use the default `/graphql` endpoint, + but it's good to know it is possible to change those settings. + To change the [settings](http://dev.apollodata.com/core/apollo-client-api.html#ApolloClient\.constructor) + of `ApolloClient`, call its constructor with different parameters. + Go to the [Apollo documentation](http://dev.apollodata.com/angular2/initialization.html#creating-client) for further resources. + +:marked + Usually you need to query an existing server. + The server for this guide is based on the [Tour of Heroes](ts/latest/tutorial/) app. + The starter app () already has an in-memory GraphQL server prepared. + + Now all that's left is to connect the in-memory server to the Apollo Client configuration + by importing `networkInterface` and adding it to the `client` constant in `client.ts`. ++makeExample('heroes-graphql/ts/src/app/client.ts', '', 'client.ts') +.l-sub-section + :marked + In order to learn how to create the GraphQL server for this example, follow the instructions on + [Appendix 2: Setting up a GraphQL server](#server). + + Another important ability of Apollo is to create a mock server in one line of code based on a GraphQL schema. + Check out the [Appendix 1: Mocking a GraphQL server](#mock-server) to learn about that as well. + +:marked + After initializing the Apollo Client, import the `ApolloModule` and `getClient` + which you just configured in `client.ts` into the app's root module: ++makeExample('heroes-graphql/ts/src/app/app.module.ts', 'import-apollo', 'app.module.ts (excerpt)') + +:marked + Next, add `ApolloModule.forRoot(getClient)` to the `@NgModule` imports array. This + is an initialization function that accepts the Apollo configuration + you created earlier as an argument and creates a new Apollo instance for the app. + ++makeExample('heroes-graphql/ts/src/app/app.module.1.ts', 'apollo-ngmodule', 'app.module.ts (excerpt)') + +:marked + Now Apollo is initialized and ready for use in the app. + +.l-main-section + +:marked + ## Performing a query + + With GraphQL you query a schema, which is organized into types and fields, + that represents the data you can query. + + The schema begins with data types and fields followed by the specific queries + you can perform on the data. These are in turn followed by + mutations, which are _actions_ that you + can call on the server, similar to a POST request in REST. + + Here is the schema the Tour of Heroes server in the app use: + ++makeExample('heroes-graphql/ts/src/app/schema.ts', '', 'schema.ts') + +:marked + Once you have a server, which is prepared already in this app, to start querying data. + + You will convert the `heroes.component` from quering the data from the REST endpoint to the GraphQL server. + + First remove all references of the exisitng `HeroService`: ++makeExample('heroes-graphql-starter/ts/src/app/heroes.component.1.ts', '', 'heroes.component.ts') +:marked + Now begin adding Apollo importing `Apollo` into `heroes.component.ts`, + and injecting it into the constructor: ++makeExample('heroes-graphql/ts/src/app/heroes.component.ts', 'import-apollo', 'heroes.component.ts (excerpt)') ++makeExample('heroes-graphql/ts/src/app/heroes.component.ts', 'inject-apollo', 'heroes.component.ts (excerpt)') +:marked + Now that the schema is available to the app, the next step is querying it. + In the component, import `gql` from the `graphql-tag` library. + The `gql` function turns your query string to something `Apollo` + can accept and understand. + ++makeExample('heroes-graphql/ts/src/app/heroes.component.ts', 'import-graphql-tag', 'heroes.component.ts') + +:marked + In order to specify the TypeScript type of the data that is recieved, import `ApolloQueryResult` from `apollo-client`: + ++makeExample('heroes-graphql/ts/src/app/heroes.component.ts', 'import-apollo-query-result', 'import type') + +:marked + To query data with the Apollo Client, pass a GraphQL query with the + data and structure that you want to the `Apollo` `watchQuery` function. + The `Apollo` ` watchQuery` function returns the data from the + server in the form of an Observable. + Replace the `getHeroes()` function with this one: + ++makeExample('heroes-graphql/ts/src/app/heroes.component.ts', 'query-heroes', 'heroes.component.ts') + +:marked + For more information on GraphQL queries, see the GraphQL documentation on + [Queries and Mutations](http://graphql.org/learn/queries/). +:marked + Now, the same template that worked before will still displays the results of the new query: ++makeExample('heroes-graphql/ts/src/app/heroes.component.1.html', 'render-heroes', 'heroes.component.html') + +:marked + At this point, if you go to the `heroes` tab, and you have a running [GraphQL server](#server), + the browser displays the fetched data. + +.l-main-section + +:marked + ## Performing a mutation + + In addition to fetching data using queries, GraphQL also makes it possible to change data through mutations. + + Mutations are identical to queries in syntax, the only difference being that you use the keyword `mutation` instead of `query` to indicate that you are performing writes to the backend. + +.l-sub-section + :marked + You can look at a mutation as the equivalent of a POST request in REST. +:marked + GraphQL mutations, like queries, are straightforward with minimal syntax. + Consider this example of a mutation: +code-example(language="json"). + mutation { + addHero(heroName: "Russell Brand") { + id + name + } + } +:marked + First, you declare that you're writing a mutation and then specify what it does. + To break it down, GraphQL mutations consist of two parts: + 1. The mutation name with arguments (`addHero`), which represents the actual + operation to be done on the server (just like calling a function). + 2. The fields you want back from the result of the mutation, which are sent back to the client. + In this example, they are `id` and `name`. This allows you to decide + which fields you get back from the server, + rather than the server dictating what's returned. + + The result of the above mutation might be: +code-example(language="json"). + { + "data": { + "addHero": { + "id": "69", + "name": "Russel Brand" + } + } + } +.l-sub-section + :marked + For an in-depth look at mutation syntax, see the [Mutations + documentation](http://graphql.org/learn/queries/#mutations) + at [GraphQL.org](http://graphql.org). +:marked + To use a mutation, you can use the same existing template with a function to add a hero: ++makeExample('heroes-graphql/ts/src/app/heroes.component.html', 'add', 'heroes.component.html') +:marked + In the component class, there is already an `add()` function. It expects a name argument of type `string` + and is `void` because it returns nothing. ++makeExample('heroes-graphql/ts/src/app/heroes.component.1.ts', 'add', 'heroes.component.ts') +:marked + Now for the fun part. Inside of the `add()` function, add an `addHero` mutation + using the `apollo.mutate` function as follows: ++makeExample('heroes-graphql/ts/src/app/heroes.component.2.ts', 'add-mutation', 'heroes.component.ts') +:marked + The mutation requires a variable and you pass it to the `mutate` function through the `variables` parameter. + + As mentioned above, with GraphQL mutations, you specify the result you want to get back from the server. + + Apollo's `mutate` function returns the result as an Observable. + The Observable returns a `mutationResult` variable that is structured + like the `ApolloQueryResult` TypeScript type, where the generic `T` type is a `Hero` type: +code-example(language="json"). + type ApolloQueryResult<T> = { + data: T; + loading: boolean; + networkStatus: NetworkStatus; + }; +:marked + If that looks familiar, it's because that's also how you reference the `mutationResult` variable in TypeScript. + To access the hero data `mutationResult` returns, use dot notation to traverse `mutationResult` and assign it to a new hero object: ++makeExample('heroes-graphql/ts/src/app/heroes.component.ts', 'access-mutation-result', 'heroes.component.ts') +:marked + Now that you have created the new object with the data, push it into the `heroes` array. + +:marked + Just like a query, the `mutate` function returns an Observable you can subscribe to + that handles the data you request. + + Now your existing heroes app can add a hero using GraphQL. +figure.image-display + img(src='/resources/images/cookbooks/heroes-graphql/heroes- graphql-mutation.gif' alt="Heroes GraphQL Mutation") + +.l-main-section + +:marked + ## Appendix 1: Mocking a GraphQL server + + When writing a GraphQL Angular app, you are quering a shared Schema. + Both the client and the server agree on a single schema that describes the data the client can query and the actions it can perform + on the server. + + Once you have that schema, there is no need for an actual server and you can mock your server with one line of code. + That mocking is good for day to day development as well as for automatic tests for your Angular app. + + Let's create the schema that is based on the [Tour of Heroes](ts/latest/tutorial/) app. + + Create a file called `schema.ts` in the `app` directory and paste in the following schema: ++makeExample('heroes-graphql/ts/src/app/schema.ts', '', 'schema.ts') +:marked + Now that you have the schema, let's mock the server so you would be able to use actual data in your app. + First install the `Apollo Test Utils` library: + +code-example(language="sh" class="code-shell"). + npm install apollo-test-utils --save + +.l-sub-section + :marked + This example uses `system.js` so you need to also add the configuration to it. + With other build systems, or when running on Node, the following process will be different and + maybe easier. + +:marked + Add the following configuration to your `systemjs.config.js` file under the `map` key: + ++makeExample('heroes-graphql/ts/src/systemjs.config.extras.js', 'systemjs-apollo-test-utils-map', 'under map: { (excerpt)') + +:marked + and the following configuration to your `systemjs.config.js` file under the `packages` key: + ++makeExample('heroes-graphql/ts/src/systemjs.config.extras.js', 'systemjs-apollo-test-utils-packages', 'under packages: { (excerpt)') +:marked + Now you need to create a mocked network interface that will use `apollo-test-utils` and the schema you created. + + Create a file called `mockedNetworkInterface.ts` and import the schema and the tools to create a mock network interface: ++makeExample('heroes-graphql/ts/src/app/mockedNetworkInterface.ts', 'imports', 'imports') +:marked + Now you need to make the schema executable, add the mocking functions to it, create the network interface and export it: ++makeExample('heroes-graphql/ts/src/app/mockedNetworkInterface.ts', 'create-interface', 'Create Network Interface') +:marked + Now all you need to do is use that network interface in your Apollo Client instead of your regular network interface: ++makeExample('heroes-graphql/ts/src/app/client.3.ts', 'import-and-use', 'Use Network Interface') + +:marked + Now every time you will query Apollo Client it will return a mocked data for your client. + + To dive deeper to more advanced mocking, check out the [Apollo-Test-Utils repository](https://github.com/apollographql/apollo-test-utils). + +.l-main-section + +:marked + ## Appendix 2: Setting up a GraphQL server + + This example shows how to run a GraphQL in the browser but running a GraphQL server on + Node.js or in the browser is very similar. + + If you don't have the option of running GraphQL on the server, + this method makes it possible to still use GraphQL in your app with the + benefit of not needing to sync multiple REST requests and join logic + on the client. + +.l-sub-section + :marked + To read more about how to run a full GraphQL backend, see the [Apollo Server documentation](http://dev.apollodata.com/tools/). + Because the real, backend server is written in Isomorphic Javascript, + it is almost identical to the local server in this appendix. + Everything you learn here applies to writing an actual GraphQL backend server. + + Additionally, there are a few GraphQL backend-as-a-service platforms available, + similar to Firebase, but based on the GraphQL API spec. + For help on getting up and running, see [Scaphold.io](https://www.scaphold.io/) and [Graph.Cool](https://www.graph.cool/). + +:marked + In order to create a GraphQL schema, you need the `graphql-tools` library. + It allows you to write a GraphQL schema as a string and make it executable. + In a terminal window, issue the following command: + +.l-sub-section + :marked + This example uses `system.js` so you need to also add the configuration to it. + With other build systems, or when running on Node, the following process will be different and + maybe easier. + +code-example(language="sh" class="code-shell"). + npm install graphql-tools --save + +.l-sub-section + :marked + This example uses `system.js` so you need to also add the configuration to it. + With other build systems, or when running on Node, the following process will be different and + maybe easier. + +:marked + Add the following configuration to your `systemjs.config.js` file under the `map` key: + ++makeExample('heroes-graphql/ts/src/systemjs.config.extras.js', 'systemjs-graphql-server-map', 'under map: { (excerpt)') + +:marked + and the following configuration to your `systemjs.config.js` file under the `packages` key: + ++makeExample('heroes-graphql/ts/src/systemjs.config.extras.js', 'systemjs-graphql-server-packages', 'under packages: { (excerpt)') + +:marked + Next, create a file called `schema.ts` in the `app` directory + and paste in the following schema: + ++makeExample('heroes-graphql/ts/src/app/schema.ts', '', 'schema.ts') +:marked + The schema starts with a represention of the model of data the server exposes. + Then the schema specifies what queries are allowed on that data, followed by + what mutations, or actions, clients are allowed to do on the server. + The end of the schema provides the definitions as the root types the GraphQL server will expose. + +.l-sub-section + :marked + While the schema includes the major points covered in this cookbook, + you can read more in the [GraphQL.org Introduction to GraphQL](http://graphql.org/learn/). +:marked + Now, create another file called `in-memory-graphql.ts` and import the schema into it: ++makeExample('heroes-graphql/ts/src/app/in-memory-graphql.ts', 'graphql-schema', 'in-memory-graphql.ts') +:marked + next, create your in-memory data: ++makeExample('heroes-graphql/ts/src/app/in-memory-graphql.ts', 'heroes-array', 'in-memory-graphql.ts (excerpt)') +:marked + The next step is writing a server that _resolves_ + the queries from the client based on the schema. + Hence, the GraphQL server consists of _resolver + functions_ that correspond to the _types_ of the schema. + + In some server functions you use the `lodash` library so don't + forget to install them from npm and import them: +code-example(language="sh" class="code-shell"). + npm install lodash --save ++makeExample('heroes-graphql/ts/src/app/in-memory-graphql.ts', 'import-lodash', 'in-memory-graphql.ts (imports)') + +:marked + To create the resolvers, copy the following code and add it to `in-memory-graphql.ts`. ++makeExample('heroes-graphql/ts/src/app/in-memory-graphql.ts', 'resolvers', 'in-memory-graphql.ts (excerpt)') + +.l-sub-section + :marked + For the full explanation about how GraphQL resolvers work see + [Execution](http://graphql.org/learn/execution/) on [GraphQL.org](http://graphql.org/). + +:marked + Notice that the server includes functions that correspond to each + type in the schema _and_ the mutations. + + This mechanism makes writing simple GraphQL servers straightforward—you simply + resolve a specific type of data. + This removes the coupling between the frontend and backend because you don't need to know the specific + query the client makes to create the server implementation. + +:marked + Now, connect the schema to the resolvers with the `makeExecutableSchema` function from + the [graphql-tools](http://dev.apollodata.com/tools/graphql-tools/index.html) library: ++makeExample('heroes-graphql/ts/src/app/in-memory-graphql.ts', 'import-graphql-tools', 'in-memory-graphql.ts (excerpt)') ++makeExample('heroes-graphql/ts/src/app/in-memory-graphql.ts', 'make-executable-schema', 'in-memory-graphql.ts (excerpt)') + +:marked + In the constant `schema`, `makeExecutableSchema` has two properties, + `typeDefs` and `resolvers`. Here, you define them with the `typeDefinitions` + and `resolveFunctions` that you created earlier in `in-memory-graphql.ts`. + This way, your GraphQL server knows where to look for definitions and resolvers. + +:marked + Now that you have an executable schema, execute it using the `graphql` + library and export it so you can use it with the Apollo Client. + First, `npm install`: +code-example(language="sh" class="code-shell"). + npm install graphql --save + +:marked + Next, add an import statement for `execute`. + ++makeExample('heroes-graphql/ts/src/app/in-memory-graphql.ts', 'import-graphql', 'in-memory-graphql.ts (excerpt)') +:marked + Now create a new `networkInterface` class and call it `InBrowserNetworkInterface`. + + This class has a `schema` property which it initializes in the constructor. + + Next, the `query` function takes as an argument the query request and executes + that query using the GraphQL `execute` function against the schema property. + + You send empty objects to the `rootValue` and `contextValue` arguments of the function with `{}` and `{}` respectively + and send the `variables` and `operationName` arguments that are related to the query request. + + Lastly, export the new `InBrowserNetworkInterface` class in order to import it to the Apollo Client. ++makeExample('heroes-graphql/ts/src/app/in-memory-graphql.ts', 'execute-and-export', 'in-memory-graphql.ts (excerpt)') +:marked + Now all that's left is to connect the new in-memory server to the Apollo Client configuration + by importing `networkInterface` and adding it to the `client` constant in `client.ts`. ++makeExample('heroes-graphql/ts/src/app/client.ts', '', 'client.ts') +:marked + That's it. Now you can run your application as if you had a GraphQL server connected to it. + However, there is no persistance—everything is running in-memory in the browser, + so when you refresh the page, all changes will be lost. + + Now that you have a local server set up, you have some options: + * You can store everything on the browser's local-storage using local-storage database libraries. + * You can make the resolver functions call your server's existing REST endpoint. + * You can start a separate Node GraphQL server and simply move the code into it for persistance. + +.l-main-section +:marked + ## Conclusion + + This cookbook covered: + + - What is GraphQL and why it can benefit Angular developers. + - How to create a basic GraphQL query. + - How to create a basic GraphQL mutation. + - How to mock a GraphQL server. + - How to build a GraphQL server. + - Resources to dive deeper. + +.l-main-section + +:marked + ## Further resources + + * [GraphQL.org](http://graphql.org/) is a great website, with the following sections + (by the way, all the examples on the website are runing live, try to edit them in the browser while you reading it): + * [Learn](http://graphql.org/learn/) + * [Implementations in any language](http://graphql.org/code/) + * [Community](http://graphql.org/community/) + * [Apollo Developer resources](http://dev.apollodata.com/) - The [team](http://www.apollodata.com/) behind the Angular GraphQL client, + there you will find a more advanced resources about: + * [Handling updates from the server and managing the local store](http://dev.apollodata.com/angular2/receiving-updates.html) + * [Authentication](http://dev.apollodata.com/angular2/auth.html) + * [Pagination](http://dev.apollodata.com/angular2/pagination.html) + * [Server-side rendering](http://dev.apollodata.com/angular2/server-side-rendering.html) + * and more.. + * [Apollo Dev Blog](https://dev-blog.apollodata.com/) - The most popular GraphQL blog + * [Apollo Client Developer Tools](https://dev-blog.apollodata.com/apollo-client-developer-tools-ff89181ebcf#.n5f3fhbg2) - GraphQL debugging tools for Apollo Client in the Chrome developer console + +.l-main-section + +:marked + ## Full Example + +block file-summary + +makeTabs( + `heroes-graphql/ts/src/app/app.component.ts, + heroes-graphql/ts/src/app/app.module.ts, + heroes-graphql/ts/src/app/heroes.component.ts, + heroes-graphql/ts/src/app/heroes.component.html, + heroes-graphql/ts/src/app/heroes.component.css, + heroes-graphql/ts/src/app/hero-detail.component.ts, + heroes-graphql/ts/src/app/hero-detail.component.html, + heroes-graphql/ts/src/app/in-memory-graphql.ts, + heroes-graphql/ts/src/app/client.ts`, + ',,,,,,,,', + `app.comp...ts, + app.mod...ts, + heroes.comp...ts, + heroes.comp...html, + heroes.comp...css, + hero-detail.comp...ts, + hero-detail.comp...html, + in-memory-graphql.ts, + client.ts` + ) + + +makeTabs( + `heroes-graphql/ts/src/app/app-routing.module.ts, + heroes-graphql/ts/src/app/hero-search.component.ts, + heroes-graphql/ts/src/app/hero-search.component.html, + heroes-graphql/ts/src/app/hero-search.component.css`, + null, + `app-routing.modules.ts, + hero-search.component.ts, + hero-search.component.html, + hero-search.component.css` + ) + +:marked + [Back to top](#top) diff --git a/public/resources/images/cookbooks/heroes-graphql/heroes- graphql-mutation.gif b/public/resources/images/cookbooks/heroes-graphql/heroes- graphql-mutation.gif new file mode 100755 index 0000000000..012f135977 Binary files /dev/null and b/public/resources/images/cookbooks/heroes-graphql/heroes- graphql-mutation.gif differ