The Next Evolution of GraphQL Front Ends
Benny Powers 🇮🇱🇨🇦
Posted on August 2, 2021
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>
`;
}
}
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>
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>
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));
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 },
});
},
});
}
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';
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 underliesuseQuery
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>
`;
}));
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>
`;
}
});
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.
Posted on August 2, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.