Package Exports
- @apollo-elements/lit-apollo
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (@apollo-elements/lit-apollo) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
@apollo-elements/lit-apollo
🚀 LitElement base classes that connect to your Apollo cache 🌜
👩🚀 It's one small step for a dev, one giant leap for the web platform! 👨🚀
📓 Contents
📑 API Docs
If you just want to see the API Docs, check them out at apolloelements.dev/lit-apollo/
🤖 Demo
#leeway is a progressive web app that uses lit-apollo to make it easier for you to avoid doing actual work. Check out the source repo for an example of how to build apps with Apollo Elements. The demo includes:
- SSR
- Code Splitting
- Aggressive minification, including
lit-htmltemplate literals - CSS-in-CSS ( e.g.
import shared from '../shared-styles.css';) - GQL-in-GQL ( e.g.
import query from './my-component-query.graphql';) - GraphQL Subscriptions over websocket
🔧 Installation
Apollo elements' lit-apollo is distributed through npm, the node package manager. To install a copy of the latest version in your project's node_modules directory, install npm on your system then run the following command in your project's root directory:
npm install --save @apollo-elements/lit-apollo👩🚀 Usage
You'll need to bundle the Apollo library with a tool like Rollup. See instructions for bundling Apollo for advice on how to build a working Apollo client. After that, typical usage involves importing the base class and extending from it to define your component:
import { ApolloQuery, html } from '@apollo-elements/lit-apollo';
import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { HttpLink } from 'apollo-link-http';
import gql from 'graphql-tag'
const protocol = host.includes('localhost') ? 'http' : 'https';
const uri = `${protocol}://${host}/graphql`;
const link = new HttpLink({ uri });
const cache = new InMemoryCache();
// Create the Apollo Client
const client = new ApolloClient({ cache, link });
// Compute graphql documents statically for performance
const query = gql`
query {
helloWorld {
name
greeting
}
}
`;
const childQuery = gql`
query {
child {
foo
bar
}
}
`;
class ConnectedElement extends ApolloQuery {
render() {
const { data, error, loading } = this;
const { helloWorld = {} } = data || {}
return (
loading ? html`
<what-spin></what-spin>`
: error ? html`
<h1>😢 Such Sad, Very Error! 😰</h1>
<div>${error ? error.message : 'Unknown Error'}</div>`
: html`
<div>${helloWorld.greeting}, ${helloWorld.name}</div>
<connected-child id="child-component"
.client="${this.client}"
.query="${childQuery}"
></connected-child>`
);
}
constructor() {
super();
this.client = client;
this.query = query;
}
};
customElements.define('connected-element', ConnectedElement)NOTE: By default, components will only render while loading or after receiving data or an error. Override the shouldUpdate method to control when the component renders.
shouldUpdate(changedProps) {
return (
changedProps.has('someProp') ||
this.loading != null ||
this.data ||
this.error
);
}🍹 Mixins
You don't need to use LitElement base class for your components if you use the mixins. You just have to handle the rendering part on your own: e.g. for a query component, you'd implement yourself what happens after data, error, loading, or networkStatus change.
📖 Subscriptions
You can create components which use GraphQL subscriptions to update over websockets.
import { ApolloQuery, html } from '@apollo-elements/lit-apollo';
import { client } from '../client';
import { format } from 'date-fns/fp';
import { errorTemplate } from './error-template.js';
import gql from 'graphql-tag';
import './chat-subscription.js';
const messageTemplate = ({ message, user, date }) => html`
<div>
<dt><time>${format('HH:mm', date)}</time> ${user}:</dt>
<dd>${message}</dd>
</div>
`;
const subscription = gql`
subscription {
messageSent {
date
message
user
}
}`
/**
* <chat-query>
* @customElement
* @extends LitElement
*/
class ChatQuery extends ApolloQuery {
render() {
return html`
<chat-subscription
.client="${this.client}"
.subscription="${subscription}"
.onSubscriptionData=${this.onSubscriptionData}>
</chat-subscription>
${( this.loading ? html`Loading...`
: this.error ? errorTemplate(this.error)
: html`<dl>${this.data.messages.map(messageTemplate)}</dl>`
)}`;
}
constructor() {
super();
this.client = client;
this.onSubscriptionData = this.onSubscriptionData.bind(this);
this.query = gql`
query {
messages {
date
message
user
}
}`;
}
onSubscriptionData({ client, subscriptionData: { data: { messageSent } } }) {
const { query } = this;
const { messages } = client.readQuery({ query });
const data = { messages: [...messages, messageSent] };
client.writeQuery({ query, data });
}
}
customElements.define('chat-query', ChatQuery);Alternatively, you can call subscribeToMore on a query component with a subscription document and an updateQuery function to have your component update it's data based on subscription results:
updateQuery(prev, { subscriptionData }) {
if (!subscriptionData.data) return prev;
return {
...prev,
messages: [...prev.messages, subscriptionData.data.messageSent]
};
}
firstUpdated() {
const { updateQuery } = this;
this.subscribeToMore({
updateQuery,
document: gql`
subscription {
messageSent {
date
message
user
}
}`
});
}
See this simple chat-app demo which demonstrates building custom elements which subscribe to a GraphQL server over websockets: Chat App Demo
😎 Cool Tricks
🏦 Managing the Cache
When defining components that issue graphql mutations, you may want to take control over how and when Apollo updates it's local cache. You can do this with the onUpdate property on elements that extend from ApolloMutation
import gql from 'graphql-tag';
import { render, html } from 'lit-html/lit-html';
import { client } from './client';
import { ApolloMutation } from '@apollo-elements/lit-apollo';
class MutatingElement extends ApolloMutation {
render() {
return html`
<loading-overlay ?active="${this.loading}"></loading-overlay>
<button ?hidden="${this.data}" @click="${this.mutate}"></button>
<div ?hidden="${!this.data}">${this.data.myResponse}</div>
`;
}
}
customElements.define('mutating-element', MutatingElement);
const mutation = gql`
mutation($id: ID!) {
MyMutation(id: $id) {
mutationResult
}
}
`;
/**
* Example update function which reads a cached query result, merges
* it with the mutation result, and then writes it back to the cache.
*/
const updateFunc = (cache, response) => {
// ostensibly looks up the cached object for mutationResult
const query = MyQuery;
const variables = { id: 1 };
const cached = cache.readQuery({ query, variables });
const changed = computeChanges(cached);
// mergeMutationResult is a made-up function.
const mutationResult = mergeMutationResult(cached, changed);
return cache.writeData({ query, data: { mutationResult } });
};
const template = html`
<mutating-element
.client="${client}"
.mutation="${mutation}"
.variables="${{id: 1}}"
.onUpdate="${updateFunc}"
></mutating-element>
`;
render(template, container);⌚️ Asynchronous Client
In some cases, you may want to wait for your Apollo client to do some initial asynchronous setup (for example reloading a persistent cache or getting a user token) before you can make your client available to your app.
// client.js
import { ApolloClient } from 'apollo-client';
import { persistCache } from 'apollo-cache-persist'
import { InMemoryCache } from 'apollo-cache-inmemory';
import { link } from './link';
const cache = new InMemoryCache();
export async function getClient() {
// Wait for the cache to be restored
await persistCache({ cache, storage: localStorage });
// Create the Apollo Client
return new ApolloClient({ cache, link });
};In that case, you can import a promise of a client and wait for it in connectedCallback:
// async-element.js
import formatDistance from 'date-fns/esm/formatDistance';
import { ApolloQuery, html } from '@apollo-elements/lit-apollo';
import { getClient } from './client';
class AsyncElement extends ApolloQuery {
render() {
const { userSession: { name, lastActive } = {} } = this.data || {}
const time = formatDistance(lastActive, Date.now(), { addSuffix: true });
return html`
<h1>👋 ${name}!</h1>
<span>Your last activity was <time>${time}</time></span>`
}
async connectedCallback() {
super.connectedCallback();
// first instantiate the client locally
this.client = await clientPromise;
// afterwards, set the query to trigger fetch-then-render
this.query = gql`query {
userSession {
name
lastActive
}
}`;
}
shouldUpdate() {
// only render when there is data.
return !!this.data;
}
};
customElements.define('async-element', AsyncElement)Alternatively, you can use the dynamic import() feature to wait on your client before loading element modules:
// app.js
import { getClient } from './client.js';
(async function init() {
window.__APOLLO_CLIENT__ = await getClient();
await Promise.all([
import('./components/connected-element.js'),
import('./components/connected-input.js'),
]);
})();👷♂️ Maintainers
apollo-elements is a community project maintained by Benny Powers.
