Die JavaScript-API
Zuletzt aktualisiert 14 August 2026

Ein Objekt liegt auf der Seite und trägt alles, worum ein Portal gebeten werden kann. Es heißt FlexiePortal, es ist da, bevor Ihre erste Zeile läuft, und es gibt nichts zu installieren, zu importieren oder zu laden.
Für wen das ist
Für alle, die ein Portal bauen und wollen, dass es etwas tut: eine Tabelle über eigene Bedienelemente filtern, aus einer Zeile einen Detaildialog öffnen, etwas zeichnen, was die eingebauten Komponenten nicht können, oder reagieren, wenn ein Kunde ein Formular abschickt.
| Sie brauchen | Sie brauchen nicht |
|---|---|
| eine HTML-Komponente auf einer Portalseite | Build-Werkzeuge, einen Paketmanager oder ein Framework |
| gewöhnliches Browser-JavaScript | irgendetwas zu installieren, zu importieren oder zu laden |
| die IDs der Komponenten, die Sie steuern wollen | zu wissen, wie das Portal gebaut ist |
Brauchen Sie überhaupt JavaScript?
Oft nicht, und zuerst danach zu greifen ist der häufigste Weg, ein Portal schwerer wartbar zu machen, als es sein müsste:
| Was Sie wollen | Wonach Sie greifen |
|---|---|
| dem Kunden seine eigenen Datensätze zeigen | eine berichtsgespeiste Komponente. Kein Code |
| einen Wert aus seinem Datensatz zeigen | Flexie Scripting in Ihrem Markup: {{ entity.first_name }} |
| eine Seite oder einen Dialog bei einem Klick öffnen | die Klick-Einstellung der Komponente selbst. Kein Code |
| eine Tabelle über eigene Bedienelemente filtern, sortieren oder blättern | diese Seite: refresh |
| etwas zeichnen, was keine Komponente zeichnen kann | diese Seite: onComponentLoaded und request |
| auf ein abgeschicktes Formular reagieren | diese Seite: das Formular-Ereignis |
Die ganze Idee in einem Beispiel
Eine vollständige HTML-Komponente. Sie bittet das Portal, ihr Bescheid zu geben, wenn sie auf dem Bildschirm ist, und verdrahtet dann Buttons, die eine Datentabelle an anderer Stelle der Seite umsortieren:
<style>
.sorts { display: flex; gap: 8px; align-items: center; }
.sorts button {
padding: 6px 12px;
border: 1px solid #d3dae5;
border-radius: 6px;
background: #fff;
cursor: pointer;
}
.sorts button.is-on { border-color: #0b84cf; font-weight: 600; }
</style>
<div class="sorts">
<button data-col="issued_on">Neueste zuerst</button>
<button data-col="total">Größte zuerst</button>
<button data-reset="1">Wie konfiguriert</button>
</div>
<script>
FlexiePortal.onComponentLoaded('sort-controls', function (el) {
var buttons = el.querySelectorAll('button')
buttons.forEach(function (button) {
button.addEventListener('click', function () {
buttons.forEach(function (b) { b.classList.remove('is-on') })
button.classList.add('is-on')
if (button.dataset.reset) {
FlexiePortal.reset('invoices')
return
}
FlexiePortal.refresh('invoices', {
order_by: button.dataset.col,
order_by_dir: 'DESC',
page: 1,
})
})
})
})
</script>
Drei Dinge darin sind das ganze Modell:
- Alles liegt auf
FlexiePortal. Es gibt nichts anderes zu lernen und keinen weiteren Namen zu merken. onComponentLoadedreicht Ihnen Ihr Element.'sort-controls'ist die eigene ID dieser Komponente, undelist das Element, in das sie gezeichnet wurde.- Sie benennen andere Komponenten über ihre ID und bitten das Portal zu handeln. Sie laden deren Daten nie selbst: das macht das Portal, auf dem Server, eingegrenzt auf den angemeldeten Kunden.
FlexiePortal ist der einzige Name
window.FlexiePortal // oder einfach FlexiePortal
Es liegt auf der Seite, bevor irgendeine Komponente gezeichnet wird, Ihr Code kann es also sofort verwenden. Es gibt keine Bereitschaftsprüfung zu schreiben und nichts, worauf zu warten wäre.
Ihr Skript bekommt keine eigenen Variablen. Ihr Element und die Parameter Ihres Dialogs erreichen Sie als Argumente einer Funktion, die Sie schreiben:
FlexiePortal.onComponentLoaded('my-component', function (el, params) {
// el das Element, in das diese Komponente gezeichnet wurde
// params warum sie geöffnet wurde. Leeres Objekt, außer bei einem Dialog
})
Schreiben Sie el, api, params oder window.portalParams an den Anfang eines Skripts, antwortet der Browser mit not defined, denn dort deklariert sie nichts.
Derselbe Code funktioniert überall: in einer Komponente, in einem Dialog, oder in Markup auf einer eigenen Seite, die gar keine Komponente ist.
Komponenten-IDs
Jede Methode nimmt eine ID, und davon gibt es zwei Arten:
| ID | Wo Sie sie finden |
|---|---|
| eine Komponenten-ID | wählen Sie die Komponente im Builder aus; sie steht im Einstellungsbereich |
| eine Seiten-ID | wählen Sie die Seite aus; derselbe Bereich |
IDs sind kurze Codes wie invoices oder wrq2wq, und sie sind stabil: eine Komponente umzubenennen ändert ihre ID nicht.
Eine falsche ID bleibt stumm. refresh('typo') löst mit null auf, getState('typo') gibt Ihnen einen Idle-Zustand, openPage('typo') tut nichts, und onComponentLoaded('typo', ...) löst schlicht nie aus. Nichts wirft und nichts wird protokolliert, ein Skript, das "gar nichts tut", ist also fast immer eine falsche ID. Prüfen Sie sie zuerst.
Kurzreferenz
Ereignisse. Jedes gibt eine Funktion zurück, die den Handler entfernt.
| Methode | Handler bekommt | Löst aus, wenn |
|---|---|---|
onComponentLoaded(id, fn) |
(el, params) |
diese Komponente gezeichnet wird, und bei jedem Neuzeichnen |
onComponentUpdated(id, fn) |
(state) |
diese Komponente neue Daten hat |
onComponentUnloaded(id, fn) |
(el, params) |
diese Komponente den Bildschirm verlässt |
onPageChanged(fn) |
(pageId) |
eine Seite gezeigt wird, auch die erste |
onReady(fn) |
nichts | das Portal sein Layout und eine Seite hat |
Komponenten.
| Methode | Gibt zurück | Tut |
|---|---|---|
refresh(id?, params?) |
Promise |
lädt eine Komponente neu, oder jede Datenkomponente einer Seite |
reset(id) |
Promise |
verwirft die von Ihrem Code gesetzten Parameter und lädt neu |
getState(id) |
Objekt | was eine Komponente gerade hält, ohne zu laden |
Das Portal.
| Methode | Gibt zurück | Tut |
|---|---|---|
openPage(id) |
nichts | wechselt die Seite |
pages() |
Array | [{id, label}], in Menüreihenfolge |
currentPage() |
String | die ID der Seite auf dem Bildschirm |
openModal(id, options?) |
nichts | öffnet einen Dialog |
closeModal() |
nichts | schließt den offenen Dialog |
reload() |
Promise |
lädt das Layout neu, dann die Seite |
currentCustomer() |
Objekt | {name, email, entity, id} |
request(url, method?, payload?) |
Promise |
ruft Ihren Endpunkt als der Kunde auf |
Und ein Flag: preview, das nur auf der Arbeitsfläche des Builders true ist.
Ereignisse
onComponentLoaded(id, handler)
var off = FlexiePortal.onComponentLoaded('agenda', function (el, params) {
// el das Element, in das diese Komponente gezeichnet wurde
// params warum sie geöffnet wurde. Leeres Objekt, außer bei einem Dialog
})
Das ist der Einstiegspunkt für fast jede Komponente, die etwas tut. Legen Sie Ihre Einrichtung hinein statt an den Anfang Ihres Skripts, und Sie bekommen das Element gereicht, in das Sie zeichnen, statt danach suchen zu müssen.
Es löst erneut aus, wenn die Komponente neu gezeichnet wird, was passiert, wenn ihr Inhalt aktualisiert wird, und jedes Mal, wenn ein Dialog geöffnet wird. Ihr Handler sollte daher gefahrlos mehrfach laufen können: zeichnen Sie von Grund auf neu, statt anzuhängen.
Jeder Handler, den Ihre Komponente registriert hat, wird automatisch entfernt, wenn sie neu gezeichnet wird oder die Seite verlässt. Ein Neuzeichnen ersetzt Ihre Listener also, statt einen zweiten Satz obendrauf zu stapeln. Sie müssen nicht hinter sich aufräumen.
onComponentUpdated(id, handler)
FlexiePortal.onComponentUpdated('invoices', function (state) {
console.log(state.data.total + ' Rechnungen')
})
Löst aus, sobald diese Komponente neue Daten hat: das erste Laden, eine von Ihnen angeforderte Aktualisierung, eine Seite, zu der der Kunde geblättert hat, eine Sortierung, die er geändert hat. Der Handler bekommt dasselbe Objekt, das getState zurückgibt.
Es löst nicht aus, während eine Komponente lädt, und es löst nicht zweimal für dieselben Daten aus. So halten Sie etwas Eigenes mit einer eingebauten Komponente im Gleichschritt, ohne zu pollen.
onComponentUnloaded(id, handler)
FlexiePortal.onComponentUnloaded('agenda', function (el, params) {
// die Komponente geht: Timer stoppen, Sockets schließen, nichts speichern
})
Löst aus, wenn die Komponente den Bildschirm verlässt: ein Seitenwechsel, ein sich schließender Dialog, oder ein Neuzeichnen. Verwenden Sie es für alles, was Ihr Markup überlebt, etwa ein Intervall oder einen Listener, den Sie an window gehängt haben.
Alles, was in Ihrem eigenen Element hängt, braucht überhaupt kein Aufräumen: es geht, wenn das Element geht.
onPageChanged(handler)
FlexiePortal.onPageChanged(function (pageId) {
document.title = 'Portal: ' + pageId
})
Löst für die erste Seite ebenso aus wie für jeden Wechsel, ein zu beliebiger Zeit registrierter Handler erfährt also von der Seite auf dem Bildschirm. Es löst nicht aus, wenn der Kunde die Seite anklickt, auf der er schon ist.
onReady(handler)
FlexiePortal.onReady(function () {
// das Layout ist da und eine Seite ist auf dem Bildschirm
})
Löst einmal aus. Registrieren, nachdem es schon passiert ist, ruft Ihren Handler trotzdem auf, es gibt also kein Wettrennen zu verlieren.
onReady ist vor allem für Markup, das keine Komponente ist: in einer bedeutet onComponentLoaded bereits, dass das Portal steht.
Regeln, die für jedes Ereignis gelten
Späte Registrierung erfährt es trotzdem. Wenn das, wonach Sie fragen, schon passiert ist, wird Ihr Handler dennoch aufgerufen: onComponentLoaded für eine Komponente, die schon auf dem Bildschirm ist, onPageChanged mit der gerade gezeigten Seite, onReady, wenn das Portal schon steht, onComponentUpdated, wenn die Daten schon angekommen sind. Die eine Ausnahme ist onComponentUnloaded, das nur künftige Abgänge meldet.
Handler werden nach der Zeile aufgerufen, die sie registriert hat, nie mittendrin. Das ist sicher:
var box = null
FlexiePortal.onComponentLoaded('mine', function (el) {
box.draw(el) // box ist gesetzt, wenn das hier läuft
})
box = makeBox()
Jede Registrierung gibt einen Weg zurück, sie rückgängig zu machen.
var off = FlexiePortal.onPageChanged(handler)
off() // nicht mehr zuhören
Die Handler Ihrer Komponente gehören Ihrer Komponente. Registrierungen im Skript einer Komponente werden verworfen, wenn diese Komponente neu gezeichnet oder entfernt wird, sie verdoppeln sich also nie. Registrierungen aus dem Markup einer Seite verwalten Sie selbst, und sie werden nie verworfen.
Ein Handler, der wirft, kann das Portal nicht kaputt machen. Der Fehler wird in der Browser-Konsole protokolliert, die anderen Handler laufen trotzdem, und die Seite zeichnet weiter.
Komponenten lesen und aktualisieren
refresh(id, params)
FlexiePortal.refresh() // jede Datenkomponente der gezeigten Seite
FlexiePortal.refresh('home') // jede Datenkomponente dieser Seite
FlexiePortal.refresh('invoices') // eine Komponente
FlexiePortal.refresh('invoices', { page: 2 })
| Argument | Nimmt |
|---|---|
id |
eine Komponenten-ID, eine Seiten-ID, oder nichts für die Seite auf dem Bildschirm |
params |
optionale Parameter für diese Komponente |
Gibt ein Promise zurück:
| Sie haben aktualisiert | Es löst auf mit |
|---|---|
| eine Komponente | den Daten dieser Komponente |
| eine Seite mit mehreren Datenkomponenten | einem Array, ein Eintrag je Komponente |
| eine Seite mit genau einer Datenkomponente | deren Daten, nicht einem Array |
| eine ID, zu der nichts passt | null |
Nur Datenkomponenten werden neu geladen. Überschriften, Absätze, Buttons und Ihr eigenes Markup haben nichts zu laden.
Parameter bleiben. Sie werden mit dem zusammengeführt, was die Komponente schon hatte, und behalten. Eine von Ihrem Code gesetzte Sortierung überlebt also die nächste Aktualisierung auf Seitenebene. Es setzt sich nichts still unter dem Kunden zurück. reset(id) ist der Weg zurück.
Was eine Komponente annimmt, alles andere wird ignoriert:
| Komponente | Angenommene Parameter |
|---|---|
| Datentabelle | limit, page, order_by, order_by_dir |
| Diagramm | limit |
| Karte | limit |
| Kalender | limit, start, end |
| Alles andere | keine |
Jeder davon grenzt ein, blättert oder sortiert um, was der Bericht ohnehin geliefert hat. Keiner kann es erweitern, und limit ist auf dem Server gedeckelt, eine Komponente lässt sich vom Browser aus also nicht in einen Datenexport verwandeln.
Wie Werte gesendet werden, denn Parameter reisen in einer Query-Zeichenkette:
| Sie übergeben | Was ankommt |
|---|---|
| einen String, eine Zahl oder einen Boolean | wie geschrieben |
| ein Array | mit Kommas verbunden |
| ein Objekt | verworfen |
null, undefined oder '' |
verworfen |
Beachten Sie die letzte Zeile: eine leere Zeichenkette zu übergeben löscht einen Parameter nicht. Verwenden Sie reset.
Schnelle Aufrufe sind unbedenklich. Überlappen sich zwei Aktualisierungen einer Komponente, gewinnt die spätere, auch wenn sie zuerst antwortet. Ein Kunde, der schnell drei Filter anklickt, sieht am Ende also den dritten.
reset(id)
FlexiePortal.reset('invoices')
Verwirft jeden Parameter, den Ihr Code an dieser Komponente gesetzt hat, und lädt sie genau so neu, wie sie im Builder konfiguriert ist. Gibt ein Promise mit den frischen Daten zurück.
getState(id)
var state = FlexiePortal.getState('invoices')
Kommt sofort zurück, ohne Anfrage:
| Feld | Hält |
|---|---|
status |
'idle', 'loading', 'ready', 'failed' oder 'unavailable' |
data |
was die Komponente empfangen hat. Die Strukturen stehen unten |
error |
der Hinweis des Servers, wenn status 'unavailable' ist. Sonst leer |
params |
die geltenden Parameter |
| Status | Bedeutet | Zu tun |
|---|---|---|
idle |
noch nicht geladen, oder die ID passt zu nichts | die ID prüfen |
loading |
eine Anfrage ist unterwegs | warten, oder onComponentUpdated verwenden |
ready |
data ist aktuell |
zeichnen |
failed |
die Anfrage kam nicht durch | einen erneuten Versuch anbieten |
unavailable |
die Komponente hat nichts zu geben und sagt das | den Leerzustand zeichnen. Nicht wiederholen |
Welche Daten jede Komponente hält
Das ist getState(id).data, und das, womit refresh auflöst. Es ist auch das, was onComponentUpdated Ihnen als state.data reicht.
Datentabelle
{
columns: [ { key: 'number', label: 'Rechnung' } ],
rows: [ { number: 'INV-1042', total: '1.200,00' } ],
total: 87, // Zeilen, die der Bericht insgesamt geliefert hat
page: 1,
limit: 20,
orderBy: 'issued_on',
orderByDir: 'DESC'
}
Die Spaltenschlüssel sind die des Berichts. Eine Zelle kann HTML enthalten, denn der Bericht entscheidet über seine eigene Formatierung.
Kennzahl
{
value: '12', // so, wie der Bericht es geschrieben hat. null, wenn es keine Zeile gibt
caption: '6 offen',
colour: 'blue',
compare: { // null, wenn keine Vergleichsspalte konfiguriert ist
previous: '9',
delta: '3',
direction: 'up', // 'up', 'down' oder 'flat'
better: true // ob diese Richtung hier eine gute Nachricht ist
},
click: { action: 'modal', target: 'invoice-detail' },
params: { open_tickets: '12' } // die Zeile, für einen Klick
}
Diagramm
{
kind: 'line_chart', // 'line_chart', 'bar_chart' oder 'pie_chart'
graph: { labels: [], datasets: [ { label: 'Summe', data: [] } ] },
total: 12
}
Kalender
{
events: [
{
id: '5002', // 'r0', 'r1'... ohne zugeordnete ID-Spalte
title: 'Jahresinspektion',
start: '2026-08-14', // oder '2026-08-14T09:05:00'
end: '2026-08-14', // fällt auf den Start zurück
allDay: true, // je Zeile: keine Uhrzeit heißt ganztägig
color: '#0b84cf', // nur mit zugeordneter Farbspalte
params: { id: '5002', startDate: '2026-08-14' }
}
],
view: 'month',
click: { action: 'modal', target: 'event-detail' },
from: '2026-08-01',
to: '2026-08-31',
total: 12 // gelieferte Zeilen, bevor unlesbare Daten verworfen wurden
}
Karte
{
markers: [
{ lat: 41.32, lng: 19.81, label: 'Lager', description: '', id: '77' }
],
zoom: 4,
cluster: false,
pin: true, // ob Kunden eine Markierung setzen dürfen
total: 120 // gelieferte Zeilen, vor Zeilen ohne Koordinaten
}
Formular
{
source: 'form',
structure: {},
prefill: {},
identifier: 'contact-request', // benennt das Absende-Ereignis, unten
js: ''
}
Navigation
FlexiePortal.openPage('documents')
FlexiePortal.currentPage() // 'documents'
FlexiePortal.pages() // [{ id: 'home', label: 'Start' }, ...]
pages() gibt sie in Menüreihenfolge zurück, Sie können also eine eigene Navigation zeichnen. pages() und currentPage() geben beide leere Werte der richtigen Struktur zurück, bevor das Layout da ist, keines braucht also eine Absicherung.
FlexiePortal.onComponentLoaded('my-nav', function (el) {
el.innerHTML = FlexiePortal.pages().map(function (page) {
return '<button data-page="' + page.id + '">' + page.label + '</button>'
}).join('')
el.addEventListener('click', function (event) {
var button = event.target.closest('[data-page]')
if (button) { FlexiePortal.openPage(button.dataset.page) }
})
})
Dialoge
FlexiePortal.openModal('invoice-detail')
FlexiePortal.openModal('invoice-detail', {
size: 'full', // 'standard' oder 'full', nur für dieses Öffnen
params: { invoiceId: 1042 }, // worum es bei diesem Öffnen geht
})
FlexiePortal.closeModal()
id ist die ID einer Dialog-Komponente. Dialoge werden nicht auf einer Seite platziert: sie liegen im Dialogfach des Builders, und etwas öffnet sie.
Es gibt kein drittes Argument. Optionen gehören in das Options-Objekt.
Der Dialog selbst liest über sein eigenes Lade-Ereignis, womit er geöffnet wurde:
// in der Dialog-Komponente "invoice-detail"
FlexiePortal.onComponentLoaded('invoice-detail', function (el, params) {
var id = params.invoiceId
el.querySelector('.body').textContent = 'Lädt ' + id + '...'
})
params erreicht auch den Server, das Markup des Dialogs wird also im Wissen gerendert, um welchen Datensatz es geht.
Ein Dialog wird bei jedem Öffnen neu aufgebaut, das Ereignis löst also einmal pro Öffnen aus, immer mit den Parametern dieses Öffnens. Sie müssen ihn nie selbst zurücksetzen.
Bauen Sie keinen eigenen Dialog. Dieser ist im Fenster zentriert und kommt mit der Kopfzeile des Produkts, Schließen-Button, Escape-Taste und Hintergrund.
Womit ein Dialog geöffnet wird
Drei Dinge können einen öffnen, und sie schlüsseln ihre Parameter unterschiedlich:
Ihr eigener Code: was auch immer Sie übergeben haben.
Ein Kalendertermin: nach Rolle geschlüsselt, das Umbenennen einer Berichtsspalte bricht den Dialog also nicht.
params.id
params.title
params.start // '2026-08-14' oder '2026-08-14T09:05:00'
params.end
params.startDate
params.startTime // fehlt bei einem ganztägigen Termin
params.endDate
params.endTime
Eine Kennzahlkarte: nach den Spaltennamen des Berichts geschlüsselt, jede skalare Zelle der Zeile, die sie gezeichnet hat. Eine Berichtsspalte umzubenennen ändert hier, was der Dialog liest.
Eine per Klick geöffnete Seite erhält keine Parameter.
Der angemeldete Kunde
var me = FlexiePortal.currentCustomer()
// { name: 'Melissa', email: 'm@example.com', entity: 'contact', id: 8874 }
Vom Server aus der Sitzung gebaut, es kann also niemals jemand anderen nennen als die angemeldete Person. Bevor das Layout da ist, leer in der richtigen Struktur ({name: '', email: '', entity: '', id: 0}), und Sie bekommen es als Kopie.
In einer Komponente brauchen Sie ihn selten. Ihr Markup wird auf dem Server gegen diesen Kunden gefüllt, bevor die Seite gesendet wird:
<p>Hallo {{ entity.first_name }}, Ihre Kundennummer ist {{ entity.id }}.</p>
Das funktioniert auch mit abgeschaltetem JavaScript und gibt Ihnen den ganzen Datensatz statt dieser vier Felder.
Schicken Sie
currentCustomer().idnie als Subjekt einer Anfrage. Es ist zum Zeichnen da. Ein Workflow hinter einem Endpunkt liest aus dem signierten Token, wer aufruft; eine ID im Anfragekörper ist das, was der Browser hineingeschrieben hat.
Den eigenen Endpunkt aufrufen
FlexiePortal.request(url, method, payload) // Promise
Ruft einen dynamischen Endpunkt (einen Workflow von Ihnen, erreichbar unter einer URL) als der angemeldete Kunde auf.
FlexiePortal.onComponentLoaded('balance', function (el) {
FlexiePortal.request('/listener/<key>/<hash>', 'POST', { detail: true })
.then(function (res) {
el.textContent = res.ok ? res.data.balance : 'Gerade nicht verfügbar.'
})
})
Warum kein einfaches fetch
Ein authentifizierter Endpunkt braucht ein signiertes Token, und Signieren braucht ein Geheimnis, das nicht in einer Seite liegen kann. Die Seite fragt das Portal, eine Sitzung, in der sie ohnehin angemeldet ist, und das Portal signiert auf dem Server. Ihr Markup fasst nie ein Geheimnis und nie ein Token an.
Argumente
| Argument | Nimmt |
|---|---|
url |
die Adresse des Endpunkts. Absolut, oder relativ zur Seite, ein führender / genügt also |
method |
Standard 'GET'. Groß- und Kleinschreibung spielt keine Rolle |
payload |
optionales Objekt |
| Methode | Nutzlast wird zu |
|---|---|
GET oder HEAD |
Parametern in der Query-Zeichenkette. Objekte und Arrays werden als JSON kodiert. Ein Schlüssel, den die URL schon trägt, wird nicht überschrieben |
| alles andere | einem JSON-Anfragekörper |
null- und undefined-Werte werden verworfen statt als "null" gesendet.
Was zurückkommt
{ ok: true, status: 200, data: {} }
| Feld | Hält |
|---|---|
ok |
ob der Endpunkt mit einem Erfolgsstatus geantwortet hat |
status |
der HTTP-Status. 0 heißt, die Anfrage ist nie angekommen: offline, DNS, ein Zertifikat |
data |
geparstes JSON, oder der rohe Text, wenn es kein JSON ist |
Ein HTTP-Fehler ist keine Ablehnung des Promise. Ein 404 oder ein 500 kommt als {ok: false} zurück:
FlexiePortal.request(url)
.then(function (res) {
if (!res.ok) {
show('Das konnte nicht geladen werden.')
return
}
draw(res.data)
})
.catch(function () {
// Die einzige Ablehnung: die Portal-Sitzung ist beendet.
window.location.reload()
})
Antwortet der Endpunkt mit 401 oder 403, signiert das Portal von sich aus ein frisches Token und versucht es einmal erneut.
Was als Endpunkt-Aufruf zählt
Eine URL eines dynamischen Endpunkts auf der eigenen Adresse des Portals:
/listener/<key>/<hash>
Alles andere wird exakt so geladen, wie es geschrieben steht, ohne angehängtes Token: ein Token für Ihren Endpunkt darf niemals an den Server eines anderen gehen. Die Regel wird auch auf dem Server durchgesetzt.
Die Regel für den Workflow hinter dem Endpunkt
Das Token sagt, wer aufruft. Es sagt nicht, was derjenige haben darf.
Jeder angemeldete Kunde kann ein Token für jeden authentifizierten Endpunkt in Ihrem Konto erhalten, weil der Browser die URL nennt. Was niemand kann, ist fälschen, wer er ist. Der Workflow liest sein Subjekt daher aus den signierten Claims:
{{__data.__headers.__jwt_data.0.entityId}}
und nie aus dem Anfragekörper oder der Query, die die Selbstauskunft des Aufrufers sind.
| Claim | Hält |
|---|---|
entityId und entity |
den Datensatz des Kunden und seinen Typ. Das Subjekt |
name und email |
so, wie das Portal sie aufgelöst hat |
portal und portalId |
bei welchem Portal er angemeldet war |
sub |
entity:id |
Andersherum geschrieben, antwortet Ihr Endpunkt auf die Datensatz-ID, die der Browser geschickt hat, und das ist ein Kunde, der die Daten eines anderen liest, ohne dass etwas es meldet.
Den Endpunkt zu bauen steht in Dynamische Endpunkte.
Das Ereignis, das ein abgeschicktes Formular auslöst
Wenn eine Formular-Komponente erfolgreich abgeschickt wird, löst das Portal auf window ein Ereignis aus, benannt nach dem Identifier des Formulars:
window.addEventListener('FlexieFormOnSuccessResponse_contact-request',
function (event) {
event.detail.data // die abgeschickten Werte
event.detail.metadata
event.detail.type // 'message', 'json', 'redirect' oder 'error'
event.detail.submission // alles, ungekürzt
})
Der Identifier steht in den Einstellungen des Formulars und in den Daten der Formular-Komponente als identifier. Es ist dasselbe Ereignis, das die Formularseiten des CRM auslösen, für Formulare anderswo geschriebene Skripte funktionieren auf einem Portal also weiter.
Vorschau auf der Arbeitsfläche des Builders
Ihre Komponente läuft an zwei Orten: im Portal und in der Vorschau auf der Arbeitsfläche des Builders. FlexiePortal gibt es in beiden, mit denselben Methoden und denselben Antwortstrukturen, Sie schreiben also nie einen Sonderfall.
Der Unterschied ist, dass die Arbeitsfläche keinen angemeldeten Kunden und kein Portal zum Navigieren hat:
| Aufruf | Im Portal | Auf der Arbeitsfläche |
|---|---|---|
onComponentLoaded |
löst aus | löst aus, mit dem Element in der Vorschau |
onComponentUnloaded |
löst aus | löst aus |
onComponentUpdated, onPageChanged, onReady |
lösen aus | lösen nie aus |
request() |
als Kunde signiert | als Vorschau signiert |
openPage, openModal, closeModal |
wirken | tun nichts |
refresh, reset, reload |
laden | lösen mit null auf |
getState, pages, currentPage |
echte Werte | leere Werte der richtigen Struktur |
currentCustomer() |
der Kunde | {name: '', email: '', entity: '', id: 0} |
preview |
nicht vorhanden | true |
Eine um onComponentLoaded herum gebaute Komponente lässt sich also korrekt vorschauen, und das ist der Hauptgrund, es zu verwenden, statt Ihren Code am Anfang des Skripts laufen zu lassen.
FlexiePortal.onComponentLoaded('balance', function (el) {
if (FlexiePortal.preview) {
el.textContent = '1.200,00 (Beispiel)'
return
}
// ...die echte Sache
})
Ein Vorschau-Aufruf trägt den internen Benutzer und keinen Kunden, ein Workflow, der seine Antwort auf einen Kunden eingrenzt, antwortet einer Vorschau also korrekterweise mit nichts. Verzweigen Sie auch dort über das Flag, wenn die Arbeitsfläche Beispieldaten zeigen soll:
{% if __data.__headers.__jwt_data.0.preview %}
Rezepte
Jedes ist eine vollständige Komponente. Fügen Sie eines ein und ändern Sie die IDs.
Eine Tabelle aktualisieren, nachdem der Kunde ein Formular abgeschickt hat
<script>
var FORM = 'contact-request' // der Identifier des Formulars
var EVENT = 'FlexieFormOnSuccessResponse_' + FORM
var GRID = 'my-requests' // die Komponenten-ID der Datentabelle
function onSubmitted() {
FlexiePortal.refresh(GRID)
}
FlexiePortal.onComponentLoaded('form-watcher', function () {
window.addEventListener(EVENT, onSubmitted)
})
// Der Listener hängt an window, überlebt also das Markup und muss von Hand
// entfernt werden. Etwas im Element müsste das nicht.
FlexiePortal.onComponentUnloaded('form-watcher', function () {
window.removeEventListener(EVENT, onSubmitted)
})
</script>
Eine eigene Zusammenfassung mit einer Tabelle im Gleichschritt halten
<p class="summary"></p>
<script>
FlexiePortal.onComponentLoaded('summary', function (el) {
var line = el.querySelector('.summary')
function draw(state) {
line.textContent = state.data.total + ' Rechnungen, Seite ' +
state.data.page
}
// Löst jetzt aus, wenn die Tabelle schon Daten hat, und wieder bei jeder
// Änderung.
FlexiePortal.onComponentUpdated('invoices', draw)
})
</script>
Eine eigene Liste zeichnen und daraus einen Dialog öffnen
<div class="list"></div>
<script>
FlexiePortal.onComponentLoaded('order-list', function (el) {
var list = el.querySelector('.list')
FlexiePortal.refresh('orders').then(function (data) {
if (!data || !data.rows) { return }
list.innerHTML = data.rows.map(function (row) {
return '<div class="row" data-order="' + row.id + '">' +
row.reference + '</div>'
}).join('')
})
list.addEventListener('click', function (event) {
var row = event.target.closest('[data-order]')
if (!row) { return }
FlexiePortal.openModal('order-detail', {
size: 'full',
params: { orderId: row.dataset.order },
})
})
})
</script>
row.id und row.reference sind die Spaltennamen des Berichts: prüfen Sie sie am Bericht, bevor Sie das schreiben.
Ein Dialog, der lädt, wofür er geöffnet wurde
<div class="detail">Wird geladen...</div>
<script>
var ENDPOINT = '/listener/<key>/<hash>'
FlexiePortal.onComponentLoaded('order-detail', function (el, params) {
var box = el.querySelector('.detail')
var payload = { id: params.orderId }
FlexiePortal.request(ENDPOINT, 'POST', payload)
.then(function (res) {
box.textContent = res.ok ? res.data.reference : 'Konnte nicht geladen werden.'
})
})
</script>
Etwas auf einem Timer, sauber aufgeräumt
<script>
var timer = null
FlexiePortal.onComponentLoaded('queue-watch', function () {
timer = setInterval(function () { FlexiePortal.refresh('queue') }, 60000)
})
FlexiePortal.onComponentUnloaded('queue-watch', function () {
clearInterval(timer)
})
</script>
Zwei Komponenten, die miteinander sprechen
Veröffentlichen Sie Methoden statt Zustand, damit die Regeln bei der Seite bleiben, der die Daten gehören:
// in der Übersichts-Komponente
FlexiePortal.onComponentLoaded('board', function (el) {
window.OrdersBoard = {
read: function (id) { return JSON.parse(JSON.stringify(orders[id])) },
commit: function (draft) { apply(draft); redraw() },
}
})
// in dem Dialog, den sie öffnet
FlexiePortal.onComponentLoaded('order-editor', function (el, params) {
var order = window.OrdersBoard.read(params.orderId)
// ...den Kunden bearbeiten lassen...
window.OrdersBoard.commit(order)
FlexiePortal.closeModal()
})
Häufige Fehler
| Fehler | Was passiert | Stattdessen |
|---|---|---|
el, api oder params am Anfang eines Skripts verwenden |
not defined |
FlexiePortal.onComponentLoaded(id, function (el, params) { }) |
| Eine ID verwenden, die es nicht gibt | gar nichts, still | sie im Einstellungsbereich prüfen |
| Die Einrichtung am Anfang des Skripts machen statt im Lade-Ereignis | funktioniert im Portal, tot auf der Arbeitsfläche des Builders, und bricht, wenn die Komponente neu gezeichnet wird | sie in onComponentLoaded legen |
| In einem Lade-Handler an das DOM anhängen | verdoppelt sich, wenn die Komponente neu gezeichnet wird | jedes Mal von Grund auf neu zeichnen |
openModal('x', 'full') |
die Option wird ignoriert | openModal('x', { size: 'full' }) |
Erwarten, dass refresh-Parameter einmalig sind |
sie bleiben, mit Absicht | reset(id) |
{ status: 'open' } an eine Tabelle senden |
verworfen: kein angenommener Parameter | die Bedingung in den Bericht bringen, oder request verwenden |
'' übergeben, um einen Parameter zu löschen |
vor dem Senden verworfen, es ändert sich also nichts | reset(id) |
Eine Komponente wiederholen, die unavailable ist |
dieselbe Antwort, immer wieder | den Leerzustand zeichnen |
Erwarten, dass request bei einem 404 wirft |
Ihr catch läuft nie |
res.ok prüfen |
| Einer Kunden-ID aus dem Anfragekörper vertrauen | jeder Browser kann sie ändern | die Token-Claims im Workflow lesen |
Eine eigene Funktion aus einem onclick="..."-Attribut aufrufen |
not defined: Inline-Attribute sehen Ihr Skript nicht |
addEventListener im Lade-Handler |
Eng verwandt
- HTML-Komponenten: die Komponente schreiben, in der das hier läuft.
- Die Komponenten: worauf sich die IDs beziehen.
- Daten und Berichte: woher die Daten einer Komponente kommen, und die Regel, die einen Kunden aus den Daten eines anderen heraushält.
- Dynamische Endpunkte: den Workflow hinter
request()bauen. - Flexie Scripting: Werte in Ihr Markup füllen, bevor die Seite gesendet wird, was ein Skript oft ganz überflüssig macht.