現在表示中:

作成者がページエディターを使用して SPA のコンテンツを編集できるようにするには、SPA が一定の要件を満たしている必要があります。このドキュメントではそれらの要件について説明します。

警告:

単一ページアプリケーション(SPA)エディターの機能は AEM 6.4 で導入され、現在は技術プレビューがおこなわれています。SPA エディターは、まもなくリリースされる AEM 6.3 および 6.4 用のサービスパックで AEM クイックスタートに個別に提供されます。

  • この機能はまだ開発段階なので、ドキュメントの内容は変わる可能性があります。
  • SPA エディターは、SPA フレームワークを基にしたクライアント側レンダリング(React など)が必要なプロジェクトで有効なソリューションです。

概要

このドキュメントでは、AEM コンテンツ作成者が AEM ページエディターを使用して SPA のコンテンツを編集する場合に、SPA が満たす必要がある要件について説明します。

注意:

以下の要件は、フレームワークには依存しません。要件が満たされると、(モジュール、コンポーネントおよびサービスで構成された)フレームワーク固有のレイヤーが提供されます。

警告:

AEM の SPA 機能はフレームワークに依存しませんが、現時点では、React フレームワークのみに対応しています。

AEM での SPA について詳しくは、次のドキュメントを参照してください。

一般的な概念

ページモデル

ページのコンテンツ構造は AEM に保存されます。ページのモデルは、SPA コンポーネントのマッピングとインスタンス化に使用されます。SPA の開発者は、SPA コンポーネントを作成して、AEM コンポーネントにマッピングします。

SPA はページモデルと同期させ、それに合わせて更新する必要があります。指定のページモデル構造に従って、コンポーネントをその場でインスタンス化するには、動的コンポーネントを利用したパターンを使用する必要があります。

メタフィールド

ページモデルでは、Sling Model API に基づく JSON 書き出し可能モデルを利用します。書き出し可能な Sling Model では、次のフィールドが公開されます。

  • :type:各リソースのタイプ(デフォルトはリソースのタイプ)
  • :items:リソースの子(ネストされた構造で、コンテナのみに存在)
  • :itemsOrder:子の順番付きリスト
    • JSON マップオブジェクトでは、フィールドの順番は保証されません。
    • マップと現在の配列の両方を利用することで、API の利用者は両方の構造のメリットが得られます。
  • :pages:ルートページと同時に読み込まれる追加のページ(フラット構造で、ルートページレベルのみに存在)
  • :pagePath:ページのコンテンツパス(ページのみに存在)
  • :pageTitle:ページのタイトル(ページのみに存在)

AEM コンテンツサービスの利用も参照してください。

PageModelManager モジュール

PageModelManager モジュールは、SPA プロジェクトの NPM パッケージとして提供されます。SPA に付属し、データモデルマネージャーとして機能します。SPA に代わり、実際のコンテンツ構造を表す JSON 構造の取得および管理を抽象化します。SPA との同期も処理し、コンポーネントの再レンダリングが必要なタイミングを通知します。

詳しくは、https://www.npmjs.com/package/@adobe/cq-spa-page-model-manager を参照してください。

ComponentMapping モジュール

ComponentMapping モジュールも、SPA プロジェクトの NPM パッケージとして提供されます。SPA が SPA コンポーネントを AEM リソースタイプにマッピングする方法を提供します。これにより、ページモデルの JSON の解析時にコンポーネントの動的解決が可能になり、JSON のリソースタイプによって記述される各コンポーネントが、マッピングされている SPA コンポーネントに合わせてレンダリングされるようになります。

詳しくは、https://www.npmjs.com/package/@adobe/cq-spa-component-mapping を参照してください。

SPA ラッパーモジュール

プロジェクトの統合の場合は、必要なライブラリ、モジュール、サービスおよびコンポーネントをまとめた NPM 専用のパッケージが提供されます。

このフレームワーク固有のモジュールによって、上述のモジュールがバンドルされます(ターゲットのフレームワークへの適応がおこなわれる場合もあります)。

React の場合、対応するパッケージは @adobe/cq-react-editable-components です。

SPA の主要コンポーネント

ModelProvider

ModelProvider は、SPA のベースコンポーネントの中核です。すべての SPA コンポーネントのラッパーとして機能し、全コンポーネントがページモデルの特定部分と同期できるようにします。SPA コンポーネントがオーサリング可能であることをエディターが認識するために必要なデータパス属性も生成します。内部では、ページモデルが更新されたときにラップされた SPA コンポーネントも更新するために、PageModelManager API が使用されます。

ModelProvider クラスの React 実装のサンプルは次のとおりです。

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

コンテナ

コンテナは、子コンポーネントを内包してレンダリングするためのコンポーネントです。コンテナはまず、ComponentMapping を使用して、子コンポーネントを動的に解決し、内包します。

Container クラスの React 実装のサンプルは次のとおりです。

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 コンポーネントは、ページのプロパティとクラス名へのアクセス権を提供するシンプルな Container コンポーネントです。

レスポンシブグリッド

関連する SPA コンポーネントでは、それぞれの要素クラス属性に、次のモデルフィールドの値を追加する必要があります。

  • gridClassNames:レスポンシブグリッドのクラス名を提供します。
  • columnClassNames:レスポンシブ列のクラス名を提供します。

ResponsiveGrid クラスの React 実装のサンプルは次のとおりです。

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 要素の構造

次のフラグメントは、ページコンテンツ構造の一般的な HTML 表現です。構造に関する次のポイントを念頭に置いてください。

  • レスポンシブグリッド要素には、aem-Grid-- というプレフィックスが付いたクラス名が含まれます。
  • レスポンシブ列要素には、aem-GridColumn-- というプレフィックスが付いたクラス名が含まれます。
  • 親グリッドの列でもあるレスポンシブグリッドは、前述の 2 つのプレフィックスが同一の要素上に表示されないようにラップされます。
  • 編集可能リソースに対応する要素には、data-cq-data-path プロパティが含まれます。詳しくは、データパス属性の節を参照してください。
  • アセットのドラッグ&ドロップ対応の要素では、cq-dd-<assetType> パターンの後にクラス名を指定する必要があります。詳しくは、ドラッグ&ドロップの有効化を参照してください。

 

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

エディターのコントラクト

データパス属性

SPA コンポーネントでは、エディターによる操作を可能にするために、次のデータ属性を生成する必要があります。

  • data-cq-content-pathPageModel によって指定されたコンポーネントの相対パス(root/responsivegrid/image など)。この属性はページには追加しないでください。
  • data-cq-page-path:リポジトリに格納されるページの絶対パス(/content/weretail/homepage など)。この属性のみをページに追加してください。

プレースホルダー

空のコンポーネントのプレースホルダー

SPA フレームワークでは、任意のグラフィックコンポーネントをデコレートできるラッパーコンポーネントを提供する必要があります。ラッパーが提供する必要がある機能は次のとおりです。

空の表現

コンポーネントが空の場合は、コンポーネントによってそのことを表現する必要があります。空のコンポーネントのプレースホルダーによって表示されるラベルを検討し、定義する必要があります。

ドラッグ&ドロップの有効化

アセットとコンポーネントのドラッグ&ドロップ機能を有効にするには、関連するアセットタイプでのアセットからコンポーネントへのマッピングの AEM droptarget フィールドに一致する、特定の名前を提供する必要があります。

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

コンテナのプレースホルダー

SPA コンポーネントは、レスポンシブグリッドなどのグラフィックコンテナにマッピングされるので、コンテンツのオーサリング時に仮想子プレースホルダーを追加する必要があります。

ページエディターによって SPA のコンテンツのオーサリングがおこなわれると、該当のコンテンツが iframe によってエディターに埋め込まれ、data-cq-editor 属性がそのコンテンツのドキュメントノードに追加されます。

data-cq-editor 属性がある場合は、次の React の例のように、コンテナが空のときに作成者が操作を加える領域を表現する HTMLElement をコンテナに含める必要があります。

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

注意:

この例で使用されているクラス名は、現時点ではページエディターで必須です。

必須のクラス名は次のとおりです。

  • new section:現在の要素がコンテナのプレースホルダーであることを示します。
  • aem-Grid-newComponent:レイアウトのオーサリング用にコンポーネントを正規化します。

EditConfig

要約すると、エディターによって編集可能と認識されるためには、SPA コンポーネントは次のコントラクトに準拠する必要があります。

  • 適切な属性を指定して SPA コンポーネントインスタンスを AEM リソースに関連付けます(React 実装では、この処理は ModelProvider によって既に完了しています)。
  • 空のプレースホルダーを作成できるようにする適切な属性とクラス名のセットを指定します。
  • アセットのドラッグ&ドロップを有効にする適切なクラス名を指定します。

React 実装では、最後の 2 つの機能は、リソースタイプへのマッピングがおこなわれる前に SPA コンポーネントに提供されます。この処理は ComponentMapping モジュールの拡張によっておこなわれ、これによって EditConfig 設定オブジェクトが導入されます。

ComponentMapping 拡張の React 実装のサンプルは次のとおりです。

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

ルーティングのコントラクト

現在の実装は、SPA が複数の子ページのルートにハッシュを使用するという前提に基づいています。

設定

ページモデルのルーター設定

PageModelManager は、ルーティングの概念をサポートしています。つまり、ハッシュの変更に反応して、対応するモデルを内部的に読み込み、必要に応じて他のモジュールがリッスンできる cq-pagemodel-route-changed イベントを送信します。

デフォルトではこの処理が自動的に有効になっています。無効にする場合は、SPA で次のメタプロパティをレンダリングする必要があります。

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

ルートが選択されると、PageModelManager によって対応するページモデルの読み込みが自動的に試行されるので、SPA のすべてのルートを AEM の既存のページに対応させる必要があります(/content/mysite/mypage など)。

SPA では、必要に応じて、PageModelManager で無視する必要があるルートの「ブラックリスト」を定義することもできます。

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

JSON 書き出し設定

ルーティング機能を有効にすると、AEM ナビゲーションコンポーネントの JSON 書き出しによって、SPA の JSON 書き出しにアプリケーションの複数のルートが含まれるという前提になります。AEM ナビゲーションコンポーネントの JSON 出力は、次の 2 つのプロパティを使用し、SPA のルートページのコンテンツポリシーで設定することができます。

  • structureDepth:書き出されたツリーの深度に対応する数字。
  • structurePatterns:書き出すページに対応する Regex または Regex 配列。

この例は、/conf/we-retail-journal/react/settings/wcm/policies/we-retail-journal/react/components/structure/page/root の SPA のサンプルコンテンツで確認できます。

ナビゲーションコンポーネントとルートコンポーネント

ルーティングに対応するためには、SPA コンポーネントの開発者はまず、ナビゲーションコンポーネントを実装する必要があります(AEM ナビゲーションコンポーネントにマッピングされます)。このコンポーネントは、マッピングされたナビゲーションコンポーネントの JSON 出力に基づいて、ルートへのリンクをレンダリングします(JSON 書き出し設定を参照)。

また、SPA はアプリケーションの複数のルートをレンダリングする必要があります。例えば React では、React Router を使用してルートを宣言できます。

SPA のサンプルコンテンツの場合、実装は、アプリケーションのページコンポーネントごとに Route が生成されるという前提に基づいています。この処理は withRoute() ヘルパー(現在はサンプルアプリケーションの一部)によっておこなわれます。このヘルパーはページをラップし、対応する React Route 要素を生成します。アプリケーションのルートページが 1 つの場合は、そのルートページが新しいルートをリッスンし、必要に応じてレンダリングする必要があります。この処理は、子ページを非同期的に読み込む際におこなわれます。

SPA の実例

AEM での SPA の利用のドキュメントに進んで簡単な SPA の仕組みを確認し、SPA を実際に試してみてください。

本作品は Creative Commons Attribution-Noncommercial-Share Alike 3.0 Unported License によってライセンス許可を受けています。  Twitter™ および Facebook の投稿には、Creative Commons の規約内容は適用されません。

法律上の注意   |   プライバシーポリシー