├── .gitattributes ├── .gitignore ├── src ├── tsconfig.base.json ├── tsconfig.types.json ├── types.ts ├── InternalConstants.ts ├── Model.ts ├── EventType.ts ├── MetaProperty.ts ├── Constants.ts ├── ModelClient.ts ├── EditorClient.ts ├── ModelRouter.ts ├── AuthoringUtils.ts ├── ModelStore.ts └── PathUtils.ts ├── .editorconfig ├── .github ├── workflows │ ├── security.yml │ ├── ci.yml │ ├── sonar.yml │ └── release.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── renovate.json ├── tsconfig.json ├── test ├── Setup.ts ├── data │ ├── types.ts │ ├── Page1Data.ts │ ├── Page3Data.ts │ ├── Page2Data.ts │ ├── EditorClientData.ts │ └── MainPageData.ts ├── ModelClient.test.ts ├── ModelRouter.test.ts ├── EditorClient.test.ts ├── AuthoringUtils.test.ts ├── ModelStore.test.ts ├── ModelManager.test.ts └── PathUtils.test.ts ├── DEV_GUIDELINES.md ├── jest.config.js ├── .releaserc.js ├── webpack.config.js ├── CONTRIBUTING.md ├── README.md ├── package.json ├── CODE_OF_CONDUCT.md ├── CHANGELOG.md └── LICENSE /.gitattributes: -------------------------------------------------------------------------------- 1 | package-lock.json binary 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | .scannerwork/ 4 | **/*.log 5 | *.tgz 6 | -------------------------------------------------------------------------------- /src/tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": false, 5 | "baseUrl": ".", 6 | "paths": { 7 | "@adobe/aem-spa-page-model-manager": ["./"], 8 | "@adobe/aem-spa-page-model-manager/*": ["./*"] 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | charset = utf-8 8 | indent_size = 2 9 | indent_style = space 10 | 11 | [*.{js,jsx,ts,tsx}] 12 | indent_size = 4 13 | 14 | [*.{json,xml}] 15 | insert_final_newline = false 16 | 17 | [package.json] 18 | insert_final_newline = true 19 | -------------------------------------------------------------------------------- /src/tsconfig.types.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "composite": true, 5 | "module": "es2015", 6 | "target": "esnext", 7 | "importHelpers": true, 8 | "removeComments": false, 9 | "declaration": true, 10 | "declarationMap": true, 11 | "declarationDir": "../dist", 12 | "emitDeclarationOnly": true 13 | }, 14 | "exclude": [ 15 | "./internal/umd.ts" 16 | ] 17 | } -------------------------------------------------------------------------------- /.github/workflows/security.yml: -------------------------------------------------------------------------------- 1 | name: Vulnerability check 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request_target: 7 | 8 | jobs: 9 | security: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout source code 13 | uses: actions/checkout@master 14 | - name: Run Snyk to check for vulnerabilities 15 | uses: snyk/actions/node@master 16 | env: 17 | SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} 18 | with: 19 | command: monitor 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "allowSyntheticDefaultImports": true, 5 | "esModuleInterop": true, 6 | "experimentalDecorators": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "incremental": true, 9 | "isolatedModules": true, 10 | "lib": [ 11 | "dom", 12 | "dom.iterable", 13 | "es2018" 14 | ], 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "outDir": "./dist", 18 | "resolveJsonModule": true, 19 | "skipLibCheck": true, 20 | "strict": true, 21 | "target": "es2018" 22 | }, 23 | "include": [ 24 | "src/**/*" 25 | ] 26 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[feature] " 5 | labels: feature-request 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /test/Setup.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | */ 12 | 13 | import fetchMock from 'jest-fetch-mock'; 14 | 15 | fetchMock.enableMocks(); 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[bug]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Package version** 14 | Provide a package version where the bug occurs. 15 | 16 | **To Reproduce** 17 | Steps to reproduce the behavior: 18 | 1. Go to '...' 19 | 2. Click on '....' 20 | 3. Scroll down to '....' 21 | 4. See error 22 | 23 | **Expected behavior** 24 | A clear and concise description of what you expected to happen. 25 | 26 | **Screenshots** 27 | If applicable, add screenshots to help explain your problem. 28 | 29 | **Additional context** 30 | Add any other context about the problem here. 31 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | */ 12 | 13 | import './ModelRouter'; 14 | 15 | export { default as ModelManager } from './ModelManager'; 16 | export * from './ModelClient'; 17 | export * from './Model'; 18 | export * from './ModelStore'; 19 | export * from './PathUtils'; 20 | export * from './AuthoringUtils'; 21 | export { AEM_MODE, default as Constants } from './Constants'; 22 | -------------------------------------------------------------------------------- /DEV_GUIDELINES.md: -------------------------------------------------------------------------------- 1 | 2 | # Development 3 | 4 | Run npm install to get all node_modules that are necessary for development. Refer to scripts under `package.json` for more useful commands. 5 | 6 | 7 | ## Build 8 | 9 | ```sh 10 | $ npm run build 11 | ``` 12 | 13 | 14 | ## Test 15 | 16 | ```sh 17 | $ npm run test 18 | ``` 19 | 20 | ## Usage example 21 | 22 | This module provides the API to manage the model representation of the pages that are composing a SPA. 23 | 24 | ``` 25 | // index.html 26 | 27 | 28 | ... 29 | 30 | ... 31 | 32 | ... 33 | 34 | // Bootstrap: index.js 35 | import { ModelManager } from '@adobe/aem-spa-page-model-manager'; 36 | 37 | ModelManager.initialize().then((model) => { 38 | // Render the App content using the provided model 39 | render(model); 40 | }); 41 | 42 | // Loading a specific portion of model 43 | ModelManager.getData("/content/site/page/jcr:content/path/to/component").then(...); 44 | ``` -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "timezone": "Europe/Zurich", 3 | "packageRules": [ 4 | { 5 | "groupName": "@adobe fixes", 6 | "updateTypes": ["patch", "pin", "digest", "minor"], 7 | "automerge": true, 8 | "packagePatterns": ["^@adobe/"], 9 | "schedule": ["at any time"] 10 | }, 11 | { 12 | "groupName": "@adobe major", 13 | "updateTypes": ["major"], 14 | "packagePatterns": ["^@adobe/"], 15 | "automerge": false, 16 | "schedule": ["at any time"] 17 | }, 18 | { 19 | "groupName": "external fixes", 20 | "updateTypes": ["patch", "pin", "digest", "minor"], 21 | "automerge": true, 22 | "schedule": ["after 1pm on Monday"], 23 | "packagePatterns": ["^.+"], 24 | "excludePackagePatterns": ["^@adobe/"] 25 | }, 26 | { 27 | "groupName": "external major", 28 | "updateTypes": ["major"], 29 | "automerge": false, 30 | "packagePatterns": ["^.+"], 31 | "excludePackagePatterns": ["^@adobe/"], 32 | "schedule": ["after 1pm on Monday"] 33 | } 34 | ] 35 | } -------------------------------------------------------------------------------- /test/data/types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | */ 12 | 13 | import { Model } from '../../src/Model'; 14 | 15 | export interface ResponsiveGridModel extends Model { 16 | gridClassNames: string; 17 | columnCount: number; 18 | } 19 | 20 | export interface PageModel extends Model { 21 | designPath?: string; 22 | title?: string; 23 | lastModifiedDate?: number; 24 | templateName?: string; 25 | cssClassNames?: string; 26 | language?: string; 27 | } 28 | -------------------------------------------------------------------------------- /src/InternalConstants.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | */ 12 | 13 | /** 14 | * @private 15 | */ 16 | export class InternalConstants { 17 | /** 18 | * Sling model selector. 19 | */ 20 | public static readonly DEFAULT_SLING_MODEL_SELECTOR = 'model'; 21 | 22 | /** 23 | * JSON model extension. 24 | */ 25 | public static readonly DEFAULT_MODEL_JSON_EXTENSION = `.${InternalConstants.DEFAULT_SLING_MODEL_SELECTOR}.json`; 26 | 27 | private constructor() { 28 | // hide constructor 29 | } 30 | } 31 | 32 | export default InternalConstants; 33 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | on: pull_request 3 | 4 | jobs: 5 | test: 6 | name: Build & Test 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout source code 10 | uses: actions/checkout@v2 11 | with: 12 | fetch-depth: 0 13 | - name: Setup Node.js 14 | uses: actions/setup-node@v2 15 | with: 16 | node-version: '14' 17 | - name: Install dependencies 18 | run: npm ci 19 | - name: Build the project 20 | run: npm run build:production 21 | - name: Run tests and do code coverage check 22 | run: npm run test:coverage 23 | - name: Run code linter 24 | uses: hallee/eslint-action@1.0.3 25 | if: ${{ github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository }} 26 | with: 27 | repo-token: ${{ secrets.GITHUB_TOKEN }} 28 | - name: Upload code coverage report to workflow as an artifact 29 | uses: actions/upload-artifact@v2 30 | with: 31 | name: istanbul-code-coverage.zip 32 | path: coverage 33 | - name: Upload code coverage report to codecov.io and comment in pull request 34 | uses: codecov/codecov-action@v1 35 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | */ 12 | 13 | module.exports = { 14 | preset: 'ts-jest', 15 | setupFilesAfterEnv: [ 16 | '/test/Setup.ts' 17 | ], 18 | testEnvironment: 'jsdom', 19 | transform: { 20 | '^.+\\.ts$': 'ts-jest' 21 | }, 22 | testMatch: [ 23 | '/test/*.test.ts' 24 | ], 25 | testPathIgnorePatterns: [ 26 | 'node_modules/', 27 | 'dist/' 28 | ], 29 | collectCoverageFrom: [ 30 | 'src/**/*.ts' 31 | ], 32 | coveragePathIgnorePatterns: [ 33 | 'src/types.ts' 34 | ], 35 | coverageDirectory: 'dist/coverage', 36 | moduleFileExtensions: [ 37 | 'ts', 38 | 'js' 39 | ] 40 | }; 41 | -------------------------------------------------------------------------------- /.releaserc.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | */ 12 | 13 | module.exports = { 14 | plugins: [ 15 | '@semantic-release/commit-analyzer', 16 | '@semantic-release/release-notes-generator', 17 | [ 18 | '@semantic-release/changelog', { 19 | 'changelogFile': 'CHANGELOG.md' 20 | } 21 | ], 22 | '@semantic-release/npm', 23 | [ 24 | '@semantic-release/github', { 25 | 'assets': [ 26 | 'package.json', 27 | 'CHANGELOG.md' 28 | ], 29 | 'message': 'chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}' 30 | } 31 | ], 32 | [ 33 | '@semantic-release/git', { 34 | 'assets': [ 35 | 'package.json', 36 | 'CHANGELOG.md' 37 | ] 38 | } 39 | ] 40 | ], 41 | branch: 'master', 42 | branches: [ 'master' ] 43 | }; 44 | -------------------------------------------------------------------------------- /src/Model.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | */ 12 | 13 | /** 14 | * Generic Model interface. 15 | * Defines common properties that pages / items have. 16 | */ 17 | export interface Model { 18 | /** 19 | * Hierarchy type. 20 | */ 21 | ':hierarchyType'?: string; 22 | 23 | /** 24 | * Path of the item/page. 25 | */ 26 | ':path'?: string; 27 | 28 | /** 29 | * Child pages (only present on page's itself, not on items). 30 | */ 31 | ':children'?: { [key: string]: Model }; 32 | 33 | /** 34 | * Items under the page/item. 35 | */ 36 | ':items'?: { [key: string]: Model }; 37 | 38 | /** 39 | * Order of the items under the page/item. 40 | * Can be used as keys for the :items property to iterate items in the proper order. 41 | */ 42 | ':itemsOrder'?: string[]; 43 | 44 | /** 45 | * Resource type of the page/item. 46 | */ 47 | ':type'?: string; 48 | } 49 | -------------------------------------------------------------------------------- /.github/workflows/sonar.yml: -------------------------------------------------------------------------------- 1 | name: Sonar 2 | on: 3 | workflow_run: 4 | workflows: ["Continuous Integration"] 5 | types: 6 | - completed 7 | jobs: 8 | sonar: 9 | name: Sonar 10 | runs-on: ubuntu-latest 11 | if: github.event.workflow_run.conclusion == 'success' 12 | steps: 13 | - name: Checkout source code 14 | uses: actions/checkout@v2 15 | with: 16 | repository: ${{ github.event.workflow_run.head_repository.full_name }} 17 | ref: ${{ github.event.workflow_run.head_branch }} 18 | fetch-depth: 0 19 | - name: "Get PR information" 20 | uses: potiuk/get-workflow-origin@v1 21 | id: source-run-info 22 | with: 23 | token: ${{ secrets.GITHUB_TOKEN }} 24 | sourceRunId: ${{ github.event.workflow_run.id }} 25 | - name: Upload Sonar report to sonarcloud.io and comment in pull request 26 | uses: sonarsource/sonarcloud-github-action@master 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 30 | with: 31 | args: > 32 | -Dsonar.organization=adobeinc 33 | -Dsonar.projectKey=adobe_aem-spa-page-model-manager 34 | -Dsonar.sources=src 35 | -Dsonar.tests=test 36 | -Dsonar.javascript.lcov.reportPaths=dist/coverage/lcov.info 37 | -Dsonar.pullrequest.key=${{ steps.source-run-info.outputs.pullRequestNumber }} 38 | -Dsonar.pullrequest.branch=${{ steps.source-run-info.outputs.sourceHeadBranch }} 39 | -Dsonar.pullrequest.base=${{ steps.source-run-info.outputs.targetBranch }} 40 | 41 | -------------------------------------------------------------------------------- /src/EventType.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | */ 12 | 13 | /** 14 | * Type of events triggered or listened by the PageModelManager and ModelRouter. 15 | * @private 16 | */ 17 | export class EventType { 18 | /** 19 | * Event which indicates that the PageModelManager has been initialized 20 | */ 21 | public static readonly PAGE_MODEL_INIT = 'cq-pagemodel-init'; 22 | 23 | /** 24 | * Event which indicates that the PageModelManager has loaded new content 25 | */ 26 | public static readonly PAGE_MODEL_LOADED = 'cq-pagemodel-loaded'; 27 | 28 | /** 29 | * Event that indicates a request to update the page model 30 | */ 31 | public static readonly PAGE_MODEL_UPDATE = 'cq-pagemodel-update'; 32 | 33 | /** 34 | * Event which indicates that ModelRouter has identified that model route has changed 35 | */ 36 | public static readonly PAGE_MODEL_ROUTE_CHANGED = 'cq-pagemodel-route-changed'; 37 | 38 | private constructor() { 39 | // hide constructor 40 | } 41 | } 42 | 43 | export default EventType; 44 | -------------------------------------------------------------------------------- /src/MetaProperty.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | */ 12 | 13 | /** 14 | * Meta property names associated with the PageModelProvider and ModelRouter. 15 | * @private 16 | */ 17 | export class MetaProperty { 18 | /** 19 | * Meta property pointing to page model root. 20 | */ 21 | public static readonly PAGE_MODEL_ROOT_URL = 'cq:pagemodel_root_url'; 22 | 23 | /** 24 | * Meta property pointing to route filters. 25 | */ 26 | public static readonly PAGE_MODEL_ROUTE_FILTERS = 'cq:pagemodel_route_filters'; 27 | 28 | /** 29 | * Meta property pointing to model router. 30 | */ 31 | public static readonly PAGE_MODEL_ROUTER = 'cq:pagemodel_router'; 32 | 33 | /** 34 | * Meta property pointing to wcm mode. 35 | */ 36 | public static readonly WCM_MODE = 'cq:wcmmode'; 37 | 38 | /** 39 | * Meta property with a editor data type hint 40 | */ 41 | public static readonly WCM_DATA_TYPE = 'cq:datatype'; 42 | 43 | private constructor() { 44 | // hide constructor 45 | } 46 | } 47 | 48 | export default MetaProperty; 49 | -------------------------------------------------------------------------------- /test/data/Page1Data.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | */ 12 | 13 | import { PageModel, ResponsiveGridModel } from './types'; 14 | 15 | export const content_test_page1_stem_child0000 = { ':type': 'test/components/componentchild1' }; 16 | 17 | export const content_test_page1_stem: ResponsiveGridModel = { 18 | 'gridClassNames': 'aem-Grid aem-Grid--12 aem-Grid--default--12', 19 | 'columnCount': 12, 20 | ':itemsOrder': [ 'child0000' ], 21 | ':items': { 22 | 'child0000': content_test_page1_stem_child0000 23 | }, 24 | ':type': 'wcm/foundation/components/responsivegrid' 25 | }; 26 | 27 | export const PAGE1: PageModel = { 28 | 'designPath': '/libs/settings/wcm/designs/default', 29 | 'title': 'React sample page Custom', 30 | 'lastModifiedDate': 1512116041058, 31 | 'templateName': 'sample-template', 32 | 'cssClassNames': 'page', 33 | 'language': 'en-US', 34 | ':itemsOrder': [ 35 | 'stem' 36 | ], 37 | ':items': { 38 | 'stem': content_test_page1_stem 39 | }, 40 | ':path': '/content/test/page1', 41 | ':type': 'we-retail-react/components/structure/page' 42 | }; 43 | -------------------------------------------------------------------------------- /test/data/Page3Data.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | */ 12 | 13 | import { Model } from '../../src/Model'; 14 | import { PageModel, ResponsiveGridModel } from './types'; 15 | 16 | export const content_test_groot_child1000: ResponsiveGridModel = { 17 | 'gridClassNames': 'aem-Grid aem-Grid--12 aem-Grid--default--12', 18 | 'columnCount': 12, 19 | ':type': 'wcm/foundation/components/responsivegrid' 20 | }; 21 | 22 | export const content_test_groot_child1001: Model = { ':type': 'test/components/componentchild1' }; 23 | 24 | export const content_test_groot: ResponsiveGridModel = { 25 | 'gridClassNames': 'aem-Grid aem-Grid--12 aem-Grid--default--12', 26 | 'columnCount': 12, 27 | ':itemsOrder': [ 'child1000', 'child1001' ], 28 | ':items': { 29 | 'child1000': content_test_groot_child1000, 30 | 'child1001': content_test_groot_child1001 31 | }, 32 | ':type': 'wcm/foundation/components/responsivegrid' 33 | }; 34 | 35 | export const PAGE3: PageModel = { 36 | ':type': 'we-retail-journal/react/components/structure/page', 37 | ':path': '/content/test', 38 | ':items': { 39 | 'groot': content_test_groot 40 | }, 41 | ':itemsOrder': [ 42 | 'groot' 43 | ] 44 | }; 45 | -------------------------------------------------------------------------------- /test/data/Page2Data.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | */ 12 | 13 | import { Model } from '../../src/Model'; 14 | import { PageModel, ResponsiveGridModel } from './types'; 15 | 16 | export const content_test_page2_root_child2001: Model = { ':type': 'test/components/componentchild1' }; 17 | 18 | export const content_test_page2_root_child2000: ResponsiveGridModel = { 19 | 'gridClassNames': 'aem-Grid aem-Grid--12 aem-Grid--default--12', 20 | 'columnCount': 12, 21 | ':type': 'wcm/foundation/components/responsivegrid' 22 | }; 23 | 24 | export const content_test_page2_root: ResponsiveGridModel = { 25 | 'gridClassNames': 'aem-Grid aem-Grid--12 aem-Grid--default--12', 26 | 'columnCount': 12, 27 | ':itemsOrder': [ 'child2000', 'child2001' ], 28 | ':items': { 29 | 'child2000': content_test_page2_root_child2000, 30 | 'child2001': content_test_page2_root_child2001 31 | }, 32 | ':type': 'wcm/foundation/components/responsivegrid' 33 | }; 34 | 35 | export const PAGE2: PageModel = { 36 | ':type': 'we-retail-journal/react/components/structure/page', 37 | ':path': '/content/test/page2', 38 | ':items': { 39 | 'root': content_test_page2_root 40 | }, 41 | ':itemsOrder': [ 42 | 'root' 43 | ] 44 | }; 45 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - master 6 | 7 | jobs: 8 | release: 9 | name: Release and publish module 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout source code 13 | uses: actions/checkout@v2 14 | with: 15 | fetch-depth: 0 16 | - name: Setup Node.js 17 | uses: actions/setup-node@v2 18 | with: 19 | node-version: 14 20 | - name: Install dependencies 21 | run: npm ci 22 | - name: Build the project 23 | run: npm run build:production 24 | - name: Run tests and do code coverage check 25 | run: npm run test:coverage 26 | - name: Upload code coverage report to codecov.io 27 | uses: codecov/codecov-action@v1 28 | - name: Upload Sonar report to sonarcloud.io 29 | uses: sonarsource/sonarcloud-github-action@master 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 33 | with: 34 | args: > 35 | -Dsonar.organization=adobeinc 36 | -Dsonar.projectKey=adobe_aem-spa-page-model-manager 37 | -Dsonar.sources=src 38 | -Dsonar.tests=test 39 | -Dsonar.javascript.lcov.reportPaths=dist/coverage/lcov.info 40 | - name: Release module and publish it in github.com and npmjs.com 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | NPM_TOKEN: ${{ secrets.ADOBE_BOT_NPM_TOKEN }} 44 | run: npm run semantic-release 45 | - name: Build documentation 46 | run: npm run docs 47 | - name: Publish documentation to github pages 48 | uses: JamesIves/github-pages-deploy-action@3.7.1 49 | with: 50 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 51 | BRANCH: gh-pages-documentation 52 | FOLDER: dist/docs 53 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | */ 12 | 13 | const path = require('path'); 14 | const nodeExternals = require('webpack-node-externals'); 15 | const { CleanWebpackPlugin } = require('clean-webpack-plugin'); 16 | 17 | const isProduction = process.env.NODE_ENV === 'production'; 18 | const mode = isProduction ? 'production' : 'development'; 19 | const devtool = isProduction ? false : 'source-map'; 20 | 21 | console.log('Building for:', mode); 22 | 23 | module.exports = { 24 | entry: './src/types.ts', 25 | mode, 26 | devtool, 27 | output: { 28 | globalObject: `(function(){ try{ return typeof self !== 'undefined';}catch(err){return false;}})() ? self : this`, 29 | path: path.resolve(__dirname, 'dist'), 30 | filename: 'aem-spa-page-model-manager.js', 31 | library: 'aemSpaPageModelManager', 32 | libraryTarget: 'umd' 33 | }, 34 | module: { 35 | rules: [ 36 | { 37 | test: /.ts$/, 38 | exclude: /(node_modules|dist)/, 39 | use: { 40 | loader: 'ts-loader' 41 | }, 42 | enforce: 'post' 43 | } 44 | ] 45 | }, 46 | resolve: { 47 | extensions: [ '.ts' ], 48 | fallback: { 49 | path: require.resolve('path-browserify'), 50 | url: require.resolve('url') 51 | } 52 | }, 53 | externals: [ nodeExternals() ], 54 | plugins: [ new CleanWebpackPlugin() ] 55 | }; 56 | -------------------------------------------------------------------------------- /src/Constants.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | */ 12 | 13 | /** 14 | * Variables for interacting with AEM components. 15 | * @private 16 | */ 17 | export class Constants { 18 | /** 19 | * Type of the item. 20 | */ 21 | public static readonly TYPE_PROP = ':type'; 22 | 23 | /** 24 | * List of child items of an item. 25 | */ 26 | public static readonly ITEMS_PROP = ':items'; 27 | 28 | /** 29 | * Order in which the items should be listed. 30 | */ 31 | public static readonly ITEMS_ORDER_PROP = ':itemsOrder'; 32 | 33 | /** 34 | * Path of an item. 35 | */ 36 | public static readonly PATH_PROP = ':path'; 37 | 38 | /** 39 | * Children of a hierarchical item. 40 | */ 41 | public static readonly CHILDREN_PROP = ':children'; 42 | 43 | /** 44 | * Hierarchical type of the item. 45 | */ 46 | public static readonly HIERARCHY_TYPE_PROP = ':hierarchyType'; 47 | 48 | /** 49 | * JCR content node. 50 | */ 51 | public static readonly JCR_CONTENT = 'jcr:content'; 52 | 53 | private constructor() { 54 | // hide constructor 55 | } 56 | } 57 | 58 | /** 59 | * AEM modes. 60 | */ 61 | export enum AEM_MODE { 62 | EDIT = 'edit', 63 | PREVIEW = 'preview', 64 | DISABLED = 'disabled' 65 | } 66 | 67 | /** 68 | * Supported tag types. 69 | * @private 70 | */ 71 | export enum TAG_TYPE { 72 | JS = 'script', 73 | STYLESHEET = 'stylesheet' 74 | } 75 | 76 | export default Constants; 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for choosing to contribute! 4 | 5 | The following are a set of guidelines to follow when contributing to this project. 6 | 7 | ## Code Of Conduct 8 | 9 | This project adheres to the Adobe [code of conduct](../CODE_OF_CONDUCT.md). By participating, 10 | you are expected to uphold this code. Please report unacceptable behavior to 11 | [Grp-opensourceoffice@adobe.com](mailto:Grp-opensourceoffice@adobe.com). 12 | 13 | ## Have A Question? 14 | 15 | Start by filing an issue. The existing committers on this project work to reach 16 | consensus around project direction and issue solutions within issue threads 17 | (when appropriate). 18 | 19 | ## Contributor License Agreement 20 | 21 | All third-party contributions to this project must be accompanied by a signed contributor 22 | license agreement. This gives Adobe permission to redistribute your contributions 23 | as part of the project. [Sign our CLA](https://opensource.adobe.com/cla.html). You 24 | only need to submit an Adobe CLA one time, so if you have submitted one previously, 25 | you are good to go! 26 | 27 | ## Code Reviews 28 | 29 | All submissions should come in the form of pull requests and need to be reviewed 30 | by project committers. Read [GitHub's pull request documentation](https://help.github.com/articles/about-pull-requests/) 31 | for more information on sending pull requests. 32 | 33 | Lastly, please follow the [pull request template](PULL_REQUEST_TEMPLATE.md) when 34 | submitting a pull request! 35 | 36 | ## From Contributor To Committer 37 | 38 | We love contributions from our community! If you'd like to go a step beyond contributor 39 | and become a committer with full write access and a say in the project, you must 40 | be invited to the project. The existing committers employ an internal nomination 41 | process that must reach lazy consensus (silence is approval) before invitations 42 | are issued. If you feel you are qualified and want to get more deeply involved, 43 | feel free to reach out to existing committers to have a conversation about that. 44 | 45 | ## Security Issues 46 | 47 | Security issues shouldn't be reported on this issue tracker. Instead, [file an issue to our security experts](https://helpx.adobe.com/security/alertus.html). 48 | 49 | ## Developer Guidelines 50 | 51 | * [Developer Guidelines](DEV_GUIDELINES.md) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Page Model Manager 2 | 3 | [![License](https://img.shields.io/badge/license-Apache%202-blue)](https://github.com/adobe/aem-spa-page-model-manager/blob/master/LICENSE) 4 | [![NPM Version](https://img.shields.io/npm/v/@adobe/aem-spa-page-model-manager.svg)](https://www.npmjs.com/package/@adobe/aem-spa-page-model-manager) 5 | [![Documentation](https://img.shields.io/badge/docs-api-blue)](https://opensource.adobe.com/aem-spa-page-model-manager/) 6 | 7 | 8 | [![codecov](https://codecov.io/gh/adobe/aem-spa-page-model-manager/branch/master/graph/badge.svg)](https://codecov.io/gh/adobe/aem-spa-page-model-manager) 9 | [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=adobe_aem-spa-page-model-manager&metric=alert_status)](https://sonarcloud.io/dashboard?id=adobe_aem-spa-page-model-manager) 10 | [![Known Vulnerabilities](https://snyk.io/test/github/adobe/aem-spa-page-model-manager/badge.svg)](https://snyk.io/test/github/adobe/aem-spa-page-model-manager) 11 | [![Dependencies](https://badges.renovateapi.com/github/adobe/aem-spa-page-model-manager)](https://app.renovatebot.com/dashboard#github/adobe/aem-spa-page-model-manager) 12 | 13 | An interpreter between Adobe Experience Manager Editor and the Adobe Experience Manager Single Page Application (SPA) Editor. The SPA Editor is recommended solution for projects that use SPA framework (React or Angular). 14 | For more information please see the [documentation](https://docs.adobe.com/content/help/en/experience-manager-65/developing/headless/spas/spa-page-component.html). 15 | 16 | ## Installation 17 | ``` 18 | npm install @adobe/aem-spa-page-model-manager 19 | ``` 20 | 21 | ## Documentation 22 | 23 | * [SPA Editor Overview](https://www.adobe.com/go/aem6_5_docs_spa_en) 24 | * [SPA Architecture](https://docs.adobe.com/content/help/en/experience-manager-65/developing/headless/spas/spa-architecture.html) 25 | * [Getting Started with the AEM SPA Editor and Angular](https://docs.adobe.com/content/help/en/experience-manager-learn/spa-angular-tutorial/overview.html) 26 | * [Getting Started with the AEM SPA Editor and React](https://docs.adobe.com/content/help/en/experience-manager-learn/spa-react-tutorial/overview.html) 27 | 28 | ## Contributing 29 | 30 | Contributions are welcome! Read the [Contributing Guide](CONTRIBUTING.md) for more information. 31 | 32 | ## Licensing 33 | 34 | This project is licensed under the Apache V2 License. See [LICENSE](LICENSE) for more information. 35 | 36 | -------------------------------------------------------------------------------- /src/ModelClient.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | */ 12 | 13 | import { Model } from './Model'; 14 | 15 | export class ModelClient { 16 | private _apiHost: string | null; 17 | 18 | /** 19 | * @constructor 20 | * @private 21 | * @param [apiHost] Http host of the API. 22 | */ 23 | constructor(apiHost?: string) { 24 | this._apiHost = apiHost || null; 25 | } 26 | 27 | /** 28 | * Returns http host of the API. 29 | * @returns API host or `null`. 30 | */ 31 | get apiHost(): string | null { 32 | return this._apiHost; 33 | } 34 | 35 | /** 36 | * Fetches a model using given resource path. 37 | * @param modelPath Absolute path to the model. 38 | * @return Promise to page model object. 39 | */ 40 | public fetch(modelPath: string): Promise { 41 | if (!modelPath) { 42 | const err = `Fetching model rejected for path: ${modelPath}`; 43 | 44 | return Promise.reject(new Error(err)); 45 | } 46 | 47 | // Either the API host has been provided or we make an absolute request relative to the current host 48 | const apihostPrefix = this._apiHost || ''; 49 | const url = `${apihostPrefix}${modelPath}`; 50 | 51 | // Assure that the default credentials value ('same-origin') is set for browsers which do not set it 52 | // or which are setting the old default value ('omit') 53 | return fetch(url, { credentials: 'same-origin' }).then((response) => { 54 | if ((response.status >= 200) && (response.status < 300)) { 55 | return response.json() as Promise; 56 | } 57 | 58 | throw { response }; 59 | }).catch((error) => { 60 | return Promise.reject(error); 61 | }); 62 | } 63 | 64 | /** 65 | * Destroys the internal references to avoid memory leaks. 66 | * @private 67 | */ 68 | public destroy(): void { 69 | this._apiHost = null; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@adobe/aem-spa-page-model-manager", 3 | "version": "1.5.0", 4 | "description": "An interpreter between AEM Editor and the AEM SPA Editor.", 5 | "keywords": [ 6 | "spa", 7 | "aem", 8 | "pagel-model-manager", 9 | "adobe" 10 | ], 11 | "author": "Adobe Systems Inc. ", 12 | "license": "Apache-2.0", 13 | "repository": "github:adobe/aem-spa-page-model-manager", 14 | "homepage": "https://docs.adobe.com/content/help/en/experience-manager-65/developing/headless/spas/spa-reference-materials.html", 15 | "bugs": { 16 | "url": "https://github.com/adobe/aem-spa-page-model-manager/issues" 17 | }, 18 | "engines": { 19 | "npm": ">=6.14.15", 20 | "node": ">=12.16.2" 21 | }, 22 | "main": "dist/aem-spa-page-model-manager.js", 23 | "types": "dist/types.d.ts", 24 | "scripts": { 25 | "build:production": "NODE_ENV=production npm run build", 26 | "build:types": "tsc -p src/tsconfig.types.json", 27 | "build": "npm run clean && npm run lint && webpack && npm run build:types", 28 | "clean": "rm -rf dist/", 29 | "docs": "npm i && npx typedoc --entryPointStrategy expand --excludePrivate ./src --out ./dist/docs", 30 | "lint": "eslint .", 31 | "semantic-release": "semantic-release", 32 | "test:coverage": "jest --clearCache && jest --coverage", 33 | "test:debug": "jest --coverage --watchAll", 34 | "test": "jest --clearCache && jest" 35 | }, 36 | "dependencies": { 37 | "clone": "^2.1.2", 38 | "path-browserify": "^1.0.1", 39 | "url": "^0.11.0" 40 | }, 41 | "devDependencies": { 42 | "@adobe/eslint-config-editorxp": "^1.0.3", 43 | "@semantic-release/changelog": "^6.0.1", 44 | "@semantic-release/git": "^10.0.1", 45 | "@semantic-release/github": "^8.0.4", 46 | "@types/clone": "^2.1.0", 47 | "@types/jest": "^26.0.14", 48 | "@types/node": "^14.11.5", 49 | "@typescript-eslint/eslint-plugin": "^4.4.0", 50 | "@typescript-eslint/parser": "^4.4.0", 51 | "clean-webpack-plugin": "^3.0.0", 52 | "commitizen": "^4.2.4", 53 | "cz-conventional-changelog": "^3.0.1", 54 | "eslint": "^7.10.0", 55 | "eslint-plugin-header": "^3.1.0", 56 | "eslint-plugin-json": "^2.1.2", 57 | "jest": "^26.5.2", 58 | "jest-fetch-mock": "^3.0.3", 59 | "semantic-release": "^19.0.2", 60 | "ts-jest": "^26.4.1", 61 | "ts-loader": "^8.1.0", 62 | "ts-mockito": "^2.6.1", 63 | "typedoc": "^0.23.0", 64 | "typescript": "^4.0.3", 65 | "webpack": "^5.0.0", 66 | "webpack-cli": "^4.0.0", 67 | "webpack-node-externals": "^2.5.2" 68 | }, 69 | "files": [ 70 | "dist/**/*.{js,ts,map}", 71 | "!**/{docs,coverage}/" 72 | ], 73 | "config": { 74 | "commitizen": { 75 | "path": "./node_modules/cz-conventional-changelog" 76 | } 77 | }, 78 | "eslintConfig": { 79 | "root": true, 80 | "extends": [ 81 | "@adobe/eslint-config-editorxp/typescript" 82 | ] 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Adobe Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language. 18 | * Being respectful of differing viewpoints and experiences. 19 | * Gracefully accepting constructive criticism. 20 | * Focusing on what is best for the community. 21 | * Showing empathy towards other community members. 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances. 27 | * Trolling, insulting/derogatory comments, and personal or political attacks. 28 | * Public or private harassment. 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission. 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting. 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at Grp-opensourceoffice@adobe.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [https://contributor-covenant.org/version/1/4][version]. 72 | 73 | [homepage]: https://contributor-covenant.org 74 | [version]: https://contributor-covenant.org/version/1/4/ -------------------------------------------------------------------------------- /test/data/EditorClientData.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | */ 12 | 13 | import { PageModel, ResponsiveGridModel } from './types'; 14 | 15 | const DEFAULT_PAGE_MODEL_PATH = window.location.pathname.replace(/\.htm(l)?$/, ''); 16 | 17 | export const CHILD0000_MODEL_JSON: ResponsiveGridModel = { 18 | 'gridClassNames': 'aem-Grid aem-Grid--12 aem-Grid--default--12', 19 | 'columnCount': 12, 20 | ':itemsOrder': [ 'child0010', 'child0011' ], 21 | ':items': { 22 | 'child0010': { ':type': 'test/components/componentchild0' }, 23 | 'child0011': { ':type': 'test/components/componentchild1' } 24 | }, 25 | ':type': 'wcm/foundation/components/responsivegrid' 26 | }; 27 | 28 | export const root: ResponsiveGridModel = { 29 | 'gridClassNames': 'aem-Grid aem-Grid--12 aem-Grid--default--12', 30 | 'columnCount': 12, 31 | ':itemsOrder': [ 'child0000', 'child0001' ], 32 | ':items': { 33 | 'child0000': CHILD0000_MODEL_JSON, 34 | 'child0001': { ':type': 'test/components/componentchild1' } 35 | }, 36 | ':type': 'wcm/foundation/components/responsivegrid' 37 | }; 38 | 39 | export const dummyResponsiveGrid: ResponsiveGridModel = { 40 | 'gridClassNames': 'aem-Grid aem-Grid--12 aem-Grid--default--12', 41 | 'columnCount': 12, 42 | ':type': 'wcm/foundation/components/responsivegrid' 43 | }; 44 | 45 | export const childPage1Root: ResponsiveGridModel = { 46 | 'gridClassNames': 'aem-Grid aem-Grid--12 aem-Grid--default--12', 47 | 'columnCount': 12, 48 | ':itemsOrder': [ 'child1000', 'child1001' ], 49 | ':items': { 50 | 'child1000': dummyResponsiveGrid, 51 | 'child1001': { ':type': 'test/components/componentchild1' } 52 | }, 53 | ':type': 'wcm/foundation/components/responsivegrid' 54 | }; 55 | 56 | export const childPage2Root: ResponsiveGridModel = { 57 | 'gridClassNames': 'aem-Grid aem-Grid--12 aem-Grid--default--12', 58 | 'columnCount': 12, 59 | ':itemsOrder': [ 'child2000', 'child2001' ], 60 | ':items': { 61 | 'child2000': dummyResponsiveGrid, 62 | 'child2001': { ':type': 'test/components/componentchild1' } 63 | }, 64 | ':type': 'wcm/foundation/components/responsivegrid' 65 | }; 66 | 67 | export const PAGE_MODEL_JSON: PageModel = { 68 | ':path': DEFAULT_PAGE_MODEL_PATH, 69 | 'designPath': '/libs/settings/wcm/designs/default', 70 | 'title': 'React sample page', 71 | 'lastModifiedDate': 1512116041058, 72 | 'templateName': 'sample-template', 73 | 'cssClassNames': 'page', 74 | 'language': 'en-US', 75 | ':itemsOrder': [ 76 | 'root' 77 | ], 78 | ':items': { 79 | 'root': root 80 | }, 81 | ':hierarchyType': 'page', 82 | ':children': { 83 | '/content/test/child_page_1': { 84 | ':type': 'we-retail-journal/react/components/structure/page', 85 | ':items': { 86 | 'root': childPage1Root 87 | }, 88 | ':itemsOrder': [ 89 | 'root' 90 | ] 91 | }, 92 | '/content/test/subpage2': { 93 | ':type': 'we-retail-journal/react/components/structure/page', 94 | ':items': { 95 | 'root': childPage2Root 96 | }, 97 | ':itemsOrder': [ 98 | 'root' 99 | ] 100 | } 101 | }, 102 | ':type': 'we-retail-react/components/structure/page' 103 | }; 104 | -------------------------------------------------------------------------------- /test/ModelClient.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | */ 12 | 13 | import * as assert from 'assert'; 14 | import fetchMock from 'jest-fetch-mock'; 15 | import { Model } from '../src/Model'; 16 | import { ModelClient } from '../src/ModelClient'; 17 | import { content_test_child_page_1, PAGE_MODEL } from './data/MainPageData'; 18 | 19 | const NON_EXISTING_URL = '/content/test/undefined'; 20 | const PAGE_URL = '/content/test/page'; 21 | const CHILD_PAGE_URL = '/content/test/child_page_1'; 22 | const CHILD_PAGE_404_URL = '/content/test/child_page_404'; 23 | const myEndPoint = 'http://localhost:4523'; 24 | 25 | function getJSONResponse(body: Model) { 26 | return { 27 | status: 200, 28 | body: JSON.stringify(body), 29 | headers: { 30 | 'Content-type': 'application/json' 31 | } 32 | }; 33 | } 34 | 35 | function mockTheFetch() { 36 | fetchMock.doMock((req) => { 37 | switch (req.url) { 38 | case myEndPoint + CHILD_PAGE_URL: 39 | return Promise.resolve(getJSONResponse(content_test_child_page_1)); 40 | 41 | case myEndPoint + PAGE_URL: 42 | return Promise.resolve(getJSONResponse(PAGE_MODEL)); 43 | 44 | case myEndPoint + CHILD_PAGE_404_URL: 45 | default: 46 | return Promise.reject({ code: 404, body: 'Not found' }); 47 | } 48 | }); 49 | } 50 | 51 | describe('ModelClient ->', () => { 52 | beforeEach(() => { 53 | fetchMock.resetMocks(); 54 | mockTheFetch(); 55 | }); 56 | 57 | describe('fetch ->', () => { 58 | it('should reject when the remote model endpoint is not found', () => { 59 | const modelClient = new ModelClient(myEndPoint); 60 | 61 | return modelClient.fetch(NON_EXISTING_URL).then((data) => { 62 | assert.fail(data, undefined); 63 | }).catch((error) => { 64 | expect(error.code).toEqual(404); 65 | }); 66 | }); 67 | 68 | it('should return the expected data', () => { 69 | const modelClient = new ModelClient(myEndPoint); 70 | 71 | return modelClient.fetch(PAGE_URL).then((data) => { 72 | assert.deepEqual(data, PAGE_MODEL); 73 | 74 | return modelClient.fetch(CHILD_PAGE_URL); 75 | }).then((data) => { 76 | assert.deepEqual(data, content_test_child_page_1); 77 | 78 | return modelClient.fetch(CHILD_PAGE_404_URL); 79 | }).catch((error) => { 80 | expect(error.code).toEqual(404); 81 | }); 82 | }); 83 | 84 | describe('handling incorrect parameter', () => { 85 | const modelClient = new ModelClient(myEndPoint); 86 | 87 | // failing as the undefined is passed to the PathUtils which can not handle this case 88 | // it('should resolve with Error - when no URL provided', (done) => { 89 | // 90 | // modelClient.fetch(undefined).catch((e) => { 91 | // assert.isNotNull(e); 92 | // done(); 93 | // }); 94 | // }); 95 | 96 | it('should resolve with Error - when empty URL provided', (done) => { 97 | modelClient.fetch('').catch((error) => { 98 | expect(error.message).toBeDefined(); 99 | done(); 100 | }); 101 | }); 102 | }); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /src/EditorClient.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | */ 12 | 13 | import clone from 'clone'; 14 | import EventType from './EventType'; 15 | import { ModelManager } from './ModelManager'; 16 | import { PathUtils } from './PathUtils'; 17 | 18 | /** 19 | * Broadcast an event to indicate the page model has been loaded 20 | * @param model - model item to be added to the broadcast payload 21 | * @fires cq-pagemodel-loaded 22 | * @private 23 | */ 24 | export function triggerPageModelLoaded(model: any): void { 25 | // Deep copy to protect the internal state of the page mode 26 | PathUtils.dispatchGlobalCustomEvent(EventType.PAGE_MODEL_LOADED, { 27 | detail: { 28 | model: clone(model) 29 | } 30 | }); 31 | } 32 | 33 | /** 34 | * The EditorClient is responsible for the interactions with the Page Editor. 35 | * @private 36 | */ 37 | export class EditorClient { 38 | public _modelManager: ModelManager; 39 | public _windowListener: any; 40 | 41 | constructor(modelManager: ModelManager) { 42 | this._modelManager = modelManager; 43 | 44 | this._windowListener = (event: any) => { 45 | if (!event || !event.detail || !event.detail.msg) { 46 | console.error('EditorService.js', 'No message passed to cq-pagemodel-update', event); 47 | 48 | return; 49 | } 50 | 51 | this._updateModel(event.detail.msg); 52 | }; 53 | 54 | if (PathUtils.isBrowser()) { 55 | window.addEventListener(EventType.PAGE_MODEL_UPDATE, this._windowListener); 56 | } 57 | } 58 | 59 | /** 60 | * Updates the page model with the given data 61 | * 62 | * @param {Object} msg - Object containing the data to update the page model 63 | * @property {String} msg.dataPath - Relative data path in the PageModel which needs to be updated 64 | * @property {String} msg.pagePath - Absolute page path corresponding to the page in the PageModel which needs to be updated 65 | * @param {String} msg.cmd - Command Action requested via Editable on the content Node 66 | * @param {Object} msg.data - Data that needs to be updated in the PageModel at {path} 67 | * @fires cq-pagemodel-loaded 68 | * @private 69 | */ 70 | public _updateModel(msg: any) { 71 | if (!msg || !msg.cmd || !msg.path) { 72 | console.error('PageModelManager.js', 'Not enough data received to update the page model'); 73 | 74 | return; 75 | } 76 | 77 | // Path in the PageModel which needs to be updated 78 | const path = msg.path; 79 | 80 | // Command Action requested via Editable on the content Node 81 | const cmd = msg.cmd; 82 | 83 | // Data that needs to be updated in the page model at the given path 84 | const data = clone(msg.data); 85 | 86 | let siblingName; 87 | let itemPath; 88 | let insertBefore; 89 | const parentNodePath = PathUtils.getParentNodePath(path); 90 | 91 | switch (cmd) { 92 | case 'replace': 93 | this._modelManager.modelStore.setData(path, data); 94 | this._modelManager._notifyListeners(path); 95 | break; 96 | 97 | case 'delete': 98 | this._modelManager.modelStore.removeData(path); 99 | 100 | if (parentNodePath) { 101 | this._modelManager._notifyListeners(parentNodePath); 102 | } 103 | 104 | break; 105 | 106 | case 'insertBefore': 107 | insertBefore = true; 108 | // No break as we want both insert command to be treated the same way 109 | // eslint-disable-next-line no-fallthrough 110 | 111 | case 'insertAfter': 112 | // The logic relative to the item path and sibling between the editor and the ModelManager is reversed 113 | // Adapting the command to the ModelManager API 114 | siblingName = PathUtils.getNodeName(path); 115 | 116 | if (parentNodePath) { 117 | itemPath = parentNodePath + '/' + data.key; 118 | this._modelManager.modelStore.insertData(itemPath, data.value, siblingName, insertBefore); 119 | this._modelManager._notifyListeners(parentNodePath); 120 | } 121 | 122 | break; 123 | 124 | default: 125 | // 'replaceContent' command not supported 126 | // 'moveBefore', 'moveAfter' commands not supported. 127 | // As instead, we are replacing source and destination parents because they can contain data about the item we want to relocate 128 | console.log('EditorClient', 'unsupported command:', cmd); 129 | } 130 | 131 | triggerPageModelLoaded(this._modelManager.modelStore.dataMap); 132 | } 133 | 134 | /** 135 | * @private 136 | */ 137 | public destroy() { 138 | if (PathUtils.isBrowser()) { 139 | window.removeEventListener(EventType.PAGE_MODEL_UPDATE, this._windowListener); 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /test/data/MainPageData.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | */ 12 | 13 | import { Model } from '../../src/Model'; 14 | import { PageModel, ResponsiveGridModel } from './types'; 15 | 16 | export const content_test_page_root_child0000_child0010: Model = { ':type': 'test/components/componentchild0' }; 17 | export const content_test_page_root_child0000_child0011: Model = { ':type': 'test/components/componentchild1' }; 18 | 19 | export const content_test_page_root_child0000: ResponsiveGridModel = { 20 | 'gridClassNames': 'aem-Grid aem-Grid--12 aem-Grid--default--12', 21 | 'columnCount': 12, 22 | ':itemsOrder': [ 'child0010', 'child0011' ], 23 | ':items': { 24 | 'child0010': content_test_page_root_child0000_child0010, 25 | 'child0011': content_test_page_root_child0000_child0011 26 | }, 27 | ':type': 'wcm/foundation/components/responsivegrid' 28 | }; 29 | 30 | export const content_test_page_root_child0001: Model = { ':type': 'test/components/componentchild1' }; 31 | export const content_test_page_root: ResponsiveGridModel = { 32 | 'gridClassNames': 'aem-Grid aem-Grid--12 aem-Grid--default--12', 33 | 'columnCount': 12, 34 | ':itemsOrder': [ 'child0000', 'child0001' ], 35 | ':items': { 36 | 'child0000': content_test_page_root_child0000, 37 | 'child0001': content_test_page_root_child0001 38 | }, 39 | ':type': 'wcm/foundation/components/responsivegrid' 40 | }; 41 | 42 | export const content_test_child_page_1_root_child1001: Model = { ':type': 'test/components/componentchild1' }; 43 | 44 | export const content_test_child_page_1_root_child1000: ResponsiveGridModel = { 45 | 'gridClassNames': 'aem-Grid aem-Grid--12 aem-Grid--default--12', 46 | 'columnCount': 12, 47 | ':type': 'wcm/foundation/components/responsivegrid' 48 | }; 49 | 50 | export const content_test_child_page_1_root: ResponsiveGridModel = { 51 | 'gridClassNames': 'aem-Grid aem-Grid--12 aem-Grid--default--12', 52 | 'columnCount': 12, 53 | ':itemsOrder': [ 'child1000', 'child1001' ], 54 | ':items': { 55 | 'child1000': content_test_child_page_1_root_child1000, 56 | 'child1001': content_test_child_page_1_root_child1001 57 | }, 58 | ':type': 'wcm/foundation/components/responsivegrid' 59 | }; 60 | 61 | export const content_test_child_page_1: PageModel = { 62 | ':type': 'we-retail-journal/react/components/structure/page', 63 | ':path': '/content/test/child_page_1', 64 | ':items': { 65 | 'root': content_test_child_page_1_root 66 | }, 67 | ':itemsOrder': [ 68 | 'root' 69 | ] 70 | }; 71 | 72 | export const content_test_subpage2_root_child2000: ResponsiveGridModel = { 73 | 'gridClassNames': 'aem-Grid aem-Grid--12 aem-Grid--default--12', 74 | 'columnCount': 12, 75 | ':type': 'wcm/foundation/components/responsivegrid' 76 | }; 77 | 78 | export const content_test_subpage2_root_child2001: Model = { ':type': 'test/components/componentchild1' }; 79 | 80 | export const content_test_subpage2_root: ResponsiveGridModel = { 81 | 'gridClassNames': 'aem-Grid aem-Grid--12 aem-Grid--default--12', 82 | 'columnCount': 12, 83 | ':itemsOrder': [ 'child2000', 'child2001' ], 84 | ':items': { 85 | 'child2000': content_test_subpage2_root_child2000, 86 | 'child2001': content_test_subpage2_root_child2001 87 | }, 88 | ':type': 'wcm/foundation/components/responsivegrid' 89 | }; 90 | 91 | export const content_test_subpage2: PageModel = { 92 | ':type': 'we-retail-journal/react/components/structure/page', 93 | ':items': { 94 | 'root': content_test_subpage2_root 95 | }, 96 | ':itemsOrder': [ 97 | 'root' 98 | ] 99 | }; 100 | 101 | export const content_test_subpage2_root_page: PageModel = { ':type': 'test/components/page' }; 102 | 103 | export const content_test_subpage2_rootPage: PageModel = { 104 | ':type': 'we-retail-journal/react/components/structure/page', 105 | ':items': { 106 | 'page': content_test_subpage2_root_page 107 | }, 108 | ':itemsOrder': [ 109 | 'page' 110 | ] 111 | }; 112 | 113 | export const PAGE_MODEL: PageModel = { 114 | 'designPath': '/libs/settings/wcm/designs/default', 115 | 'title': 'React sample page', 116 | 'lastModifiedDate': 1512116041058, 117 | 'templateName': 'sample-template', 118 | 'cssClassNames': 'page', 119 | 'language': 'en-US', 120 | ':itemsOrder': [ 121 | 'root' 122 | ], 123 | ':items': { 124 | 'root': content_test_page_root 125 | }, 126 | ':hierarchyType': 'page', 127 | ':children': { 128 | '/content/test/child_page_1': content_test_child_page_1, 129 | '/content/test/subpage2/subpage22': content_test_subpage2_rootPage, 130 | '/content/test/subpage2': content_test_subpage2 131 | }, 132 | ':path': '/content/test/page', 133 | ':type': 'we-retail-react/components/structure/page' 134 | }; 135 | 136 | export const ERROR_PAGE_MODEL_404: PageModel = { 137 | 'designPath': '/libs/settings/wcm/designs/default', 138 | 'title': 'Example Error Page 404', 139 | 'lastModifiedDate': 1512116041058, 140 | 'templateName': 'sample-template', 141 | 'cssClassNames': 'page', 142 | 'language': 'en-US', 143 | ':itemsOrder': [ 144 | 'root' 145 | ], 146 | ':items': { 147 | 'root': content_test_page_root 148 | }, 149 | ':hierarchyType': 'page', 150 | ':children': {}, 151 | ':path': '/content/test/page', 152 | ':type': 'we-retail-react/components/structure/page' 153 | }; 154 | 155 | export const ERROR_PAGE_MODEL_500: PageModel = { 156 | 'designPath': '/libs/settings/wcm/designs/default', 157 | 'title': 'Example Error Page 500', 158 | 'lastModifiedDate': 1512116041058, 159 | 'templateName': 'sample-template', 160 | 'cssClassNames': 'page', 161 | 'language': 'en-US', 162 | ':itemsOrder': [ 163 | 'root' 164 | ], 165 | ':items': { 166 | 'root': content_test_page_root 167 | }, 168 | ':hierarchyType': 'page', 169 | ':children': {}, 170 | ':path': '/content/test/page', 171 | ':type': 'we-retail-react/components/structure/page' 172 | }; 173 | -------------------------------------------------------------------------------- /src/ModelRouter.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | */ 12 | 13 | import EventType from './EventType'; 14 | import MetaProperty from './MetaProperty'; 15 | import ModelManager from './ModelManager'; 16 | import { PathUtils } from './PathUtils'; 17 | 18 | /** 19 | *

The ModelRouter listens for HTML5 History API popstate events 20 | * and calls {@link PageModelManager#getData()} with the model path it extracted from the URL.

21 | * 22 | *

Configuration

23 | * 24 | *

The Model Router can be configured using meta properties located in the head section of the document.

25 | * 26 | *

Meta properties

27 | *
    28 | *
  • cq:page_model_router - default=undefined, options=disable
  • 29 | *
  • cq:page_model_route_filters - default=undefined, options=RegExp<>
  • 30 | *
31 | * 32 | *

Defaults

33 | *
    34 | *
  • The ModelRouter is enabled and uses the History API to extract the model path from the current content path
  • 35 | *
36 | * 37 | *

Examples and Usages

38 | * 39 | *

Disables the page model router

40 | *
 41 |  *     e.g. <meta property="cq:page_model_router" content="disable"\>
 42 |  * 
43 | * 44 | *

Filters paths from the model routing with the given patterns

45 | *
 46 |  *     e.g. <meta property="cq:page_model_route_filters" content="route/not/found,^(.*)(?:exclude/path)(.*)"\>
 47 |  * 
48 | * 49 | * @module ModelRouter 50 | */ 51 | 52 | /** 53 | * Modes in which the Model Router operates. 54 | * @private 55 | */ 56 | export class RouterModes { 57 | /** 58 | * Flag that indicates that the model router should be disabled. 59 | */ 60 | public static readonly DISABLED = 'disabled'; 61 | 62 | /** 63 | * Flag that indicates that the model router should extract the model path from the content path section of the URL. 64 | */ 65 | public static readonly CONTENT_PATH = 'path'; 66 | 67 | private constructor() { 68 | // hide constructor 69 | } 70 | } 71 | 72 | /** 73 | * Returns the model path. If no URL is provided the current window URL is used 74 | * @param [url] url from which to extract the model path 75 | * @private 76 | * @return 77 | */ 78 | export function getModelPath(url?: string | null | URL): string | null { 79 | const localUrl = (url || (PathUtils.isBrowser() && window.location.pathname)) as string; 80 | 81 | if (localUrl) { 82 | return PathUtils.sanitize(localUrl); 83 | } 84 | 85 | return null; 86 | } 87 | 88 | /** 89 | * Returns the list of provided route filters 90 | * 91 | * @returns {string[]} 92 | * 93 | * @private 94 | */ 95 | export function getRouteFilters(): string[] { 96 | const routeFilters = PathUtils.getMetaPropertyValue(MetaProperty.PAGE_MODEL_ROUTE_FILTERS); 97 | 98 | return routeFilters ? routeFilters.split(',') : []; 99 | } 100 | 101 | /** 102 | * Should the route be excluded 103 | * 104 | * @param route 105 | * @returns {boolean} 106 | * 107 | * @private 108 | */ 109 | export function isRouteExcluded(route: string): boolean { 110 | const routeFilters = getRouteFilters(); 111 | 112 | for (let i = 0, length = routeFilters.length; i < length; i++) { 113 | if (new RegExp(routeFilters[i]).test(route)) { 114 | return true; 115 | } 116 | } 117 | 118 | return false; 119 | } 120 | 121 | /** 122 | * Is the model router enabled. Enabled by default 123 | * @returns {boolean} 124 | * @private 125 | */ 126 | export function isModelRouterEnabled(): boolean { 127 | if (!PathUtils.isBrowser()) { 128 | return false; 129 | } 130 | 131 | const modelRouterMetaType = PathUtils.getMetaPropertyValue(MetaProperty.PAGE_MODEL_ROUTER); 132 | 133 | // Enable the the page model routing by default 134 | return !modelRouterMetaType || (RouterModes.DISABLED !== modelRouterMetaType); 135 | } 136 | 137 | /** 138 | * Fetches the model from the PageModelManager and then dispatches it 139 | * 140 | * @fires cq-pagemodel-route-changed 141 | * 142 | * @param {string} [path] - path of the model to be dispatched 143 | * 144 | * @private 145 | */ 146 | export function dispatchRouteChanged(path: string): void { 147 | // Triggering the page model manager to load a new child page model 148 | // No need to use a cache as the PageModelManager already does it 149 | ModelManager.getData({ path }).then((model) => { 150 | PathUtils.dispatchGlobalCustomEvent(EventType.PAGE_MODEL_ROUTE_CHANGED, { 151 | detail: { 152 | model 153 | } 154 | }); 155 | }); 156 | } 157 | 158 | /** 159 | * Triggers the PageModelManager to fetch data based on the current route 160 | * 161 | * @fires cq-pagemodel-route-changed - with the root page model object 162 | * 163 | * @param {string} [url] - url from which to extract the model path 164 | * 165 | * @private 166 | */ 167 | export function routeModel(url?: string | undefined | null | URL): void { 168 | if (!isModelRouterEnabled()) { 169 | return; 170 | } 171 | 172 | const path = getModelPath(url); 173 | 174 | // don't fetch the model 175 | // for the root path 176 | // or when the route is excluded 177 | if (!path || ('/' === path) || isRouteExcluded(path)) { 178 | return; 179 | } 180 | 181 | dispatchRouteChanged(path); 182 | } 183 | 184 | export function initModelRouter(): void { 185 | // Activate the model router 186 | if (isModelRouterEnabled() && PathUtils.isBrowser()) { 187 | // Encapsulate the history.pushState and history.replaceState functions to prefetch the page model for the current route 188 | const pushState = window.history.pushState; 189 | const replaceState = window.history.replaceState; 190 | 191 | window.addEventListener('popstate', e => { 192 | const target = e?.target as Window; 193 | 194 | routeModel(target?.location?.pathname || null); 195 | }); 196 | 197 | window.history.pushState = (state, title, url) => { 198 | routeModel(url); 199 | 200 | return pushState.apply(history, [ state, title, url ]); 201 | }; 202 | 203 | window.history.replaceState = (state, title, url) => { 204 | routeModel(url || null); 205 | 206 | return replaceState.apply(history, [ state, title, url ]); 207 | }; 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /test/ModelRouter.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | */ 12 | 13 | import MetaProperty from '../src/MetaProperty'; 14 | import { Model } from '../src/Model'; 15 | import ModelManager from '../src/ModelManager'; 16 | import { 17 | dispatchRouteChanged, getModelPath, getRouteFilters, initModelRouter, isModelRouterEnabled, isRouteExcluded, routeModel, RouterModes 18 | } from '../src/ModelRouter'; 19 | import { PathUtils } from '../src/PathUtils'; 20 | 21 | let metaProps: { [key: string]: string } = {}; 22 | const modelManagerSpy: jest.SpyInstance> = jest.spyOn(ModelManager, 'getData'); 23 | 24 | describe('ModelRouter ->', () => { 25 | const DEFAULT_PAGE_MODEL_PATH = window.location.pathname.replace(/\.htm(l)?$/, ''); 26 | const TEST_PATH = '/test'; 27 | const TEST_MODEL = { test: 'model' }; 28 | const MODEL_ROUTE_FILTERS = [ 'f1', 'f2', 'f3' ]; 29 | const MODEL_ROUTE_FILTERS_STR = MODEL_ROUTE_FILTERS.join(','); 30 | 31 | beforeEach(() => { 32 | metaProps = {}; 33 | jest.spyOn(PathUtils, 'getMetaPropertyValue').mockImplementation((val) => metaProps[val]); 34 | }); 35 | 36 | describe('getModelPath ->', () => { 37 | it('should get the current window URL', () => { 38 | expect(getModelPath()).toEqual(DEFAULT_PAGE_MODEL_PATH); 39 | }); 40 | 41 | it('should get the current window URL', () => { 42 | expect(getModelPath('/path.model.json')).toEqual('/path'); 43 | }); 44 | 45 | it('should get the current window URL', () => { 46 | expect(getModelPath('/zyx/abc?test=test')).toEqual('/zyx/abc'); 47 | }); 48 | 49 | it('should get the current window URL', () => { 50 | expect(getModelPath('/zyx/abc?date=03.10.2021')).toEqual('/zyx/abc'); 51 | }); 52 | it('should return null', () => { 53 | const isBrowserSpy = jest.spyOn(PathUtils, 'isBrowser').mockImplementation(() => false); 54 | 55 | expect(getModelPath()).toBeNull(); 56 | isBrowserSpy.mockRestore(); 57 | }); 58 | }); 59 | 60 | describe('dispatchRouteChanged ->', () => { 61 | beforeEach(() => { 62 | modelManagerSpy.mockResolvedValue(TEST_MODEL as Model); 63 | }); 64 | 65 | afterEach(() => { 66 | modelManagerSpy.mockReset(); 67 | }); 68 | 69 | it('should get the current window URL', () => { 70 | dispatchRouteChanged(TEST_PATH); 71 | expect(modelManagerSpy).toHaveBeenCalledWith({ path: TEST_PATH }); 72 | }); 73 | }); 74 | 75 | describe('routeModel ->', () => { 76 | beforeEach(() => { 77 | modelManagerSpy.mockResolvedValue(TEST_MODEL as Model); 78 | }); 79 | 80 | afterEach(() => { 81 | modelManagerSpy.mockReset(); 82 | }); 83 | 84 | it('should route the model based on the window URL', () => { 85 | const { location } = window; 86 | 87 | // @ts-ignore 88 | delete window.location; 89 | 90 | // @ts-ignore 91 | window.location = { 92 | pathname: '/some/path/name' 93 | }; 94 | 95 | routeModel(); 96 | expect(modelManagerSpy).toHaveBeenCalledWith({ path: '/some/path/name' }); 97 | 98 | window.location = location; 99 | }); 100 | 101 | it('should route the model based on provided path', () => { 102 | routeModel(TEST_PATH); 103 | expect(modelManagerSpy).toHaveBeenCalledWith({ path: TEST_PATH }); 104 | }); 105 | }); 106 | 107 | describe('getRouteFilters ->', () => { 108 | afterEach(() => { 109 | metaProps = {}; 110 | }); 111 | 112 | it('should return an empty list of route filters', () => { 113 | expect(getRouteFilters()).toEqual([]); 114 | }); 115 | 116 | it('should return a list of route filters', () => { 117 | metaProps[MetaProperty.PAGE_MODEL_ROUTE_FILTERS] = MODEL_ROUTE_FILTERS_STR; 118 | expect(getRouteFilters()).toEqual(expect.arrayContaining(MODEL_ROUTE_FILTERS)); 119 | }); 120 | }); 121 | 122 | describe('isModelRouterEnabled ->', () => { 123 | afterEach(() => { 124 | metaProps = {}; 125 | }); 126 | 127 | it('should return an enabled route model by default', () => { 128 | expect(isModelRouterEnabled()).toEqual(true); 129 | }); 130 | 131 | it('should return a disabled route model', () => { 132 | metaProps[MetaProperty.PAGE_MODEL_ROUTER] = RouterModes.DISABLED; 133 | 134 | expect(isModelRouterEnabled()).toEqual(false); 135 | }); 136 | }); 137 | 138 | describe('isRouteExcluded ->', () => { 139 | afterEach(() => { 140 | metaProps = {}; 141 | }); 142 | 143 | it('should filter a route', () => { 144 | expect(isRouteExcluded(MODEL_ROUTE_FILTERS[0])).toEqual(false); 145 | expect(isRouteExcluded(MODEL_ROUTE_FILTERS[1])).toEqual(false); 146 | expect(isRouteExcluded(MODEL_ROUTE_FILTERS[2])).toEqual(false); 147 | }); 148 | 149 | it('should filter a route', () => { 150 | metaProps[MetaProperty.PAGE_MODEL_ROUTE_FILTERS] = MODEL_ROUTE_FILTERS_STR; 151 | 152 | expect(isRouteExcluded(MODEL_ROUTE_FILTERS[0])).toEqual(true); 153 | expect(isRouteExcluded(MODEL_ROUTE_FILTERS[1])).toEqual(true); 154 | expect(isRouteExcluded(MODEL_ROUTE_FILTERS[2])).toEqual(true); 155 | }); 156 | }); 157 | 158 | describe('router model on history ->', () => { 159 | const originalLocation = window.location; 160 | 161 | beforeEach(() => { 162 | initModelRouter(); 163 | modelManagerSpy.mockResolvedValue(TEST_MODEL as Model); 164 | }); 165 | 166 | afterAll(() => { 167 | Object.defineProperty(window, 'location', { configurable: true, value: originalLocation }); 168 | modelManagerSpy.mockReset(); 169 | }); 170 | it('should fetch model on history push', () => { 171 | window.history.pushState({}, '', '/test'); 172 | expect(modelManagerSpy).toHaveBeenCalledWith({ path: '/test' }); 173 | }); 174 | it('should fetch model on history replace', () => { 175 | window.history.replaceState({}, '', '/test'); 176 | expect(modelManagerSpy).toHaveBeenCalledWith({ path: '/test' }); 177 | }); 178 | it('should fetch model on history pop', () => { 179 | Object.defineProperty(window, 'location', { 180 | configurable: true, 181 | value: { pathname: '/test' } 182 | }); 183 | window.dispatchEvent(new Event('popstate')); 184 | expect(modelManagerSpy).toHaveBeenCalledWith({ path: '/test' }); 185 | }); 186 | 187 | }); 188 | }); 189 | -------------------------------------------------------------------------------- /src/AuthoringUtils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | */ 12 | 13 | import { AEM_MODE } from './Constants'; 14 | import { PathUtils } from './PathUtils'; 15 | import MetaProperty from './MetaProperty'; 16 | 17 | export class AuthoringUtils { 18 | private readonly _apiDomain: string | null; 19 | 20 | /** 21 | * Base path for editor clientlibs. 22 | */ 23 | public static readonly EDITOR_CLIENTLIB_PATH = '/etc.clientlibs/cq/gui/components/authoring/editors/clientlibs/'; 24 | 25 | /** 26 | * Authoring libraries. 27 | */ 28 | public static readonly AUTHORING_LIBRARIES = { 29 | JS: [ 30 | AuthoringUtils.EDITOR_CLIENTLIB_PATH + 'internal/messaging.js', 31 | AuthoringUtils.EDITOR_CLIENTLIB_PATH + 'utils.js', 32 | AuthoringUtils.EDITOR_CLIENTLIB_PATH + 'internal/page.js', 33 | AuthoringUtils.EDITOR_CLIENTLIB_PATH + 'internal/pagemodel/messaging.js' 34 | ], 35 | CSS: [ 36 | AuthoringUtils.EDITOR_CLIENTLIB_PATH + 'internal/page.css' 37 | ], 38 | META: { 39 | [MetaProperty.WCM_DATA_TYPE]: 'JSON' 40 | } 41 | }; 42 | 43 | /** 44 | * @private 45 | */ 46 | constructor(domain: string | null) { 47 | this._apiDomain = domain; 48 | } 49 | 50 | /** 51 | * @private 52 | */ 53 | getApiDomain(): string | null { 54 | return this._apiDomain; 55 | } 56 | 57 | /** 58 | * Generates