diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/README.md b/README.md index 2f0f67d..bfcaac9 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,487 @@ -# vue-apollo -Vue apollo integration +# Apollo in Vue + +Integrates [apollo](http://www.apollostack.com/) in your vue components with declarative queries. + +## Installation + + + npm install vue-apollo + +## Usage + +### Configuration + +```javascript +import Vue from 'vue'; +import ApolloClient from 'apollo-client'; +import VueApollo from 'vue-apollo' + +const apolloClient = new ApolloClient({ + /* ... */ +}); + +Vue.use(VueApollo, { + apolloClient, +}); +``` + +### Usage in components + +To declare apollo queries in your Vue component, add an `apollo` object : + +```javascript +new Vue({ + apollo: { + // Apollo specific options + } +}); +``` + +You can access the [apollo-client](http://docs.apollostack.com/apollo-client/index.html) instance with `this.$apollo.client` in all your vue components. + +### Queries + +In the `data` object, add an attribute for each property you want to feed with the result of an Apollo query. + +#### Simple query + +Put the [gql](http://docs.apollostack.com/apollo-client/core.html#gql) query directly as the value: + +```javascript +apollo: { + // Non-reactive query + data: { + // Simple query that will update the 'hello' vue property + hello: gql`{hello}` + } +} +``` + +Don't forget to initialize your property in your vue component: + +```javascript +data () { + return { + // Initialize your apollo data + hello: '' + } +} +``` + +Server-side, add the corresponding schema and resolver: + +```javascript +export const schema = ` +type Query { + hello: String +} + +schema { + query: Query +} +`; + +export const resolvers = { + Query: { + hello(root, args, context) { + return "Hello world!"; + } + } +}; +``` + +For more info, visit the [apollo doc](http://docs.apollostack.com/apollo-server/index.html). + +You can then use your property as usual in your vue component: + +```html + +``` + +#### Query with parameters + +You can add variables (read parameters) to your `gql` query by declaring `query` and `variables` in an object: + +```javascript +// Apollo-specific options +apollo: { + // Non-reactive query + data: { + // Query with parameters + ping: { + // gql query + query: gql`query PingMessage($message: String!) { + ping(message: $message) + }`, + // Static parameters + variables: { + message: 'Meow' + } + } + } +} +``` + +You can use the following apollo options in the object: + - `forceFetch` + - `fragments` + +See the [apollo doc](http://docs.apollostack.com/apollo-client/queries.html#query) for more details. + +For example, you could add the `forceFetch` apollo option like this: + +```javascript +apollo: { + data: { + // Query with parameters + ping: { + query: gql`query PingMessage($message: String!) { + ping(message: $message) + }`, + variables: { + message: 'Meow' + }, + // Additional options here + forceFetch: true + } + } +} +``` + +Don't forget to initialize your property in your vue component: + +```javascript +data () { + return { + // Initialize your apollo data + ping: '' + } +} +``` + +Server-side, add the corresponding schema and resolver: + +```javascript +export const schema = ` +type Query { + ping(message: String!): String +} + +schema { + query: Query +} +`; + +export const resolvers = { + Query: { + ping(root, { message }, context) { + return `Answering ${message}`; + } + } +}; +``` + +And then use it in your vue component: + +```html + +``` + +#### Reactive parameters + +Use a function instead to make the parameters reactive with vue properties: + +```javascript +// Apollo-specific options +apollo: { + // Non-reactive query + data: { + // Query with parameters + ping: { + query: gql`query PingMessage($message: String!) { + ping(message: $message) + }`, + // Reactive parameters + variables() { + // Use vue reactive properties here + return { + message: this.pingInput + } + } + } + } +} +``` + +This will re-fetch the query each time a parameter changes, for example: + +```html + +``` + +#### Advanced options + +These are the available advanced options you can use: +- `update(data) {return ...}` to customize the value that is set in the vue property, for example if the field names don't match +- `result(data)` is a hook called when a result is received +- `error(errors, type)` is a hook called when there are errors, `type` value can either be `'sending'` or `'execution'` +- `loadingKey` will update the component data property you pass as the value. You should initialize this property to `0` in the component `data()` hook. When the query is loading, this property will be incremented by 1 and as soon as it no longer is, the property will be decremented by 1. That way, the property can represent a counter of currently loading queries. +- `watchLoading(isLoading, countModifier)` is a hook called when the loading state of the query changes. The `countModifier` parameter is either equal to `1` when the query is now loading, or `-1` when the query is no longer loading. + + +```javascript +// Apollo-specific options +apollo: { + // Non-reactive query + data: { + // Advanced query with parameters + // The 'variables' method is watched by vue + pingMessage: { + query: gql`query PingMessage($message: String!) { + ping(message: $message) + }`, + // Reactive parameters + variables() { + // Use vue reactive properties here + return { + message: this.pingInput + } + }, + // We use a custom update callback because + // the field names don't match + // By default, the 'pingMessage' attribute + // would be used on the 'data' result object + // Here we know the result is in the 'ping' attribute + // considering the way the apollo server works + update(data) { + console.log(data); + // The returned value will update + // the vue property 'pingMessage' + return data.ping; + }, + // Optional result hook + result(data) { + console.log("We got some result!"); + }, + // Error handling + error(errors, type) { + console.error(`We've got ${errors.length} errors of type '${type}'`); + }, + // Loading state + // loadingKey is the name of the data property + // that will be incremented when the query is loading + // and decremented when it no longer is. + loadingKey: 'loadingQueriesCount', + // watchLoading will be called whenever the loading state changes + watchLoading(isLoading, countModifier) { + // isLoading is a boolean + // countModifier is either 1 or -1 + } + } + } +} +``` + +If you use ES2015, you can also write the `update` like this: + +```javascript +update: data => data.ping +``` + +### Reactive Queries + +*For now, the reactivity in apollo is quite limited, since you can only do polling.* + +For more info, see the [apollo doc](http://docs.apollostack.com/apollo-client/core.html#watchQuery). + +Add your queries in a `watch` object instead of `data`: + +```javascript +// Apollo-specific options +apollo: { + // Reactive query + watch: { + // 'tags' data property on vue instance + tags: { + query: gql`{ + tags { + id, + label + } + }`, + pollInterval: 300 // ms + } + } +} +``` + +You can use the apollo options, for example: + - `forceFetch` + - `returnPartialData` + - `pollInterval` + - `fragments` + +See the [apollo doc](http://docs.apollostack.com/apollo-client/queries.html#watchQuery) for more details. + +You can also use the advanced options detailed above, like `result` or `watchLoading`. + +Here is how the server-side looks like: + +```javascript +export const schema = ` +type Tag { + id: Int + label: String +} + +type Query { + tags: [Tag] +} + +schema { + query: Query +} +`; + +// Fake word generator +import casual from 'casual'; + +// Let's generate some tags +var id = 0; +var tags = []; +for (let i = 0; i < 42; i++) { + addTag(casual.word); +} + +function addTag(label) { + let t = { + id: id++, + label + }; + tags.push(t); + return t; +} + +export const resolvers = { + Query: { + tags(root, args, context) { + return tags; + } + } +}; +``` + +### Mutations + +Mutations are queries that changes your data state on your apollo server. For more info, visit the [apollo doc](http://docs.apollostack.com/apollo-client/core.html#Mutations). + +```javascript +methods: { + addTag() { + // Mutate the tags data + // You can also use this.$apollo.client.mutate + this.$apollo.mutate({ + mutation: gql`mutation AddTag($label: String!) { + addTag(label: $label) { + id, + label + } + }`, + // Parameters + variables: { + label: this.tagLabel + } + }).then((data) => { + // Result + console.log(data); + this.tagLabel = ''; + }).catch((error) => { + // Error + console.error(error); + }); + } +} +``` + +Server-side: + +```javascript +export const schema = ` +type Tag { + id: Int + label: String +} + +type Query { + tags: [Tag] +} + +type Mutation { + addTag(label: String!): Tag +} + +schema { + query: Query + mutation: Mutation +} +`; + +// Fake word generator +import faker from 'faker'; + +// Let's generate some tags +var id = 0; +var tags = []; +for (let i = 0; i < 42; i++) { + addTag(faker.random.word()); +} + +function addTag(label) { + let t = { + id: id++, + label + }; + tags.push(t); + return t; +} + +export const resolvers = { + Query: { + tags(root, args, context) { + return tags; + } + }, + Mutation: { + addTag(root, { label }, context) { + console.log(`adding tag '${label}'`); + return addTag(label); + } + } +}; +``` + +--- + +LICENCE ISC - Created by Guillaume CHAU (@Akryum) diff --git a/index.js b/index.js new file mode 100644 index 0000000..2f764dd --- /dev/null +++ b/index.js @@ -0,0 +1,2 @@ +import VueApollo from './lib/vue-plugin'; +export default VueApollo; diff --git a/lib/vue-plugin.js b/lib/vue-plugin.js new file mode 100644 index 0000000..a048704 --- /dev/null +++ b/lib/vue-plugin.js @@ -0,0 +1,230 @@ +import _ from 'lodash'; + +let apolloClient = null; + +class DollarApollo { + constructor(vm) { + this.vm = vm; + this.querySubscriptions = {}; + } + + get client() { + return apolloClient; + } + + get query() { + return this.client.query; + } + + watchQuery(options) { + const vm = this.vm; + const observable = this.client.watchQuery(options); + const _subscribe = observable.subscribe.bind(observable); + observable.subscribe = (function(options) { + let sub = _subscribe(options); + vm._apolloSubscriptions.push(sub); + return sub; + }).bind(observable); + return observable; + } + + get mutate() { + return this.client.mutate; + } + + option(key, options, watch) { + const vm = this.vm; + const $apollo = this; + + let query, observer, sub; + let simpleQuery = false; + + let firstLoadingDone = false; + + let loadingKey = options.loadingKey; + let loadingChangeCb = options.watchLoading; + + if (options.pollInterval) { + watch = true; + } + + if (typeof loadingChangeCb === 'function') { + loadingChangeCb = loadingChangeCb.bind(vm); + } + + // Simple query + if (!options.query) { + query = options; + simpleQuery = true; + } + + function generateApolloOptions(variables) { + const apolloOptions = _.omit(options, [ + 'variables', + 'watch', + 'update', + 'result', + 'error', + 'loadingKey', + 'watchLoading', + ]); + apolloOptions.variables = variables; + return apolloOptions; + } + + function q(variables) { + applyLoadingModifier(1); + + if (simpleQuery) { + + $apollo.query({ + query + }).then(nextResult).catch(catchError); + + } else if (watch) { + + if (options.forceFetch && observer) { + // Refresh query + observer.refetch(variables, { + forceFetch: !!options.forceFetch + }); + } else { + if (sub) { + sub.unsubscribe(); + } + + // Create observer + observer = $apollo.watchQuery(generateApolloOptions(variables)); + + // Create subscription + sub = observer.subscribe({ + next: nextResult, + error: catchError + }); + } + + } else { + + $apollo.query(generateApolloOptions(variables)).then(nextResult).catch(catchError); + + } + } + + if (typeof options.variables === 'function') { + vm.$watch(options.variables.bind(vm), q, { + immediate: true + }); + } else { + q(options.variables); + } + + function nextResult({ data }) { + applyData(data); + } + + function applyData(data) { + loadingDone(); + + console.log(data, key); + + if (typeof options.update === 'function') { + vm[key] = options.update.call(vm, data); + } else if (data[key] === undefined) { + console.error(`Missing ${key} attribute on result`, data); + } else { + vm[key] = data[key]; + } + + if (typeof options.result === 'function') { + options.result.call(vm, data); + } + } + + function applyLoadingModifier(value) { + if (loadingKey) { + vm.$set(loadingKey, vm.$get(loadingKey) + value); + } + + if (loadingChangeCb) { + loadingChangeCb(value === 1, value); + } + } + + function loadingDone() { + if (!firstLoadingDone) { + applyLoadingModifier(-1); + firstLoadingDone = true; + } + } + + function catchError(error) { + loadingDone(); + + if (error.graphQLErrors && error.graphQLErrors.length !== 0) { + console.error(`GraphQL execution errors for query ${query}`); + for (let e of error.graphQLErrors) { + console.error(e); + } + } else if (error.networkError) { + console.error(`Error sending the query ${query}`, error.networkError); + } else { + console.error(error); + } + + if (typeof options.error === 'function') { + options.error(error); + } + } + } +} + +function prepare() { + this._apolloSubscriptions = []; + + this.$apollo = new DollarApollo(this); + + let apollo = this.$options.apollo; + + if (apollo) { + // One-time queries with $query(), called each time a Vue dependency is updated (using $watch) + if (apollo.data) { + for (let key in apollo.data) { + this.$apollo.option(key, apollo.data[key], false); + } + } + + // Auto updating queries with $watchQuery(), re-called each time a Vue dependency is updated (using $watch) + if (apollo.watch) { + for (let key in apollo.watch) { + this.$apollo.option(key, apollo.watch[key], true); + } + } + } +} + +export default { + install(Vue, options) { + + apolloClient = options.apolloClient; + + Vue.mixin({ + + // Vue 1.x + beforeCompile: prepare, + // Vue 2.x + beforeCreate: prepare, + + destroyed: function() { + this._apolloSubscriptions.forEach((sub) => { + sub.unsubscribe(); + }); + this._apolloSubscriptions = null; + if (this.$apollo) { + this.$apollo = null; + } + } + + }); + + } +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..1e76461 --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "vue-apollo", + "version": "1.0.0", + "description": "Vue apollo integration", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Akryum/vue-apollo.git" + }, + "keywords": [ + "vue", + "apollo", + "graphql" + ], + "author": "Guillaume Chau ", + "license": "ISC", + "bugs": { + "url": "https://github.com/Akryum/vue-apollo/issues" + }, + "homepage": "https://github.com/Akryum/vue-apollo#readme", + "dependencies": { + "lodash": "^4.15.0" + } +}