Skip to content

Commit

Permalink
Create generic staete mixin system for webcomponents
Browse files Browse the repository at this point in the history
  • Loading branch information
jorg-vr committed Jun 17, 2022
1 parent f8aad09 commit 361bc86
Show file tree
Hide file tree
Showing 4 changed files with 168 additions and 14 deletions.
26 changes: 12 additions & 14 deletions app/assets/javascripts/components/saved_annotation_input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { customElement, property } from "lit/decorators.js";
import { html, TemplateResult } from "lit";
import { ShadowlessLitElement } from "components/shadowless_lit_element";
import "components/datalist_input";
import { getSavedAnnotations, SavedAnnotation } from "state/SavedAnnotations";
import { stateMixin } from "state/StateMixin";

/**
* This component represents an input for a saved annotation id.
Expand All @@ -18,7 +20,7 @@ import "components/datalist_input";
* @fires input - on value change, event details contain {title: string, id: string, annotation_text: string}
*/
@customElement("d-saved-annotation-input")
export class SavedAnnotationInput extends ShadowlessLitElement {
export class SavedAnnotationInput extends stateMixin(ShadowlessLitElement) {
@property({ type: String })
name = "";
@property({ type: Number, attribute: "course-id" })
Expand All @@ -30,22 +32,18 @@ export class SavedAnnotationInput extends ShadowlessLitElement {
@property({ type: String })
value: string;

@property({ state: true })
savedAnnotations: {title: string, id: string, annotation_text: string}[] = [];
state = ["getSavedAnnotations"];

get options(): {label: string, value: string}[] {
return this.savedAnnotations.map(sa => ({ label: sa.title, value: sa.id.toString() }));
}

connectedCallback(): void {
super.connectedCallback();
this.fetchAnnotations();
get savedAnnotations(): SavedAnnotation[] {
return getSavedAnnotations({
"course_id": this.courseId.toString(),
"exercise_id": this.exerciseId.toString(),
"user_id": this.userId.toString()
});
}

async fetchAnnotations(): Promise<void> {
const url = `/saved_annotations.json?course_id=${this.courseId}&exercise_id=${this.exerciseId}&user_id=${this.userId}`;
const response = await fetch(url);
this.savedAnnotations = await response.json();
get options(): {label: string, value: string}[] {
return this.savedAnnotations.map(sa => ({ label: sa.title, value: sa.id.toString() }));
}

processInput(e: CustomEvent): void {
Expand Down
53 changes: 53 additions & 0 deletions app/assets/javascripts/state/PubSub.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* heavily based on https://github.com/hankchizljaw/vanilla-js-state-management/blob/master/src/js/lib/pubsub.js
* Copyright (c) 2018 Andy Bell
*
*
*/
export class PubSub {
events: Map<string, Array<(...params: Array<any>) => any>>;

constructor() {
this.events = new Map<string, Array<(...params: Array<any>) => any>>();
}

/**
* Either create a new event instance for passed `event` name
* or push a new callback into the existing collection
*
* @param {string} event
* @param {function} callback
* @memberof PubSub
*/
subscribe(event: string, callback: (...params: Array<any>) => any): void {
// If there's not already an event with this name set in our collection
// go ahead and create a new one and set it with an empty array, so we don't
// have to type check it later down-the-line
if (!this.events.has(event)) {
this.events.set(event, []);
}

// We know we've got an array for this event, so push our callback in there with no fuss
this.events.get(event).push(callback);
}

/**
* If the passed event has callbacks attached to it, loop through each one
* and call it
*
* @param {string} event
* @param {array} params - function params
* @memberof PubSub
*/
publish(event: string, ...params: Array<any>): void {
// There's no event to publish to, so bail out
if (!this.events.has(event)) {
return;
}

// Get each subscription and call its callback with the passed data
this.events.get(event).map(callback => callback(...params));
}
}

export const events = new PubSub();
81 changes: 81 additions & 0 deletions app/assets/javascripts/state/SavedAnnotations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { events } from "state/PubSub";
import { updateURLParameter } from "util.js";
export type SavedAnnotation = {title: string, id: number, annotation_text: string};
const URL = "/saved_annotations";

let fetchParams: Record<string, string>;
let savedAnnotations: SavedAnnotation[];
let savedAnnotationsById: Map<number, SavedAnnotation>;

export async function fetchSavedAnnotations(params?: Record<string, string>): Promise<Array<SavedAnnotation>> {
if (params !== undefined) {
fetchParams = params;
}
let url = `${URL}.json`;
for (const param in fetchParams) {
// eslint-disable-next-line no-prototype-builtins
if (fetchParams.hasOwnProperty(param)) {
url = updateURLParameter(url, param, fetchParams[param]);
}
}
const response = await fetch(url);
savedAnnotations = await response.json();
events.publish("getSavedAnnotations");
return savedAnnotations;
}

export async function fetchSavedAnnotation(id: number): Promise<SavedAnnotation> {
const url = `${URL}/${id}.json`;
const response = await fetch(url);
savedAnnotationsById.set(id, await response.json());
events.publish(`getSavedAnnotation${id}`);
return savedAnnotationsById.get(id);
}

export async function createSavedAnnotation(form: FormData): Promise<number> {
const url = `${URL}.json`;
const response = await fetch(url, {
method: "post",
body: form,
});
const savedAnnotation: SavedAnnotation = await response.json();
events.publish("fetchSavedAnnotations");
events.publish(`fetchSavedAnnotation${savedAnnotation.id}`, savedAnnotation.id);
return savedAnnotation.id;
}

export async function updateSavedAnnotation(number, id: number, form: FormData): Promise<void> {
const url = `${URL}/${id}`;
await fetch(url, {
method: "put",
body: form,
});
events.publish("fetchSavedAnnotations");
events.publish(`fetchSavedAnnotation${id}`, id);
}

export async function deleteSavedAnnotation(id: number): Promise<void> {
const url = `${URL}/${id}`;
await fetch(url, {
method: "delete",
});
events.publish("fetchSavedAnnotations");
events.publish(`fetchSavedAnnotation${id}`, id);
}

export function getSavedAnnotations(params?: Record<string, string>): Array<SavedAnnotation> {
if (savedAnnotations === undefined) {
events.subscribe("fetchSavedAnnotations", fetchSavedAnnotations);
fetchSavedAnnotations(params);
}
return savedAnnotations || [];
}

export function getSavedAnnotation(id: number): SavedAnnotation {
if (!savedAnnotationsById.has(id)) {
events.subscribe(`fetchSavedAnnotation${id}`, fetchSavedAnnotation);
fetchSavedAnnotation(id);
}
return savedAnnotationsById.get(id);
}

22 changes: 22 additions & 0 deletions app/assets/javascripts/state/StateMixin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { LitElement } from "lit";
import { events } from "state/PubSub";

type Constructor = new (...args: any[]) => LitElement;

export function stateMixin<T extends Constructor>(superClass: T): T {
class StateMixinClass extends superClass {
state: string[];
connectedCallback(): void {
super.connectedCallback();
this.initReactiveState();
}

private initReactiveState(): void {
this.state = this.state || [];
this.state.forEach( event => events.subscribe(event, () => this.requestUpdate()));
}
}

// Cast return type to the superClass type passed in
return StateMixinClass as T;
}

0 comments on commit 361bc86

Please sign in to comment.