Sign Up for Free

RunKit +

Try any Node.js package right in your browser

This is a playground to test code. It runs a full Node.js environment and already has all of npm’s 400,000 packages pre-installed, including @apollo-elements/lit-apollo with all npm packages installed. Try it out:

require("lit-element/package.json"); // lit-element is a peer dependency. require("lit-html/package.json"); // lit-html is a peer dependency. var litApollo = require("@apollo-elements/lit-apollo")

This service is provided by RunKit and is not affiliated with npm, Inc or the package authors.

@apollo-elements/lit-apollo v1.1.5

👩‍🚀🌛 LitElements for Apollo GraphQL 🚀👨‍🚀

@apollo-elements/lit-apollo

Published on npm Published on webcomponents.org Actions Status

🚀 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-html template 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

Lighthouse Scores: 98 (performance), 100 (accessibility), 93 (best practises), 100 (SEO), 12/12 (PWA)

🔧 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.

Contact me on Codementor

RunKit is a free, in-browser JavaScript dev environment for prototyping Node.js code, with every npm package installed. Sign up to share your code.
Sign Up for Free