Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save justinfagnani/1c07603073702b9e02b4e3ae2b24cb87 to your computer and use it in GitHub Desktop.
Save justinfagnani/1c07603073702b9e02b4e3ae2b24cb87 to your computer and use it in GitHub Desktop.
Apollo Controllers
import {ApolloClient, NormalizedCacheObject} from '@apollo/client/core';
/**
* Fired when an Apollo controller is connected to the document tree via its
* host. Listeners should supply an Apollo client by setting the `client`
* property on the event.
*/
export class ApolloControllerConnectedEvent extends Event {
static readonly eventName = 'apollo-controller-connected';
client?: ApolloClient<NormalizedCacheObject>;
constructor() {
super(ApolloControllerConnectedEvent.eventName, {
bubbles: true,
composed: true,
});
}
}
declare global {
interface HTMLElementEventMap {
'apollo-controller-connected': ApolloControllerConnectedEvent;
}
}
import {
ApolloClient,
NormalizedCacheObject,
MutationOptions,
// TypedDocumentNode,
DocumentNode,
} from '@apollo/client/core';
import {ApolloControllerConnectedEvent} from './apollo-controller-connected-event.js';
import {Controller, StudioElement} from './st-element.js';
export class MutationController<D = unknown, V = unknown>
implements Controller {
private _host: StudioElement;
private _client?: ApolloClient<NormalizedCacheObject>;
private _options: Omit<MutationOptions<D, V>, 'mutation'>;
private _mutation: DocumentNode<D, V>;
data?: D;
// TODO: status: ready | pending | complete | error
constructor(
host: StudioElement,
mutation: DocumentNode<D, V>,
options: Omit<MutationOptions<D, V>, 'mutation'>
) {
this._host = host;
this._mutation = mutation;
this._options = options;
host.addController(this);
}
async mutate(variables: V) {
const result = await this._client!.mutate({
...this._options,
variables,
mutation: this._mutation,
});
this.data = result.data ?? undefined;
// TODO: pull other state from the result into instance props
this._host.requestUpdate();
return result;
}
connectedCallback() {
const event = new ApolloControllerConnectedEvent();
this._host.dispatchEvent(event);
if (event.client === undefined) {
throw new Error('No Apollo client found');
}
this._client = event.client;
}
}
import {TypedDocumentNode as DocumentNode} from '@graphql-typed-document-node/core';
import {
ApolloClient,
ApolloError,
ApolloQueryResult,
NormalizedCacheObject,
ObservableQuery,
WatchQueryOptions,
} from '@apollo/client/core';
import {ApolloControllerConnectedEvent} from './apollo-controller-connected-event.js';
import {Controller, StudioElement} from './st-element.js';
export type Options<D = unknown, V = Record<string, any>> = Omit<
WatchQueryOptions<D, V>,
'query'
> & {
dataKey?: string;
shouldQuery?: () => boolean;
getVariables?: () => V;
onData?: (data: D) => void;
};
export class QueryController<D = unknown, V = Record<string, any>>
implements Controller {
private _host: StudioElement;
private _client?: ApolloClient<NormalizedCacheObject>;
private _options?: Options<D, V>;
private _query: DocumentNode<D, V>;
private _observableQuery?: ObservableQuery<D, V>;
private _variables?: V;
data?: D;
_nextPromise: Promise<D>;
_resolveNextPromise!: (data: D) => void;
// TODO: status: ready | pending | complete | error
constructor(
host: StudioElement,
query: DocumentNode<D, V>,
options?: Options<D, V>
) {
this._host = host;
this._query = query;
this._options = options;
host.addController(this);
this._nextPromise = new Promise((res) => {
this._resolveNextPromise = res;
});
}
update() {
const oldVariables = this._variables;
const newVariables = this._options?.getVariables?.();
let variablesChanged = false;
if (
(oldVariables === undefined && newVariables !== undefined) ||
(oldVariables !== undefined && newVariables === undefined)
) {
variablesChanged = true;
} else if (newVariables !== undefined && oldVariables !== undefined) {
for (const [k, v] of Object.entries(newVariables)) {
if (oldVariables[k as keyof V] !== v) {
variablesChanged = true;
break;
}
}
}
if (variablesChanged) {
this._variables = newVariables;
if (this._observableQuery) {
this._observableQuery.refetch(newVariables);
} else {
this._watch();
}
}
}
private _watch() {
if (!(this._options?.shouldQuery?.() ?? true)) {
return;
}
if (this._observableQuery) {
// TODO?
} else {
if (this._client !== undefined) {
const variables = this._options?.getVariables?.() ?? this._variables;
const options: WatchQueryOptions<V, D> = {
// TODO: omit `dataKey`
...(this._options as WatchQueryOptions<V, D>),
query: this._query,
variables,
};
this._observableQuery = this._client.watchQuery(options);
this._observableQuery.subscribe({
next: this.nextData.bind(this),
error: this.nextError.bind(this),
});
}
}
}
get next() {
return this._nextPromise;
}
protected nextData(result: ApolloQueryResult<D>): void {
this.data = result.data;
this._options?.onData?.(result.data);
this._host.requestUpdate(this._options?.dataKey);
this._resolveNextPromise(this.data);
this._nextPromise = new Promise((res) => {
this._resolveNextPromise = res;
});
// this.error = result.error;
// this.errors = result.errors;
// this.loading = result.loading;
// this.networkStatus = result.networkStatus;
// this.partial = result.partial;
}
protected nextError(error: ApolloError): void {
console.error(error);
this.error = error;
this.loading = false;
this._host.requestUpdate();
}
connectedCallback() {
const event = new ApolloControllerConnectedEvent();
this._host.dispatchEvent(event);
// TODO: if client is different, re-subscribe
this._client = event.client;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment