Apollo Elements Apollo Elements Guides API Blog Toggle darkmode

Mutations: apollo-mutation Element

This page is a HOW-TO guide. For detailed docs on the <apollo-mutation> element's API, see the API docs

This generic mutation component inherits ApolloMutation, so you can use it by assigning a mutation and some variables and calling it's mutate() method:

const mutationElement = document.querySelector('apollo-mutation');
      mutationElement.mutation = gql`...`;
      mutationElement.variables = { /*...*/ };
      mutationElement.mutate();

But it comes with some extras that let you define your operation declaratively right in the template. You can define the mutation variables in several ways:

  • By assigning to the variables DOM property
  • By adding a <script type="application/json"> child element
  • By defining data attributes (e.g. data-foo="bar" for variables { foo: 'bar' })
  • By slotting in input-like elements with data-variable="variableName" attributes
  • By listening for the will-mutate event and setting the variables property in the handler.

And when you slot in a button-like element with a trigger attribute, the element will mutate on click.

Example: Mutate on click

Here we use ApolloMutation's HTML API to define the mutation and variables. You can also use the JavaScript API.

<apollo-mutation data-id="post-42" input-key="input">
  <label>New Title <input data-variable="title"/></label>
  <button trigger>Save</button>
  <script type="application/graphql">
    mutation UpdateTitle($input: UpdateTitleInput) {
      updateTitle(input: $input) {
        id title
      }
    }
  </script>
</apollo-mutation>

Example: Mutate on keyup

The trigger attribute can be a boolean attribute (with no value), in which case the element will trigger a mutation when it is clicked (or tapped, etc). If you set a value to the trigger attribute, that will be the event that triggers the mutation.

For example, set the trigger attribute to keyup on multiple variable inputs, you can mutate on keypress. If you do set the element to mutate on keyup, you might also want to debounce the mutation calls. Set the debounce attribute on the mutation element to define the debounce timeout in milliseconds.

The following example will update the user with id "007" once every 500 milliseconds, starting the last time the user performed a 'keyup' (i.e. raised their finger) while focussed on one of the inputs.

<apollo-mutation data-user-id="007" debounce="500">
  <script type="application/graphql">
    mutation UpdateUser($userId: ID!, $name: String, $age: Int) {
      updateUser(userId: $userId, name: $name, age: $age) {
        name
        age
      }
    }
  </script>
  <label>Name: <input data-variable="name" trigger="keyup"/></label>
  <label>Age:  <input data-variable="age" trigger="keyup" type="number"/></label>
</apollo-mutation>

Data Templates

Templates use stampino and jexpr for efficiently updating data expressions. See their respective READMEs for more information.

jexpr expressions are like handlebars, nunjucks, polymer, etc. expressions. You can do most things you can do in JavaScript using jexpr. Try it out for yourself on the Stampino REPL

<apollo-mutation>
  <script type="application/graphql" src="AddUser.mutation.graphql"></script>
  <label>Name <input data-variable="name"/></label>
  <button trigger>Add</button>
  <template>
    <link rel="stylesheet" href="add-user.css">
    <slot></slot>
    <output class="{{ data ? 'resolved' : 'transparent' }}">
      <p>You have added {{ data.addUser.name }}.</p>
    </output>
  </template>
</apollo-mutation>
<script type="module" src="client.js"></script>

When using shadow DOM templates, be sure you either add a <slot> element or use variables-for and trigger-for attribute on sibling nodes, so that your controls remain visible.

Learn more about template expressions and bindings in the <apollo-query> HTML element guide

Example: Conditionally Mutating

In some cases you might want to prevent a mutation, for example, if clicking the button is meant to create a new entity, but subsequently toggle it's edit state. For cases like those, listen for the will-mutate event and prevent it's default action to stop the mutation.

<apollo-mutation>
  <button trigger>Publish</button>
  <script type="application/graphql">
    mutation CreatePost($input: CreatePostInput) {
      createPost(input: $input) {
        id
        body
        title
      }
    }
  </script>
</apollo-mutation>

<script>
  document.currentScript.getRootNode()
    .querySelector('apollo-mutation')
    .addEventListener('will-mutate', function onWillMutate(event) {
      onWillMutate(event) {
        // Post exists, don't mutate.
        // Toggle the host component's edit state instead.
        if (new URL(location.href).searchParams.has('postId')) {
          event.preventDefault();
          this.querySelector('[trigger]').textContent = 'Edit';
        }
      }
    });
</script>
const template = document.createElement('template');
template.innerHTML = `
  <apollo-mutation>
    <button>Publish</button>
  </apollo-mutation>
`;

class PostsDashboard extends HTMLElement {
  $(selector) { return this.shadowRoot.querySelector(selector); }

  editing = false;

  #postId;
  get postId() { return this.#postId; }
  set postId(v) {
    this.#postId = v;
    this.$('button').textContent = !!v ? 'Edit' : 'Publish';
  }

  constructor() {
    super();
    this
      .attachShadow({ mode: 'open' })
      .append(template.content.cloneNode(true));

    this.$('apollo-mutation').mutation = CreatePostMutation;
    this.$('apollo-mutation').addEventListener('will-mutate', this.onWillMutate.bind(this));
  }

  onWillMutate(event) {
    // Post exists, don't mutate.
    // Toggle the host component's edit state instead.
    if (this.postId) {
      event.preventDefault();
      this.editing = !this.editing;
    }
  }
}
class PostsDashboard extends LitElement {
  @property({ type: String }) postId;

  @property({ type: Boolean }) editing = false;

  onWillMutate(event) {
    // Post exists, don't mutate.
    // Toggle the host component's edit state instead.
    if (this.postId) {
      event.preventDefault();
      this.editing = !this.editing;
    }
  }

  render() {
    return html`
      <apollo-mutation
          .mutation="${CreatePostMutation}"
          @will-mutate="${this.onWillMutate}">
        <button>${this.postId ? 'Edit' : 'Publish'}</button>
      </apollo-mutation>
    `;
  }
}
const template: ViewTemplate<PostsDashboard> = html`
  <apollo-mutation
      .mutation="${x => CreatePostMutation}"
      @will-mutate="${(x, e) => x.onWillMutate(e)}">
    <fast-button>${x => x.postId ? 'Edit' : 'Publish'}</fast-button>
  </apollo-mutation>
`;
@customElement({ name: 'posts-dashboard', template })
class PostsDashboard extends FASTElement {
  onWillMutate(event) {
    // Post exists, don't mutate.
    // Toggle the host component's edit state instead.
    if (this.postId) {
      event.preventDefault();
      this.editing = !this.editing;
    }
  }
}
function PostsDashboard {
  const [postId, setPostId] = useState(undefined);
  const [editing, setEditing] = useState(false);

  function onWillMutate(event) {
    // Post exists, don't mutate.
    // Toggle the host component's edit state instead.
    if (postId) {
      event.preventDefault();
      setEditing(!editing);
    }
  }

  return html`
    <apollo-mutation
        .mutation="${CreatePostMutation}"
        @will-mutate="${onWillMutate}">
      <button>${postId ? 'Edit' : 'Publish'}</button>
    </apollo-mutation>
  `;
}
function PostsDashboard {
  const [postId, setPostId] = useState(undefined);
  const [editing, setEditing] = useState(false);

  function onWillMutate(event) {
    // Post exists, don't mutate.
    // Toggle the host component's edit state instead.
    if (postId) {
      event.preventDefault();
      setEditing(!editing);
    }
  }

  return (
    <host>
      <apollo-mutation
          mutation={CreatePostMutation}
          onwill-mutate={onWillMutate}>
        <button>{postId ? 'Edit' : 'Publish'}</button>
      </apollo-mutation>
    </host>
  );
}
function onWillMutate(host, event) {
  // Post exists, don't mutate.
  // Toggle the host component's edit state instead.
  if (host.postId) {
    event.preventDefault();
    host.editing = !host.editing;
  }
}

define('posts-dashboard', {
  postId: property(),
  editing: property(false),
  render: host => html`
    <apollo-mutation
        mutation="${CreatePostMutation}"
        onwill-mutate="${onWillMutate}">
      <button>${host.postId ? 'Edit' : 'Publish'}</button>
    </apollo-mutation>
  `,
});

Example: Navigating on Mutation

Consider the "create post" case from above. If you want to navigate to the new post's page after creating it, slot an anchor into the element's trigger and use the href attribute:

<apollo-mutation href="/posts/latest/" input-key="input">
  <label>Title <input data-variable="title"/></label>
  <label>Body <textarea data-variable="body"></textarea></label>

  <a trigger href="/posts/latest/" tabindex="-1">
    <button>Create Post</button>
  </a>

  <script type="application/graphql">
    mutation CreatePost($input: CreatePostInput) {
      createPost(input: $input) { id title body }
    }
  </script>
</apollo-mutation>

If the URL you want to navigate to depends on the new entity (e.g. a url slug for the post), set the resolveURL function property.

const element = document.getElementById('create-post');
element.resolveURL = data => `/posts/${data.createPost.slug}`;

Alternatively, listen for the will-navigate event.

element.href = '/posts/';
element.addEventListener('will-navigate', event => {
  // Use your own client-side router.
  router.go(`/posts/${data.createPost.slug}`);
});

Next Steps