From cc6cf8e6b28620faf7a779201007920e3ea28478 Mon Sep 17 00:00:00 2001 From: Kamil Kisiela Date: Fri, 12 Aug 2016 20:08:05 +0200 Subject: [PATCH 1/3] feat(angular2Apollo): add observeVariables --- CHANGELOG.md | 1 + global.d.ts | 5 ++ package.json | 3 +- src/angular2Apollo.ts | 38 +++++++++- src/apolloQueryObservable.ts | 23 ++---- src/utils/observableQuery.ts | 34 +++++++++ src/utils/observeVariables.ts | 41 ++++++++++ tests/_mocks.ts | 11 +-- tests/angular2Apollo.ts | 130 +++++++++++++++++++++++++++++--- tests/apolloQueryObservable.ts | 17 ++++- tests/index.ts | 3 +- tests/utils/index.ts | 1 + tests/utils/observeVariables.ts | 61 +++++++++++++++ 13 files changed, 323 insertions(+), 45 deletions(-) create mode 100644 src/utils/observableQuery.ts create mode 100644 src/utils/observeVariables.ts create mode 100644 tests/utils/index.ts create mode 100644 tests/utils/observeVariables.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 39632f3fc..6edc8e6d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ### vNEXT - Added `ApolloModule` (with RC5 of Angular2 comes NgModules) ([PR #63](https://github.com/apollostack/angular2-apollo/pull/63)) +- Added ability to use query variables as observables. With this, the query can be automatically re-run when those obserables emit new values. ([PR #64]((https://github.com/apollostack/angular2-apollo/pull/64))) ### v0.4.2 diff --git a/global.d.ts b/global.d.ts index 2a59d31e0..4374c0b98 100644 --- a/global.d.ts +++ b/global.d.ts @@ -12,3 +12,8 @@ declare module 'lodash.assign' { import main = require('~lodash/index'); export = main.assign; } + +declare module 'lodash.omit' { + import main = require('~lodash/index'); + export = main.omit; +} diff --git a/package.json b/package.json index e2081ba67..df456af00 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,8 @@ "dependencies": { "lodash.assign": "^4.0.9", "lodash.forin": "^4.2.0", - "lodash.isequal": "^4.2.0" + "lodash.isequal": "^4.2.0", + "lodash.omit": "^4.4.1" }, "devDependencies": { "@angular/common": "^2.0.0-rc.5", diff --git a/src/angular2Apollo.ts b/src/angular2Apollo.ts index 32eaae074..0cf121200 100644 --- a/src/angular2Apollo.ts +++ b/src/angular2Apollo.ts @@ -8,10 +8,23 @@ import { Inject, } from '@angular/core'; +import 'rxjs/add/operator/switchMap'; + +import assign = require('lodash.assign'); +import omit = require('lodash.omit'); + import { ApolloQueryObservable, } from './apolloQueryObservable'; +import { + ObservableQueryRef, +} from './utils/observableQuery'; + +import { + observeVariables, +} from './utils/observeVariables'; + export const angularApolloClient = new OpaqueToken('AngularApolloClient'); export const defaultApolloClient = (client: ApolloClient): Provider => { return provide(angularApolloClient, { @@ -23,12 +36,29 @@ export const defaultApolloClient = (client: ApolloClient): Provider => { export class Angular2Apollo { constructor( @Inject(angularApolloClient) private client: any - ) { - - } + ) {} public watchQuery(options): ApolloQueryObservable { - return new ApolloQueryObservable(this.client.watchQuery(options)); + const apolloRef = new ObservableQueryRef(); + if (typeof options.variables === 'object') { + const varObs = observeVariables(options.variables); + + return new ApolloQueryObservable(apolloRef, subscriber => { + const sub = varObs.switchMap(newVariables => { + const cleanOptions = omit(options, 'variables'); + const newOptions = assign(cleanOptions, { variables: newVariables }); + + apolloRef.apollo = this.client.watchQuery(newOptions); + + return apolloRef.apollo; + }).subscribe(subscriber); + + return () => sub.unsubscribe(); + }); + } + + apolloRef.apollo = this.client.watchQuery(options); + return new ApolloQueryObservable(apolloRef); } public query(options) { diff --git a/src/apolloQueryObservable.ts b/src/apolloQueryObservable.ts index c8301dd79..ed6654c02 100644 --- a/src/apolloQueryObservable.ts +++ b/src/apolloQueryObservable.ts @@ -2,13 +2,11 @@ import { Observable } from 'rxjs/Observable'; import { Subscriber } from 'rxjs/Subscriber'; import { Subscription } from 'rxjs/Subscription'; import { Operator } from 'rxjs/Operator'; -import { $$observable } from 'rxjs/symbol/observable'; -import { FetchMoreOptions } from 'apollo-client/ObservableQuery'; -import { FetchMoreQueryOptions } from 'apollo-client/watchQueryOptions'; +import { ObservableQueryRef, IObservableQuery } from './utils/observableQuery'; -export class ApolloQueryObservable extends Observable { - constructor(public apollo: any, subscribe?: (subscriber: Subscriber) => Subscription | Function | void) { +export class ApolloQueryObservable extends Observable implements IObservableQuery { + constructor(public apollo: ObservableQueryRef, subscribe?: (subscriber: Subscriber) => Subscription | Function | void) { super(subscribe); } @@ -23,26 +21,19 @@ export class ApolloQueryObservable extends Observable { // apollo-specific methods - public refetch(variables?: any): Promise { + public refetch(variables) { return this.apollo.refetch(variables); } - public stopPolling(): void { + public stopPolling() { return this.apollo.stopPolling(); } - public startPolling(p: number): void { + public startPolling(p) { return this.apollo.startPolling(p); } - public fetchMore(options: FetchMoreQueryOptions & FetchMoreOptions): Promise { + public fetchMore(options) { return this.apollo.fetchMore(options); } - - // where magic happens - - protected _subscribe(subscriber: Subscriber) { - const apollo = this.apollo; - return apollo[$$observable]().subscribe(subscriber); - } } diff --git a/src/utils/observableQuery.ts b/src/utils/observableQuery.ts new file mode 100644 index 000000000..0e4affcdb --- /dev/null +++ b/src/utils/observableQuery.ts @@ -0,0 +1,34 @@ +import { + ObservableQuery, +} from 'apollo-client/ObservableQuery'; + +import { + ApolloQueryResult, +} from 'apollo-client'; + +export interface IObservableQuery { + refetch: (variables?: any) => Promise; + fetchMore: (options: any) => Promise; + stopPolling: () => void; + startPolling: (p: number) => void; +} + +export class ObservableQueryRef implements IObservableQuery { + public apollo: ObservableQuery; + + public refetch(variables) { + return this.apollo.refetch(variables); + } + + public stopPolling() { + return this.apollo.stopPolling(); + } + + public startPolling(p) { + return this.apollo.startPolling(p); + } + + public fetchMore(options) { + return this.apollo.fetchMore(options); + } +} diff --git a/src/utils/observeVariables.ts b/src/utils/observeVariables.ts new file mode 100644 index 000000000..6aa526122 --- /dev/null +++ b/src/utils/observeVariables.ts @@ -0,0 +1,41 @@ +import { Observable } from 'rxjs/Observable'; +import { Observer } from 'rxjs/Observer'; + +import 'rxjs/add/observable/combineLatest'; + +export function observeVariables(variables?: Object): Observable { + const keys = Object.keys(variables); + + return Observable.create((observer: Observer) => { + Observable.combineLatest(mapVariablesToObservables(variables)) + .subscribe((values) => { + const resultVariables = {}; + + values.forEach((value, i) => { + const key = keys[i]; + resultVariables[key] = value; + }); + + observer.next(resultVariables); + }); + }); +}; + +function mapVariablesToObservables(variables?: Object) { + return Object.keys(variables) + .map(key => getVariableToObservable(variables[key])); +} + +function getVariableToObservable(variable: any | Observable) { + if (variable instanceof Observable) { + return variable; + } else if (typeof variable !== 'undefined') { + return new Observable(subscriber => { + subscriber.next(variable); + }); + } else { + return new Observable(subscriber => { + subscriber.next(undefined); + }); + } +} diff --git a/tests/_mocks.ts b/tests/_mocks.ts index 3e6945aa4..98f0eea3c 100644 --- a/tests/_mocks.ts +++ b/tests/_mocks.ts @@ -15,8 +15,6 @@ import { print, } from 'graphql-tag/printer'; -// Pass in multiple mocked responses, so that you can test flows that end up -// making multiple queries to the server export default function mockNetworkInterface( ...mockedResponses: MockedResponse[] ): NetworkInterface { @@ -25,7 +23,7 @@ export default function mockNetworkInterface( export function mockBatchedNetworkInterface( ...mockedResponses: MockedResponse[] -): NetworkInterface { +): BatchedNetworkInterface { return new MockBatchedNetworkInterface(...mockedResponses); } @@ -71,9 +69,8 @@ export class MockNetworkInterface implements NetworkInterface { const key = requestToKey(parsedRequest); const responses = this.mockedResponsesByKey[key]; - if (!responses || responses.length === 0) { - throw new Error('No more mocked responses for the query: ' + print(request.query)); + throw new Error(`No more mocked responses for the query: ${print(request.query)}, variables: ${JSON.stringify(request.variables)}`); } const { result, error, delay } = responses.shift(); @@ -92,7 +89,6 @@ export class MockNetworkInterface implements NetworkInterface { }); } } - export class MockBatchedNetworkInterface extends MockNetworkInterface implements BatchedNetworkInterface { public batchQuery(requests: Request[]): Promise { @@ -100,14 +96,11 @@ extends MockNetworkInterface implements BatchedNetworkInterface { requests.forEach((request) => { resultPromises.push(this.query(request)); }); - return Promise.all(resultPromises); } } - function requestToKey(request: ParsedRequest): string { const queryString = request.query && print(request.query); - return JSON.stringify({ variables: request.variables, debugName: request.debugName, diff --git a/tests/angular2Apollo.ts b/tests/angular2Apollo.ts index 8b4bfe89c..cd41b3131 100644 --- a/tests/angular2Apollo.ts +++ b/tests/angular2Apollo.ts @@ -2,6 +2,12 @@ import { Provider, ReflectiveInjector, } from '@angular/core'; +import { + Subject, +} from 'rxjs/Subject'; + +import 'rxjs/add/operator/map'; + import { mockClient, } from './_mocks'; @@ -20,8 +26,6 @@ import { ApolloQueryObservable, } from '../src/apolloQueryObservable'; -import ApolloClient from 'apollo-client'; - import gql from 'graphql-tag'; const query = gql` @@ -40,12 +44,40 @@ const data = { }, }; -const client = mockClient({ - request: { query }, - result: { data }, -}); +const data2 = { + allHeroes: { + heroes: [{ name: 'Mrs Foo' }, { name: 'Mrs Bar' }], + }, +}; + +const data3 = { + allHeroes: { + heroes: [{ name: 'Mr Bar' }], + }, +}; describe('angular2Apollo', () => { + let client; + + beforeEach(() => { + client = mockClient({ + request: { query }, + result: { data }, + }, { + request: { query, variables: { foo: 'Foo' } }, + result: { data: data2 }, + }, { + request: { query, variables: { foo: 'Bar' } }, + result: { data: data3 }, + }, { + request: { query, variables: { foo: 'Foo', bar: 'Bar' } }, + result: { data: data2 }, + }, { + request: { query, variables: { foo: 'Foo', bar: 'Baz' } }, + result: { data: data3 }, + }); + }); + describe('Angular2Apollo', () => { let angular2Apollo; @@ -65,14 +97,90 @@ describe('angular2Apollo', () => { expect(client.watchQuery).toHaveBeenCalledWith(options); }); - describe('result', () => { - let obs; + it('should be able to use obserable variable', (done) => { + const variables = { + foo: new Subject(), + }; + // XXX forceFetch? see https://github.com/apollostack/apollo-client/issues/535 + const options = { query, variables, forceFetch: true }; + let calls = 0; + + angular2Apollo + .watchQuery(options) + .map(result => result.data) + .subscribe((result) => { + calls++; + if (calls === 1) { + expect(result).toEqual(data2); + } else if (calls === 2) { + expect(result).toEqual(data3); + done(); + } + }); + + variables.foo.next('Foo'); + + setTimeout(() => { + variables.foo.next('Bar'); + }, 200); + }); + + it('should be able to use obserable variables', (done) => { + const variables = { + foo: new Subject(), + bar: new Subject(), + }; + // XXX forceFetch? see https://github.com/apollostack/apollo-client/issues/535 + const options = { query, variables, forceFetch: true }; + let calls = 0; + + angular2Apollo + .watchQuery(options) + .map(result => result.data) + .subscribe((result) => { + calls++; + if (calls === 1) { + expect(result).toEqual(data2); + } else if (calls === 2) { + expect(result).toEqual(data3); + done(); + } + }); + + variables.foo.next('Foo'); + variables.bar.next('Bar'); + + setTimeout(() => { + variables.bar.next('Baz'); + }, 200); + }); + + it('should be able to refetch', (done) => { + const variables = { foo: 'foo' }; + const options = { query, variables }; + + const obs = angular2Apollo + .watchQuery(options); - beforeEach(() => { - obs = angular2Apollo.watchQuery({ query }); + obs.subscribe(() => {}); + + obs.refetch({ foo: 'Bar' }).then(result => { + expect(result.data).toEqual(data3); + done(); + }); + }); + + describe('result', () => { + it('should return the ApolloQueryObserable when no variables', () => { + const obs = angular2Apollo.watchQuery({ query }); + expect(obs instanceof ApolloQueryObservable).toEqual(true); }); - it('should return the ApolloQueryObserable', () => { + it('should return the ApolloQueryObserable when variables', () => { + const variables = { + foo: new Subject(), + }; + const obs = angular2Apollo.watchQuery({ query, variables }); expect(obs instanceof ApolloQueryObservable).toEqual(true); }); }); diff --git a/tests/apolloQueryObservable.ts b/tests/apolloQueryObservable.ts index 395eb3544..c825c1041 100644 --- a/tests/apolloQueryObservable.ts +++ b/tests/apolloQueryObservable.ts @@ -2,11 +2,16 @@ import { ApolloQueryObservable, } from '../src/apolloQueryObservable'; +import { + ObservableQueryRef, +} from '../src/utils/observableQuery'; + import { mockClient, } from './_mocks'; import gql from 'graphql-tag'; +import ApolloClient from 'apollo-client'; import 'rxjs/add/operator/map'; @@ -26,17 +31,23 @@ const data = { }; describe('ApolloQueryObservable', () => { + let apolloRef: ObservableQueryRef; let obsApollo; - let obsQuery; - let client; + let obsQuery: ApolloQueryObservable; + let client: ApolloClient; beforeEach(() => { client = mockClient({ request: { query }, result: { data }, }); + apolloRef = new ObservableQueryRef(); obsApollo = client.watchQuery({ query }); - obsQuery = new ApolloQueryObservable(obsApollo); + apolloRef.apollo = obsApollo; + obsQuery = new ApolloQueryObservable(apolloRef, subscriber => { + const sub = obsApollo.subscribe(subscriber); + return () => sub.unsubscribe(); + }); }); describe('regular', () => { diff --git a/tests/index.ts b/tests/index.ts index d28b9c9ae..ed7f91272 100644 --- a/tests/index.ts +++ b/tests/index.ts @@ -4,6 +4,7 @@ import 'reflect-metadata'; // tests import './angular2Apollo'; +import './utils'; import './apolloQueryObservable'; import './apolloQueryPipe'; -import './apolloDecorator/index'; +import './apolloDecorator'; diff --git a/tests/utils/index.ts b/tests/utils/index.ts new file mode 100644 index 000000000..a5313c421 --- /dev/null +++ b/tests/utils/index.ts @@ -0,0 +1 @@ +import './observeVariables'; diff --git a/tests/utils/observeVariables.ts b/tests/utils/observeVariables.ts new file mode 100644 index 000000000..b4594e2ba --- /dev/null +++ b/tests/utils/observeVariables.ts @@ -0,0 +1,61 @@ +import { Subject } from 'rxjs/Subject'; +import { Observable } from 'rxjs/Observable'; + +import 'rxjs/add/observable/of'; +import 'rxjs/add/operator/switch'; + +import { observeVariables } from '../../src/utils/observeVariables'; + +describe('observeVariables', () => { + it('should be able to subscribe to all Subjects', (done) => { + const variables = { + foo: new Subject(), + bar: new Subject(), + }; + + observeVariables(variables).subscribe(values => { + expect(values).toEqual({ + foo: 'fooV', + bar: 'barV', + }); + done(); + }); + + variables.foo.next('fooV'); + variables.bar.next('barV'); + }); + + it('should be able to handle no Subjects', (done) => { + const variables = { + foo: 'fooV', + bar: 'barV', + }; + + observeVariables(variables).subscribe(values => { + expect(values).toEqual({ + foo: 'fooV', + bar: 'barV', + }); + done(); + }); + }); + + it('should be able to handle values mixed with Subjects and undefined', (done) => { + const variables = { + foo: 'fooV', + bar: new Subject(), + baz: undefined, + }; + + observeVariables(variables).subscribe(values => { + expect(values).toEqual({ + foo: 'fooV', + bar: 'barV', + baz: undefined, + }); + done(); + }); + + variables.bar.next('barV'); + }); +}); From 9f73d329e779f78c0ac2aa3513e02388fdfc576f Mon Sep 17 00:00:00 2001 From: Kamil Kisiela Date: Sat, 13 Aug 2016 11:36:11 +0200 Subject: [PATCH 2/3] docs(examples): use observable variables in the HelloWorld --- .../client/imports/app.component.html | 2 +- .../client/imports/app.component.ts | 29 ++++++++++++++----- .../hello-world/client/imports/app.module.ts | 3 +- 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/examples/hello-world/client/imports/app.component.html b/examples/hello-world/client/imports/app.component.html index 32ac6d206..41fe87369 100644 --- a/examples/hello-world/client/imports/app.component.html +++ b/examples/hello-world/client/imports/app.component.html @@ -7,7 +7,7 @@

Add new

List

- +
  • diff --git a/examples/hello-world/client/imports/app.component.ts b/examples/hello-world/client/imports/app.component.ts index e8adeb895..1618a12ec 100644 --- a/examples/hello-world/client/imports/app.component.ts +++ b/examples/hello-world/client/imports/app.component.ts @@ -1,11 +1,15 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, AfterViewInit } from '@angular/core'; +import { FormControl } from '@angular/forms'; import { Angular2Apollo, ApolloQueryPipe, ApolloQueryObservable } from 'angular2-apollo'; import { ApolloQueryResult } from 'apollo-client'; +import { Subject } from 'rxjs/Subject'; import { User } from './user.interface'; import gql from 'graphql-tag'; +import 'rxjs/add/operator/debounceTime'; + import template from './app.component.html'; interface Data { @@ -17,15 +21,16 @@ interface Data { template, pipes: [ApolloQueryPipe], }) -export class AppComponent implements OnInit { - data: ApolloQueryObservable; - firstName: string; - lastName: string; - nameFilter: string; +export class AppComponent implements OnInit, AfterViewInit { + public data: ApolloQueryObservable; + public firstName: string; + public lastName: string; + public nameControl = new FormControl(); + public nameFilter: Subject = new Subject(); constructor(private angular2Apollo: Angular2Apollo) {} - ngOnInit() { + public ngOnInit() { this.data = this.angular2Apollo.watchQuery({ query: gql` query getUsers($name: String) { @@ -43,9 +48,17 @@ export class AppComponent implements OnInit { name: this.nameFilter, }, }); + + this.nameControl.valueChanges.debounceTime(300).subscribe(name => { + this.nameFilter.next(name); + }); + } + + public ngAfterViewInit() { + this.nameFilter.next(null); } - newUser(firstName: string) { + public newUser(firstName: string) { this.angular2Apollo.mutate({ mutation: gql` mutation addUser( diff --git a/examples/hello-world/client/imports/app.module.ts b/examples/hello-world/client/imports/app.module.ts index 650da81e4..141befbd1 100644 --- a/examples/hello-world/client/imports/app.module.ts +++ b/examples/hello-world/client/imports/app.module.ts @@ -1,6 +1,6 @@ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; -import { FormsModule } from '@angular/forms'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { ApolloModule } from 'angular2-apollo'; import { AppComponent } from './app.component'; @@ -11,6 +11,7 @@ import { client } from './client'; imports: [ BrowserModule, FormsModule, + ReactiveFormsModule, ApolloModule.withClient(client), ], bootstrap: [ AppComponent ], From 50b30a96172f6751a47bb7c7b90fe6447b753e6c Mon Sep 17 00:00:00 2001 From: Kamil Kisiela Date: Mon, 15 Aug 2016 10:57:58 +0200 Subject: [PATCH 3/3] WIP: Add typings back --- src/apolloQueryObservable.ts | 9 +++++---- src/utils/observableQuery.ts | 8 ++++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/apolloQueryObservable.ts b/src/apolloQueryObservable.ts index ed6654c02..dddf0e64f 100644 --- a/src/apolloQueryObservable.ts +++ b/src/apolloQueryObservable.ts @@ -2,6 +2,7 @@ import { Observable } from 'rxjs/Observable'; import { Subscriber } from 'rxjs/Subscriber'; import { Subscription } from 'rxjs/Subscription'; import { Operator } from 'rxjs/Operator'; +import { ApolloQueryResult } from 'apollo-client'; import { ObservableQueryRef, IObservableQuery } from './utils/observableQuery'; @@ -21,19 +22,19 @@ export class ApolloQueryObservable extends Observable implements IObservab // apollo-specific methods - public refetch(variables) { + public refetch(variables?: any): Promise { return this.apollo.refetch(variables); } - public stopPolling() { + public stopPolling(): void { return this.apollo.stopPolling(); } - public startPolling(p) { + public startPolling(p: number): void { return this.apollo.startPolling(p); } - public fetchMore(options) { + public fetchMore(options: any): Promise { return this.apollo.fetchMore(options); } } diff --git a/src/utils/observableQuery.ts b/src/utils/observableQuery.ts index 0e4affcdb..d683ade0e 100644 --- a/src/utils/observableQuery.ts +++ b/src/utils/observableQuery.ts @@ -16,19 +16,19 @@ export interface IObservableQuery { export class ObservableQueryRef implements IObservableQuery { public apollo: ObservableQuery; - public refetch(variables) { + public refetch(variables?: any): Promise { return this.apollo.refetch(variables); } - public stopPolling() { + public stopPolling(): void { return this.apollo.stopPolling(); } - public startPolling(p) { + public startPolling(p: number): void { return this.apollo.startPolling(p); } - public fetchMore(options) { + public fetchMore(options: any): Promise { return this.apollo.fetchMore(options); } }