Vous consultez actuellement l'aide de la version:

Pour permettre à l’auteur d’utiliser l’éditeur de page en vue de modifier le contenu d’une application sur une seule page (SPA), cette dernière doit satisfaire certaines exigences, qui sont décrites dans ce document.

Attention :

La fonction d’éditeur d’application d’une seule page (SPA) a été introduite dans AEM 6.4 et constitue actuellement un aperçu technologique. L’éditeur SPA sera publié séparément dans le démarrage rapide AEM dans le cadre d’un service pack à venir pour AEM 6.3 et 6.4.

  • Cette fonctionnalité en est encore au stade du développement et la documentation peut faire l’objet de modifications.
  • L’éditeur SPA est la solution recommandée pour les projets nécessitant un rendu côté client basé sur la structure SPA (React, par exemple).

Introduction

Ce document présente les exigences qu’une application sur une seule page doit satisfaire pour permettre à l’auteur de contenu AEM d’utiliser l’éditeur de page AEM afin d’en modifier le contenu.

Remarque :

Les exigences suivantes sont indépendantes de la structure. Si ces exigences sont satisfaites, un calque spécifique à l’infrastructure, constitué de modules, de composants et de services, peut être fourni.

Attention :

Bien que les fonctionnalités SPA d’AEM soient indépendantes de la structure, seule la structure React est prise en charge actuellement.

Pour plus d’informations sur les applications sur une seule page dans AEM, consultez les documents suivants :

Concepts généraux

Modèle de page

La structure de contenu de la page est stockée dans AEM. Le modèle de la page est utilisé pour mapper et instancier les composants de l’application d’une seule page (SPA). Les développeurs d’applications d’une seule page créent des composants SPA qu’ils mappent sur des composants AEM.

Le composant SPA doit être synchronisé avec le modèle de page et mettre à jour son contenu en conséquence. Un modèle qui a recours à des composants dynamiques doit être utilisé pour instancier des composants à la volée, suivant la structure de modèle de page fournie.

Champs Meta

Le modèle de page utilise le modèle exportable JSON, lui-même basé sur l’API Sling Model. Les modèles SLING exportables présentent la liste de champs suivante :

  • :type : type de chaque ressource (par défaut = type de ressource)
  • :items : enfants de la ressource (structure imbriquée, présente uniquement sur des conteneurs)
  • :itemsOrder : liste triée des enfants
    • L’objet carte JSON ne garantit pas l’ordre de ses champs.
    • En disposant de la carte et du tableau actif, le consommateur de l’API bénéficie des avantages des deux structures.
  • :pages : pages supplémentaires chargées avec la page racine (structure plate, présente uniquement au niveau de la page racine)
  • :pagePath : chemin d’accès au contenu de la page (présent uniquement sur les pages)
  • :pageTitle : titre de la page (présent uniquement sur les pages)

Voir aussi Prise en main d’AEM Content Services.

Module PageModelManager

Le module PageModelManager est fourni sous la forme d’un package NPM au projet SPA. Il accompagne l’application sur une seule page et fait office de gestionnaire de modèles de données. Au nom de l’application sur une seule page, il extrait la récupération et la gestion de la structure JSON qui représente la structure de contenu proprement dite. Il est également responsable de la synchronisation avec l’application sur une seule page, en lui faisant savoir à quel moment un nouveau rendu de ses composants doit être effectué.

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

Module ComponentMapping

La variable ComponentMapping est également fournie sous la forme d’un package NPM au projet SPA. Elle fournit une méthode permettant à l’application sur une seule page d’associer des composants SPA à des types de ressource AEM. Cela permet une résolution dynamique des composants lors de l’analyse du JSON du modèle de page, où chaque composant décrit par un type de ressource du JSON est rendu en fonction du composant SPA auquel il a été associé.

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

Module SPA Wrapper

Pour l’intégration de projet, un module spécifique à NPM regroupant les bibliothèques, modules, services et composants nécessaires est fourni.

Ce module spécifique à la structure rassemble les modules mentionnés ci-dessus (et les adapte éventuellement à la structure ciblée) :

Pour React, le package correspondant est @adobe/cq-react-editable-components.

Composants clés de l’application sur une seule page

ModelProvider

ModelProvider est un élément central des composants de base de l’application sur une seule page. Il fait office d’enveloppe pour tous les composants SPA et leur permet d’être synchronisés avec une partie donnée du modèle de page. Il génère également des attributs de chemins d’accès aux données requis pour que l’éditeur comprenne qu’un composant SPA peut être créé. En arrière-plan, il utilise l’API PageModelManager pour s’assurer que le composant SPA enveloppé est actualisé lors de la mise à jour du modèle de page.

Vous trouverez, ci-dessous, un exemple d’implémentation React de la classe ModelProvider.

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

Conteneur

Un conteneur est un composant conçu pour contenir des composants enfants et en effectuer le rendu. Le conteneur utilise d’abord ComponentMapping pour résoudre et inclure des composants enfants de manière dynamique.

Vous trouverez, ci-dessous, un exemple d’implémentation React de la classe Container.

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

Page est un composant Container simple qui permet d’accéder aux propriétés de la page et aux noms de classe.

Grille réactive

Les composants SPA associés doivent ajouter la valeur des champs de modèle suivants aux attributs de classe d’élément correspondants :

  • gridClassNames : fournit des noms de classe pour la grille réactive
  • columnClassNames : fournit des noms de classe pour la colonne réactive

Vous trouverez, ci-dessous, un exemple d’implémentation React de la classe ResponsiveGrid.

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);

Structure d’élément HTML

Le fragment suivant illustre la représentation HTML type d’une structure de contenu de page. Veuillez tenir compte des points suivants au sujet de la structure :

  • L’élément de grille réactive contient les noms de classe, précédés de aem-Grid--.
  • L’élément de colonne réactive contient les noms de classe, précédés de aem-GridColumn--.
  • Une grille réactive, qui est également la colonne d’une grille parent, est enveloppée de telle sorte que les deux préfixes précédents ne figurent pas sur le même élément.
  • Les éléments correspondant à des ressources modifiables possèdent une propriété data-cq-data-path. Consultez la section Attributs de chemin de données
  • Les éléments prenant en charge le déplacement de ressources par glisser-déplacer doivent fournir un nom de classe conforme au modèle cq-dd-<assetType>. Reportez-vous à la section Activation du glisser-déplacer

 

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

Contrat Éditeur

Attributs de chemin de données

Les composants SPA doivent générer les attributs de données suivants pour permettre à l’éditeur d’interagir avec eux :

  • data-cq-content-path : chemin d’accès relatif du composant, tel qu’il est fourni par PageModel(root/responsivegrid/image, par exemple). Cet attribut ne doit pas être ajouté aux pages.
  • data-cq-page-path : chemin d’accès absolu de la page stockée dans le référentiel (/content/weretail/homepage, par exemple). Cet attribut doit uniquement être ajouté aux pages.

Espaces réservés

Espace réservé pour les composants vides

La structure SPA doit fournir un composant wrapper capable de décorer tous les composants graphiques. Les fonctions suivantes doivent être fournies par le wrapper.

Représentation du vide

Si un composant est vide, cela doit être représenté. Vous devez définir ce que doit être le libellé affiché par l’espace réservé d’un composant vide.

Activation du glisser-déplacer

Pour activer la fonctionnalité de glisser-déplacer des ressources et composants, vous devez indiquer un nom spécifique correspondant au champ AEM droptarget du mappage ressource-composant pour le type de ressource associé.

<?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"/>

Espace réservé du conteneur

Le composant SPA est mappé sur un conteneur graphique, tel que Grille réactive, et doit ajouter un espace réservé enfant virtuel lorsque la création du contenu est en cours.

Lorsque le contenu de l’application sur une seule page est créé par l’éditeur de page, il est incorporé dans ce dernier à l’aide d’un iFrame et l’attribut data-cq-editor est ajouté au nœud de document de ce contenu.

Lorsque la variable data-cq-editor est présente, le conteneur doit inclure un HTMLElement pour représenter la zone avec laquelle l’auteur interagit lorsque le conteneur est vide, comme dans l’exemple de code React suivant.

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

Remarque :

Les noms de classe utilisés dans l’exemple sont actuellement requis par l’éditeur de page.

Noms de classe requis :

  • new section : indique que l’élément en cours est l’espace réservé du conteneur.
  • aem-Grid-newComponent : normalise le composant pour la création de dispositions.

EditConfig

En résumé, pour pouvoir être identifié comme modifiable par l’éditeur de page, un composant SPA doit respecter le contrat suivant :

  • Fournissez les attributs attendus pour relier une instance du composant SPA à une ressource AEM (cela est déjà effectué via le ModelProvider dans le cas de l’implémentation React).
  • Fournissez la série attendue d’attributs et de noms de classe qui permet de créer des espaces réservés vides.
  • Fournissez les noms de classe attendus qui autorisent le déplacement des actifs.

Dans l’implémentation React, les deux dernières fonctionnalités sont fournies aux composants SPA avant que le mappage sur les types de ressource n’ait lieu. Cela est réalisé par une extension du module ComponentMapping, qui introduit l’objet de configuration EditConfig.

Vous trouverez, ci-dessous, un exemple d’implémentation React de l’extension ComponentMapping.

/**
 * 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) {};

Contrat de routage

L’implémentation actuelle repose sur l’hypothèse que l’application sur une seule page utilise des hachages pour les routes des différentes pages enfants.

Configuration

Configuration du routeur de modèle de page

La variable PageModelManager prend en charge le concept de routage. Cela signifie qu’elle réagit aux modifications de hachage, charge le modèle correspondant en interne et envoie un événement cq-pagemodel-route-changed que d’autres modules peuvent écouter, le cas échéant.

Par défaut, ce comportement est automatiquement activé. Pour le désactiver, l’application sur une seule page doit effectuer le rendu de la propriété meta suivante :

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

Chaque route de l’application sur une seule page doit correspondre à une page existante dans AEM (/content/mysite/mypage, par exemple), étant donné que PageModelManager essaie automatiquement de charger le modèle de page correspondant une fois la route sélectionnée.

Si nécessaire, l’application sur une seule page peut également définir une « liste noire » de routes que PageModelManager doit ignorer :

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

Configuration de l’exportation JSON

Lorsque les fonctionnalités de routage sont activées, on part du principe que l’exportation JSON de l’application sur une seule page contient les différentes routes de l’application grâce à l’exportation JSON du composant de navigation AEM. La sortie JSON du composant de navigation AEM peut être configurée dans la stratégie de contenu de page racine de l’application sur une seule page au moyen des deux propriétés suivantes :

  • structureDepth : nombre correspondant à la profondeur de l’arborescence exportée
  • structurePatterns : regex ou tableau de regex correspondant à la page à exporter

Cela peut être affiché dans l’exemple de contenu de l’application sur une seule page dans /conf/we-retail-journal/react/settings/wcm/policies/we-retail-journal/react/components/structure/page/root.

Composants de navigation et de routage

Pour prendre en charge le routage, le développeur du composant SPA doit d’abord implémenter un composant Navigation (mappé sur un composant de navigation AEM). Ce composant va effectuer le rendu des liens vers les routes sur la base de la sortie JSON du composant de navigation mappé (voir Configuration de l’exportation JSON).

Le composant SPA doit, en outre, effectuer le rendu des différentes routes de l’application. Dans React, par exemple, il est possible de déclarer des routes via React Router.

Dans l’exemple de contenu SPA, l’implémentation est basée sur le fait qu’une Route est générée pour chaque composant de page de l’application. Cela est effectué avec la fonction d’aide withRoute() (qui fait actuellement partie de l’exemple d’application) qui enveloppe la page et génère l’élément React Route correspondant. Si l’application comporte une seule page racine, cette dernière doit être chargée d’écouter les nouvelles routes et, au besoin, d’en effectuer le rendu. C’est ce qui se produit lors du chargement asynchrone d’une page enfant.

Démonstration d’une application sur une seule page

Découvrez le fonctionnement d’une application sur une seule page simple et exercez-vous avec une application de ce type en consultant le document Prise en main des applications sur une seule page dans AEM.

Ce produit est distribué sous licence Creative Commons Attribution - Pas d’utilisation commerciale - Partage à l’identique 3.0 non transposé  Les publications Twitter™ et Facebook ne sont pas couvertes par les dispositions Creative Commons.

Mentions légales   |   Politique de confidentialité en ligne