11 · Ansichtsbaum
Für alle, die eine eigene Oberfläche für Fibel bauen wollen — für iOS, für den Desktop, für ein Terminal, für was auch immer.
Fibel hat zwei Ausgänge aus demselben Interpreter:
┌─→ render.js → HTML
.fibel → AST → ─┤
└─→ baum.js → Ansichtsbaum (schlichte Objekte)Der linke Weg macht Webseiten. Der rechte macht eine Struktur aus Objekten, die eine beliebige Oberfläche zeichnen kann. Beide gehen durch denselben Parser, dieselben Ausdrücke, dieselben Routen, dieselben Rechte. Deshalb kann es keinen Riss zwischen „geht im Browser" und „geht in der App" geben.
Die drei Handgriffe
import { starteGeraet } from './src/geraet.js';
const app = starteGeraet({
quelle: '…der Inhalt der .fibel-Datei…',
woerter: null, // Inhalt einer woerter.json, oder null
startdaten: null, // Anfangsbestand, oder null
speicher: { // wo die Daten liegen; darf fehlen
laden: () => localStorage.getItem('meine-app'),
sichern: (text) => localStorage.setItem('meine-app', text),
},
});
app.zustand(); // einmal beim Start
app.zeichne('/regler'); // ein Ansichtsbaum für diesen Pfad
app.handle(aktion); // ein Knopfdruck, ein FormularDas ist die ganze Schnittstelle. geraet.js ist reines JavaScript ohne node:-Importe — es läuft im Browser, auf dem Server und in jeder JS-Maschine, die ein Gerät mitbringt.
zustand()
{
"name": "Schaufenster",
"design": { "accent": { "raw": "#c7a74a" }, "mode": { "word": "auto" } },
"startpfad": "/",
"angemeldet": false,
"brauchtAnmeldung": false
}design ist der design-Block, wie er dasteht. Was deine Oberfläche daraus macht, entscheidest du — die Akzentfarbe zu übernehmen ist das Mindeste, sonst sieht dieselbe App bei dir anders aus als im Browser.
zeichne(pfad)
{
"typ": "seite",
"titel": "Regler",
"pfad": "/regler",
"menue": [ { "typ": "link", "text": "Bühne", "ziel": "/", "mods": [] } ],
"inhalt": [ … ],
"fuss": [ … ]
}Statt seite kann auch kommen:
typ | Wann |
|---|---|
anmeldung | die Seite verlangt eine Anmeldung |
meldung | Seite gibt es nicht, oder der Eintrag gehört jemand anderem |
Die Knoten
Jeder Knoten hat typ und mods (eine Liste von Modifikatoren in ihrer kanonischen Form: center, large, muted, parallax …).
Text
{ "typ": "title", "text": "Regler", "mods": [] }title heading sub text note strong quote badge — alle mit text.
Behälter
{ "typ": "card", "mods": [], "kinder": [ … ] }card box row col center grid columns main aside.
Leer
{ "typ": "line", "mods": [] }
{ "typ": "space", "mods": ["large"] }Bühne
{
"typ": "stage",
"bild": "/__datei/ac0013….jpg",
"mods": ["small", "dark"],
"kinder": [ … ]
}bild kann null sein. Die Höhe steckt in mods (small, large, huge, full), ebenso, wie stark der Schleier fällt (dark, bright) und ob das Bild beim Blättern nachziehen soll (parallax).
Bilder
{ "typ": "image", "quelle": "/__datei/abc.jpg", "mods": [] }
{ "typ": "avatar", "quelle": null, "buchstabe": "A", "mods": [] }Beim Avatar ist quelle null, wenn es kein Bild gibt — dann malst du buchstabe.
Bedienung
{ "typ": "link", "text": "Weiter", "ziel": "/regler", "mods": [] }
{ "typ": "button", "text": "Löschen", "art": "gefahr",
"aktion": { "art": "loesche", "model": "Notiz", "id": "n1" } }Ein link wird an zeichne(ziel) weitergereicht, ein button an handle. art ist gefahr, still oder fehlt.
Formulare
{
"typ": "formular",
"aktion": { "art": "neu", "model": "Notiz", "ziel": "/notizen" },
"absenden": "Merken",
"felder": [
{ "name": "was", "art": "text", "beschriftung": "Wofür?",
"wert": "", "pflicht": true },
{ "name": "wieviel", "art": "regler", "beschriftung": "Wieviel Euro?",
"wert": "0", "von": 0, "bis": 5000, "schritt": 50 }
]
}Feldarten: text langtext zahl regler janein auswahl datei unterschrift email webadresse fest.
emailundwebadressesind Textfelder wietext. Sie sagen nur, welche Tastatur erscheinen soll — eine mit@, eine mit/. Wer sie nicht unterscheidet, zeichnet sie wietext; falsch wird dadurch nichts.festwird nicht angezeigt; seinwertgeht unverändert mit.auswahlbringtoptionen: [{ wert, text }]mit.reglerbringtvon,bis,schrittmit.
Aktionen zurückschicken
app.handle({ art: 'neu', model: 'Notiz', werte: { was: 'Milch', wieviel: '3' },
ziel: '/notizen' });art | Dazu |
|---|---|
neu | model, werte, ziel |
aendere | model, id, werte, ziel |
loesche | model, id |
plus | model, id, feld, menge |
anmelden | name, ziel |
abmelden | — |
gehe | ziel |
Zurück kommt:
{ "ok": true, "ziel": "/notizen" }
{ "ok": false, "meldung": "Das darfst du nicht." }Bei ok und einem ziel gehst du dorthin, sonst zeichnest du dieselbe Seite neu.
Was du nicht selbst machen musst
Rechte. Alles, was zeichne herausgibt, hat die Prüfung schon hinter sich: Seiten, die der Betrachter nicht darf, kommen als meldung oder anmeldung; Datensätze ohne Leserecht tauchen in keiner Liste auf; Knöpfe zum Löschen fremder Einträge sind gar nicht erst im Baum. Und handle prüft dasselbe noch einmal — eine Oberfläche, die einen Knopf zeigt, den es nicht geben sollte, kann damit trotzdem nichts anrichten.
Ein minimaler Zeichner
function zeichne(knoten) {
switch (knoten.typ) {
case 'title': return h1(knoten.text);
case 'text': return p(knoten.text);
case 'card': return rahmen(knoten.kinder.map(zeichne));
case 'link': return knopf(knoten.text, () => gehe(knoten.ziel));
case 'button': return knopf(knoten.text, () => app.handle(knoten.aktion));
default: return null; // was du nicht kennst, lässt du weg
}
}Unbekannte Typen einfach zu überspringen ist der richtige Umgang: Deine Oberfläche bleibt dann brauchbar, auch wenn die Sprache einen Baustein dazubekommt, den du noch nicht kennst.
Weiter: Native App — der Zeichner, den Fibel selbst mitbringt