Zawartość pomocy dla wersji :

To enable the author to use the page editor to edit the content of an SPA, there are requirements that an SPA must fulfill, which are described in this document.

Uwaga:

The Single-Page Application (SPA) Editor feature was introduced with AEM 6.4 and is currently a technology preview. The SPA Editor will be delivered separately to the AEM quickstart in an upcoming service pack for AEM 6.3 and 6.4.

  • This feature is still in development and documentation is subject to change.
  • The SPA Editor is the recommended solution for projects that require SPA framework based client-side rendering (e.g. React).

Introduction

This document describes the requirements an SPA must fulfill to enable the AEM content author to use the AEM Page Editor to edit the content of an SPA.

Uwaga:

The following requirements are framework-independent. If these requirements are fulfilled, a framework-specific layer composed of modules, components, and services can be provided.

Uwaga:

Although the SPA capabilities of AEM are framework-independent, currently only the React framework is supported.

For more information about SPAs in AEM, see the following documents:

General Concepts

Page Model

The content structure of the page is stored in AEM. The model of the page is used to map and instantiate SPA components. The SPA developers create SPA components which they map to AEM components.

The SPA must be in synch with the page model and update its content accordingly. A pattern leveraging dynamic components must be used to instantiate components on the fly following the provided page model structure.

Meta Fields

The page model leverages the JSON Exportable Model, which is itself based on the Sling Model API. The exportable sling models expose the following list of fields:

  • :type: Type of each resource (default = resource type)
  • :items: Children of the resource (nested structure, only present on containers)
  • :itemsOrder: Ordered list of the children
    • The JSON map object doesn't guarantee the order of its fields.
    • Having both the map and the current array, the consumer of the API has the benefits of both structures.
  • :pages: Additional pages loaded alongside the root page (flat structure, only present at the root page level)
  • :pagePath: Content path of the page (only present on pages)
  • :pageTitle: Title of the page (only present on pages)

See also Getting Started with AEM Content Services.

PageModelManager Module

The PageModelManager module is provided as an npm package to the SPA project. It accompanies the SPA and serves as a data model manager. On behalf of the SPA, it abstracts the retrieval and management of the JSON structure that represents the actual content structure. It is also responsible for syncing with the SPA, letting it know when it has to re-render its components.

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

ComponentMapping Module

The ComponentMapping module is also provided as an NPM package to the SPA project. It provides a way for the SPA to map SPA components to AEM resource types. This enables dynamic resolution of the components when parsing the page model JSON, where each component described by a resource type in the JSON is rendered according to the SPA component it has been mapped with.

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

SPA Wrapper Module

For project integration, an NPM-specific package, aggregating the necessary libraries, modules, services, and components is provided.

This framework-specific module then bundles the previously-mentioned modules mentioned above (and possibly adapt them to the targeted framework):

For React, the corresponding package is @adobe/cq-react-editable-components.

Key SPA Components

ModelProvider

The ModelProvider is a central piece of the SPA base components. It acts as a wrapper for all SPA components and allows them to be in synch with a given portion of the page model. It also generates the required data paths attributes for the editor to understand that a SPA component is authorable. Under the hood, it uses the PageModelManager API to make sure that the wrapped SPA component gets updated when the page model is updated.

The following is a sample React implementation of the ModelProvider class.

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

A container is a component meant to contain and render child components. The container first uses the ComponentMapping to dynamically resolve and include child components.

The following is a sample React implementation of the Container class.

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;

Page

The Page component is a simple Container components that provides access to the page properties and class names.

Responsive Grid

The related SPA components must add the value of the following model fields to their respective element class attributes:

  • gridClassNames: Provides class names for the responsive grid
  • columnClassNames: Provides class names for the responsive column

The following is a sample React implementation of the ResponsiveGrid class.

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 Element Structure

The following fragment illustrates the typical HTML representation of a page content structure. Keep in mind the following points about the structure:

  • The responsive grid element carries class names prefixed with aem-Grid--.
  • The responsive column element carries class names prefixed with aem-GridColumn--.
  • A responsive grid, which is also the column of a parent grid, is wrapped such that the two previous prefixes do not appear on the same element.
  • Elements corresponding to editable resources carry a data-cq-data-path property. See the Data Path Attributes section.
  • Elements supporting the drag-and-drop of assets must provide a class name following the cq-dd-<assetType> pattern. See the Enabling Drag-and-Drop section.

 

<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 Contract

Data Path Attributes

The SPA components must generate the following data attributes to allow the editor to interact with them:

  • data-cq-content-path: Relative path of the component as provided by the PageModel (e.g., root/responsivegrid/image). This attribute should not be added on pages.
  • data-cq-page-path: Absolute path of the page stored in the repository (e.g., /content/weretail/homepage). This attribute should be only added on pages.

Placeholders

Placeholder for Empty Components

The SPA Framework must provide a wrapper component that can decorate any graphical components. The following features must be provided by the wrapper.

Representation of the Emptiness

If a component is empty, this must be represented by the component. You must consider and define what the label displayed by the placeholder of an empty component should be.

Enabling Drag-and-Drop

To allow drag-and-drop functionality of assets and components you must provide a specific name that corresponds to the AEM droptarget field of the asset to component mapping for the related asset type.

<?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 Placeholder

The SPA component is mapped to a graphical container such as the Responsive Grid and must add a virtual child placeholder when the content is being authored.

When the content of the SPA is being authored by the Page Editor, that content is embedded into the editor using an iframe and the data-cq-editor attribute is added to the document node of that content.

When the data-cq-editor attribute is present, the container must include an HTMLElement to represent the area with which the author interacts when the container is empty, such as in the following React example.

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

Uwaga:

The class names used in the example are currently required by the page editor.

Required class names:

  • new section: Indicates that the current element is the container's placeholder
  • aem-Grid-newComponent: Normalizes the component for layout authoring

EditConfig

In summary, to be interpreted by the page editor as editable, an SPA component must respect the following contract:

  • Provide the expected attributes to relate an SPA component instance to an AEM resource (this is already done via the ModelProvider in the case of the React implementation)
  • Provide the expected series of attributes and class names that enables the creation of empty placeholders
  • Provide the expected class names enabling the drag-and-drop of assets

In the React implementation, the last two functionalities are provided to the SPA components before the mapping to resource types takes place. This is done by an extension of the ComponentMapping module, which introduces the EditConfig configuration object.

The following is an example React implementation of the ComponentMapping extension.

/**
 * 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 Contract

The current implementation is based on the assumption that the SPA uses hashes for the routes of the different child pages.

Configuration

Page Model Router Configuration

The PageModelManager supports the concept of routing. This means that it reacts to hash changes, internally loads the corresponding model, and sends a cq-pagemodel-route-changed event that other modules can listen to if needed.

By default, this behavior is automatically enabled. To disable it, the SPA should render the following meta property:

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

Every route of the SPA should correspond to an existing page in AEM (e.g., /content/mysite/mypage) since the PageModelManager will automatically try to load the corresponding page model once the route is selected.

If needed, the SPA can also define a "black list" of routes that should be ignored by the PageModelManager:

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

JSON Export Configuration

When routing capabilities are enabled, the assumption is that the JSON export of the SPA contains the different routes of the application thanks to the JSON export of the AEM navigation component. The JSON output of the AEM navigation component can be configured in the SPA's root page content policy through the following two properties:

  • structureDepth: Number corresponding to the depth of the tree exported
  • structurePatterns: Regex or array of regexes corresponding to the page to be exported

This can be shown in the SPA sample content in /conf/we-retail-journal/react/settings/wcm/policies/we-retail-journal/react/components/structure/page/root.

To support routing, the SPA component developer first needs to implement a Navigation component (mapped to an AEM navigation component). This component would render links to the routes based on the JSON output of the mapped navigation component (see JSON Export Configuration).

Furthermore, the SPA needs to render the different routes of the application. In React for instance, it's possible to declare routes via React Router.

In the SPA sample content, the implementation is based on the fact that a Route is generated for each page component of the application. This is accomplished with the withRoute() helper (for now, part of the sample application), that wraps page and generate the corresponding React Route element. In case the application has a single root page, this root page should be in charge of listening to new routes, and rendering them if needed. This is what happens when asynchronously loading a child page.

An SPA in Action

See how a simple SPA works and experiment with an SPA yourself by continuing on to the document Getting Started with SPAs in AEM.

Ta zawartość jest licencjonowana na warunkach licencji Creative Commons Attribution-Noncommercial-Share Alike 3.0 Unported License  Posty z serwisów Twitter™ i Facebook nie są objęte licencją Creative Commons.

Informacje prawne   |   Zasady prywatności online