Fibel

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

js
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 Formular

Das 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()

json
{
  "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)

json
{
  "typ": "seite",
  "titel": "Regler",
  "pfad": "/regler",
  "menue": [ { "typ": "link", "text": "Bühne", "ziel": "/", "mods": [] } ],
  "inhalt": [ … ],
  "fuss": [ … ]
}

Statt seite kann auch kommen:

typWann
anmeldungdie Seite verlangt eine Anmeldung
meldungSeite 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

json
{ "typ": "title",  "text": "Regler", "mods": [] }

title heading sub text note strong quote badge — alle mit text.

Behälter

json
{ "typ": "card", "mods": [], "kinder": [ … ] }

card box row col center grid columns main aside.

Leer

json
{ "typ": "line", "mods": [] }
{ "typ": "space", "mods": ["large"] }

Bühne

json
{
  "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

json
{ "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

json
{ "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

json
{
  "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.

  • email und webadresse sind Textfelder wie text. Sie sagen nur, welche Tastatur erscheinen soll — eine mit @, eine mit /. Wer sie nicht unterscheidet, zeichnet sie wie text; falsch wird dadurch nichts.
  • fest wird nicht angezeigt; sein wert geht unverändert mit.
  • auswahl bringt optionen: [{ wert, text }] mit.
  • regler bringt von, bis, schritt mit.

Aktionen zurückschicken

js
app.handle({ art: 'neu', model: 'Notiz', werte: { was: 'Milch', wieviel: '3' },
             ziel: '/notizen' });
artDazu
neumodel, werte, ziel
aenderemodel, id, werte, ziel
loeschemodel, id
plusmodel, id, feld, menge
anmeldenname, ziel
abmelden—
geheziel

Zurück kommt:

json
{ "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

js
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