DokumentationMethacore Apps

main.js & Handler

9 min Lesezeit · zuletzt aktualisiert 23. September 2026

Was sich mit Aktionen nicht zusammenstecken lässt, schreibst du in main.js: jede exportierte Funktion ist ein Handler, den du im Builder als Aktion „Skript-Handler (main.js)“ auswählst. Handler bekommen einen Kontext von der Runtime – sie laden Daten, lesen und schreiben den State, speichern, exportieren und melden sich beim Host.

Gehostete Apps laden kein SDK. Alles, was ein Handler braucht, steckt im Kontext.

Der erste Handler

/** @type {MethacoreHandler} */
export async function begruessen({ data, setState, toast }) {
  const profil = await data.load("me.profile");
  setState("profil", profil);
  await toast(`Hallo ${profil.firstName}!`, "green");
}

Im Builder: Button → Aktionen → Beim Klick → Skript-Handler (main.js)begruessen. In app.json:

{ "type": "script", "handler": "begruessen" }

Die Vorlage, mit der jede neue App startet:

// Handlers exported here can be used as "script" actions in the builder.
// Each handler receives { state, getState, setState, data, storage, exportRows, toast, navigate, event, item, args }.

/** @type {MethacoreHandler} */
export async function onStart({ setState, data }) {
  const profile = await data.load("me.profile");
  setState("profile", profile);
}

Soll ein Handler beim Start laufen, hängst du ihn an das Ereignis Beim Laden des Wurzelknotens.

Der Kontext

FeldTypBedeutung
stateobjectder State im Moment, in dem die Aktion begann
getState()() ⇒ objectder aktuelle State, auch nach einem await
setState(key, value)voidsetzt einen Schlüssel; Punkte trennen Ebenen ("formular.name"). Die App zeichnet sich danach neu.
data.load(source, params?)Promiselädt eine Datenquelle, z. B. data.load("me.groups")
storageobjectder App-Speicher: list, get, set, delete
exportRows(rows, options)Promiseexportiert eine Liste als CSV, XLSX oder PDF
toast(message, color?)PromiseHinweis im Host; color ist green, blue, orange oder red
navigate(path)Promisebittet den Host, zu einer seiner Seiten zu wechseln
eventanyNutzlast des Ereignisses, z. B. { value } bei change
itemanyZeile einer Tabelle (rowClick) oder Eintrag einer Liste
argsobjectdie in der Aktion eingestellten args, bereits ausgewertet

state ist eine Momentaufnahme. Brauchst du nach einem await den neuesten Stand, nimm getState().

Typen und Autovervollständigung

Der Code-Bereich des Studios kennt die Typen aus HANDLER_TYPES der Runtime. Mit /** @type {MethacoreHandler} */ über einer Funktion bekommst du Autovervollständigung für den ganzen Kontext:

declare type MethacoreState = Record<string, any>;
declare type MethacoreSource = "me.profile" | "me.groups" | "me.events" | "me.trainingplans" | "me.competitions" | "me.membercard" | "storage.list" | "storage.get" | "storage.put" | "storage.delete" | "request";
declare type MethacoreStorageScope = "org" | "user";
declare interface MethacoreExportOptions {
  format: "csv" | "xlsx" | "pdf";
  filename?: string;
  columns?: { key: string; label?: string }[];
  /** CSV only: semicolon and BOM for German Excel. */
  excel?: boolean;
  /** PDF only. */
  title?: string;
}
declare interface MethacoreHandlerContext {
  /** State at the moment the action started. */
  state: MethacoreState;
  /** Current state, also after awaits. */
  getState(): MethacoreState;
  setState(key: string, value: unknown): void;
  /** Payload of the event, e.g. { value } of a change. */
  event?: any;
  /** Row of a table or list the event came from. */
  item?: any;
  /** Arguments configured on the script action. */
  args?: Record<string, any>;
  /** Loads a data source of the Builder, e.g. await data.load("me.groups"). */
  data: { load(source: MethacoreSource, params?: Record<string, unknown>): Promise<any> };
  /** App storage of the organisation (default) or of the member. */
  storage: {
    list(collection: string, options?: { scope?: MethacoreStorageScope; limit?: number; offset?: number }): Promise<{ items: { key: string; value: any; updatedAt: string }[]; total: number }>;
    get(collection: string, key: string, options?: { scope?: MethacoreStorageScope }): Promise<{ key: string; value: any; updatedAt: string }>;
    set(collection: string, key: string, value: unknown, options?: { scope?: MethacoreStorageScope }): Promise<void>;
    delete(collection: string, key: string, options?: { scope?: MethacoreStorageScope }): Promise<void>;
  };
  exportRows(rows: Record<string, unknown>[], options: MethacoreExportOptions): Promise<void>;
  toast(message: string, color?: "green" | "blue" | "orange" | "red"): Promise<void>;
  navigate(path: string): Promise<void>;
}
declare type MethacoreHandler = (context: MethacoreHandlerContext) => unknown;

Beispiele

Speichern mit eigenem Schlüssel

/** @type {MethacoreHandler} */
export async function anheften({ getState, setState, storage, toast }) {
  const text = String(getState().neu ?? "").trim();
  if (!text) return toast("Schreib zuerst etwas.", "orange");

  const key = `n-${Date.now().toString(36)}`;
  await storage.set("pinnwand", key, { text, am: new Date().toISOString() });

  const seite = await storage.list("pinnwand", { limit: 100 });
  setState("notizen", seite.items);
  setState("neu", "");
}

Tabellenzeile anklicken

/** @type {MethacoreHandler} */
export function auswaehlen({ item, setState }) {
  setState("auswahl", item);
  setState("detailOffen", true);
}

Ein Knoten mit "visible": "{{state.detailOffen}}" zeigt danach die Details.

Gefilterter Export

/** @type {MethacoreHandler} */
export async function exportieren({ getState, exportRows, args }) {
  const termine = (getState().termine ?? []).filter((termin) => termin.location === args.ort);
  await exportRows(termine, {
    format: "pdf",
    filename: `termine-${args.ort}`,
    title: `Termine in ${args.ort}`,
    columns: [
      { key: "title", label: "Termin" },
      { key: "start", label: "Beginn" },
    ],
  });
}
{ "type": "script", "handler": "exportieren", "args": { "ort": "{{state.ort}}" } }

Eingaben prüfen, bevor gespeichert wird

/** @type {MethacoreHandler} */
export async function anmelden({ state, storage, toast, navigate }) {
  const { name, email } = state.formular ?? {};
  if (!name || !/^[^@\s]+@[^@\s]+$/.test(email ?? "")) {
    return toast("Bitte Name und eine gültige E-Mail angeben.", "red");
  }
  await storage.set("anmeldungen", `a-${Date.now()}`, { name, email }, { scope: "user" });
  await toast("Du bist angemeldet.", "green");
  await navigate("/apps");
}

Fehler

Wirft ein Handler oder schlägt ein Aufruf fehl, zeigt die App „Aktion fehlgeschlagen: <Meldung>“ und schreibt den Fehler in die Konsole. Die folgenden Aktionen derselben Kette laufen dann nicht mehr. Fang Fehler selbst ab, wenn du eine eigene Meldung zeigen willst:

export async function laden({ data, setState, toast }) {
  try {
    setState("ausweis", (await data.load("me.membercard")).card);
  } catch (error) {
    await toast("Der Ausweis ist gerade nicht erreichbar.", "orange");
  }
}

Was main.js kann – und was nicht

  • main.js ist ein ES-Modul. Weitere .js-Dateien des Entwurfs lädst du mit relativem Import: import { format } from "./format.js";
  • Exportiert werden benannte Funktionen oder ein Default-Objekt mit Funktionen. Handler-Namen folgen den Regeln für JavaScript-Bezeichner.
  • Netzwerk nur über den Kontext: Die Sicherheitsrichtlinie erlaubt Verbindungen zu *.api.methacore.de und zum DevHub, sonst nichts. fetch zu anderen Adressen scheitert.
  • Die App läuft in einem Sandbox-Iframe ohne eigene Origin: kein localStorage, keine Cookies. Für Dauerhaftes gibt es den App-Speicher.
  • Kein Zugriff auf das DOM des Hosts. Das Aussehen steuerst du über styling.css, nicht per Skript.
  • Kann main.js nicht geladen werden, startet die App trotzdem – nur Skript-Aktionen melden dann, dass ihr Handler fehlt.

Der alte sdk-Parameter

Frühere Handler bekamen ein sdk-Objekt. Es steht noch im Kontext, ist aber veraltet und verschwindet in einer späteren Schema-Version. So stellst du um:

AltNeu
sdk.api.apps.me.profile()data.load("me.profile")
sdk.api.apps.me.groups()data.load("me.groups")
sdk.api.apps.storage.list("c", { scope })storage.list("c", { scope })
sdk.api.apps.storage.put("c", "k", v)storage.set("c", "k", v)
sdk.api.apps.storage.delete("c", "k")storage.delete("c", "k")
sdk.api.request("members", "/members")data.load("request", { section: "members", path: "/members" })
sdk.host.toast(text, color)toast(text, color)
sdk.host.navigate(path)navigate(path)
sdk.exportRows(rows, options)exportRows(rows, options)