2 hungrige Vögel in einem Nest

Vermeidung von tiefer Verschachtelung von Inhalten in Neos CMS

Kategorien:

Wie Sie Inhaltssammlungen verwenden können, ohne Ihrer Inhaltsstruktur unnötige Ebenen hinzuzufügen. Dieser Post wurde größtenteils automatisiert aus dem Englischen übersetzt.

Neos CMS ermöglicht die Erstellung von Inhaltselementen, die andere Elemente (auch Nodes genannt) enthalten können. Um dies dynamisch zu gestalten, existiert der Typ Neos.Neos:ContentCollection. Fast alle Code-Beispiele, die ich gesehen habe, und auch die meisten Elemente, die ich selbst gebaut habe, haben eine Struktur, die dem folgenden vereinfachten Beispiel für ein mehrspaltiges Element ähnelt:

MultiColumn:
  ContentCollection:
    SingleColumn:
      ContentCollection:
        Headline
        Text
    SingleColumn:
      ContentCollection:
        Headline
        Text

Die beiden ContentCollection-Nodes werden verwendet, um untergeordnete Nodes zu enthalten, aber sie haben keine weitere semantische Verwendung, da wir bereits Eltern haben, die ihre Kinder enthalten. 

Stark verschachtelter Inhaltsbaum

Wir haben dadurch mehrere Nachteile:

  • In unserem Code müssen wir eine Ebene tiefer gehen und nach den Kindern einer Collection suchen.
  • Die Redakteure werden eine nutzlose Verschachtelungsebene in ihrem Inhaltsbaum haben.
  • Wir haben zusätzliche Nodes in der Datenbank, die ebenfalls für die Übersetzung usw. dupliziert werden.

Ich sehe regelmäßig die Frage in der Neos-Community, wie man diese Verschachtelungsebenen loswerden kann, und ich habe mehrere Projekte gesehen, die in ihrer Nutzbarkeit leiden, weil sie eine noch komplexere Struktur mit 8 oder mehr Ebenen haben. Das hat mich dazu veranlasst, diesen Blogbeitrag zu schreiben, um zu zeigen, wie dieses Problem gelöst werden kann.

Unser Ziel

Um die genannten Probleme zu lösen, müssen wir unsere NodeTypes etwas anders definieren und auch den Fusioncode anpassen. Es gibt eine einfache Methode, die keine komponentenbasierte Darstellung verwendet, und eine etwas komplexere, die Komponenten verwendet. Ich werde mit der einfachen Methode beginnen, da beide die gleichen NodeType-Definitionen verwenden.

Die folgende Struktur wollen wir erreichen:

MultiColumn:
  SingleColumn:
    Headline
    Text
  SingleColumn:
    Headline
    Text

Wir haben jetzt nur noch semantisch relevante Knoten in unserem Element, die der Struktur eines mehrspaltigen Elements entsprechen würden, wenn wir es jemand anderem beschreiben würden..

Definieren der NodeTypes

Damit unser Beispiel funktioniert, brauchen wir ein paar NodeTypes. 

Die ersten NodeTypes sind für das mehrspaltige Element, das später mehrere Spalten aufnehmen kann:

'My.Site:Content.Columns':
  superTypes:
    'Neos.Neos:Content': true
    'Neos.Neos:ContentCollection': true
  ui:
    label: 'Columns'
    icon: columns
    group: 'structure'
  constraints:
    nodeTypes:
      '*': false
      'My.Site:Content.Column': true

Der große Unterschied zu den Elementen, die Sie normalerweise sehen, ist, dass wir gleichzeitig von Neos.Neos:Content und von Neos.Neos:ContentCollection erben. Dies ist notwendig, um das Hinzufügen des Elements in der Neos-Oberfläche zu ermöglichen und auch die Funktionalität des Hinzufügens oder Entfernens von Kindknoten zu ermöglichen.

Zusätzlich definieren wir eine Einschränkung, um nur das Hinzufügen von Kindern des Spaltentyps zu ermöglichen. Auf diese Weise können wir uns in unserem Code darauf verlassen, welche Art von Inhalt wir dort finden werden.

Die nächsten Knotentypen sind für die Spalte. Es gibt eine abstrakte Spaltendefinition und eine einfache Implementierung.

'My.Site:Mixin.Column':
  abstract: true
  superTypes:
    'Neos.Neos:Content': true
    'Neos.Neos:ContentCollection': true
  ui:
    label: 'Column'
    icon: columns
    group: 'structure'

'My.Site:Content.Column':
  superTypes:
    'My.Site:Mixin.Column': true

Diese Aufteilung erlaubt es uns, später zusätzliche Spaltentypen zu erstellen, die ihr eigenes spezielles vordefiniertes Verhalten mitbringen können, ohne dass viel mehr Konfiguration nötig ist.

Mit diesen NodeTypes können Sie bereits mit der Erstellung von Inhalten in Ihrer Neos-Instanz beginnen und Ihre Struktur aufbauen. Aber Sie werden auch eine Menge Fehlermeldungen erhalten, da die Darstellung nicht definiert ist. Dies wird im nächsten Schritt gelöst.

Grundlegendes Fusion-Rendering

Der Fusioncode ist für diese NodeTypes sehr wichtig, und die meisten Menschen haben damit Schwierigkeiten, besonders wenn sie versuchen, Fusionkomponenten zu verwenden. Deshalb erkläre ich zunächst, wie man diese Spalten auf die einfachste Art und Weise rendern kann. 

Wir beginnen wieder mit dem mehrspaltigen Element selbst.

prototype(My.Site:Content.Columns) < prototype(Neos.Neos:ContentCollection) {
    @context.node = ${node}
}

Durch die Vererbung von Neos.Neos:ContentCollection haben wir bereits alle Funktionen, die wir benötigen. Es verfügt über die notwendige Cache-Konfiguration und rendert ein Tag mit der Content-Sammlungsklasse. Wir brauchen den Knoten im Kontext nicht einmal zu überschreiben, da der Standard funktionieren würde, aber dies verhindert den Aufruf einer internen Methode, die versucht, die nächstliegende Sammlung zu finden.

Die nächste Frage, die sich normalerweise stellt, ist, wie Sie Ihre eigene CSS-Klasse hinzufügen können:

prototype(My.Site:Content.Columns) < prototype(Neos.Neos:ContentCollection) {
    @context.node = ${node}
    attributes.class = 'columns'																		    
}

Damit haben wir bereits ein gültiges Rendering für das mehrspaltige Element.

Schauen wir uns nun den Fusioncode für die Spalte an:

prototype(My.Site:Content.Column) < prototype(Neos.Neos:ContentCollection) {
    @context.node = ${node}
    attributes.class = 'column'
}
Weniger verschachtelter Inhaltsbaum

Es ist im Grunde derselbe Code wie vorher.

Mit diesen beiden Fusion-Prototypen in Ihrem Code sollten Sie bereits ein voll funktionsfähiges Element haben. Sie können so viele Spalten in Ihrem mehrspaltigen Element hinzufügen, wie Sie wollen, und Sie können jede Art von Kind in jede Spalte einfügen.

Natürlich möchte man oft nicht beliebig viele Spalten haben, sondern einige mehrspaltige Elemente mit zwei, drei oder vier Spalten vordefinieren, die ein Redakteur verwenden kann. Dies erfordert nur einige kleine Änderungen.

Dies wird derzeit noch nicht mit Dokumentennodes funktionieren, da es ein bestimmtes Verhalten in der Benutzeroberfläche gibt, das das direkte Hinzufügen von Kindern verhindert. Das bedeutet, dass Sie nicht verhindern können, dass eine ContentCollection zwischen dem Dokumentnode im Strukturbaum und der ersten Ebene des Inhalts liegt. Außer natürlich, wenn Sie feste Unterelemente in Ihrem NodeType definieren.

Sie können auf dem Screenshot sehen, wie es im Neos-Backend aussehen würde.

Fixierte Elemente der Spaltenanzahl

Zuerst teilen wir unsere Spalten-NodeType auf, um die Flexibilität zu erhöhen:

'My.Site:Mixin.Columns':
  abstract: true
  superTypes:
    'Neos.Neos:Content': true
  ui:
    icon: columns
    group: 'structure'
    inspector:
      groups:
        layout:
          label: 'Layout'
          icon: arrows-h
  constraints:
    nodeTypes:
      'Neos.Neos:Content': false
      'My.Site:Mixin.Column': true

'My.Site:Content.Columns':
  superTypes:
    'My.Site:Mixin.Columns': true
    'Neos.Neos:ContentCollection': true
  ui:
    label: 'Columns'

Ich habe den größten Teil unserer Konfiguration in den abstrakten NodeType verschoben, um ihn für unsere zusätzlichen mehrspaltigen Elemente mit einer festen Anzahl von Spalten wiederverwendbar zu machen. Wir werden den übergeordneten Typ Neos.Neos:ContentCollection nicht in den abstrakten NodeType verschieben, da er nur benötigt wird, wenn ein interaktiver Container benötigt wird, der das Hinzufügen und Entfernen von Kindern erlaubt.

Jetzt können wir ein Element erstellen, das zwei Spalten bietet:

'My.Site:Content.TwoColumns':
  superTypes:
    'My.Site:Mixin.Columns': true
  ui:
    label: 'Two columns'
  childNodes:
    column0:
      nodeType: 'My.Site:Content.Column'
    column1:
      nodeType: 'My.Site:Content.Column'

Der neue NodeType ist ganz einfach. Wir haben einen vordefinierten Kind-Knoten des gleichen Typs, den wir bereits zuvor verwendet haben. 

Der Fusionteil ist so einfach wie bisher:

prototype(My.Site:Content.TwoColumns) < prototype(Neos.Neos:ContentCollection) {
    @context.node = ${node}
}

Hier verwenden wir immer noch den Neos.Neos:ContentCollection-Fusion-Prototypen, der wie bisher vererbt wird, da er den gesamten notwendigen Rendering-Code bereitstellt, den wir benötigen.

Erledigt. Sie könnten jetzt dasselbe für drei oder mehr Spaltenelemente tun.

Lesen Sie weiter, wenn Sie daran interessiert sind, komponentenbasierte Elemente zu bauen, und wie Sie den Elementen ein flexibles Styling verleihen können.

Erweiterte Ausgabe mit Fusion

Die derzeit beste Methode zur Darstellung von Elementen mit Fusion ist die Verwendung von Komponenten. Dies erhöht die Flexibilität und Wiederverwendbarkeit und macht den Code meist besser wartbar.

Wenn Sie einfach den übergeordneten Prototyp für die Spalten auf Neos.Neos:ContentComponent umschalten und den Renderer auf Neos.Neos:ContentCollection setzen, wird er zwar gerendert, aber es wird für jedes Element zusätzliches Markup im HTML-Inhalt geben. Dies geschieht, weil beide Prototypen etwas Context-Wrapping hinzufügen, das für die Neos-Benutzeroberfläche erforderlich ist. Zusätzliches Markup verursacht oft Probleme mit bestehenden Vorlagen oder bei der Verwendung von Javascript-Slidern. Wir wollen es also loswerden.

Die NodeType-Definitionen bleiben unverändert. Wir müssen nur das Fusion-Rendering ändern.

Wir beginnen zunächst wieder mit dem Spaltenelement:

prototype(My.Site:Content.Columns) < prototype(Neos.Neos:ContentComponent) {
    renderer = afx`
        <div class="columns">
            <Neos.Neos:ContentCollectionRenderer/>
        </div>
    `
}

Anmerkung (hinzugefügt am 19. Februar 2020):

Derzeit ist es nur möglich, ein Wrapping-Div um den ContentCollectionRenderer herum zu haben. Mit Neos 5.2 wird es eine Änderung geben, so dass Sie die Neos.Neos:ContentCollection einfach an jeder beliebigen Stelle verwenden können. Es ist nicht klar, ob die Änderung für ältere Neos-Versionen zurückportiert wird.

Dieser Code wird Ihr Element bereits wieder darstellen und Sie können Spalten hinzufügen und entfernen. Aber es gibt einige Probleme damit. Wenn Sie ein Kind modifizieren, wird das gesamte Element möglicherweise nicht aktualisiert, da uns die notwendige Cache-Konfiguration von Neos.Neos:ContentCollection fehlt.

Wir erstellen also zunächst eine Hilfskomponente, die die Cache-Konfiguration für unsere beiden Prototypen enthält.

prototype(My.Site:Component.ContentCollection) < prototype(Neos.Neos:ContentComponent) {
    @cache {
        mode = 'cached'
        entryIdentifier {
            collection = ${node}
        }
        entryTags {
            1 = ${Neos.Caching.descendantOfTag(node)}
            2 = ${Neos.Caching.nodeTag(node)}
        }
        maximumLifetime = ${q(node).context({'invisibleContentShown': true}).children().cacheLifetime()}
    }

    @exceptionHandler = 'Neos\\Neos\\Fusion\\ExceptionHandlers\\NodeWrappingHandler'
}

Und jetzt passen wir den Prototyp der Spalten an:

prototype(My.Site:Content.Columns) < prototype(My.Site:Component.ContentCollection) {
    renderer = afx`
        <div class="columns">
            <Neos.Neos:ContentCollectionRenderer/>
        </div>
    `
}

Mit dieser Änderung wird das Caching-Verhalten korrigiert und wir erhalten auch bessere Ausnahmen, wenn es ein Problem mit einem Kind-Knoten gibt. 

Der letzte Schritt besteht darin, den Spaltenknoten auf die gleiche Weise anzupassen:

prototype(My.Site:Content.Column) < prototype(My.Site:Component.ContentCollection) {
    renderer = afx`
        <div class="column">
            <Neos.Neos:ContentCollectionRenderer/>
        </div>
    `
}

Erledigt. Sie können nun mit diesen Elementen wie mit jeder anderen Komponente arbeiten. Sie können diese Art der Definition der über- und untergeordneten Knoten auch für andere Elementtypen verwenden. Dies können Slider, Akkordeons, Listen und mehr sein. Es ist auch möglich, auf diese Weise eine tiefere Verschachtelung zu erstellen, wenn Sie mehr als 2 Ebenen der Verschachtelung benötigen. Der Code ist immer derselbe.

Im nächsten und letzten Kapitel werden einige grundlegende, aber flexible Möglichkeiten zur Gestaltung der Elemente vorgestellt.

Die Dinge hübsch machen

Normalerweise wären Sie bereits fertig, da Sie die notwendigen CSS-Klassen in Ihrer HTML-Datei haben, um die Dinge zu verschönern und zu stylen.

Aber während der Erstellung des Beispiel-Codes hatte ich eine Idee für eine flexible Inline-Styling-Variante für mehrspaltige Elemente, die ich vorstellen wollte.

Passen Sie zunächst den Spalten-NodeType wie folgt an:

'My.Site:Content.Columns':
  superTypes:
    'My.Site:Mixin.Columns': true
    'Neos.Neos:ContentCollection': true
  ui:
    label: 'Columns'
  properties:
    columnBaseWidth:
      type: string
      ui:
        label: 'Column min width'
        reloadIfChanged: true
        inspector:
          group: layout
          editorOptions:
            placeholder: 'e.g. 33% or 200px'

Dann passen Sie die einspaltigen NodeTypes an:

'My.Site:Mixin.Column':
  abstract: true
  superTypes:
    'Neos.Neos:Content': true
    'Neos.Neos:ContentCollection': true
  ui:
    label: 'Column'
    icon: columns
    group: 'structure'
    inspector:
      groups:
        layout:
          label: 'Layout'
          icon: arrows-h

'My.Site:Content.Column':
  superTypes:
    'My.Site:Mixin.Column': true
  properties:
    width:
      type: integer
      defaultValue: 1
      ui:
        label: 'Width'
        reloadIfChanged: true
        inspector:
          group: layout

Mit diesen Änderungen können wir nun die Grundbreite einer Spalte im Elternteil definieren und das individuelle Verhältnis einer einzelnen Spalte festlegen. Wir benötigen diese beiden Werte später, um das Flexbox-basierte Styling für das Frontend zu erstellen. Die columnBaseWidth definiert die Startbreite einer einzelnen Spalte, die später mit ihrem proportionalen Wert multipliziert wird. Dies ist erforderlich, um die Spalten auf einer bestimmten Breite zu halten, bevor sie in eine andere Zeile umgebrochen werden, wenn mehr als eine Spalte vorhanden ist und der Ansichtsbereich kleiner wird. Die proportionale Breite definiert das Verhältnis zwischen mehreren Spalten. Eine Spalte mit "2" als Breite und eine mit "1" wird im Browser als 66% / 33% Layout angezeigt.

Auch der Fusionteil muss angepasst werden. Wir beginnen mit einem kleinen Helfer zum Rendern von Inline-Stilen:

prototype(My.Site:Helper.Styles) < prototype(Neos.Fusion:DataStructure) {
    @process {
        filter = ${Array.filter(value, (val) => val != '' && val != null)}
        map = ${Array.map(value, (val, prop) => prop + ':' + val)}
        join = ${Array.join(value, ';')}
    }
}

Dieser Helfer kann dazu verwendet werden, schnell einige Inline-Stile zu einer Komponente hinzuzufügen. Leere Werte werden gefiltert, so dass Sie sich bei der Verwendung von Bedingungen keine Sorgen machen müssen.

Jetzt passen wir den Spalten-Renderer erneut an, um die notwendigen Stile für Flexbox hinzuzufügen.

prototype(My.Site:Content.Columns) < prototype(My.Site:Component.ContentCollection) {
    renderer = afx`
        <div class="columns">
            <My.Site:Helper.Styles @path="attributes.style"
                display="flex"
                flex-wrap="wrap"
                margin="-.5rem"
            />
            <Neos.Neos:ContentCollectionRenderer/>
        </div>
    `
}

Mit diesen Inline-Stilen erhalten wir Spalten, die nebeneinander bleiben und bei Bedarf umgebrochen werden. Margin ermöglicht es uns, zwischen den Spalten etwas Abstand zu gewinnen.

Nun wollen wir auch das Spaltenelement ändern:

prototype(My.Site:Content.Column) < prototype(My.Site:Component.ContentCollection) {
    width = ${q(node).property('width')}
    columnBaseWidth = ${q(node).parent().property('columnBaseWidth')}

    renderer = afx`
        <div class="column">
            <My.Site:Helper.Styles @path="attributes.style"
                flex={props.width}
                flex-basis={'calc(' + props.width + '*' + props.columnBaseWidth + ')'}
                padding=".5rem"
            />
            <Neos.Neos:ContentCollectionRenderer/>
        </div>
    `

    @cache.entryTags.parent = ${Neos.Caching.nodeTag(q(node).parent())}
}

Wir rufen die beiden breitenbezogenen Werte ab und fügen sie in die Inline-Stile für die Spalte ein. Damit sich eine Spalte mit einem größeren Anteil korrekt verhält, verwenden wir die Funktion css calc, um die Basisbreite anzupassen. Wir müssen auch die Cache-Konfiguration ändern, damit die Spalte bei Änderung des Elternteils gerendert wird.

Wir sind fertig! 

Sie haben jetzt ein sehr flexibles mehrspaltiges Element, das es erlaubt, einzelne Spalten schmaler oder breiter zu machen, und Sie können das Umbruchverhalten über das Elternteil beeinflussen. Das gleiche funktioniert natürlich auch mit dem zuvor gezeigten Zwei-Spalten-Element oder jeder anderen Variante.

Wenn Ihnen die Inline-Stile nicht gefallen, könnten Sie stattdessen Auswahlfelder für die Breiten-Eigenschaften verwenden und dann eine css-Klasse auf der Grundlage dieser für das Umbruch-Div rendern. Eine weitere Verbesserung wäre auch eine gewisse Validierung, so dass nicht irgendein Wert eingegeben werden kann.

Sie finden den gesamten Code auch als gist.

Nachtrag vom 22.01.2020: Ich habe inzwischen auch die CSS-in-Fusion Funktionalität als Plugin für Neos bereitgestellt.

Fazit

Mit den vorgeschlagenen NodeType-Definitionen und dem Fusion-Rendering erhalten Sie eine stark reduzierte Inhaltsstruktur. Dies hat Vorteile für die Leistung und die Benutzerfreundlichkeit. Ihre Redakteure werden produktiver und zufriedener sein, wenn sie Neos CMS verwenden.

Meine Beispiele sind natürlich sehr einfach und sollten in einem realen Projekt verbessert und erweitert werden. Aber wenn Sie glauben, dass selbst für diesen Blog-Post etwas besser gemacht werden könnte, oder wenn Sie einen Fehler gefunden haben, wenden Sie sich bitte an mich.

Aktualisierung 5.11.2019: Zwei Screenshots der Struktur im Neos-Backend hinzugefügt