India’s largest Azure Conference
(Space, HJKL or →← Keys to Navigate)
(F for Fullscreen)
HTML
with Apollo ElementsData-Driven Web Apps that Use the Platform
{% include ../../../graphql.svg % }
Query Language for APIs
Published by Facebook in 2015
Used by GitHub, Twitter, Atlassian, IBM, Amazon...
The GraphQL Schema tells your server the type of each object.
type Person {
}
type Person {
name: String
picture: String
}
type Person {
name: String
picture: String
friends: [Person]
}
The resolvers define the contents of each field.
export const PersonResolvers = {
}
export const PersonResolvers = {
picture: ({ id }, args, context) =>
context.dataSources.Person.getPictureById(id),
}
export const PersonResolvers = {
picture: ({ id }, args, context) =>
context.dataSources.Person.getPictureById(id),
friends: ({ friends }, args, context) =>
friends
.map(id => context.dataSources.Person.getPersonById(id)),
}
Unlike REST APIs, GraphQL exposes a single endpoint.
There are only two verbs in GraphQL: query
reads the graph and mutation
updates the graph.
GET
query
POST
,PUT
,DELETE
mutation
query PersonQuery {
}
query PersonQuery($id: ID!) {
}
query PersonQuery($id: ID!) { # query operation
person(id: $id) { # field on root query
}
}
query PersonQuery($id: ID!) { # query operation
person(id: $id) { # field on root query
name # field on person type
picture
}
}
query PersonQuery($id: ID!) {
person(id: $id) {
name
picture
friends {
name
}
}
}
{
"data": {
"person": {
"__typename": "Person",
"name": "Charlie Chaplin",
"picture": "https://photo.charliechaplin.com/images/photos/0000/0296/CC_233_big.jpg",
"friends": [
{ "__typename": "Person", "name": "Buster Keaton" },
{ "__typename": "Person", "name": "Hedy Lamarr" },
{ "__typename": "Person", "name": "Conrad Veidt" }
]
}
}
}
Even if the database schema doesn't match your query...
{
"id": "spencer_harley",
"first_name": "Charlie",
"last_name": "Chaplin",
"address": "1085 Summit Dr., Beverly Hills, California",
"friend_ids": [ "keaton_it_real", "signal_hopper", "casa_veidt" ]
}
...you can still respond sensibly.
On the server, GraphQL lends itself to deeply nested, networked data.
Client-side, GraphQL frontends naturally revolve around components
Web browsers come with their own built-in component model, called Web Components.
These are custom HTML elements which you, the user, define.
Web components can have their own HTML templates, methods, events, and properties.
You build web components using HTML, CSS, and JavaScript
class PersonElement extends HTMLElement {
}
class PersonElement extends HTMLElement {
connectedCallback() {
this.attachShadow({ mode: 'open' }).innerHTML = `
<span id="emoji">${this.getAttribute('emoji')} says:</span>
<slot></slot>
`;
}
}
class PersonElement extends HTMLElement {
connectedCallback() {
this.attachShadow({ mode: 'open' }).innerHTML = `
<span id="emoji">${this.getAttribute('emoji')} says:</span>
<slot></slot>
`;
}
}
class PersonElement extends HTMLElement {
connectedCallback() {
this.attachShadow({ mode: 'open' }).innerHTML = `
<span id="emoji">${this.getAttribute('emoji')} says:</span>
<slot></slot>
`;
}
}
// TODO: use <template> for performance
customElements.define('person-element', PersonElement);
<header>
<h1>Web Components, huh?</h1>
</header>
<person-element emoji="👩🔬">
<blockquote>
<p>Every web component is an HTML Element.</p>
<p>And you get to define how it behaves.</p>
</blockquote>
</person-element>
<person-element emoji="🧙♂️">
#shadow-root
<link href="person-element.css"/>
<span id="emoji">🧙♂️ says:</span>
<slot></slot>
</person-element>
document.getElementById('emoji');
// => null
document.querySelector('person-element')
.shadowRoot.getElementById('emoji');
// => <span id="emoji">...</span>
:host { font-family: 'Open Sans', sans-serif; }
#emoji { font-style: italic; }
::slotted(blockquote)::before {
content: "‟";
font-size: 400%;
}
<person-element emoji="🧙♂️">
<blockquote>Hello World!</blockquote>
</person-element>
<blockquote id="emoji">
Shadow DOM makes components safe to use
</blockquote>
Apollo Elements binds GraphQL to web components.
There are two main ways to use it:
<apollo-client uri="https://api.spacex.land/graphql">
</apollo-client>
<apollo-client uri="https://api.spacex.land/graphql">
<apollo-query>
<script type="application/graphql">
query NextLaunch {
launchNext {
launch_site { site_name }
mission_name
rocket { rocket_name }
}
}
</script>
</apollo-query>
</apollo-client>
<apollo-client uri="https://api.spacex.land/graphql">
<apollo-query>
<script type="application/graphql" src="NextLaunch.query.graphql"></script>
</apollo-query>
</apollo-client>
<apollo-client uri="https://api.spacex.land/graphql">
<apollo-query>
<script type="application/graphql" src="NextLaunch.query.graphql"></script>
<template>
<h1 part="mission">{{ data.launchNext.mission_name }}</h1>
<p .hidden="{{ loading }}">launches from
<em>{{ data.launchNext.launch_site.site_name }}</em> aboard
<em>{{ data.launchNext.rocket.rocket_name }}</em></p>
</template>
</apollo-query>
</apollo-client>
<apollo-client uri="https://api.spacex.land/graphql">
<apollo-query>
<script type="application/graphql" src="NextLaunch.query.graphql"></script>
<template>
<h1 part="mission">{{ data.launchNext.mission_name }}</h1>
<p .hidden="{{ loading }}">launches from
<em>{{ data.launchNext.launch_site.site_name }}</em> aboard
<em>{{ data.launchNext.rocket.rocket_name }}</em></p>
</template>
</apollo-query>
</apollo-client>
<script type="module" src="main.js"></script>
query NextLaunch {
launchNext {
launch_site { site_name }
mission_name
rocket { rocket_name }
}
}
@import url('https://rsms.me/inter/inter.css');
html {
font-family: 'Inter var', sans-serif;
background-color: black;
color: white;
font-size: 48px;
}
apollo-query::part(mission) {
font-size: 1.5em;
margin-block: 0;
}
import '@apollo-elements/components';
<apollo-client uri="https://api.spacex.land/graphql">
</apollo-client>
<apollo-query>
<script type="application/graphql" src="LatestUsers.query.graphql"></script>
<template>
<ol>
<template type="repeat" repeat="{{ data.users ?? [] }}">
<li>{{ item.name }}</li>
</template>
</ol>
</template>
</apollo-query>
<apollo-mutation refetch-queries="LatestUsers" await-refetch-queries>
<script type="application/graphql" src="InsertUser.mutation.graphql"></script>
</apollo-mutation>
<sl-input data-variable="name" label="User name"></sl-input>
<sl-button trigger label="Add user"></sl-button>
<sl-input data-variable="name" label="User name"></sl-input>
<sl-button trigger label="Add user"></sl-button>
<apollo-client uri="https://api.spacex.land/graphql">
<apollo-query>
<script type="application/graphql" src="LatestUsers.query.graphql"></script>
<template>
<ol>
<template type="repeat" repeat="{{ data.users ?? [] }}">
<li>{{ item.name }}</li>
</template>
</ol>
</template>
</apollo-query>
<apollo-mutation refetch-queries="LatestUsers" await-refetch-queries>
<script type="application/graphql" src="InsertUser.mutation.graphql"></script>
<sl-input data-variable="name" label="User name"></sl-input>
<sl-button trigger>Add User</sl-button>
</apollo-mutation>
</apollo-client>
<script type="module" src="main.js"></script>
<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.0.0-beta.50/dist/shoelace.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.0.0-beta.50/dist/themes/dark.css">
mutation InsertUser($name: String!) {
insert_users(objects: {name: $name}) {
returning {
name
id
timestamp
}
}
}
query LatestUsers($limit: Int = 3){
users(limit: $limit, order_by: { timestamp: desc }) {
name
}
}
@import url('https://rsms.me/inter/inter.css');
html {
font-family: 'Inter var', sans-serif;
background-color: black;
color: white;
font-size: 48px;
padding: 12px;
}
apollo-mutation {
display: grid;
gap: 12px;
max-width: 90vw;
}
sl-input {
width: 90vw;
}
import '@apollo-elements/components';
document.documentElement.classList.add("sl-theme-dark");
<apollo-mutation refetch-queries="LatestUsers" await-refetch-queries>
<script type="application/graphql" src="InsertUser.mutation.graphql"></script>
<sl-input data-variable="name" label="User name"></sl-input>
<sl-button trigger>Add User</sl-button>
</apollo-mutation>
<apollo-mutation refetch-queries="LatestUsers" await-refetch-queries>
<apollo-mutation><!-- ... --></apollo-mutation>
<apollo-mutation><!-- ... --></apollo-mutation>
<script type="module">
const muttnEl = document.querySelector('apollo-mutation');
const queryEl = document.querySelector('apollo-query');
await muttnEl.updateComplete;
muttnEl.options.update = function(cache, result) {
const { query } = queryEl;
const cached = cache.readQuery({ query });
cache.writeQuery({
query,
data: {
...cached,
users: [result.insert_users.returning[0], ...cached.users],
},
});
}
</script>
npm i @apollo-elements/components
import '@apollo-elements/components';
<script type="module"
src="https://unpkg.com/@apollo-elements/components?module">
</script>
...but there's more
A new 'composition primitive' for web components, developed by the Lit team @ Google
Not limited to LitElement, can work with any component system even React, Angular, etc.
Complements or replaces class inheritance and mixins
Membrane Transport Proteins Argonne Leadership Computing Facility, US DOE
export class ClockController {
value = new Date();
timeFormat = new Intl.DateTimeFormat('en-US', {
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
});
constructor(host, timeout = 1000) {
(this.host = host).addController(this);
this.timeout = timeout;
}
/* playground-fold */
hostConnected() {
// Start a timer when the host is connected
this._timerID = setInterval(() => {
this.value = new Date();
// Update the host with new value
this.host.requestUpdate();
}, this.timeout);
}
/* playground-fold-end */
/* playground-fold */
hostDisconnected() {
// Clear the timer when the host is disconnected
clearInterval(this._timerID);
this._timerID = undefined;
}
/* playground-fold-end */
getFormatted() {
return this.timeFormat.format(this.value);
}
}
import {LitElement, html} from 'lit';
import {ClockController} from './clock-controller.js';
class MyElement extends LitElement {
// Create the controller and store it
clock = new ClockController(this, 100);
// Use the controller in render()
render() {
return html`
<p>The time is
<time datetime="${this.clock.value}">
${this.clock.getFormatted()}
</time>
</p>
`;
}
}
customElements.define('my-element', MyElement);
<script type="module" src="./my-element.js"></script>
<my-element></my-element>
<style>:root { font-size: 4em }</style>
class MyElement extends LitElement {
}
import { ClockController } from './clock-controller.js';
class MyElement extends LitElement {
clock = new ClockController(this);
}
import { ClockController } from './clock-controller.js';
class MyElement extends LitElement {
clock = new ClockController(this);
render() {
return html`
<p>time: <time>${this.clock.value}</time></p>
`;
}
}
import { ClockController } from './clock-controller.js';
import { ControllerHostMixin } from '@apollo-elements/mixins/controller-host-mixin.js';
class MyElement extends ControllerHostMixin(HTMLElement) {
clock = new ClockController(this);
}
import { ClockController } from './clock-controller.js';
import { ControllerHostMixin } from '@apollo-elements/mixins/controller-host-mixin.js';
class MyElement extends ControllerHostMixin(HTMLElement) {
clock = new ClockController(this);
update(changed) {
super.update(changed);
this.shadowRoot.querySelector('time').textContent =
this.clock.value;
}
}
import { ControllerHostMixin } from '@apollo-elements/mixins/controller-host-mixin';
import { MouseController } from './mouse-controller.js';
const template = document.createElement('template');
template.innerHTML = `
%3Clink rel="stylesheet" href="color-picker.css">
<div id="loupe"><div id="cursor">⊹</div></div>
`;
class ColorPicker extends ControllerHostMixin(HTMLElement) {
mouse = new MouseController(this);
constructor() {
super();
this
.attachShadow({ mode: 'open' })
.append(template.content.cloneNode(true));
this.loupe = this.shadowRoot.getElementById('loupe');
this.cursor = this.shadowRoot.getElementById('cursor');
this.addEventListener('click', () => {
this.pick();
this.cursor.animate({ scale: ['100%', '120%', '100%'], easing: 'ease-in-out' }, 300);
});
}
update() {
const [x, y] = this.mouse.pos;
const { clientWidth, clientHeight } = document.documentElement;
const hue = Math.floor((x / clientWidth) * 360);
const saturation = 100 - Math.floor((y / clientHeight) * 100);
this.style.setProperty('--x', `${x}px`);
this.style.setProperty('--y', `${y}px`);
this.style.setProperty('--hue', hue);
this.style.setProperty('--saturation', `${saturation}%`);
if (this.mouse.down)
this.pick();
super.update();
}
async pick() {
await this.updateComplete;
this.dispatchEvent(new CustomEvent('pick', {
bubbles: true,
detail: getComputedStyle(this.loupe).getPropertyValue('background-color')
}));
}
};
customElements.define('color-picker', ColorPicker);
:host {
display: block;
overflow: hidden;
cursor: none;
height: 100%;
width: 100%;
background:
linear-gradient(to bottom, hsla(0 100% 50% / 0%), hsla(0 0% 50% / 100%)),
linear-gradient(to right,
hsl(0 100% 50%) 0%,
hsl(0.2turn 100% 50%) 20%,
hsl(0.3turn 100% 50%) 30%,
hsl(0.4turn 100% 50%) 40%,
hsl(0.5turn 100% 50%) 50%,
hsl(0.6turn 100% 50%) 60%,
hsl(0.7turn 100% 50%) 70%,
hsl(0.8turn 100% 50%) 80%,
hsl(0.9turn 100% 50%) 90%,
hsl(1turn 100% 50%) 100%
);
}
#loupe {
--cursor-size: 15px;
position: relative;
display: block;
position: relative;
height: 40px;
width: 40px;
border: 3px solid black;
border-radius: 100%;
background: hsl(var(--hue) var(--saturation) 50%);
transform: translate(var(--x), var(--y));
}
#cursor {
user-select: none;
font-family: monospace;
display: block;
font-size: var(--cursor-size);
line-height: var(--cursor-size);
position: absolute;
top: calc(-0.5 * var(--cursor-size));
left: calc(-0.5 * var(--cursor-size));
width: var(--cursor-size);
height: var(--cursor-size);
transform-origin: center;
}
export class MouseController {
down = false;
pos = [0, 0];
constructor(host) {
this.host = host;
host.addController(this);
}
#onMousemove = e => {
this.pos = [e.clientX, e.clientY];
this.host.requestUpdate();
};
#onMousedown = () => {
this.down = true;
this.host.requestUpdate();
};
#onMouseup = () => {
this.down = false;
this.host.requestUpdate();
};
hostConnected() {
window.addEventListener('mousemove', this.#onMousemove);
window.addEventListener('mousedown', this.#onMousedown);
window.addEventListener('mouseup', this.#onMouseup);
}
hostDisconnected() {
window.removeEventListener('mousemove', this.#onMousemove);
window.removeEventListener('mousedown', this.#onMousedown);
window.removeEventListener('mouseup', this.#onMouseup);
}
}
<script type="module" src="color-picker.js"></script>
<color-picker></color-picker>
<output id="picked"></output>
<script>
document.querySelector('color-picker')
.addEventListener('pick', ({ detail }) => picked.style.background = detail);
</script>
html, body {
font-family: sans-serif;
width: 100vw;
height: 100vh;
padding: 0;
margin: 0;
}
#picked {
background-image: /* tint image */
linear-gradient(to right, rgba(192, 192, 192, 0.75), rgba(192, 192, 192, 0.75)),
/* checkered effect */
linear-gradient(to right, black 50%, white 50%),
linear-gradient(to bottom, black 50%, white 50%);
background-blend-mode: normal, difference, normal;
background-size: 2em 2em;
position: fixed;
bottom: 0px;
left: 0;
right: 0;
height: 400px;
display: block;
}
ApolloQueryController
data
- the result of the queryloading
error
fetchMore()
for infinite scrolling, pagination, etc.ApolloQueryController
flavoursquery = new ApolloQueryController(this, UserProfileQuery);
query = new ApolloQueryBehavior(this, UserProfileQuery);
const { data, loading } = useQuery(UserProfileQuery);
query: query(UserProfileQuery),
import { LitElement, html, css } from 'lit';
import { customElement } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js'
import { ApolloQueryController } from '@apollo-elements/core';
import { gql, TypedDocumentNode } from '@apollo/client/core';
import { client } from './client.js';
interface ProfileQueryData {
profile: {
name: string;
}
}
const UserProfile: TypedDocumentNode<ProfileQueryData> = gql`
query UserProfile {
profile {
name
}
}
`;
@customElement('user-profile')
class UserProfileElement extends LitElement {
static styles = css`.loading { opacity: 0 }`;
query = new ApolloQueryController(this, UserProfile, { client });
render() {
const { data, loading } = this.query;
return html`
<h2 class=${classMap({ loading })}>
Welcome, ${data?.profile?.name}!
</h2>
`;
}
}
<user-profile></user-profile>
<script type="module" src="user-profile.js"></script>
import { ApolloClient, InMemoryCache } from '@apollo/client/core';
import { SchemaLink } from '@apollo/client/link/schema';
import { makeExecutableSchema } from '@graphql-tools/schema';
const PROFILE = { name: 'AzConfDev' };
export const client = new ApolloClient({
cache: new InMemoryCache(),
link: new SchemaLink({
schema: makeExecutableSchema({
typeDefs: `
type User { name: String }
type Query { profile: User }
input UserInput { name: String }
type Mutation { updateProfile(user: UserInput): User }
`,
resolvers: {
Query: {
async profile() {
await new Promise(r => setTimeout(r, Math.random() * 5000));
return PROFILE;
},
},
Mutation: {
async updateProfile(_, args) {
await new Promise(r => setTimeout(r, Math.random() * 5000));
Object.assign(PROFILE, args.user);
return PROFILE;
}
}
},
}),
}),
});
ApolloMutationController
mutate()
function with options:
variables
optimisticResponse
, etc.data
- the mutation resultloading
error
update
function or refetchQueries
to manage cacheApolloMutationController
flavoursmutation = new ApolloMutationController(this, UpdateProfileMutation);
mutation = new ApolloMutationBehavior(this, UpdateProfileMutation);
const [updateProfile, { data, loading }] = useMutation(UpdateProfileMutation);
mutation: mutation(UpdateProfileMutation),
import { LitElement, html, css } from 'lit';
import { customElement, query } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js'
import { client } from './client.js';
import { ApolloQueryController, ApolloMutationController } from '@apollo-elements/core';
import { UserProfile } from './UserProfile.query.graphql.js';
import { UpdateProfile } from './UpdateProfile.mutation.graphql.js';
@customElement('user-profile')
class UserProfileElement extends LitElement {
static styles = css`.loading { opacity: 0 }`;
@query('sl-input') input: HTMLInputElement;
query = new ApolloQueryController(this, UserProfile, { client });
muttn = new ApolloMutationController(this, UpdateProfile, {
client,
update: (cache, result) => cache.writeQuery({
query: UserProfile,
data: { profile: result.data.updateProfile },
}),
});
onClickSave() {
this.muttn.mutate({ variables: { user: { name: this.input.value } } });
}
render() {
const { data } = this.query;
const loading = this.query.loading || this.muttn.loading;
return html`
<h2 class=${classMap({ loading })}>
Welcome, ${data?.profile?.name}!
</h2>
<sl-input label="Edit Username"
value=${data?.profile?.name ?? ''}
.disabled=${loading}></sl-input>
<sl-button type="primary"
.disabled=${loading}
@click=${this.onClickSave}>
Save
</sl-button>
`;
}
}
<user-profile></user-profile>
<script type="module" src="user-profile.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.0.0-beta.52/dist/themes/light.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.0.0-beta.52/dist/shoelace.js"></script>
<style>
html {
font-size: 2em;
}
</style>
import { gql, TypedDocumentNode } from '@apollo/client/core';
interface Profile {
name: string;
}
export const UserProfile: TypedDocumentNode<{ profile: Profile }> = gql`
query UserProfile {
profile {
name
}
}
`;
import { gql, TypedDocumentNode } from '@apollo/client/core';
interface Profile {
name: string;
}
export const UpdateProfile: TypedDocumentNode<{ profile: Profile }, Profile> = gql`
mutation UpdateProfile($user: UserInput) {
updateProfile(user: $user) {
name
}
}
`;
import { ApolloClient, InMemoryCache } from '@apollo/client/core';
import { SchemaLink } from '@apollo/client/link/schema';
import { makeExecutableSchema } from '@graphql-tools/schema';
const PROFILE = { name: 'AzConfDev' };
export const client = new ApolloClient({
cache: new InMemoryCache(),
link: new SchemaLink({
schema: makeExecutableSchema({
typeDefs: `
type User { name: String }
type Query { profile: User }
input UserInput { name: String }
type Mutation { updateProfile(user: UserInput): User }
`,
resolvers: {
Query: {
async profile() {
await new Promise(r => setTimeout(r, Math.random() * 5000));
return PROFILE;
},
},
Mutation: {
async updateProfile(_, args) {
await new Promise(r => setTimeout(r, Math.random() * 5000));
Object.assign(PROFILE, args.user);
return PROFILE;
}
}
},
}),
}),
});
open Fable.Lit
open Fable.Core.JsInterop
type Profile =
abstract name : string
[<ImportMember("@apollo-elements/core")>]
type ApolloQueryController =
abstract member data : {| profile: Profile |}
abstract member loading : boolean
[<Emit("new ApolloQueryController($0, $1, $2)")>]
let createController host document options: ApolloQueryController =
jsNative
let UserProfileDocument =
importMember "./UserProfile.query.graphql.js'"
let client = importMember "./client.js"
[<LitElement("user-profile")>]
let UserProfile () =
let host, props = LitElement.init ()
let query =
createController host UserProfileDocument {| client = client |}
let classes = Lit.classes [ "loading", query.loading ]
html
$"""
<h2 class={classes}>
Welcome, {data.profile.name}
</h2>
"""
import [ class_map ], from: "lit/directives/class-map.js"
import [ client ], from: "./client.js"
import [ ApolloQueryController ], from: "@apollo-elements/core"
import [ UserProfile ], from: "./UserProfile.query.graphql.js"
class UserProfileElement < LitElement
attr_accessor :query
def self.styles
<<~CSS
.loading { opacity: 0 }
CSS
end
custom_element "user-profile"
def initialize
self.query = ApolloQueryController.new(self, UserProfile, client: client)
end
def render
<<~HTML
<h2 class=#{class_map(loading: query.loading)}>
Welcome, #{query.data&.profile&.name}!
</h2>
HTML
end
end
npm i @apollo-elements/core
import { ApolloQueryController } from '@apollo-elements/core';
import { ApolloQueryController } from 'https://unpkg.com/@apollo-elements/core?module';
Dashboard SPAs
<portal-home>
<apollo-query>
<script type="application/graphql" src="Uptime.query.graphql"></script>
<template><!-- ... --></template>
</apollo-query>
</portal-home>
Business SDKs
npm install @ourcorp/html-sdk
Microfrontend Widgets
@customElement('users-online')
export class UsersOnline extends LitElement {
data = new ApolloQueryController(this, UsersOnlineQuery);
render() {
return html`<!-- ... -->`;
}
}
Made with 🥰 using...