Sie sehen sich Hilfeinhalte der folgenden Version an:

Damit der Autor mit dem Seiten-Editor den Inhalt einer SPA bearbeiten kann, muss die SPA die in diesem Dokument beschriebenen Anforderungen erfüllen.

Vorsicht:

Die Editor-Funktion für Single Page Applications (SPAs) wurde mit AEM 6.4 eingeführt und ist derzeit als Technologievorschau verfügbar. Der SPA-Editor wird separat zum AEM-Schnellstart im nächsten Service Pack für AEM 6.3 und 6.4 bereitgestellt

  • Diese Funktion befindet sich noch in der Entwicklung und die Dokumentation kann jederzeit geändert werden.
  • Der SPA Editor ist die empfohlene Lösung für Projekte, die clientseitiges Rendering basierend auf einem SPA-Framework erfordern (z. B. React).

Einführung

Dieses Dokument beschreibt die Anforderungen, die eine SPA erfüllen muss, damit der AEM-Inhaltsautor mit dem AEM-Seiten-Editor den Inhalt einer SPA bearbeiten kann.

Hinweis:

Die folgenden Bedingungen gelten unabhängig vom Framework. Sofern diese Anforderungen erfüllt sind, kann eine Framework-spezifische Schicht aus Modulen, Komponenten und Services bereitgestellt werden.

Vorsicht:

Obwohl die SPA-Möglichkeiten von AEM Framework-unabhängig sind, wird derzeit nur das React-Framework unterstützt.

Weitere Informationen zu SPAs in AEM finden Sie in den folgenden Dokumenten:

Allgemeine Konzepte

Seitenmodell

Die Inhaltsstruktur der Seite wird in AEM gespeichert. Das Modell der Seite wird verwendet, um SPA-Komponenten zuzuordnen und zu instanziieren. Die SPA-Entwickler erstellen SPA-Komponenten, die sie den AEM-Komponenten zuordnen.

Die SPA muss mit dem Seitenmodell synchron sein und ihren Inhalt entsprechend aktualisieren. Sie müssen ein Muster verwenden, das dynamische Komponenten nutzt, um Komponenten entsprechend der vorgegebenen Seitenmodellstruktur spontan zu instanziieren.

Meta-Felder

Das Seitenmodell nutzt das JSON Exportable Model, das auf der Sling Model-API basiert. Die exportierbaren Sling-Modelle stellen die folgende Liste von Feldern zur Verfügung:

  • :type: Typ jeder Ressource(Standard = Ressourcentyp)
  • :items: Untergeordnete Elemente der Ressource (verschachtelte Struktur, nur bei Containern vorhanden)
  • :itemsOrder: Sortierte Liste der untergeordneten Elemente
    • Das JSON-Zuordnungsobjekt garantiert die Reihenfolge seiner Felder nicht.
    • Mit dem Zuordnungsobjekt und dem aktuellen Array hat der Nutzer der API die Vorteile beider Strukturen.
  • :pages: Weitere Seiten, die zusätzlich zur Stammseite geladen werden (flache Struktur, nur auf Stammseitenebene vorhanden)
  • :pagePath: Inhaltspfad der Seite (nur bei Seiten vorhanden)
  • :pageTitle: Titel der Seite (nur bei Seiten vorhanden)

Siehe auch Erste Schritte mit AEM Content Services.

PageModelManager-Modul

Das PageModelManager-Modul wird dem SPA-Projekt als npm-Paket bereitgestellt. Es begleitet die SPA und dient als Datenmodellmanager. Es abstrahiert den Abruf und die Verwaltung der JSON-Struktur, die die eigentliche Inhaltsstruktur darstellt, im Namen der SPA. Es ist auch für die Synchronisierung mit der SPA verantwortlich und informiert sie darüber, wenn ihre Komponenten neu gerendert werden müssen.

Siehe https://www.npmjs.com/package/@adobe/cq-spa-page-model-manager

ComponentMapping-Modul

Das ComponentMapping-Modul wird dem SPA-Projekt ebenfalls als NPM-Paket bereitgestellt. Es bietet der SPA eine Möglichkeit, SPA-Komponenten zu AEM-Ressourcenarten zuzuordnen. Dies ermöglicht eine dynamische Auflösung der Komponenten beim Parsing der Seitenmodell-JSON. Dabei wird jede Komponente, die in der JSON von einer Ressourcenart beschrieben wird, entsprechend der SPA-Komponente gerendert, der sie zugeordnet wurde.

Siehe https://www.npmjs.com/package/@adobe/cq-spa-component-mapping

SPA-Wrapper-Modul

Für die Projektintegration wird ein NPM-spezifisches Paket bereitgestellt, in dem die erforderlichen Bibliotheken, Module, Services und Komponenten gesammelt sind.

Dieses Framework-spezifische Modul fasst anschließend die zuvor erwähnten Module zusammen und passt sie ggf. an das Framework an:

Für React ist das entsprechende Paket @adobe/cq-react-editable-components.

SPA-Schlüsselkomponenten

ModelProvider

ModelProvider ist eine der wichtigsten SPA-Grundkomponenten. Sie agiert als Wrapper für alle SPA-Komponenten und sorgt dafür, dass sie mit einem bestimmten Teil des Seitenmodells synchron sind. Sie erstellt außerdem die erforderlichen Datenpfadattribute, damit der Editor erkennt, dass eine SPA-Komponente editierbar ist. Intern verwendet sie die PageModelManager-API, um sicherzustellen, dass die eingeschlossene SPA-Komponente aktualisiert wird, wenn das Seitenmodell aktualisiert wird.

Im Folgenden finden Sie ein Beispiel für die React-Implementierung der ModelProvider-Klasse.

import React, {Component, Children} from 'react';
import {render, findDOMNode} from 'react-dom';
import Constants from '../Constants';
import {PageModelManager} from '@adobe/cq-spa-page-model-manager';
 
/**
 * Wrapper component responsible for synchronizing a child component with a given portion of the page model.
 * The location of the portion of the page model corresponds to the location of the resource in the page and is accessible via the data_path / page_path properties of the component.
 * Those properties are then output in the form of data attributes (data-cq-page-path and data-cq-data-path) to allow the editor to understand to which AEM resource this component corresponds.
 *
 * When the model gets updated the wrapped component gets re-rendered with the latest version of the model passed as the cq_model parameter.
 * <p>The ModelProvider supports content items as well as child pages</p>
 *
 *
 * @class
 * @extends React.Component
 * @memberOf components
 *
 * @param {{}} props                      - the provided component properties
 * @param {string} props.data_path        - relative path of the current configuration in the overall page model
 * @param {string} props.page_path        - absolute path of the containing page
 * @param {boolean} props.force_reload    - should the cache be ignored
 */
class ModelProvider extends Component {
 
    constructor(props) {
        super(props);
 
        this.state = {
            data_path: props && props.data_path ? props.data_path : '',
            page_path: props && props.page_path ? props.page_path : '',
            cq_model: props && props.cq_model
        };
 
        this.updateData();
    }
 
    /**
     * Updates the state and data of the current Object
     *
     * @protected
     */
    updateData() {
        const that = this;
        const path = this.state.data_path || '';
 
        // Fetching the latest data for the item at the given path
        this.getData().then(model => {
            if (!model) {
                return;
            }
 
            model[Constants.DATA_PATH_PROP] = path;
 
            that.setState({
                data_path: path,
                page_path: that.getPagePath(),
                cq_model: model
            });
        });
    }
 
    componentDidMount() {
        PageModelManager.addListener({pagePath: this.state.page_path, dataPath: this.state.data_path, callback: this.updateData.bind(this)});
    }
 
    componentWillUnmount() {
        // Clean up listener
        PageModelManager.removeListener({pagePath: this.state.page_path, dataPath: this.state.data_path, callback: this.updateData.bind(this)});
    }
 
    componentDidUpdate() {
        this.decorateChildElements();
    }
 
    componentWillReceiveProps(nextProps) {
        if (nextProps.data_path !== this.props.data_path || nextProps.page_path !== this.props.page_path) {
            // Path has been updated.
            const newDataPath = nextProps.data_path || '';
            const newPagePath = nextProps.page_path || '';
            // Remove old listener associated with the old location
            PageModelManager.removeListener({pagePath: this.state.page_path, dataPath: this.state.data_path, callback: this.updateData.bind(this)});
            // Add new listener on the new location.
            // We can not use state because it is not updated yet
            PageModelManager.addListener({pagePath: newPagePath, dataPath: newDataPath, callback: this.updateData.bind(this)});
            // Update state
            this.setState({page_path: newPagePath, data_path : newDataPath}, this.updateData.bind(this));
        }
    }
 
    /**
     * Returns the provided page path property
     *
     * @returns {string}
     *
     * @protected
     */
    getPagePath() {
        // 1. The model is a page and exposes its path
        // 2. The path is provided as a property
        return (this.state && this.state.cq_model && this.state.cq_model[Constants.PAGE_PATH_PROP]) || this.props && this.props.page_path || '';
    }
 
    /**
     * Does the current component has the model of a page
     *
     * @returns {boolean}
     *
     * @protected
     */
    isPageModel() {
        return !!(this.state && this.state.cq_model && this.state.cq_model.hasOwnProperty(Constants.PAGE_PATH_PROP));
    }
 
    /**
     * Decorate a child {@link HTMLElement} with extra data attributes
     *
     * @param {HTMLElement} element     - Element to be decorated
     *
     * @protected
     */
    decorateChildElement(element) {
        if (!element) {
            return;
        }
 
        let childAttrs = {};
 
        let pagePath = this.getPagePath();
 
        // a child page isn't a piece of content of the parent page
        if (this.isPageModel() && pagePath) {
            childAttrs.cqPagePath = pagePath;
        } else {
            childAttrs.cqDataPath = this.state.data_path;
        }
 
        Object.keys(childAttrs).forEach(attr => element.dataset[attr] = childAttrs[attr]);
    }
 
    /**
     * Decorate all the child {@link HTMLElement}s with extra data attributes
     *
     * @protected
     */
    decorateChildElements() {
        // for each child ref find DOM node and set attrs
        Object.keys(this.refs).forEach(ref => this.decorateChildElement(findDOMNode(this.refs[ref])))
    }
 
    /**
     * Returns the model data from the page model
     *
     * @returns {Promise}
     *
     * @protected
     */
    getData() {
        return PageModelManager.getData({pagePath: this.getPagePath(), dataPath: this.state.data_path, forceReload: this.props.force_reload});
    }
 
    render() {
        if (!this.props.children || this.props.children.length < 1) {
            return null;
        }
 
        // List and clone the children to passing the data as properties
        return Children.map(this.props.children, child =>
            React.cloneElement(child, { ref: this.state.data_path, cq_model: this.state.cq_model, cq_model_page_path: this.state.page_path, cq_model_data_path: this.state.data_path }));
    }
}
 
export default ModelProvider

Container

Ein Container ist eine Komponente, die untergeordnete Komponenten enthält und rendert. Der Container verwendet zunächst ComponentMapping, um untergeordnete Komponenten dynamisch aufzulösen und aufzunehmen.

Im Folgenden finden Sie ein Beispiel für die React-Implementierung der Container-Klasse.

import React, {Component} from 'react';
import Constants from '../Constants';
import {ComponentMapping} from '../ComponentMapping';
import ModelProvider from "./ModelProvider";
 
/**
 * Container component that provides the common features required by all containers such as the dynamic inclusion of child components.
 * <p>The Container supports content items as well as child pages</p>
 *
 * @class
 * @extends React.Component
 * @memberOf components
 *
 *
 * @param {{}} props                                - the provided component properties
 * @param {{}} [props.cq_model]                     - the page model configuration object
 * @param {string} [props.cq_model.:dataPath]       - relative path of the current configuration in the overall page model
 */
class Container extends Component {
 
    /**
     * Wrapper class in which the content is eventually wrapped
     *
     * @returns {ModelProvider}
     *
     * @protected
     */
    get modelProvider() {
        return ModelProvider;
    }
 
    /**
     * Returns the path of the page the current component is part of
     *
     * @returns {*}
     *
     * @protected
     */
    getPagePath() {
        return this.props && this.props.cq_model && this.props.cq_model[Constants.PAGE_PATH_PROP] || this.props.cq_model_page_path;
    }
 
    /**
     * Returns the {@link React.Component} mapped to the type of the item
     * @param {{}} item     - item of the model
     * @returns {boolean}
     *
     * @protected
     */
    getDynamicComponent(item) {
        if (!item) {
            return false;
        }
 
        // console.debug("Container.js", "add item", item.path, item, that);
        const type = item[Constants.TYPE_PROP];
 
        if (!type) {
            // console.debug("Container.js", "no type", item, that);
            return false;
        }
 
        // Get the constructor of the component to later be dynamically instantiated
        return ComponentMapping.get(type);
    }
 
    /**
     * Returns the component optionally wrapped into the current ModelProvider implementation
     *
     * @param {string} field                    - name of the field where the item is located
     * @param {string} itemKey                  - map key where the item is located in the field
     * @param {string} containerDataPath        - relative path of the item's container
     * @param {function} propertiesCallback     - properties to dynamically decorate the wrapper element with
     * @returns {React.Component}
     *
     * @protected
     */
    getWrappedDynamicComponent(field, itemKey, containerDataPath, propertiesCallback) {
        if (!this.props.cq_model[field]) {
            return false;
        }
 
        const item = this.props.cq_model[field][itemKey];
 
        if (!item) {
            return false;
        }
 
        item[Constants.DATA_PATH_PROP] = containerDataPath + itemKey;
 
        const DynamicComponent = this.getDynamicComponent(item);
 
        if (!DynamicComponent) {
            // console.debug("Container.js", "no dynamic component", item, that);
            return false;
        }
 
        let Wrapper = this.modelProvider;
 
        if (Wrapper) {
            propertiesCallback = propertiesCallback || function noOp(){return {}};
 
            return <Wrapper key={item[Constants.DATA_PATH_PROP]} {...propertiesCallback()}><DynamicComponent cq_model={item} cq_model_page_path={this.props.cq_model_page_path} cq_model_data_path={this.props.cq_model_data_path}/></Wrapper>
        }
 
        return <DynamicComponent cq_model={item} cq_model_page_path={this.props.cq_model_page_path} cq_model_data_path={this.props.cq_model_data_path}/>;
    }
 
    /**
     * Returns a list of item instances
     *
     * @param {string} field                - name of the field where the item is located
     * @param fieldOrder                    - name of the field that contains the order in which the items are listed
     * @param containerDataPath             - relative path of the item's container
     * @returns {React.Component[]}
     *
     * @protected
     */
    getDynamicItemComponents(field, fieldOrder, containerDataPath) {
        let dynamicComponents =  [];
 
        this.props.cq_model && this.props.cq_model[fieldOrder] && this.props.cq_model[fieldOrder].forEach(itemKey => {
            dynamicComponents.push(this.getWrappedDynamicComponent(field, itemKey, containerDataPath,  () => {
                let dataPath = containerDataPath + itemKey;
                // either the model contains page path fields or we use the propagated value
                let pagePath = this.getPagePath();
 
                return {
                    data_path: dataPath,
                    page_path: pagePath
                }
            }));
        });
 
        return dynamicComponents || [];
    }
 
    /**
     * Returns a list of page instances
     *
     * @param {string} field                - name of the field where the item is located
     * @param containerDataPath             - relative path of the item's container
     * @returns {React.Component[]}
     *
     * @protected
     */
    getDynamicPageComponents(field, containerDataPath) {
        if (!this.props.cq_model || !this.props.cq_model[field]) {
            return [];
        }
 
        let dynamicComponents = [];
 
        const model = this.props.cq_model[field];
 
        for (let itemKey in model) {
            if (model.hasOwnProperty(itemKey)) {
                dynamicComponents.push(this.getWrappedDynamicComponent(field, itemKey, containerDataPath, () => {
                    return {
                        page_path: itemKey
                    }
                }));
            }
        }
 
        return dynamicComponents || [];
    }
 
    /**
     * Returns the path of the current resource
     *
     * @returns {string|undefined}
     *
     * @protected
     */
    get path() {
        return this.props && this.props.cq_model && this.props.cq_model[Constants.DATA_PATH_PROP];
    }
 
    /**
     * Returns a list of child components
     *
     * @returns {Array.<React.Component>}
     *
     * @protected
     */
    get innerContent() {
        let containerPath = this.path || this.props.data_path || '';
 
        // Prepare container path for concatenation
        if ('/' === containerPath) {
            containerPath = '';
        }
 
        containerPath = containerPath.length > 0 ? containerPath + '/' : containerPath;
 
        let dynamicComponents = this.getDynamicItemComponents(Constants.ITEMS_PROP, Constants.ITEMS_ORDER_PROP, containerPath);
 
        return dynamicComponents.concat(this.getDynamicPageComponents(Constants.PAGES_PROP, containerPath));
    }
 
    render() {
        return <div>{this.innerContent}</div>;
    }
}
 
export default Container;

Seite

Die Page-Komponente ist eine einfache Container-Komponente, die Zugriff auf die Seiteneigenschaften und Klassennamen bietet.

Responsives Raster

Die zugehörigen SPA-Komponenten müssen den Wert der folgenden Modellfelder zu ihren jeweiligen Elementklassenattributen hinzufügen:

  • gridClassNames: stellt Klassennamen für das responsive Raster bereit
  • columnClassNames: stellt Klassennamen für die responsive Spalte bereit

Im Folgenden finden Sie ein Beispiel für die React-Implementierung der ResponsiveGrid-Klasse.

import React, {Component} from 'react';
import { MapTo } from '../ComponentMapping';
import Container from './Container';
import ResponsiveColumnModelProvider from './ResponsiveColumnModelProvider';
import Constants from '../Constants';
import Utils from '../Utils';
 
const CONTAINER_CLASS_NAMES = 'aem-container';
const PLACEHOLDER_CLASS_NAMES = Constants.NEW_SECTION_CLASS_NAMES + ' aem-Grid-newComponent';
 
/**
 * Placeholder of the responsive grid component
 *
 * @class
 * @extends React.Component
 * @private
 */
class Placeholder extends Component {
 
 
    render() {
        return <div data-cq-data-path={this.props.cq_model && this.props.cq_model[Constants.DATA_PATH_PROP] + "/*"} className={PLACEHOLDER_CLASS_NAMES} />
    }
}
 
/**
 * Container that provides the capabilities of the responsive grid.
 *
 * Like the Container component, the ResponsiveGrid dynamically resolves and includes child component classes.
 * Instead of using a ModelProvider it uses a ResponsiveColumnModelProvider that will - on top of providing access to the model - also decorate the rendered elements with class names relative to the layout.
 *
 * @class
 * @extends components.Container
 * @memberOf components
 *
 * @param {{}} props                                    - the provided component properties
 * @param {{}} [props.cq_model]                         - the page model configuration object
 * @param {string} [props.cq_model.gridClassNames]      - the grid class names as provided by the content services
 * @param {string} [props.cq_model.classNames]          - the class names as provided by the content services
 */
class ResponsiveGrid extends Container {
 
    constructor(props) {
        super(props);
 
        this.state = {
            cq_model: props.cq_model,
            classNames: '',
            gridClassNames: ''
        };
    }
 
    /**
     * Returns the class names of the grid element
     *
     * @returns {string|boolean}
     */
    get gridClassNames() {
        return this.props && this.props.cq_model && this.props.cq_model.gridClassNames;
    }
 
    /**
     * Provides the class names of the grid wrapper
     *
     * @returns {string}
     */
    get classNames() {
        if (!this.props || !this.props.cq_model) {
            return '';
        }
 
        let classNames = CONTAINER_CLASS_NAMES;
 
        if (this.props.cq_model.classNames) {
            classNames += ' ' + this.props.cq_model.classNames;
        }
 
        if (this.props.cq_model.columnClassNames) {
            classNames += ' ' + this.props.cq_model.columnClassNames;
        }
 
        return classNames;
    }
 
    /**
     * Returns the content of the responsive grid placeholder
     * @returns {{}}
     */
    get placeholder() {
        // Add a grid placeholder when the page is being authored
        if (Utils.isInEditor()) {
            return <Placeholder {...this.props} />;
        }
    }
 
    /**
     * @inheritDoc
     * @returns {ResponsiveColumnModelProvider}
     */
    get modelProvider() {
        return ResponsiveColumnModelProvider;
    }
 
    render() {
        return (
            <div className={this.classNames}>
                <div className={this.gridClassNames}>
                    {this.innerContent}
                    {this.placeholder}
                </div>
            </div>
        )
    }
}
 
export default MapTo('wcm/foundation/components/responsivegrid')(ResponsiveGrid);

HTML-Elementstruktur

Das folgende Fragment zeigt die typische HTML-Darstellung einer Seiteninhaltsstruktur. Beachten Sie folgende Punkte zur Struktur:

  • Das responsive Rasterelement überträgt Klassennamen mit dem Präfix aem-Grid--.
  • Das responsive Spaltenelement überträgt Klassennamen mit dem Präfix aem-GridColumn--.
  • Ein responsives Raster, das auch die Spalte eines übergeordneten Rasters ist, ist so eingeschlossen, dass die beiden oben genannten Präfixe nicht auf demselben Element vorkommen.
  • Elemente, die bearbeitbaren Ressourcen entsprechen, haben die Eigenschaft data-cq-data-path. Siehe Abschnitt Datenpfadattribute.
  • Elemente, die Drag-and-Drop von Assets unterstützen, müssen ein Klassennamen nach dem Muster cq-dd-<Asset-Typ> angeben. Siehe den Abschnitt Aktivieren von Drag-and-Drop.

 

<div data-cq-page-path="/content/page">
    <div class="aem-Grid aem-Grid--12 aem-Grid--default--12">
        <div class="aem-container aem-GridColumn aem-GridColumn--default--12" data-cq-data-path="root/responsivegrid">
            <div class="aem-Grid aem-Grid--12 aem-Grid--default--12">
                <div class="cmp-image cq-dd-image aem-GridColumn aem-GridColumn--default--12" data-cq-data-path="root/responsivegrid/image">
                    <img src="/content/we-retail-spa-sample/react/jcr%3acontent/root/responsivegrid/image.img.jpeg/1512113734019.jpeg">
                </div>
            </div>
        </div>
    </div>
</div>

Editor-Vertrag

Datenpfadattribute

Die SPA-Komponenten müssen die folgenden Datenattribute generieren, damit der Editor mit ihnen interagieren kann:

  • data-cq-content-path: Relativer Pfad der Komponente, der von PageModel angegeben wird (z. B. root/responsivegrid/image). Dieses Attribut sollte nicht zu Seiten hinzugefügt werden.
  • data-cq-page-path: Absoluter Pfad der im Repository gespeicherten Seite (z. B. /content/weretail/homepage). Dieses Attribut sollte nur zu Seiten hinzugefügt werden.

Platzhalter

Platzhalter für leere Komponenten

Das SPA-Framework muss eine Wrapper-Komponente bereitstellen, die grafische Komponenten dekorieren kann. Die folgenden Funktionen müssen vom Wrapper bereitgestellt werden.

Darstellung von Leere

Wenn eine Komponente leer ist, muss dies durch die Komponente dargestellt werden. Sie müssen sich überlegen und festlegen, welche Beschriftung ein Platzhalter für eine leere Komponente anzeigen soll.

Aktivieren von Drag-and-Drop

Um Drag-and-Drop-Funktionen von Assets und Komponenten zu ermöglichen, müssen Sie einen bestimmten Namen angeben, der dem AEM-Feld droptarget der Asset-Komponenten-Zuordnung des entsprechenden Asset-Typs entspricht.

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0" xmlns:rep="internal"
    jcr:mixinTypes="[rep:AccessControllable]"
    jcr:primaryType="cq:Page">
    <rep:policy/>
    <wcm jcr:primaryType="nt:unstructured">
        <foundation jcr:primaryType="nt:unstructured">
            <components jcr:primaryType="nt:unstructured">
                <responsivegrid jcr:primaryType="nt:unstructured">
                    <we-retail-default
                        jcr:description="Policy for editable layout containers"
                        jcr:lastModified="{Date}2018-02-05T18:34:17.420+01:00"
                        jcr:lastModifiedBy="admin"
                        jcr:primaryType="nt:unstructured"
                        jcr:title="We.Retail Default"
                        sling:resourceType="wcm/core/components/policy/policy"
                        components="[/libs/cq/experience-fragments/editor/components/experiencefragment,/libs/wcm/foundation/components/responsivegrid,group:We.Retail,group:We.Retail Commerce]"
                        policyResourceType="wcm/foundation/components/responsivegrid">
                        <cq:authoring jcr:primaryType="nt:unstructured">
                            <assetToComponentMapping jcr:primaryType="nt:unstructured">
                                <image
                                    jcr:primaryType="nt:unstructured"
                                    assetGroup="media"
                                    assetMimetype="image/*"
                                    droptarget="image"
                                    resourceType="weretail/components/content/image"
                                    type="Images"/>
                                <product
                                    jcr:primaryType="nt:unstructured"
                                    assetGroup="product"
                                    droptarget="product-data-reference"
                                    resourceType="commerce/components/product"
                                    type="Products"/>

Container-Platzhalter

Die SPA-Komponente ist einem grafischen Container wie dem responsiven Raster zugeordnet und muss einen virtuellen untergeordneten Platzhalter hinzufügen, wenn der Inhalt bearbeitet wird.

Wenn der Inhalt der SPA im Seiteneditor bearbeitet wird, wird dieser Inhalt mit einem iFrame in den Editor eingebettet und das Attribut data-cq-editor wird dem document-Knoten des Inhalts hinzugefügt.

Wenn das Attribut data-cq-editor vorhanden ist, muss der Container ein HTMLElement aufnehmen, um den Bereich darzustellen, mit dem der Autor interagiert, wenn der Container leer ist. Dies wird im folgenden React-Beispiel gezeigt.

<div data-cq-content-path={this.props.path + "/*"} className="new section aem-Grid-newComponent"/>

Hinweis:

Die im Beispiel verwendeten Klassennamen werden zurzeit vom Seiten-Editor erfordert.

Erforderliche Klassennamen:

  • new section: Zeigt an, dass das aktuelle Element der Platzhalter des Containers ist
  • aem-Grid-newComponent: Normalisiert die Komponente für die Layout-Bearbeitung

EditConfig

Zusammenfassend muss eine SPA-Komponente den folgenden Vertrag respektieren, damit sie vom Seiteneditor als bearbeitbar erkannt wird:

  • Stellen Sie die erwarteten Attribute bereit, um eine SPA-Komponenteninstanz mit einer AEM-Ressource zu verknüpfen (in der React-Implementierung wird dies bereits über ModelProvider durchgeführt)
  • Stellen Sie die erwartete Folge von Attributen und Klassennamen bereit, die das Erstellen von leeren Platzhaltern ermöglicht
  • Stellen Sie die erwarteten Klassennamen bereit, die Drag-and-Drop von Assets ermöglichen

In der React-Implementierung werden die letzten beiden Funktionen für die SPA-Komponenten bereitgestellt, bevor die Zuordnung zu Ressourcenarten stattfindet. Dies geschieht durch eine Erweiterung des ComponentMapping-Moduls, das das EditConfig-Konfigurationsobjekt einführt.

Im Folgenden sehen Sie ein Beispiel einer React-Implementierung der ComponentMapping-Erweiterung.

/**
 * Configuration object in charge of providing the necessary data expected by the page editor to initiate the authoring. The provided data will be decorating the associated component
 *
 * @typedef {{}} EditConfig
 * @property {String} [dragDropName]       If defined, adds a specific class name enabling the drag and drop functionality
 * @property {String} emptyLabel           Label to be displayed by the placeholder when the component is empty. Optionally returns an empty text value
 * @property {function} isEmpty            Should the component be considered empty. The function is called using the context of the wrapper component giving you access to the component model
 */
 
/**
 * Map a React component with the given resource types. If an {@link EditConfig} is provided the <i>clazz</i> is wrapped to provide edition capabilities on the AEM Page Editor
 *
 * @param {string[]} resourceTypes                      - List of resource types for which to use the given <i>clazz</i>
 * @param {class} clazz                                 - Class to be instantiated for the given resource types
 * @param {EditConfig} [editConfig]                     - Configuration object for enabling the edition capabilities
 * @returns {class}                                     - The resulting decorated Class
 */
ComponentMapping.map = function map (resourceTypes, clazz, editConfig) {};

Routing-Vertrag

Die aktuelle Implementierung basiert auf der Annahme, dass die SPA Hashes für die Routen der verschiedenen untergeordneten Seiten verwendet.

Konfiguration

Seitenmodell-Router-Konfiguration

Der PageModelManager unterstützt Routing. Das bedeutet, dass er auf Hash-Änderungen reagiert, das entsprechende Modell intern lädt und ein cq-pagemodel-route-changed-Ereignis sendet, auf das andere Module warten können, wenn nötig.

Standardmäßig ist dieses Verhalten automatisch aktiviert. Um es zu deaktivieren, sollte die SPA die folgende Meta-Eigenschaft rendern:

<meta property="cq:page_model_router" content="false"\>

Jede Route der SPA sollte einer vorhandenen Seite in AEM entsprechen (z. B. /content/mysite/mypage), da der PageModelManager automatisch versucht, das entsprechende Seitenmodell zu laden, wenn die Route ausgewählt wird.

Bei Bedarf kann die SPA auch eine „schwarze Liste“ mit Routen definieren, die der PageModelManager ignorieren soll:

<meta property="cq:page_model_route_filters" content="route/not/found,^(.*)(?:exclude/path)(.*)"/>

JSON-Export-Konfiguration

Wenn Routing-Funktionen aktiviert sind, wird angenommen, dass der JSON-Export der SPA aufgrund des JSON-Exports der AEM-Navigationskomponente die verschiedenen Routen der Anwendung enthält. Die JSON-Ausgabe der AEM-Navigationskomponente kann in der SPA-Stammseiten-Inhaltsrichtlinie mit den folgenden beiden Eigenschaften konfiguriert werden:

  • structureDepth: Zahl, die die Tiefe der exportierten Baumstruktur angibt
  • structurePatterns: Regex oder Array aus Regexes, die der zu exportierenden Seite entsprechen

Dies kann im SPA-Beispielinhalt in /conf/we-retail-journal/react/settings/wcm/policies/we-retail-journal/react/components/structure/page/root gezeigt werden.

Um Routing zu unterstützen, muss der SPA-Komponentenentwickler zunächst eine Navigationskomponente implementieren, die einer AEM-Navigationskomponente zugeordnet ist. Diese Komponente rendert Links zu den Routen basierend auf der JSON-Ausgabe der zugeordneten Navigationskomponente (siehe JSON-Export-Konfiguration).

Außerdem muss die SPA die verschiedenen Routen der Anwendung rendern. In React ist es beispielsweise möglich, Routen über React Router zu deklarieren.

Im SPA-Beispielinhalt basiert die Implementierung auf der Tatsache, dass eine Route für jede Seitenkomponente der Anwendung generiert wird. Dies wird durch den withRoute()-Helper erreicht (der vorerst Teil der Beispielanwendung ist), der die Seite umschließt und das entsprechende React-Route-Element generiert. Wenn die Anwendung eine einzelne Stammseite hat, sollte diese Stammseite für das Warten auf neue Pfade verantwortlich sein und sie bei Bedarf rendern. Dies passiert, wenn eine untergeordnete Seite asynchron geladen wird.

Eine SPA in Aktion

Erfahren Sie, wie eine einfache SPA funktioniert, und experimentieren Sie selbst mit einer SPA, indem Sie mit dem Dokument Erste Schritte mit SPAs in AEM fortfahren.

Dieses Werk unterliegt den Bedingungen der Creative Commons Attribution-Noncommercial-Share Alike 3.0 Unported License.  Twitter™- und Facebook-Beiträge fallen nicht unter die Bedingungen der Creative Commons-Lizenz.

Rechtliche Hinweise   |   Online-Datenschutzrichtlinie