README
Note: For Polymer 1.x checkout branch 1.x
Polymer-Apollo
Examples
GitHunt-Polymer - An example of a client-side app built with Polymer and Apollo Client.
NEWS App - A news app built using polymer-apollo
Polymer Apollo Frontpage App - Polymer Apollo Hello World app
Polymer Apollo Meteor App - Github api app using polymer-apollo meteor and synthesis
Table of contents
- Installation
- Configuration
- Usage in components
- Queries
- Fragments
- Mutations
- Subscriptions
- Pagination with
fetchMore
Installation
npm install --save polymer-apollo apollo-client
Usage
Configuration
//config.js
import ApolloClient, { createNetworkInterface, addTypename } from 'apollo-client';
// Create the apollo client
export const apolloClient = new ApolloClient({
networkInterface: createNetworkInterface({
uri: 'http://localhost:8080/graphql',
transportBatching: true,
})
});
Usage in components
//my-element.js
// if you want the es5 compiled version use
// import { PolymerApolloMixin } from 'polymer-apollo/es5';
import { PolymerApolloMixin } from 'polymer-apollo';
import { apolloClient } from './config.js';
class MyElement extends PolymerApolloMixin({apolloClient}, Polymer.Element) {
static get is() {
return 'my-element'
}
get apollo() {
// Apollo specific options
}
...
});
You can access the apollo-client instance with this.$apollo.client
in all your polymer components.
Queries
In the apollo
object, add an attribute for each property you want to feed with the result of an Apollo query.
Simple query
Use gql
to write your GraphQL queries:
import gql from 'graphql-tag';
Put the gql query directly as the value:
get apollo() {
return {
// Simple query that will update the 'hello' polymer property
hello: gql`{hello}`,
};
}
Don't forget to initialize your property in your polymer component:
//my-element.js
...
properties : {
// Initialize your apollo data
hello: String,
},
...
Server-side, add the corresponding schema and resolver:
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.
You can then use your property as usual in your polymer component:
<!--my-element.js-->
<dom-module id="my-element">
<template>
<div class="apollo">
<h3>Hello</h3>
<p>
{{hello}}
</p>
</div>
</template>
</dom-module>
Query with parameters
You can add variables (read parameters) to your gql
query by declaring query
and variables
in an object:
Options can be computed properties or static.
Initial values of variables should be given if options are computed, since polymer options wont be triggered if arguments are undefined, which maybe the case during first rendering.
eg
...
static get properties() {
return {
computedProp: {
type: Object,
computed: 'computeFn(prop1, prop2)'
}
};
}
get apollo() {
return {
query1: {
query: someQuery,
// watches prop1 and prop2 changes
// you should either initialize values of prop1,prop2 == !undefined or
// set an initial value to variables property
variables: {
var1: 'blah',
var2: 'blah blah',
},
options: 'computedProp',
}
};
}
computeFn: function(prop1, prop2) {
return { variables: { var1: prop1, var2: prop2 + 10 } };
}
...
. In this graphql variables var1 and var2 change when the polymer properties prop1 and prop2 change (similar to computed feature);
...
static get properties() {
return {
computedProp: {
type: Object,
computed: 'computeFn(prop1, prop2)'
}
};
}
// Apollo-specific options
get apollo() {
return {
// Query with parameters
ping: {
// gql query
query: gql`query PingMessage($message: String!) {
ping(message: $message)
}`,
options: 'computedProp',
variables: {
message: '',
},
},
};
}
computedFn: function(prop1, prop2) {
return { variables: { message: `${prop1} ping...`} };
}
In the above example you can use the apollo watchQuery
options in the property ping or in the computed function return, like:
forceFetch
fragments
returnPartialData
pollInterval
- ...
See the apollo doc for more details.
For example, you could add the forceFetch
apollo option like this:
static get properties() {
return {
computedProp: {
type: Object,
computed: 'computeFn(prop1, prop2)'
}
};
}
get apollo() {
// Query with parameters
return {
pingQuery: {
query: gql`query PingMessage($message: String!) {
ping(message: $message)
}`,
options: 'computedProp',
variables: {
message: 'blah',
},
skip: true,
// Additional options here. static.
forceFetch: true,
};
}
}
computedFn: function(prop1, prop2) {
// Additional options if added here becomes reactive
return {
variables: {
message: prop1,
},
skip: prop2,
};
}
Don't forget to initialize your property in your polymer component.
//my-element.js
...
static get properties() {
// Initialize your apollo data
ping: String,
},
...
Server-side, add the corresponding schema and resolver:
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 polymer component:
<dom-module id="my-element">
<template>
<div class="apollo">
<h3>Ping</h3>
<p>
{{ping}}
</p>
</div>
</template>
</dom-module>
options
Options can be added in two ways - computed and static.
For computed options you may want to give initial values since
computing function is not invoked until all dependent properties are defined (!== undefined). So each dependent properties should have a default value defined in properties (or otherwise be initialized to a non-undefined value) to ensure the property is computed.
from polymer doc https://www.polymer-project.org/1.0/docs/devguide/observers#computed-properties
computed eg.
...
static get properties() {
return {
computedProp: {
type: Object,
computed: 'computeFn(prop1, prop2)'
}
};
}
get apollo() {
return {
// Query with parameters
ping: {
query: gql`query PingMessage($message: String!) {
ping(message: $message)
}`,
options: 'computedProp',
// Additional options here. static.
forceFetch: true,
};
}
}
computedFn: function(prop1, prop2) {
// Additional options if added here becomes reactive
return {
variables: {
message: prop1,
},
skip: prop2,
};
}
static eg.
get apollo() {
return {
// Query with parameters
ping: {
query: gql`query PingMessage($message: String!) {
ping(message: $message)
}`,
options: {
variables: {
message: 'hai',
},
skip: true,
},
},
// Additional options here. static. you can add skip here also
forceFetch: true,
};
}
Advanced Options
Options
You can add these to options/directly to the ping property in the above example if you dont want them to be polymer reactive.
skip
Used to set the state of the query subscribtion. Check example below.loadingKey
will update the component data property you pass as the value. You should initialize this property tofalse
in properties. When the query is loading, this property will be set totrue
and as soon as it no longer is, the property will be set tofalse
.
Hooks
These are the available advanced options you can use:
error(error)
is a hook called when there are errors,error
being an Apollo error object with either agraphQLErrors
property or anetworkError
property.success(result)
is a hook called when query/subscription returns successfully. Note. result = { data, loading, networkStatus}watchLoading(isLoading)
is a hook called when the loading state of the query changes.
...
static get properties() {
return {
computedProp: {
type: Object,
computed: 'computeFn(prop1, prop2)'
}
};
}
// Apollo-specific options
get apollo() {
return {
// Advanced query with parameters
pingMessage: {
query: gql`query PingMessage($message: String!) {
ping(message: $message)
}`,
// Reactive parameters
options: 'computedProp',
// Loading state
// loadingKey is the name of the data property
// that will be unset when the query is loading
// and set when it no longer is.
loadingKey: 'loadingQueriesCount',
// Error handling
error(error) {
console.error('We\'ve got an error!', error);
},
success(result) {
console.error('Success.!', result); // result is of the format { data, loading, networkStatus };
},
// watchLoading will be called whenever the loading state changes
watchLoading(isLoading) {
// isLoading is a boolean
},
},
};
}
computedFn: function(prop1, prop2) {
return { variables: { message: `${prop1} ping...`}, skip: prop2 };
}
Refetch Query
Use $apollo.refetch(key);
<paper-icon-button on-tap="refetchTags" icon="refresh"></paper-icon-button>
// Apollo-specific options
{
// 'tags' property of your polymer element
tags: {
query: gql`query tagList {
tags {
id,
label
}
}`,
},
},
refetchTags(){
this.$apollo.refetch("tags");
}
Reactive Query Example
Here is a reactive query example using polling:
...
static get properties() {
return {
computedProp: {
type: Object,
computed: 'computeFn(prop1, prop2)'
}
};
}
// Apollo-specific options
get apollo() {
return {
// 'tags' property of your polymer element
tags: {
query: gql`query tagList {
tags {
id,
label
}
}`,
options: 'computedProp',
},
};
}
computedFn: function(prop1, prop2) {
return {
variables: { var1: prop1 },
pollInterval: prop2, // ms
};
}
Here is how the server-side looks like:
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;
},
},
};
Skip query example
...
static get properties() {
return {
computedProp: {
type: Object,
computed: 'computeFn(prop1, prop2)'
}
};
}
// Apollo-specific options
get apollo() {
return {
// 'tags' property of your polymer element
tags: {
query: gql`query tagList {
tags {
id,
label
}
}`,
options: 'computedProp',
},
};
}
computedFn: function(prop1, prop2) {
return {
variables: { var1: prop1 },
skip: prop2, // Boolean
};
}
Fragments
import gql from 'graphql-tag';
const fragment = gql`fragment CommonFields on tags {
id,
label
}`;
Embed the fragment in your query document directly with:
import gql from 'graphql-tag';
// Apollo-specific options
return {
// 'tags' property of your polymer element
tags: {
query: gql`query tagList {
tags: tags(rate: 0) {
...CommonFields
},
besttags: tags(rate: 10) {
...CommonFields
}
}
${fragment}`
},
}
Mutations
Mutations are queries that changes your data state on your apollo server. For more info, visit the apollo doc.
addTag() {
// We save the user input in case of an error
const newTag = this.newTag;
// We clear it early to give the UI a snappy feel
this.newTag = '';
// Call to the graphql mutation
this.$apollo.mutate({
// Query
mutation: gql`mutation ($label: String!) {
addTag(label: $label) {
id
label
}
}`,
// Parameters
variables: {
label: newTag,
},
// Update the cache with the result
// 'tagList' is the name of the query declared before
// that will be updated with the optimistic response
// and the result of the mutation
updateQueries: {
tagList: (previousQueryResult, { mutationResult }) => {
// We incorporate any received result (either optimistic or real)
// into the 'tagList' query we set up earlier
return {
tags: [...previousQueryResult.tags, mutationResult.data.addTag],
};
},
},
// Optimistic UI
// Will be treated as a 'fake' result as soon as the request is made
// so that the UI can react quickly and the user be happy
optimisticResponse: {
__typename: 'Mutation',
addTag: {
__typename: 'Tag',
id: -1,
label: newTag,
},
},
}).then((data) => {
// Result
console.log(data);
}).catch((error) => {
// Error
console.error(error);
// We restore the initial user input
this.set("newTag",newTag);
});
},
},
Server-side:
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);
},
},
};
Subscriptions
To make enable the websocket-based subscription, a bit of additional setup is required:
import ApolloClient, { createNetworkInterface } from 'apollo-client';
// New Imports
import { Client } from 'subscriptions-transport-ws';
import { print } from 'graphql-tag/printer';
// quick way to add the subscribe and unsubscribe functions to the network interface
const addGraphQLSubscriptions = (networkInterface, wsClient) => Object.assign(networkInterface, {
subscribe: (request, handler) => wsClient.subscribe({
query: print(request.query),
variables: request.variables,
}, handler),
unsubscribe: (id) => {
wsClient.unsubscribe(id);
},
});
// Create the network interface
const networkInterface = createNetworkInterface({
uri: 'http://localhost:3000/graphql',
transportBatching: true,
});
// Create the subscription websocket client
const wsClient = new Client('ws://localhost:3030');
// Extend the network interface with the subscription client
const networkInterfaceWithSubscriptions = addGraphQLSubscriptions(
networkInterface,
wsClient,
);
// Create the apollo client with the new network interface
export const apolloClient = new ApolloClient({
networkInterface: networkInterfaceWithSubscriptions,
});
// Your app is now subscription-ready!
Use the $apollo.subscribe()
method to subscribe to a GraphQL subscription that will get killed automatically when the component is detached. To disable this feature set onReady = true. :
attached() {
const subQuery = gql`subscription tags($type: String!) {
tagAdded(type: $type) {
id
label
type
}
}`;
const observer = this.$apollo.subscribe({
query: subQuery,
variables: {
type: 'City',
},
});
observer.subscribe({
next(data) {
console.log(data);
},
error(error) {
console.error(error);
},
});
},
You can declare subscriptions in the apollo
option with the subscribe
keyword:
static get properties() {
return {
cityComputed: {
type: Object,
computed: 'getCity(city)'
}
};
}
get apollo() {
return {
// Subscriptions
subscribe: {
// When a tag is added
tags: {
query: gql`subscription tags($type: String!) {
tagAdded(type: $type) {
id
label
type
}
}`,
options: 'cityComputed',
// Reactive variables
// Success hook
success(data) {
console.log(data);
// Let's update the local data
this.tags.push(data.tagAdded);
},
},
},
};
}
getCity(city) {
return {
variables: {
// This works just like regular queries
// and will re-subscribe with the right variables
// each time the values change
type: city,
},
};
}
You can then access the subscription ObservableQuery
object with this.$apollo.subscriptions.<name>
.
fetchMore
Pagination with Use the fetchMore()
method on the query:
<template>
<div>
<h2>Pagination</h2>
<div class="tag-list" hidden="{{!tagsPage}}">
<template is="dom-repeat" items="[[tagsPage.tags]]" as="tag">
<div class="tag-list-item">
{{ tag.id }} - {{ tag.label }} - {{ tag.type }}
</div>
</template>
<div class="actions">
<paper-button hidden="{{!showMoreEnabled}}" on-tap="showMore">Show more</paper-button>
</div>
</div>
</div>
</template>
<script>
import { PolymerApolloMixin } from 'polymer-apollo';
import { apolloClient } from './config.js';
import gql from 'graphql-tag';
class MyElement extends PolymerApolloMixin({apolloClient}, Polymer.Element) {
static get is() {
return 'example-element'
}
static get properties() {
return {
page: {
type: Number,
value: 0,
},
pageSize: {
type: Number,
value: 10,
},
showMoreEnabled: {
type: Number,
value: true,
},
}
}
get apollo() {
return {
// Pages
tagsPage: {
// GraphQL Query
query: gql`query tagsPage ($page: Int!, $pageSize: Int!) {
tagsPage(page: $page, size: $pageSize) {
tags {
id
label
type
}
hasMore
}
}`,
options: {
// Initial variables
variables: {
page: 'page',
pageSize: 'pageSize',
},
},
},
};
}
showMore() {
this.page ++;
// Fetch more data and transform the original result
this.$apollo.queries.tagsPage.fetchMore({
// New variables
variables: {
page: this.page,
pageSize: 20,
},
// Transform the previous result with new data
updateQuery: (previousResult, { fetchMoreResult }) => {
const newTags = fetchMoreResult.data.tagsPage.tags;
const hasMore = fetchMoreResult.data.tagsPage.hasMore;
this.showMoreEnabled = hasMore;
return {
tagsPage: {
// Merging the tag list
tags: [...previousResult.tagsPage.tags, ...newTags],
hasMore,
},
};
},
});
}
};
</script>
Similar to fetchMore the following methods can be used. for queries $apollo.queries[name] for subscriptions $apollo.subscriptions[name]
- refetch()
- fetchMore()
- updateQuery()
- startPolling()
- stopPolling()
- subscribeToMore()
- currentResult()
- variables : an object containing variables used to get this result.
- loading : boolean, useful if you set notifyOnNetworkStatusChange to true in query options.
- networkStatus : the status of the request ,useful if you set notifyOnNetworkStatusChange to true in query options
Like it?
:star: this repo
Found a bug?
Raise an issue!
Contributors
Anthony Hinsinger (@atoy40)
Arun Kumar T K (@aruntk)
Edward Watson (@edge0701)