DEV Community

Cover image for The Next Evolution of GraphQL Front Ends
Benny Powers ๐Ÿ‡ฎ๐Ÿ‡ฑ๐Ÿ‡จ๐Ÿ‡ฆ
Benny Powers ๐Ÿ‡ฎ๐Ÿ‡ฑ๐Ÿ‡จ๐Ÿ‡ฆ

Posted on • Originally published at apolloelements.dev

The Next Evolution of GraphQL Front Ends

Originally posted on the Apollo Elements blog. Read there to enjoy interactive demos.

Apollo Elements has come a long way since its first release as lit-apollo in 2017. What started as a way to build GraphQL-querying LitElements has blossomed into a multi-library, multi-paradigm project with extensive docs.

Today we're releasing the next version of Apollo Elements' packages, including a major change: introducing GraphQL Controllers, and GraphQL HTML Elements.

Reactive GraphQL Controllers

The latest version of Lit introduced a concept called "reactive controllers". They're a way to pack up reusable functionality in JavaScript classes that you can share between elements. If you've use JavaScript class mixins before (not the same as React mixins), they you're familiar with sharing code between elements. Controllers go one-better by being sharable and composable without requiring you to apply a mixin to the host element, as long as it implements the ReactiveControllerHost interface.

You can even have multiple copies of the same controller active on a given host. In the words of the Lit team, controllers represent a "has a _" relationship to the host element, where mixins represent an "is a _" relationship.

For Apollo Elements, it means now you can add many GraphQL operations to one component, like multiple queries or a query and a mutation. Here's an interactive example of the latter:

import type { TextField } from '@material/mwc-textfield';
import { ApolloQueryController, ApolloMutationController } from '@apollo-elements/core';
import { LitElement, html } from 'lit';
import { customElement, query } from 'lit/decorators.js';
import { UsersQuery, AddUserMutation } from './graphql.documents.js';
import { style } from './Users.css.js';

@customElement('users-view')
class UsersView extends LitElement {
  static styles = style;

  @query('mwc-textfield') nameField: TextField;

  users = new ApolloQueryController(this, UsersQuery);

  addUser = new ApolloMutationController(this, AddUserMutation, {
    awaitRefetchQueries: true,
    refetchQueries: [{ query: UsersQuery }],
  });

  onSubmit() { this.addUser.mutate({ variables: { name: this.nameField.value } }); }

  render() {
    const users = this.users.data?.users ?? [];
    const loading = this.users.loading || this.addUser.loading;
    return html`
      <form>
        <h2>Add a New User</h2>
        <mwc-textfield label="Name" ?disabled="${loading}"></mwc-textfield>
        <mwc-linear-progress indeterminate ?closed="${!loading}"></mwc-linear-progress>
        <mwc-button label="Submit" ?disabled="${loading}" @click="${this.onSubmit}"></mwc-button>
      </form>
      <h2>All Users</h2>
      <mwc-list>${users.map(x => html`
        <mwc-list-item noninteractive graphic="avatar">
          <img slot="graphic" ?hidden="${!x.picture}" .src="${x.picture}" role="presentation"/>
          ${x.name}
        </mwc-list-item>`)}
      </mwc-list>
    `;
  }
}
Enter fullscreen mode Exit fullscreen mode

View a Live Demo of this snippet

Controllers are great for lots of reasons. One reason we've found while developing and testing Apollo Elements is that unlike the class-based API of e.g. @apollo-elements/lit-apollo or @apollo-elements/mixins, when using controllers there's no need to pass in type parameters to the host class. By passing a TypedDocumentNode object as the argument to the controller, you'll get that typechecking and autocomplete you know and love in your class template and methods, without awkward <DataType, VarsType> class generics.

If you're working on an existing app that uses Apollo Elements' base classes, not to worry, you can still import { ApolloQuery } from '@apollo-elements/lit-apollo', We worked hard to keep the breaking changes to a minimum. Those base classes now use the controllers at their heart, so go ahead: mix-and-match query components with controller-host components in your app, it won't bloat your bundles.

We hope you have as much fun using Apollo Elements controllers as we've had writing them.

Dynamic GraphQL Templates in HTML

The previous major version of @apollo-elements/components included <apollo-client> and <apollo-mutation>. Those are still here and they're better than ever, but now they're part of a set with <apollo-query> and <apollo-subscription> as well.

With these new elements, and their older sibling <apollo-mutation>, you can write entire GraphQL apps in nothing but HTML. You read that right, declarative, data-driven GraphQL apps in HTML. You still have access to the Apollo Client API, so feel free to sprinkle in a little JS here and there for added spice.

This is all made possible by a pair of libraries from the Lit team's Justin Fagnani called Stampino and jexpr. Together, they let you define dynamic parts in HTML <template> elements, filling them with JavaScript expressions based on your GraphQL data.

Here's the demo app from above, but written in HTML:

<apollo-client>
  <apollo-query>
    <script type="application/graphql" src="Users.query.graphql"></script>
    <template>
      <h2>Add a New User</h2>
      <apollo-mutation refetch-queries="Users" await-refetch-queries>
        <script type="application/graphql" src="AddUser.mutation.graphql"></script>
        <mwc-textfield label="Name"
                       slot="name"
                       data-variable="name"
                       .disabled="{{ loading }}"></mwc-textfield>
        <mwc-button label="Submit"
                    trigger
                    slot="name"
                    .disabled="{{ loading }}"></mwc-button>
        <template>
          <form>
            <slot name="name"></slot>
            <mwc-linear-progress indeterminate .closed="{{ !loading }}"></mwc-linear-progress>
            <slot name="submit"></slot>
          </form>
        </template>
      </apollo-mutation>
      <h2>All Users</h2>
      <mwc-list>
        <template type="repeat" repeat="{{ data.users ?? [] }}">
          <mwc-list-item noninteractive graphic="avatar">
            <img .src="{{ item.picture }}" slot="graphic" alt=""/>
            {{ item.name }}
          </mwc-list-item>
        </template>
      </mwc-list>
    </template>
  </apollo-query>
</apollo-client>
<script type="module" src="components.js"></script>
Enter fullscreen mode Exit fullscreen mode

View a Live Demo of this snippet

There's a tonne of potential here and we're very keen to see what you come up with using these new components. Bear in mind that the stampino API isn't stable yet: there may be changes coming down the pipe in the future, but we'll do our best to keep those changes private.

More Flexible HTML Mutations

The <apollo-mutation> component lets you declare GraphQL mutations in HTML. Now, the latest version gives you more options to layout your pages. Add a stampino template to render the mutation result into the light or shadow DOM. Use the variable-for="<id>" and trigger-for="<id>" attributes on sibling elements to better integrate with 3rd-party components, and specify the event which triggers the mutation by specifying a value to the trigger attribute.

<link rel="stylesheet" href="https://unpkg.com/@shoelace-style/shoelace@2.0.0-beta.47/dist/themes/base.css">
<script src="https://unpkg.com/@shoelace-style/shoelace@2.0.0-beta.47/dist/shoelace.js?module" type="module"></script>

<sl-button id="toggle">Add a User</sl-button>

<sl-dialog label="Add User">
  <sl-input label="What is your name?"
            variable-for="add-user-mutation"
            data-variable="name"></sl-input>
  <sl-button slot="footer"
             type="primary"
             trigger-for="add-user-mutation">Add</sl-button>
</sl-dialog>

<apollo-mutation id="add-user-mutation">
  <script type="application/graphql" src="AddUser.mutation.graphql"></script>
  <template>
    <sl-alert type="primary" duration="3000" closable ?open="{{ data }}">
      <sl-icon slot="icon" name="info-circle"></sl-icon>
      <p>Added {{ data.addUser.name }}</p>
    </sl-alert>
  </template>
</apollo-mutation>
<script type="module" src="imports.js"></script>

<script type="module">
  const toggle = document.getElementById('toggle');
  const dialog = document.querySelector('sl-dialog');
  const mutation = document.getElementById('add-user-mutation');
  toggle.addEventListener('click', () => dialog.show());
  mutation.addEventListener('mutation-completed', () => dialog.hide());
</script>
Enter fullscreen mode Exit fullscreen mode

Demonstrating how to use <apollo-mutation> with Shoelace web components. View a Live Demo of this snippet

Atomico support

On the heels of the controllers release, we're happy to add a new package to the roster. Apollo Elements now has first-class support for Atomico, a new hooks-based web components library with JSX or template-string templating.

import { useQuery, c } from '@apollo-elements/atomico';
import { LaunchesQuery } from './Launches.query.graphql.js';

function Launches() {
  const { data } = useQuery(LaunchesQuery, { variables: { limit: 3 } });

  const launches = data?.launchesPast ?? [];

  return (
    <host shadowDom>
      <link rel="stylesheet" href="launches.css"/>
      <ol>{launches.map(x => (
        <li>
          <article>
            <span>{x.mission_name}</span>
            <img src={x.links.mission_patch_small} alt="Badge" role="presentation"/>
          </article>
        </li>))}
      </ol>
    </host>
  );
}

customElements.define('spacex-launches', c(Launches));
Enter fullscreen mode Exit fullscreen mode

FAST Behaviors

FAST is an innovative web component library and design system from Microsoft. Apollo Elements added support for FAST in 2020, in the form of Apollo* base classes. The latest release transitions to FAST Behaviors, which are analogous to Lit ReactiveControllers.

@customElement({ name, styles, template })
class UserProfile extends FASTElement {
  profile = new ApolloQueryBehavior(this, MyProfileQuery);
  updateProfile = new ApolloMutationBehavior(this, UpdateProfileMutation, {
    update(cache, result) {
      cache.writeQuery({
        query: MyProfileQuery,
        data: { profile: result.data.updateProfile },
      });
    },
  });
}
Enter fullscreen mode Exit fullscreen mode

The FAST team were instrumental in getting this feature over the line, so many thanks to them.

If you're already using @apollo-elements/fast, we recommend migrating your code to behaviors as soon as you're able, but you can continue to use the element base classes, just change your import paths to /bases. These may be removed in the next major release, though.

-  import { ApolloQuery } from '@apollo-elements/fast/apollo-query';
+  import { ApolloQuery } from '@apollo-elements/fast/bases/apollo-query';
Enter fullscreen mode Exit fullscreen mode

New and Improved Docs

It wouldn't be an Apollo Elements release without some docs goodies. This time, in addition to new and updated docs and guides for components and controllers, we've replaced our webcomponents.dev iframes with <playground-ide> elements. All the "Edit Live" demos on this site, including the ones in this blog post, are running locally in your browser via a service worker. Talk about serverless, amirite?

The docs also got a major upgrade care of Pascal Schilp's untiring work in the Webcomponents Community Group to get the custom elements manifest v1 published. This latest iteration of the API docs generates package manifests directly from source code, and converts them to API docs via Rocket.

SSR

As part of the release, we updated our demo apps leeway and LaunchCTL. In the case of leeway, we took the opportunity to implement extensive SSR with the help of a new browser standard called Declarative Shadow DOM. It's early days for this technique but it's already looking very promising. You can try it out in any chromium browser (Chrome, Brave, Edge, Opera) by disabling JavaScript and visiting https://leeway.apolloelements.dev.

Behind the Scenes

Bringing this release into the light involved more than just refactoring and updating the apollo-elements/apollo-elements repo. It represents work across many projects, including PRs to

  • Stampino and jexpr, to iron out bugs, decrease bundle size, and add features
  • Hybrids, to add support for reactive controllers
  • Atomico and Haunted, to add the useController hook which underlies useQuery and co.

Additionally, here in apollo-elements, we added the ControllerHostMixin as a way to maintain the previous element-per-graphql-document API without breaking backwards (too much). You can use this generic mixin to add controller support to any web component.

Fixes and Enhancements

The last release included support for the web components hooks library haunted, but that support hid a dirty little secret within. Any time you called a hook inside a Haunted function component, apollo elements would sneakily mix the GraphQL interface onto the custom element's prototype. It was a good hack as long as you only call one hook per component, but would break down as soon as you compose multiple operations.

With controllers at the core, and the useController hook, you can use as many Apollo hooks as you want in your elements without clobbering each other or polluting the element interface.

import { useQuery, html, component } from '@apollo-elements/haunted';
import { client } from './client.js';
import { FruitsQuery } from './Fruits.query.graphql.js';
import { VeggiesQuery } from './Veggies.query.graphql.js';

customElements.define('healthy-snack', component(function HealthySnack() {
  const { data: fruits } = useQuery(FruitsQuery, { client });
  const { data: veggies } = useQuery(VeggiesQuery, { client });
  const snack = [ ...fruits?.fruits ?? [], ...veggies?.veggies ?? [] ];
  return html`
    <link rel="stylesheet" href="healthy-snack.css"/>
    <ul>${snack.map(x => html`<li>${x}</li>`)}</ul>
  `;
}));
Enter fullscreen mode Exit fullscreen mode

Demonstrating how to use multiple GraphQL hooks in a haunted component. View a Live Demo of this snippet

The same is true of the hybrids support, it now uses the controllers underneath the hood, letting you mix multiple operations in a single hybrid.

import { query, html, define } from '@apollo-elements/hybrids';
import { client } from './client.js';
import { FruitsQuery } from './Fruits.query.graphql.js';
import { VeggiesQuery } from './Veggies.query.graphql.js';

define('healthy-snack', {
  fruits: query(FruitsQuery, { client }),
  veggies: query(VeggiesQuery, { client }),
  render(host) {
    const snack = [ ...host.fruits.data?.fruits ?? [], ...host.veggies.data?.veggies ?? [] ];
    return html`
      <link rel="stylesheet" href="healthy-snack.css"/>
      <ul>${snack.map(x => html`<li>${x}</li>`)}</ul>
    `;
  }
});
Enter fullscreen mode Exit fullscreen mode

Demonstrating how to use multiple GraphQL hooks in an atomico component. View a Live Demo of this snippet

Try it Out

Apollo Elements next is available in prerelease on npm. We hope you enjoy using it and look forward to seeing what you come up with.

Are you using Apollo Elements at work? Consider sponsoring the project via Open Collective to receive perks like priority support.

Top comments (2)

Collapse
 
rangercoder99 profile image
RangerCoder99 • Edited

Seems the hype of GraphQL has tune down a lot, years ago developers were going like its the 2nd coming, but this days it seem people are more interested in good old apis again... Not sure why...

Collapse
 
bennypowers profile image
Benny Powers ๐Ÿ‡ฎ๐Ÿ‡ฑ๐Ÿ‡จ๐Ÿ‡ฆ

I wouldn't say it's for everything

but from a front end perspective, components/my-view/MyView.query.graphql is a nice way to work.

And with Apollo Elements, you can do it without dragging in a huge javascript framework