main.js & Handler
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
| Feld | Typ | Bedeutung |
|---|---|---|
state | object | der State im Moment, in dem die Aktion begann |
getState() | () ⇒ object | der aktuelle State, auch nach einem await |
setState(key, value) | void | setzt einen Schlüssel; Punkte trennen Ebenen ("formular.name"). Die App zeichnet sich danach neu. |
data.load(source, params?) | Promise | lädt eine Datenquelle, z. B. data.load("me.groups") |
storage | object | der App-Speicher: list, get, set, delete |
exportRows(rows, options) | Promise | exportiert eine Liste als CSV, XLSX oder PDF |
toast(message, color?) | Promise | Hinweis im Host; color ist green, blue, orange oder red |
navigate(path) | Promise | bittet den Host, zu einer seiner Seiten zu wechseln |
event | any | Nutzlast des Ereignisses, z. B. { value } bei change |
item | any | Zeile einer Tabelle (rowClick) oder Eintrag einer Liste |
args | object | die 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.jsist 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.deund zum DevHub, sonst nichts.fetchzu 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.jsnicht 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:
| Alt | Neu |
|---|---|
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) |