<link rel="stylesheet" href="/_merged_assets/_static/search/noscript.css">
Azure Community Conference Logo

Azure Community Conference 2021

India’s largest Azure Conference

Multicoloured Indian Palaces
Title Sponsor
EY Building a Better World
Gold Sponsor
Microsoft
Community Partner
elastic
Portrait of Speaker

Benny Powers

Principal Web Developer, Red Hat

Multicoloured Indian Palaces

GraphQL in HTML with Apollo Elements

Data-Driven Web Apps that Use the Platform

What We'll Cover

  1. GraphQL
    • on the server
    • on the client
  2. Web Components
    • 'Vanilla' Example
    • Shadow DOM
  3. Apollo Elements
    • GraphQL in HTML
    • Reactive Controllers
    • Custom Components

{% include ../../../graphql.svg % }

GraphQL

Query Language for APIs

Published by Facebook in 2015

Used by GitHub, Twitter, Atlassian, IBM, Amazon...

Tradeoffs ⚖️

✅ Pros

  • Strongly Typed at Runtime
  • Highly Abstract and Expressive
  • Deeply Nested Data is Easy to Manage
  • Fields Defined by Functions Called Resolvers

😕 Cons

  • Implementation Overhead
  • Unfamiliar Debugging Story
  • Performance Pitfalls
  • Rife with Gotcha's

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)),
}

Queries and Mutations

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

Queries

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.

GraphQL
Frontends

On the server, GraphQL lends itself to deeply nested, networked data.

Client-side, GraphQL frontends naturally revolve around components

Components Directory

ui = f(query)

query Users {
  users {
    id
    name
    picture
    bio
  }
}
Webcomponents

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

HTML5 Logo CSS3 Logo JavaScript Logo

'Vanilla' Web Component 🍦

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>

Shadow DOM 🦇

<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>
Hello World!
Shadow DOM makes components safe to use

Apollo Elements

Apollo Elements binds GraphQL to web components.

There are two main ways to use it:

  1. Premade HTML elements like <apollo-query>
  2. Custom GraphQL elements using one or more web component libraries

Mix-and-match libraries and elements on the same page.

HTML
🕵️‍♂️ Queries

HTML Queries

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

HTML
👾 Mutations

HTML Mutations

<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 Updater Functions

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

📥 Install via NPM

npm i @apollo-elements/components
import '@apollo-elements/components';

🚛 Or load over CDN

<script type="module"
        src="https://unpkg.com/@apollo-elements/components?module">
</script>

Apollo Elements components are...

  1. Declarative
  2. Dynamic
  3. HTML-First

...but there's more

Reactive Controllers

What are Reactive Controllers?

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

Mixins / Inheritance
x "is a" y
Controllers
x "has a" y

Membrane Transport Proteins Argonne Leadership Computing Facility, US DOE

LitElement ReactiveControllerHost ReactiveController addController requestUpdatehostConnectedhostUpdated
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);
  }
}



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);

<color-picker> MouseController Window hostConnected() addEventListener() MouseEvent host.requestUpdate() this.mouse.pos <color-picker> MouseController Window

Apollo Controllers

ApolloQueryController

ApolloQueryController flavours

Lit

query = new ApolloQueryController(this, UserProfileQuery);

FAST

query = new ApolloQueryBehavior(this, UserProfileQuery);

Hooks (Haunted, Atomico)

const { data, loading } = useQuery(UserProfileQuery);

Hybrids

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>
    `;
  }
}

ApolloMutationController

ApolloMutationController flavours

Lit

mutation = new ApolloMutationController(this, UpdateProfileMutation);

FAST

mutation = new ApolloMutationBehavior(this, UpdateProfileMutation);

Hooks (Haunted, Atomico)

const [updateProfile, { data, loading }] = useMutation(UpdateProfileMutation);

Hybrids

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>
    `;
  }
}

Advantages to Controllers

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

📥 Install via NPM

npm i @apollo-elements/core
import { ApolloQueryController } from '@apollo-elements/core';

🚛 Or load over CDN

import { ApolloQueryController } from 'https://unpkg.com/@apollo-elements/core?module';
  1. Web Components
    • Custom HTML Elements
    • Isolated Shadow DOM
    • Fast updates
  2. GraphQL HTML Elements
    • Declarative
    • Dynamic Templates
    • HTML-First
  3. GraphQL Controllers
    • Cross-Compatible
    • Composable
    • Compact

Try It for...

Communities

Communities

Our Partners

PacktDevpostRSAFeitian We Build SecurityDevelopers Road Ahead

Q & A

Feedback

Thanks for Watching

Made with 🥰 using...