├── .editorconfig ├── .eslintrc.js ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── renovate.json └── workflows │ ├── ci.yml │ ├── release.yml │ ├── security.yml │ └── sonar.yml ├── .gitignore ├── .prettierrc ├── .releaserc.js ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── DEV_GUIDELINES.md ├── LICENSE ├── README.md ├── babel.config.js ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── api │ ├── README.md │ └── fetchModel.ts ├── components │ ├── AllowedComponentsContainer.tsx │ ├── Container.tsx │ ├── Page.tsx │ ├── Placeholder.tsx │ ├── README.md │ └── ResponsiveGrid.tsx ├── constants │ ├── classnames.ts │ ├── events.ts │ ├── index.ts │ ├── properties.ts │ └── texts.ts ├── core │ ├── ComponentMapping.tsx │ ├── EditableComponent.tsx │ └── README.md ├── hooks │ └── useEditor.ts ├── types.ts ├── types │ ├── AEMModel.ts │ └── EditConfig.ts └── utils │ └── Utils.ts ├── test ├── ComponentMapping.test.tsx ├── EditableComponent.test.tsx ├── api │ └── fetchModel.test.ts ├── components │ ├── AllowedComponentsContainer.test.tsx │ ├── Container.test.tsx │ ├── Page.test.tsx │ └── ResponsiveGrid.test.tsx ├── data │ └── types.ts └── specs │ └── composition_attribute_propagation.test.tsx ├── tsconfig.base.json ├── tsconfig.json └── webpack.config.babel.ts /.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,json}] 12 | indent_size = 2 13 | 14 | [*.{json,xml}] 15 | insert_final_newline = false 16 | 17 | [package.json] 18 | insert_final_newline = true 19 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 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 OFF = 0; 14 | const WARN = 1; 15 | const ERROR = 2; 16 | 17 | module.exports = { 18 | parser: '@typescript-eslint/parser', 19 | env: { 20 | browser: true, 21 | es6: true, 22 | 'jest/globals': true, 23 | }, 24 | parserOptions: { 25 | requireConfigFile: false, 26 | ecmaVersion: 2020, 27 | sourceType: 'module', 28 | warnOnUnsupportedTypeScriptVersion: false, 29 | }, 30 | globals: {}, 31 | ignorePatterns: ['typesoutput/'], 32 | extends: [ 33 | '@adobe/eslint-config-editorxp', 34 | 'plugin:react/recommended', 35 | 'plugin:@typescript-eslint/recommended', 36 | 'plugin:jest/recommended', 37 | 'plugin:prettier/recommended', 38 | 'prettier', 39 | ], 40 | plugins: ['prettier', 'react', 'react-hooks', 'jest'], 41 | rules: { 42 | 'max-lines-per-function': [ 43 | WARN, 44 | { 45 | max: 75, 46 | skipBlankLines: true, 47 | skipComments: true, 48 | }, 49 | ], 50 | 'max-params': [ 51 | WARN, 52 | { 53 | max: 6, 54 | }, 55 | ], 56 | 'max-statements': [ 57 | WARN, 58 | { 59 | max: 15, 60 | }, 61 | ], 62 | 'no-unused-vars': [ 63 | WARN, 64 | { 65 | argsIgnorePattern: '^_', 66 | }, 67 | ], 68 | 'padding-line-between-statements': [OFF], 69 | 'react-hooks/exhaustive-deps': ERROR, 70 | 'react-hooks/rules-of-hooks': ERROR, 71 | 'react/jsx-curly-brace-presence': [WARN, 'never'], 72 | 'react/prop-types': OFF, 73 | complexity: [ 74 | WARN, 75 | { 76 | max: 15, 77 | }, 78 | ], 79 | }, 80 | settings: { 81 | react: { 82 | version: 'detect', 83 | }, 84 | }, 85 | overrides: [ 86 | { 87 | files: ['*.test.js', '*.test.ts', '*.test.tsx'], 88 | rules: { 89 | 'max-lines-per-function': [OFF], 90 | 'no-irregular-whitespace': [OFF], 91 | 'max-statements': [OFF], 92 | }, 93 | }, 94 | ], 95 | }; 96 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | package-lock.json binary 2 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | } -------------------------------------------------------------------------------- /.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: Dump GitHub context 10 | env: 11 | GITHUB_CONTEXT: ${{ toJson(github) }} 12 | run: echo "$GITHUB_CONTEXT" 13 | - name: Checkout source code 14 | uses: actions/checkout@v3 15 | with: 16 | ref: ${{ github.event.pull_request.head.sha }} 17 | fetch-depth: 0 18 | - name: Setup Node.js 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: 14 22 | - name: Install dependencies 23 | run: npm ci 24 | - name: Build the project 25 | run: npm run build:prod 26 | - name: Run tests and do code coverage check 27 | run: npm run test:coverage 28 | - name: Prettify code 29 | uses: creyD/prettier_action@v4.3 30 | with: 31 | repo-token: ${{ secrets.GITHUB_TOKEN }} 32 | - name: Upload code coverage report to workflow as an artifact 33 | uses: actions/upload-artifact@v3 34 | with: 35 | name: istanbul-code-coverage.zip 36 | path: coverage 37 | - name: Upload code coverage report to codecov.io and comment in pull request 38 | uses: codecov/codecov-action@v3 39 | -------------------------------------------------------------------------------- /.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@v3 14 | with: 15 | fetch-depth: 0 16 | - name: Setup Node.js 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: 14 20 | - name: Install dependencies 21 | run: npm ci 22 | - name: Build the project 23 | run: npm run build:prod 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@v3 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-react-editable-components 37 | -Dsonar.sources=src 38 | -Dsonar.tests=test 39 | -Dsonar.javascript.lcov.reportPaths=dist/coverage/lcov.info 40 | -Dsonar.coverage.exclusions=src/types.ts 41 | - name: Release module and publish it in github.com and npmjs.com 42 | env: 43 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 44 | NPM_TOKEN: ${{ secrets.ADOBE_BOT_NPM_TOKEN }} 45 | run: npm run semantic-release 46 | - name: Build documentation 47 | run: npm run docs 48 | - name: Publish documentation to github pages 49 | uses: JamesIves/github-pages-deploy-action@v4.7.2 50 | with: 51 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 52 | BRANCH: gh-pages-documentation 53 | FOLDER: dist/docs 54 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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-react-editable-components 34 | -Dsonar.sources=src 35 | -Dsonar.tests=test 36 | -Dsonar.javascript.lcov.reportPaths=dist/coverage/lcov.info 37 | -Dsonar.coverage.exclusions=src/types.ts 38 | -Dsonar.pullrequest.key=${{ steps.source-run-info.outputs.pullRequestNumber }} 39 | -Dsonar.pullrequest.branch=${{ steps.source-run-info.outputs.sourceHeadBranch }} 40 | -Dsonar.pullrequest.base=${{ steps.source-run-info.outputs.targetBranch }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | .scannerwork/ 4 | **/*.log 5 | *.tgz 6 | *.tsbuildinfo -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": true, 3 | "printWidth": 120, 4 | "arrowParens": "always", 5 | "singleQuote": true, 6 | "trailingComma": "all", 7 | "tabWidth": 2 8 | } 9 | -------------------------------------------------------------------------------- /.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 | { 20 | changelogFile: 'CHANGELOG.md', 21 | }, 22 | ], 23 | '@semantic-release/npm', 24 | [ 25 | '@semantic-release/github', 26 | { 27 | assets: ['package.json', 'CHANGELOG.md'], 28 | message: 'chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}', 29 | }, 30 | ], 31 | [ 32 | '@semantic-release/git', 33 | { 34 | assets: ['package.json', 'CHANGELOG.md'], 35 | }, 36 | ], 37 | ], 38 | branch: 'master', 39 | branches: ['master'], 40 | }; 41 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [2.1.1](https://github.com/adobe/aem-react-editable-components/compare/v2.1.0...v2.1.1) (2023-12-11) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * refresh model for dynamic path ([2570871](https://github.com/adobe/aem-react-editable-components/commit/2570871bde7528db0f4c7f659db6b2587e900688)) 7 | 8 | # [2.1.0](https://github.com/adobe/aem-react-editable-components/compare/v2.0.5...v2.1.0) (2023-09-13) 9 | 10 | 11 | ### Features 12 | 13 | * ability to export child components ([a6a7047](https://github.com/adobe/aem-react-editable-components/commit/a6a704785c70c7d7edaf5a77f1027cf44da761e8)), closes [#189](https://github.com/adobe/aem-react-editable-components/issues/189) 14 | * ability to export child components ([6162d62](https://github.com/adobe/aem-react-editable-components/commit/6162d622bceaa8d00107614a9bdbe8762077d27c)), closes [#189](https://github.com/adobe/aem-react-editable-components/issues/189) 15 | 16 | ## [2.0.5](https://github.com/adobe/aem-react-editable-components/compare/v2.0.4...v2.0.5) (2023-06-28) 17 | 18 | 19 | ### Bug Fixes 20 | 21 | * resolve reload issue on remote spa ([7bf16d8](https://github.com/adobe/aem-react-editable-components/commit/7bf16d8858c11915094939d55fe861f5ed6b22b2)) 22 | 23 | ## [2.0.4](https://github.com/adobe/aem-react-editable-components/compare/v2.0.3...v2.0.4) (2023-03-01) 24 | 25 | 26 | ### Bug Fixes 27 | 28 | * jcr:title of the component should be used in Template Editor ([0564707](https://github.com/adobe/aem-react-editable-components/commit/0564707c62104a35269c36c93754efd2799352ad)) 29 | 30 | ## [2.0.3](https://github.com/adobe/aem-react-editable-components/compare/v2.0.2...v2.0.3) (2022-10-20) 31 | 32 | 33 | ### Bug Fixes 34 | 35 | * pass props to child component ([ba4be70](https://github.com/adobe/aem-react-editable-components/commit/ba4be703d281a8c71df721be27b73127500867a5)) 36 | 37 | ## [2.0.2](https://github.com/adobe/aem-react-editable-components/compare/v2.0.1...v2.0.2) (2022-09-21) 38 | 39 | 40 | ### Bug Fixes 41 | 42 | * transpilation target incompatible with CRA ([061906f](https://github.com/adobe/aem-react-editable-components/commit/061906f731f477c249e94b7ba120e8f389f742c9)) 43 | 44 | ## [2.0.1](https://github.com/adobe/aem-react-editable-components/compare/v2.0.0...v2.0.1) (2022-08-02) 45 | 46 | 47 | ### Bug Fixes 48 | 49 | * include types in build ([3ad1f34](https://github.com/adobe/aem-react-editable-components/commit/3ad1f340af65bf3fa7d72387e4b262472a71b362)) 50 | 51 | # [2.0.0](https://github.com/adobe/aem-react-editable-components/compare/v1.1.11...v2.0.0) (2022-06-21) 52 | 53 | 54 | ### Bug Fixes 55 | 56 | * adjust src and tests according to prettier rules ([6634e74](https://github.com/adobe/aem-react-editable-components/commit/6634e746c9a806f8b64cfb3c3ab2cfba3b18719c)) 57 | * further refactoring ([#106](https://github.com/adobe/aem-react-editable-components/issues/106)) ([5619a31](https://github.com/adobe/aem-react-editable-components/commit/5619a31898646e0fe83565b89afdf5b72dc25884)) 58 | * move circular-dependency-plugin to dev deps ([1f3c350](https://github.com/adobe/aem-react-editable-components/commit/1f3c3509599f9ae2bd70449ec93b7c47fe1aa79b)) 59 | * move circular-dependency-plugin to dev deps ([f0af6e3](https://github.com/adobe/aem-react-editable-components/commit/f0af6e383e0c5a854e6894d32558898a49298260)) 60 | * setup project ([2d42638](https://github.com/adobe/aem-react-editable-components/commit/2d42638aff36a9ef6662afafbc8d4b03b3ed6d80)) 61 | * update package-lock.json ([5e4b645](https://github.com/adobe/aem-react-editable-components/commit/5e4b645bde12e007d5de549a422a157d64929517)) 62 | 63 | 64 | * Merge pull request #115 from adobe/feat/functional-components ([9098aa5](https://github.com/adobe/aem-react-editable-components/commit/9098aa5a43549063553a037eeedee8c4792f8686)), closes [#115](https://github.com/adobe/aem-react-editable-components/issues/115) 65 | 66 | 67 | ### Features 68 | 69 | * Refactor to use components by composition ([85d0012](https://github.com/adobe/aem-react-editable-components/commit/85d00122a1fe56dfee202905520554dfdc9ba2ea)) 70 | 71 | 72 | ### BREAKING CHANGES 73 | 74 | * withMappable and other helpers removed 75 | * EditableComponent to cover scenarios provided by withMappable 76 | 77 | ## [1.1.11](https://github.com/adobe/aem-react-editable-components/compare/v1.1.10...v1.1.11) (2022-03-29) 78 | 79 | 80 | ### Bug Fixes 81 | 82 | * add circular-dependency-plugin dev dep ([b86274a](https://github.com/adobe/aem-react-editable-components/commit/b86274a8b3f3b8f70dfbdf3251b96ecac272c50e)) 83 | 84 | ## [1.1.10](https://github.com/adobe/aem-react-editable-components/compare/v1.1.9...v1.1.10) (2021-09-15) 85 | 86 | 87 | ### Reverts 88 | 89 | * Revert "chore: enable external commits to run pipelines" ([f2a81c4](https://github.com/adobe/aem-react-editable-components/commit/f2a81c463f47ac2231746dc217aef1c0e02368bb)) 90 | 91 | ## [1.1.9](https://github.com/adobe/aem-react-editable-components/compare/v1.1.8...v1.1.9) (2021-09-07) 92 | 93 | 94 | ### Bug Fixes 95 | 96 | * version bump ([4202782](https://github.com/adobe/aem-react-editable-components/commit/4202782a01cf7b50256498f92313d3246c44d1ba)) 97 | 98 | ## [1.1.8](https://github.com/adobe/aem-react-editable-components/compare/v1.1.7...v1.1.8) (2021-08-25) 99 | 100 | 101 | ### Bug Fixes 102 | 103 | * bump version ([#76](https://github.com/adobe/aem-react-editable-components/issues/76)) ([365dced](https://github.com/adobe/aem-react-editable-components/commit/365dcedfe8e2f17fedd280da4a76fa456646c198)) 104 | 105 | ## [1.1.7](https://github.com/adobe/aem-react-editable-components/compare/v1.1.6...v1.1.7) (2021-08-10) 106 | 107 | 108 | ### Bug Fixes 109 | 110 | * add StyleSystem support ([2c3e400](https://github.com/adobe/aem-react-editable-components/commit/2c3e400930cdf4fdffc3f49512bcb6a2fa8c06bb)) 111 | 112 | ## [1.1.6](https://github.com/adobe/aem-react-editable-components/compare/v1.1.5...v1.1.6) (2021-04-15) 113 | 114 | 115 | ### Bug Fixes 116 | 117 | * **mapto:** fix return type of get/getLazy ([#55](https://github.com/adobe/aem-react-editable-components/issues/55)) ([171254c](https://github.com/adobe/aem-react-editable-components/commit/171254cdb357291c9c625c698dbb1efe495cb8f4)) 118 | 119 | ## [1.1.5](https://github.com/adobe/aem-react-editable-components/compare/v1.1.4...v1.1.5) (2021-02-11) 120 | 121 | 122 | ### Bug Fixes 123 | 124 | * **npm:** fix release version ([f18aa96](https://github.com/adobe/aem-react-editable-components/commit/f18aa9603e3331772ee5762670a5729c1d4cf2a6)) 125 | 126 | ## [1.1.4](https://github.com/adobe/aem-react-editable-components/compare/v1.1.3...v1.1.4) (2021-02-11) 127 | 128 | 129 | ### Bug Fixes 130 | 131 | * deprecating isInEditor and updating usage ([6f92db0](https://github.com/adobe/aem-react-editable-components/commit/6f92db0a703b13949a761e16520d049ebb7e8e26)) 132 | * removing unused imports and functions ([3a8d3ff](https://github.com/adobe/aem-react-editable-components/commit/3a8d3ffaae558293547cc9c9e01daa2fea7c8a32)) 133 | * update deprecated function to return AuthoringUtils.isInEditor ([b7eea37](https://github.com/adobe/aem-react-editable-components/commit/b7eea37b064a124a436113418929d40385a422bb)) 134 | * update isInEditor to check for remote app ([8ec4530](https://github.com/adobe/aem-react-editable-components/commit/8ec4530b3769ee2a30c0cc97070ef1d1b2028c33)) 135 | 136 | ## [1.1.3](https://github.com/adobe/aem-react-editable-components/compare/v1.1.2...v1.1.3) (2021-01-27) 137 | 138 | 139 | ### Bug Fixes 140 | 141 | * enable editing on AEM for dynamic container components ([7c13657](https://github.com/adobe/aem-react-editable-components/commit/7c1365787f2d39d681a594437bcec5ead9ced80e)) 142 | * fire event only when loaded in AEM editor ([da933f2](https://github.com/adobe/aem-react-editable-components/commit/da933f2e139f158cd5200f70034afdf0a1fb9f67)) 143 | 144 | ## [1.1.2](https://github.com/adobe/aem-react-editable-components/compare/v1.1.1...v1.1.2) (2021-01-05) 145 | 146 | 147 | ### Bug Fixes 148 | 149 | * **docs:** remove --mode from docs build after a typedoc breaking change ([683da63](https://github.com/adobe/aem-react-editable-components/commit/683da63c499ffdcdc6f2cfa3067fe72eee56bd6b)) 150 | 151 | ## [1.1.1](https://github.com/adobe/aem-react-editable-components/compare/v1.1.0...v1.1.1) (2020-12-04) 152 | 153 | 154 | ### Bug Fixes 155 | 156 | * refresh component content on AEM edit ([1c0d7b0](https://github.com/adobe/aem-react-editable-components/commit/1c0d7b0dcc355a25bbccc0b5bc9e3eb348ed24cd)) 157 | 158 | # [1.1.0](https://github.com/adobe/aem-react-editable-components/compare/v1.0.6...v1.1.0) (2020-10-23) 159 | 160 | 161 | ### Features 162 | 163 | * support for virtual components ([#34](https://github.com/adobe/aem-react-editable-components/issues/34)) ([8839f97](https://github.com/adobe/aem-react-editable-components/commit/8839f97b847e3b1bd8dc6d5694acaf63ab09f72d)) 164 | 165 | ## [1.0.6](https://github.com/adobe/aem-react-editable-components/compare/v1.0.5...v1.0.6) (2020-10-07) 166 | 167 | 168 | ### Bug Fixes 169 | 170 | * update page-model-manager dependency ([4b0c9d2](https://github.com/adobe/aem-react-editable-components/commit/4b0c9d255d7e60da786f8c8b000fc496153a5014)) 171 | 172 | ## [1.0.5](https://github.com/adobe/aem-react-editable-components/compare/v1.0.4...v1.0.5) (2020-10-07) 173 | 174 | 175 | ### Bug Fixes 176 | 177 | * validation added for empty path and fetched data in withModel() ([ce403c0](https://github.com/adobe/aem-react-editable-components/commit/ce403c060593c08cadf5c606882dd60016e9f14a)) 178 | 179 | ## [1.0.4](https://github.com/adobe/aem-react-editable-components/compare/v1.0.3...v1.0.4) (2020-08-30) 180 | 181 | 182 | ### Bug Fixes 183 | 184 | * **semantic-release:** provide semantic-release automation ([3c5aee7](https://github.com/adobe/aem-react-editable-components/commit/3c5aee71056105bc3ca1cc3a0f51ae1dc141192f)) 185 | 186 | ## [1.0.0](https://github.com/adobe/aem-react-editable-components/releases/tag/v1.0.0) (2020-08-24) 187 | 188 | Initial public release of `aem-react-editable-components`. Renamed from cq-react-editable-components. 189 | -------------------------------------------------------------------------------- /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/ 75 | -------------------------------------------------------------------------------- /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 | ## Important 8 | 9 | Please follow the [Commit Message Conventions](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#-git-commit-guidelines). 10 | 11 | ## Code Of Conduct 12 | 13 | This project adheres to the Adobe [code of conduct](../CODE_OF_CONDUCT.md). By participating, 14 | you are expected to uphold this code. Please report unacceptable behavior to 15 | [Grp-opensourceoffice@adobe.com](mailto:Grp-opensourceoffice@adobe.com). 16 | 17 | ## Have A Question? 18 | 19 | Start by filing an issue. The existing committers on this project work to reach 20 | consensus around project direction and issue solutions within issue threads 21 | (when appropriate). 22 | 23 | ## Contributor License Agreement 24 | 25 | All third-party contributions to this project must be accompanied by a signed contributor 26 | license agreement. This gives Adobe permission to redistribute your contributions 27 | as part of the project. [Sign our CLA](https://opensource.adobe.com/cla.html). You 28 | only need to submit an Adobe CLA one time, so if you have submitted one previously, 29 | you are good to go! 30 | 31 | ## Code Reviews 32 | 33 | All submissions should come in the form of pull requests and need to be reviewed 34 | by project committers. Read [GitHub's pull request documentation](https://help.github.com/articles/about-pull-requests/) 35 | for more information on sending pull requests. 36 | 37 | Some things that will increase the chance that your pull request is accepted: 38 | 39 | 1. Write a [good commit message]((https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#-git-commit-guidelines)). 40 | 2. Make sure the PR merges cleanly with the latest master. 41 | 3. Describe your feature/bugfix and why it's needed/important in the pull request description. 42 | 4. Lastly, please follow the pull request template when submitting a pull request. 43 | 44 | 45 | ## From Contributor To Committer 46 | 47 | We love contributions from our community! If you'd like to go a step beyond contributor 48 | and become a committer with full write access and a say in the project, you must 49 | be invited to the project. The existing committers employ an internal nomination 50 | process that must reach lazy consensus (silence is approval) before invitations 51 | are issued. If you feel you are qualified and want to get more deeply involved, 52 | feel free to reach out to existing committers to have a conversation about that. 53 | 54 | ## Security Issues 55 | 56 | 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). 57 | 58 | ## Developer Guidelines 59 | 60 | * [Developer Guidelines](DEV_GUIDELINES.md) 61 | -------------------------------------------------------------------------------- /DEV_GUIDELINES.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | Run `npm install` to get all node_modules that are necessary for development. Refer to scripts under `package.json` for more useful commands. 4 | 5 | ## Build 6 | 7 | ```sh 8 | $ npm run build 9 | ``` 10 | 11 | ## Test 12 | 13 | ```sh 14 | $ npm run test 15 | ``` 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2020 Adobe 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AEM SPA React Editable Components 2 | 3 | [![License](https://img.shields.io/badge/license-Apache%202-blue)](https://github.com/adobe/aem-react-editable-components/blob/master/LICENSE) 4 | [![NPM Version](https://img.shields.io/npm/v/@adobe/aem-react-editable-components.svg)](https://www.npmjs.com/package/@adobe/aem-react-editable-components) 5 | [![Documentation](https://img.shields.io/badge/docs-api-blue)](https://opensource.adobe.com/aem-react-editable-components/) 6 | 7 | [![codecov](https://codecov.io/gh/adobe/aem-react-editable-components/branch/master/graph/badge.svg)](https://codecov.io/gh/adobe/aem-react-editable-components) 8 | [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=adobe_aem-react-editable-components&metric=alert_status)](https://sonarcloud.io/dashboard?id=adobe_aem-react-editable-components) 9 | [![Known Vulnerabilities](https://snyk.io/test/github/adobe/aem-react-editable-components/badge.svg)](https://snyk.io/test/github/adobe/aem-react-editable-components) 10 | [![Dependencies](https://badges.renovateapi.com/github/adobe/aem-react-editable-components)](https://app.renovatebot.com/dashboard#github/adobe/aem-react-editable-components) 11 | 12 | This project provides the React components and integration layer to get you started with the Adobe Experience Manager SPA Editor. 13 | 14 | 15 | ## Installation 16 | ``` 17 | npm install @adobe/aem-react-editable-components 18 | ``` 19 | 20 | ## Prerequisites 21 | 22 | - [AEM SPA Model Manager](https://github.com/adobe/aem-spa-page-model-manager) is installed and initialized. 23 | - App uses **React v16.8.0** or higher 24 | 25 | ## Documentation 26 | 27 | * [SPA Editor Overview](https://experienceleague.adobe.com/docs/experience-manager-64/developing/headless/spas/spa-overview.html?lang=en) 28 | * [Getting Started with the AEM SPA Editor and Angular](https://docs.adobe.com/content/help/en/experience-manager-learn/spa-angular-tutorial/overview.html) 29 | * [Getting Started with the AEM SPA Editor and React](https://docs.adobe.com/content/help/en/experience-manager-learn/spa-react-tutorial/overview.html) 30 | * [Getting Started with the AEM SPA Editor and a remote React SPA](https://experienceleague.adobe.com/docs/experience-manager-learn/getting-started-with-aem-headless/spa-editor/remote-spa/overview.html?lang=en) 31 | 32 | ## Features 33 | 34 | - [Components](./src/components) 35 | - [Integration with AEM](./src/core) 36 | - [Helpers](./src/api) 37 | 38 | 39 | ## Contributing 40 | 41 | Contributions are welcome! Read the [Contributing Guide](CONTRIBUTING.md) for more information. 42 | 43 | ### Releasing 44 | 45 | Merging the PR to master will trigger an automatic release Github Action. It is important to follow [Angular Commit Message Conventions](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#-git-commit-guidelines). Only **fix** and **feat** can trigger a release. 46 | 47 | ### Licensing 48 | 49 | This project is licensed under the Apache V2 License. See [LICENSE](LICENSE) for more information. 50 | 51 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 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 = function (api) { 14 | api.cache(true); 15 | 16 | const presets = ['@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript']; 17 | 18 | const plugins = [ 19 | '@babel/plugin-syntax-dynamic-import', 20 | '@babel/plugin-proposal-class-properties', 21 | '@babel/plugin-transform-runtime', 22 | '@babel/plugin-proposal-export-default-from', 23 | ]; 24 | 25 | return { 26 | presets, 27 | plugins, 28 | }; 29 | }; 30 | -------------------------------------------------------------------------------- /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 | testEnvironment: 'jsdom', 16 | transform: { 17 | '^.+\\.(js|js|ts)x?$': 'babel-jest', 18 | }, 19 | testMatch: ['/test/**/*.test.{ts,tsx}'], 20 | testPathIgnorePatterns: ['node_modules/', 'dist/', 'typesoutput/'], 21 | collectCoverageFrom: ['src/**/*.{ts,tsx}'], 22 | coveragePathIgnorePatterns: ['src/types.ts', 'src/types/'], 23 | coverageDirectory: 'dist/coverage', 24 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], 25 | }; 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@adobe/aem-react-editable-components", 3 | "version": "2.1.1", 4 | "description": "Provides React components and integration layer with Adobe Experience Manager Page Editor.", 5 | "keywords": [ 6 | "spa", 7 | "aem", 8 | "react", 9 | "adobe" 10 | ], 11 | "author": "Adobe Systems Inc. ", 12 | "license": "Apache-2.0", 13 | "repository": "github:adobe/aem-react-editable-components", 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-react-editable-components/issues" 17 | }, 18 | "engines": { 19 | "npm": ">=8.7.0", 20 | "node": ">=12.16.2" 21 | }, 22 | "main": "dist/aem-react-editable-components.js", 23 | "types": "dist/types.d.ts", 24 | "scripts": { 25 | "build:prod": "NODE_ENV=production npm run build", 26 | "build:types": "tsc -p tsconfig.base.json", 27 | "build": "npm run lint && npm run build:types && webpack", 28 | "clean": "rm -rf dist/ node_modules/ package-lock.json", 29 | "docs": "npm i && npx typedoc --entryPoints ./src/types.ts --out ./dist/docs", 30 | "lint:fix": "eslint . --fix", 31 | "lint": "eslint .", 32 | "semantic-release": "semantic-release", 33 | "test:coverage": "jest --clearCache && jest --coverage", 34 | "test:debug": "jest --coverage --watchAll", 35 | "test": "jest --clearCache && jest", 36 | "watch": "webpack --watch" 37 | }, 38 | "pre-commit": [ 39 | "lint" 40 | ], 41 | "lint-staged": { 42 | "*.{js,json,md}": [ 43 | "prettier --write", 44 | "git add" 45 | ] 46 | }, 47 | "sideEffects": false, 48 | "dependencies": { 49 | "@adobe/aem-spa-component-mapping": "~1.1.1", 50 | "@braintree/sanitize-url": "^6.0.0" 51 | }, 52 | "peerDependencies": { 53 | "@adobe/aem-spa-page-model-manager": "^1.4.3" 54 | }, 55 | "devDependencies": { 56 | "@adobe/aem-spa-page-model-manager": "~1.5.0", 57 | "@adobe/eslint-config-editorxp": "^1.0.8", 58 | "@babel/eslint-parser": "^7.16.5", 59 | "@babel/plugin-proposal-class-properties": "^7.16.7", 60 | "@babel/plugin-proposal-export-default-from": "^7.16.7", 61 | "@babel/plugin-syntax-dynamic-import": "^7.8.3", 62 | "@babel/plugin-transform-runtime": "^7.16.8", 63 | "@babel/preset-env": "^7.16.8", 64 | "@babel/preset-react": "^7.16.7", 65 | "@babel/preset-typescript": "^7.16.7", 66 | "@babel/register": "^7.17.7", 67 | "@jest/globals": "^27.4.6", 68 | "@semantic-release/changelog": "^6.0.0", 69 | "@semantic-release/git": "^10.0.0", 70 | "@semantic-release/github": "^8.0.0", 71 | "@testing-library/react": "^12.1.4", 72 | "@types/clone": "^2.1.0", 73 | "@types/jest": "^27.0.0", 74 | "@types/node": "^17.0.0", 75 | "@types/react": "^18.0.0", 76 | "@types/react-dom": "^18.0.0", 77 | "@types/webpack-node-externals": "^2.5.3", 78 | "@typescript-eslint/eslint-plugin": "^5.0.0", 79 | "@typescript-eslint/parser": "^5.0.0", 80 | "circular-dependency-plugin": "^5.2.2", 81 | "clean-webpack-plugin": "^4.0.0", 82 | "eslint": "^8.0.0", 83 | "eslint-config-prettier": "^8.3.0", 84 | "eslint-plugin-header": "^3.1.1", 85 | "eslint-plugin-jest": "^25.7.0", 86 | "eslint-plugin-json": "^3.0.0", 87 | "eslint-plugin-prettier": "^4.0.0", 88 | "eslint-plugin-react": "^7.23.2", 89 | "eslint-plugin-react-hooks": "^4.3.0", 90 | "jest": "^27.5.1", 91 | "jest-fetch-mock": "^3.0.3", 92 | "lint-staged": "^12.1.7", 93 | "prettier": "^2.5.1", 94 | "process": "^0.11.10", 95 | "react": "^16.13.1", 96 | "react-dom": "^16.13.1", 97 | "semantic-release": "^19.0.0", 98 | "ts-jest": "^27.0.0", 99 | "ts-loader": "^9.0.0", 100 | "typescript": "^4.2.4", 101 | "webpack": "^5.66.0", 102 | "webpack-cli": "^4.9.1", 103 | "webpack-merge": "^5.8.0", 104 | "webpack-node-externals": "^3.0.0" 105 | }, 106 | "files": [ 107 | "dist/**/*.{js,ts,map}", 108 | "!**/{docs,coverage}/" 109 | ] 110 | } 111 | -------------------------------------------------------------------------------- /src/api/README.md: -------------------------------------------------------------------------------- 1 | # Helpers 2 | 3 | ## Fetch Model from AEM 4 | 5 | Helps fetch the model.json from the AEM instance explicitly.This can be useful is SSR scenarios where the model needs to be prefetched prior on server side or prior to ModelManager initialization. 6 | 7 | ### Usage 8 | 9 | ``` 10 | const model = await fetchModel({ 11 | pagePath: '/content/wknd-app/us/en/home', // path to the page 12 | itemPath: 'root/responsivegrid' // path to the item within the page for which model is required. 13 | }); 14 | ``` 15 | 16 | where _model_ will contain the fetched model of the container at the path _'root/responsivegrid'_ within the page _'/content/wknd-app/us/en/home'_. 17 | 18 | You can also directly provide the _cqPath_ of the component for which model needs to be fetched if required. 19 | 20 | ``` 21 | const model = await fetchModel({ 22 | cqPath: '/content/wknd-app/us/en/home/jcr:content/root/responsivegrid' // path to the component for which model needs to be fetched 23 | }); 24 | ``` 25 | 26 | If the fetch needs to be done prior to ModelManager initialization, you would also have to communicate the host information of the AEM instance from which model is to be fetched as well as any options required for performing the fetch request. 27 | 28 | ``` 29 | const model = await fetchModel({ 30 | cqPath: '/content/wknd-app/us/en/home/jcr:content/root/responsivegrid', 31 | host: ${AEM_HOST}, 32 | options: ${FETCH_REQUEST_OPTIONS} 33 | }); 34 | ``` 35 | -------------------------------------------------------------------------------- /src/api/fetchModel.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 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 | import { ModelManager } from '@adobe/aem-spa-page-model-manager'; 13 | import { Utils } from '../utils/Utils'; 14 | import { sanitizeUrl } from '@braintree/sanitize-url'; 15 | import { ModelProps } from '../types/AEMModel'; 16 | 17 | type RequireAtLeastOne = { [K in keyof T]-?: Required> & Partial>> }[keyof T]; 18 | 19 | export interface Path { 20 | cqPath: string; 21 | pagePath: string; 22 | } 23 | 24 | export type FetchProps = RequireAtLeastOne & { 25 | forceReload?: boolean; 26 | host?: string; 27 | options?: { 28 | headers?: HeadersInit; 29 | }; 30 | itemPath?: string; 31 | }; 32 | 33 | /** 34 | * Fetch the model for a given path from AEM 35 | * 36 | * @param {Object} options 37 | * @param options.cqPath Complete path to component on AEM 38 | * @param options.pagePath Path to page containing the desired component 39 | * @param options.itemPath Path to item within the page defined by pagePath 40 | * @param options.host Host information of the AEM instance if fetch is to be done prior to ModelManager init 41 | * @param options.options Fetch request options is fetching model using host 42 | 43 | * @returns The fetched model transformed into usable props 44 | */ 45 | export async function fetchModel({ 46 | cqPath, 47 | forceReload = false, 48 | pagePath, 49 | itemPath, 50 | host, 51 | options, 52 | }: FetchProps): Promise { 53 | let model = {}, 54 | data; 55 | if (cqPath || pagePath) { 56 | const path = (cqPath && sanitizeUrl(cqPath)) || Utils.getCQPath({ pagePath, itemPath }); 57 | if (host) { 58 | const hostURL = sanitizeUrl(`${host}/${path}`).replace(/\/+/g, '/'); 59 | const response = await fetch(`${hostURL}.model.json`, options); 60 | if (response.ok) { 61 | data = await response.json(); 62 | } 63 | } else { 64 | data = await ModelManager.getData({ path, forceReload }).catch((err: Error) => console.error(err)); 65 | } 66 | if (data && Object.keys(data).length) { 67 | model = Utils.modelToProps(data); 68 | } 69 | } 70 | return model; 71 | } 72 | -------------------------------------------------------------------------------- /src/components/AllowedComponentsContainer.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 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 React from 'react'; 14 | import { AllowedComponentList, ModelProps } from '../types/AEMModel'; 15 | import { ClassNames, Texts } from '../constants'; 16 | 17 | type Props = { 18 | allowedComponents: AllowedComponentList; 19 | className: string; 20 | placeholderClassNames?: string; 21 | title: string; 22 | getItemClassNames?: (_key: string) => string; 23 | } & ModelProps; 24 | 25 | /** 26 | * Represents allowed components container in AEM. 27 | */ 28 | const AllowedComponentsContainer = (props: Props): JSX.Element => { 29 | const { placeholderClassNames = '', allowedComponents, title } = props; 30 | const { components } = allowedComponents; 31 | const listLabel = components && components.length > 0 ? title : Texts.EMPTY_LABEL; 32 | 33 | return ( 34 |
35 |
36 | {components.map((component) => ( 37 |
43 | ))} 44 |
45 | ); 46 | }; 47 | 48 | export default AllowedComponentsContainer; 49 | -------------------------------------------------------------------------------- /src/components/Container.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 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 | import React, { ComponentType, ReactElement } from 'react'; 13 | import { Utils } from '../utils/Utils'; 14 | import { Properties, ClassNames } from '../constants'; 15 | import { ModelProps, PageModel } from '../types/AEMModel'; 16 | import { ComponentMapping, MapTo } from '../core/ComponentMapping'; 17 | import { Config, MappedComponentProperties } from '../types/EditConfig'; 18 | import { AuthoringUtils } from '@adobe/aem-spa-page-model-manager'; 19 | 20 | export type ContainerProps = { 21 | className?: string; 22 | itemPath?: string; 23 | isPage?: boolean; 24 | childPages?: JSX.Element; 25 | getItemClassNames?: (_key: string) => string; 26 | placeholderClassNames?: string; 27 | isInEditor?: boolean; 28 | componentMapping?: typeof ComponentMapping; 29 | removeDefaultStyles?: boolean; 30 | config?: Config; 31 | components?: { [key: string]: ComponentType }; 32 | model?: PageModel; 33 | } & ModelProps; 34 | 35 | const getItemPath = (cqPath: string, itemKey: string, isPage = false): string => { 36 | let itemPath = itemKey; 37 | if (cqPath) { 38 | if (isPage) { 39 | itemPath = `${cqPath}/${Properties.JCR_CONTENT}/${itemKey}`; 40 | } else { 41 | itemPath = `${cqPath}/${itemKey}`; 42 | } 43 | } 44 | return itemPath; 45 | }; 46 | 47 | const ComponentList = ({ 48 | cqItemsOrder, 49 | cqItems, 50 | cqPath = '', 51 | getItemClassNames, 52 | isPage, 53 | removeDefaultStyles, 54 | ...props 55 | }: ContainerProps) => { 56 | if (!cqItemsOrder || !cqItems || !cqItemsOrder.length) { 57 | return <>; 58 | } 59 | 60 | return ( 61 | <> 62 | {getChildComponents({ cqItemsOrder, cqItems, cqPath, getItemClassNames, isPage, removeDefaultStyles, ...props })} 63 | 64 | ); 65 | }; 66 | 67 | /** 68 | * Retrieves the child components from the container. 69 | * @summary Retrieves the child components from the cqItems and cqItemsOrder for use in custom containers like Tabs, Carousel, Accordion, etc. 70 | * @param {ContainerProps} props 71 | * @returns {JSX.Element} Array of child components ready to be rendered 72 | */ 73 | export const getChildComponents = ({ 74 | cqItemsOrder = [], 75 | cqItems = {}, 76 | cqPath = '', 77 | getItemClassNames, 78 | isPage, 79 | removeDefaultStyles, 80 | isInEditor = AuthoringUtils.isInEditor(), 81 | ...props 82 | }: ContainerProps): ReactElement[] => { 83 | const componentMapping = props.componentMapping || ComponentMapping; 84 | const components: Array = []; 85 | cqItemsOrder.forEach((itemKey: string) => { 86 | const itemProps = Utils.modelToProps(cqItems[itemKey]); 87 | const itemClassNames = (getItemClassNames && getItemClassNames(itemKey)) || ''; 88 | if (itemProps && itemProps.cqType) { 89 | const ItemComponent: React.ElementType = componentMapping.get(itemProps.cqType); 90 | const itemPath = getItemPath(cqPath, itemKey, isPage); 91 | if (ItemComponent) { 92 | components.push( 93 | , 102 | ); 103 | } else { 104 | console.error('Component not mapped for resourcetype:', itemProps.cqType); 105 | } 106 | } 107 | }); 108 | return components; 109 | }; 110 | 111 | export const Container = (props: ContainerProps): JSX.Element => { 112 | const { 113 | cqPath = '', 114 | className = '', 115 | isPage = false, 116 | isInEditor = AuthoringUtils.isInEditor(), 117 | childPages, 118 | placeholderClassNames = '', 119 | components, 120 | model = {}, 121 | } = props; 122 | 123 | if (components && Object.keys(components).length) { 124 | for (const resourceType in components) { 125 | MapTo(resourceType)(components[resourceType]); 126 | } 127 | } 128 | 129 | const containerProps = 130 | (isInEditor && { 131 | [Properties.DATA_PATH_ATTR]: cqPath, 132 | }) || 133 | {}; 134 | 135 | const childComponents = ; 136 | 137 | return isInEditor ? ( 138 |
139 | {childComponents} 140 | {childPages} 141 | {!isPage && isInEditor && ( 142 |
143 | )} 144 |
145 | ) : ( 146 |
147 | {childComponents} 148 | {childPages} 149 |
150 | ); 151 | }; 152 | -------------------------------------------------------------------------------- /src/components/Page.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 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 React, { ReactElement } from 'react'; 14 | import { Container } from './Container'; 15 | import { ClassNames } from '../constants'; 16 | import { ModelProps } from '../types/AEMModel'; 17 | import { Utils } from '../utils/Utils'; 18 | import { ComponentMapping } from '../core/ComponentMapping'; 19 | import { EditableComponent } from '../core/EditableComponent'; 20 | 21 | export type PageProps = { 22 | isInEditor: boolean; 23 | componentMapping: typeof ComponentMapping; 24 | className?: string; 25 | } & ModelProps; 26 | 27 | const PageList = ({ cqChildren, ...props }: PageProps): JSX.Element => { 28 | const componentMapping = props.componentMapping || ComponentMapping; 29 | 30 | if (!cqChildren) { 31 | return <>; 32 | } 33 | const pages: Array = []; 34 | Object.keys(cqChildren).forEach((itemKey) => { 35 | const itemProps = Utils.modelToProps(cqChildren[itemKey]); 36 | const { cqPath, cqType } = itemProps; 37 | if (cqType) { 38 | const ItemComponent: React.ElementType = componentMapping.get(cqType); 39 | if (ItemComponent) { 40 | pages.push(); 41 | } else { 42 | console.error('Component not mapped for resourcetype:', cqType); 43 | } 44 | } 45 | }); 46 | 47 | return <>{pages}; 48 | }; 49 | 50 | export const Page = ({ className, ...props }: PageProps): JSX.Element => ( 51 | 52 | } 56 | {...props} 57 | /> 58 | 59 | ); 60 | -------------------------------------------------------------------------------- /src/components/Placeholder.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 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 React from 'react'; 14 | import { EditableComponentProps } from '../core/EditableComponent'; 15 | import { ClassNames } from '../constants'; 16 | 17 | const Placeholder = ({ config, ...props }: EditableComponentProps) => { 18 | const { emptyLabel = '', isEmpty } = config || {}; 19 | const editorProps = {}; 20 | if (typeof isEmpty === 'function' && isEmpty(props)) { 21 | Object.assign(editorProps, { className: ClassNames.DEFAULT_PLACEHOLDER, 'data-emptytext': emptyLabel }); 22 | } 23 | return
; 24 | }; 25 | 26 | export default Placeholder; 27 | -------------------------------------------------------------------------------- /src/components/README.md: -------------------------------------------------------------------------------- 1 | # Components 2 | 3 | 4 | ## Page ## 5 | 6 | Render an AEM page and its content and enable authoring on AEM. All child components still need to be mapped to their AEM resourcetypes using **MapTo** or [the **components** prop](#component-mapping). 7 | All mapped components also need to be updated to use the newly introduced wrapper [EditableComponent](../core/README.md). 8 | 9 | ### Default SPA 10 | 11 | For default SPA on AEM, the component can be used as-is OOTB. 12 | 13 | #### Migration to v2 14 | 15 | - *ComponentMappingContent* is now handled internally, so the [usage as illustrated in the sample WKND project](https://github.com/adobe/aem-guides-wknd-spa/blob/React/latest/ui.frontend/src/components/Page/Page.js) as - 16 | 17 | ```js 18 | export default MapTo('wknd-spa-react/components/page')( 19 | withComponentMappingContext(withRoute(AppPage)) 20 | ); 21 | ``` 22 | 23 | can be simplified and now instead be - 24 | ```js 25 | export default MapTo('wknd-spa-react/components/page')( 26 | withRoute(AppPage) 27 | ); 28 | ``` 29 | 30 | - *Model fetching* is now handled internally, so [the usage of withModel](https://github.com/adobe/aem-guides-wknd-spa/blob/React/latest/ui.frontend/src/App.js#L16) - 31 | 32 | ```js 33 | export default withModel(App); 34 | ``` 35 | 36 | can be removed and can be used simply as - 37 | 38 | ```js 39 | export default App; 40 | ``` 41 | 42 | ### Remote SPA 43 | When using the component directly within the app for remote SPA, an additional prop _pagePath_ can be used to pass the path of the corresponding page on AEM. 44 | 45 | ```jsx 46 | 48 | ``` 49 | 50 | Here, the Page component will render content on the AEM page at _us/en/home_ within the project _wknd-app_ 51 | 52 | 53 | ## ResponsiveGrid 54 | 55 | Render an AEM Layout Container and its content and enable authoring on AEM. 56 | Child components to be rendered within the container [should be mapped to their AEM resourcetypes using **MapTo**](https://experienceleague.adobe.com/docs/experience-manager-learn/getting-started-with-aem-headless/spa-editor/react/map-components.html?lang=en). 57 | 58 | The OOTB ResponsiveGrid component maps to the resourceType _wcm/foundation/components/responsivegrid_ by default. 59 | 60 | ### Default SPA 61 | For default SPA on AEM, the component can be used as-is OOTB. 62 | 63 | ### Remote SPA 64 | When using the component directly within the app for remote SPA, two additional props _pagePath_ and _itemPath_ can be used to pass the path of the corresponding content on AEM. 65 | 66 | ```jsx 67 | 70 | ``` 71 | 72 | Here,the ResponsiveGrid component will render content of the layout container _/content/wknd-app/us/en/home/jcr:content/root/responsivegrid_ where _/content/wknd-app/us/en/home_ is the page on AEM and _root/responsivegrid_ the path to the item to be rendered within the page. 73 | 74 | #### Virtual Container 75 | The ResponsiveGrid component can still be used if content does not exist yet on AEM at the defined path. This will simply add an overlay on AEM for the author when opened for editing in AEM. More details are available [in the docs](https://experienceleague.adobe.com/docs/experience-manager-cloud-service/content/implementing/developing/hybrid/editing-external-spa.html?lang=en#virtual-containers). 76 | 77 | #### Custom class for ResponsiveGrid 78 | 79 | If a custom class name needs to be added to the OOTB ResponsiveGrid component, this can be done by passing in the class names as a string via the prop _customClassName_ 80 | 81 | ```jsx 82 | 86 | ``` 87 | 88 | 89 | ## Container 90 | 91 | For all container components other than ResponsiveGrid, you can either use the Container component as-is or use the helper method _getChildComponents()_ and then render them according to the markup requirements. 92 | 93 | ### Default SPA 94 | Lets say you want to use the WCM Core Container component, and based on user's selection it can either be a _ResponsiveGrid_ or a normal _Container_. 95 | 96 | Here is a sample snippet of how it can be acheived. 97 | 98 | ```jsx 99 | const MyContainerEditConfig = { 100 | emptyLabel: 'Container', 101 | isEmpty: function(props){ 102 | return !props.cqItemsOrder || !props.cqItemsOrder.length ===0 103 | } 104 | } 105 | 106 | const MyContainer = (props) => 107 | { 108 | const MyComponent = (props.layout!=="SIMPLE")?ResponsiveGrid:Container; 109 | return ; 110 | } 111 | 112 | MapTo('/wknd/components/content/container')(MyContainer, MyContainerEditConfig); 113 | ``` 114 | 115 | Now lets say you want to render a custom markup for your container, example - a component like Tabs which is also a container, then the sample snippet above can be refactored. 116 | 117 | ```jsx 118 | const MyTabsEditConfig = { 119 | emptyLabel: 'Tabs', 120 | isEmpty: function(props){ 121 | return !props.cqItemsOrder || !props.cqItemsOrder.length ===0 122 | } 123 | } 124 | 125 | const MyTabs = (props) => 126 | { 127 | const tabItemsInOrder = Container.getChildComponents(props); 128 | 129 | const tabItemsJsx = tabItemsInOrder.map((tabItemJsx) =>
{tabItemJsx}
); 130 | 131 | return
{tabItemsJsx}
; 132 | } 133 | 134 | MapTo('/wknd/components/content/tabs')(MyTabs, MyTabsEditConfig); 135 | ``` 136 | 137 | #### Migration to v2 138 | 139 | Before SPA 2.0, Container component is a class based component an it required the consumers to instantiate the class or use class based inheritance to ontain the child components e.g. this.childComponents in your custom implementation loaded all the child components as an array. Now this needs to be replaced with the helper method as shown in sample snippet above. 140 | 141 | ### Remote SPA 142 | 143 | ```jsx 144 | 147 | ``` 148 | 149 | For a custom container, you shall be able to adopt same guidelines followed for normal SPA. 150 | 151 | # Additional Features 152 | 153 | ### Prefetched Model 154 | 155 | If the model for rendering the component has already been fetched (for eg: in SSR), this can be passed into the component via a prop, so that the component doesn't need to fetch it again on the client side. 156 | 157 | ```jsx 158 | const App = ({ model }) => ( 159 | 163 | ); 164 | ``` 165 | 166 | ### Remove AEM grid styling 167 | AEM layouting styles are applied by default when using the ResponsiveGrid and Page components. If you would prefer to use your own custom layouting over the AEM authored layouts, an additional prop _removeDefaultStyles_ can be passed into the components. 168 | 169 | ```jsx 170 | 174 | ``` 175 | This will remove all styles specific to the AEM grid system and corresponsing DOM wrappers. 176 | 177 | ### Component Mapping 178 | 179 | Mapping child components to a container component can now be done via a prop instead of having to do _MapTo_ on initial load. 180 | 181 | If the container components has 2 child components, _Text_ and _Image_ of resource type _wknd/text_ and _wknd/image_ respectively, these can now be mapped to a grid component as below - 182 | 183 | ```jsx 184 | 191 | ``` 192 | 193 | Mapping of the resource type to the corresponding component will then be handled internally by the SPA SDK. 194 | 195 | ### Lazy Loading 196 | 197 | Child components can be lazy loaded to ensure that they are dynamically imported only when needed, thus reducing the amount of code on initial load. 198 | 199 | #### Prerequisites 200 | 201 | - The container component with the child components to be lazy loaded [should be within a _Suspense_ component with fallback content](https://reactjs.org/docs/code-splitting.html#reactlazy). 202 | - The component to be lazy loaded should be a default export. 203 | 204 | #### Using with RemotePage component 205 | 206 | To ensure the lazy loaded chunks are imported from the appropriate origin and not the AEM instance when the app is rendered for authoring in the AEM editor, [update the SPA to explicitly set the public path](https://webpack.js.org/guides/public-path/#on-the-fly) to the host URL of the SPA. 207 | 208 | #### MapTo 209 | 210 | ``` 211 | import Text from ./components/Text'; 212 | MapTo('wknd/text')(Text); 213 | ``` 214 | 215 | can be updated to lazy load the Text component on usage as below - 216 | 217 | ``` 218 | MapTo('wknd/text')(React.lazy(() => import('./components/Text'))); 219 | ``` 220 | 221 | #### _components_ Prop 222 | 223 | ```jsx 224 | 229 | ``` 230 | can be updated to lazy load the child components on usage as below - 231 | 232 | ```jsx 233 | import('./components/Text')) 237 | }} /> 238 | ``` -------------------------------------------------------------------------------- /src/components/ResponsiveGrid.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 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 | import React from 'react'; 13 | import { AuthoringUtils } from '@adobe/aem-spa-page-model-manager'; 14 | import { ComponentMapping } from '@adobe/aem-spa-component-mapping'; 15 | import { MapTo } from '../core/ComponentMapping'; 16 | import { EditableComponent } from '../core/EditableComponent'; 17 | import { Container } from './Container'; 18 | import AllowedComponentsContainer from './AllowedComponentsContainer'; 19 | import { ResponsiveGridProps } from '../types/AEMModel'; 20 | import { Config, MappedComponentProperties } from '../types/EditConfig'; 21 | import { ClassNames } from '../constants'; 22 | 23 | export type ResponsiveGridComponentProps = { 24 | title?: string; 25 | isInEditor: boolean; 26 | componentMapping?: typeof ComponentMapping; 27 | config?: Config; 28 | customClassName?: string; 29 | removeDefaultStyles?: boolean; 30 | } & ResponsiveGridProps; 31 | 32 | const RESOURCE_TYPE = 'wcm/foundation/components/responsivegrid'; 33 | 34 | const LayoutContainer = ({ 35 | title = 'Layout Container', 36 | columnClassNames, 37 | isInEditor, 38 | ...props 39 | }: ResponsiveGridComponentProps): JSX.Element => { 40 | const getItemClassNames = (itemKey: string) => { 41 | return columnClassNames && columnClassNames[itemKey] ? columnClassNames[itemKey] : ''; 42 | }; 43 | 44 | let className = props.customClassName || ''; 45 | if (isInEditor || !props.removeDefaultStyles) { 46 | className = `${className} ${props.gridClassNames || ''} ${ClassNames.CONTAINER}`; 47 | } 48 | 49 | const gridProps = { 50 | ...props, 51 | className, 52 | getItemClassNames, 53 | placeholderClassNames: ClassNames.RESPONSIVE_GRID_PLACEHOLDER_CLASS_NAMES, 54 | isInEditor, 55 | }; 56 | 57 | return props.allowedComponents?.applicable && isInEditor ? ( 58 | 59 | ) : ( 60 | 61 | ); 62 | }; 63 | 64 | export const ResponsiveGrid = ({ 65 | isInEditor = AuthoringUtils.isInEditor(), 66 | ...props 67 | }: ResponsiveGridComponentProps): JSX.Element => { 68 | const config = { 69 | isEmpty: (gridProps: ResponsiveGridProps): boolean => { 70 | return (gridProps.cqItemsOrder && gridProps.cqItemsOrder.length > 0) || false; 71 | }, 72 | resourceType: RESOURCE_TYPE, 73 | }; 74 | 75 | return ( 76 | 77 | 78 | 79 | ); 80 | }; 81 | 82 | MapTo(RESOURCE_TYPE)(ResponsiveGrid); 83 | -------------------------------------------------------------------------------- /src/constants/classnames.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 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 | * Constants for interacting with AEM components. 15 | */ 16 | export const ClassNames = Object.freeze({ 17 | /** 18 | * Default CSS Class names associated with a component. 19 | */ 20 | DEFAULT: 'appliedCssClassNames', 21 | /** 22 | * Class names associated with a new section component. 23 | */ 24 | NEW_SECTION: 'new section', 25 | /** 26 | * Class name used to denote aem-container root element. 27 | */ 28 | CONTAINER: 'aem-container', 29 | /** 30 | * Class name used to identify the placeholder used to represent an empty component. 31 | */ 32 | DEFAULT_PLACEHOLDER: 'cq-placeholder', 33 | PAGE: 'aem-page', 34 | RESPONSIVE_GRID_PLACEHOLDER: 'aem-Grid-newComponent', 35 | ALLOWED_LIST_PLACEHOLDER: 'aem-AllowedComponent--list', 36 | ALLOWED_COMPONENT_TITLE: 'aem-AllowedComponent--title', 37 | ALLOWED_COMPONENT_PLACEHOLDER: 'aem-AllowedComponent--component cq-placeholder placeholder', 38 | /** 39 | * Class name used to identify the placeholder used to represent an empty ResponsiveGrid component. 40 | */ 41 | RESPONSIVE_GRID_PLACEHOLDER_CLASS_NAMES: 'aem-Grid-newComponent', 42 | }); 43 | -------------------------------------------------------------------------------- /src/constants/events.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 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 | * Constants for interacting with AEM components. 15 | */ 16 | export const Events = Object.freeze({ 17 | /** 18 | * Event which indicates that content of remote component has been fetched and loaded in the app 19 | */ 20 | ASYNC_CONTENT_LOADED_EVENT: 'cq-async-content-loaded', 21 | }); 22 | -------------------------------------------------------------------------------- /src/constants/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 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 { Properties } from './properties'; 14 | import { ClassNames } from './classnames'; 15 | import { Events } from './events'; 16 | import { Texts } from './texts'; 17 | 18 | export { Properties, ClassNames, Events, Texts }; 19 | -------------------------------------------------------------------------------- /src/constants/properties.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 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 | import { Constants } from '@adobe/aem-spa-page-model-manager'; 13 | 14 | /** 15 | * Constants for interacting with AEM components. 16 | */ 17 | export const Properties = Object.freeze({ 18 | /** 19 | * Name of the data-cq-data-path data attribute. 20 | */ 21 | DATA_PATH_ATTR: 'data-cq-data-path', 22 | /** 23 | * Name of the data-resource-type data attribute. 24 | */ 25 | DATA_CQ_RESOURCE_TYPE_ATTR: 'data-cq-resource-type', 26 | /** 27 | * Event which indicates that content of remote component has been fetched and loaded in the app 28 | */ 29 | ASYNC_CONTENT_LOADED_EVENT: 'cq-async-content-loaded', 30 | /** 31 | * Selector for WCM mode state meta property. 32 | */ 33 | _WCM_MODE_META_SELECTOR: 'meta[property="cq:wcmmode"]', 34 | 35 | TYPE: Constants.TYPE_PROP, 36 | ITEMS: Constants.ITEMS_PROP, 37 | ITEMS_ORDER: Constants.ITEMS_ORDER_PROP, 38 | PATH: Constants.PATH_PROP, 39 | CHILDREN: Constants.CHILDREN_PROP, 40 | HIERARCHY_TYPE: Constants.HIERARCHY_TYPE_PROP, 41 | JCR_CONTENT: Constants.JCR_CONTENT, 42 | }); 43 | -------------------------------------------------------------------------------- /src/constants/texts.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 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 | * Constants for interacting with AEM components. 15 | */ 16 | export const Texts = Object.freeze({ 17 | /** 18 | * The label to be displayed when no components are allowed in AllowedComponentsContainer 19 | */ 20 | EMPTY_LABEL: 'No allowed components', 21 | EMPTY_COMPONENT_TITLE: '–', 22 | }); 23 | -------------------------------------------------------------------------------- /src/core/ComponentMapping.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 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 { ComponentType } from 'react'; 14 | import { ComponentMapping } from '@adobe/aem-spa-component-mapping'; 15 | import { MappedComponentProperties } from '../types/EditConfig'; 16 | 17 | /** 18 | * @private 19 | */ 20 | const wrappedMapFct = ComponentMapping.map; 21 | /** 22 | * @private 23 | */ 24 | const wrappedGetFct = ComponentMapping.get; 25 | 26 | /** 27 | * Map a React component with the given resource types. 28 | * If an {@link EditConfig} is provided the component is wrapped to provide editing capabilities on the AEM Page Editor 29 | * 30 | * @param resourceTypes List of resource types for which to use the given component. 31 | * @param component React representation for the given resource types. 32 | * @param editConfig Configuration object for enabling the edition capabilities. 33 | * @param config Model configuration object. 34 | * @returns The resulting decorated Component 35 | */ 36 | ComponentMapping.map = function map

( 37 | resourceTypes: string | string[], 38 | component: ComponentType

, 39 | ) { 40 | wrappedMapFct.call(ComponentMapping, resourceTypes, component); 41 | return component; 42 | }; 43 | 44 | ComponentMapping.get = wrappedGetFct; 45 | 46 | /** 47 | * @private 48 | */ 49 | type MapperFunction

= (_component: ComponentType

) => ComponentType

; 50 | 51 | const MapTo =

(resourceTypes: string | string[]): MapperFunction

=> { 52 | const mapper = (component: ComponentType

) => ComponentMapping.map(resourceTypes, component); 53 | 54 | return mapper as MapperFunction

; 55 | }; 56 | 57 | export { ComponentMapping, MapTo }; 58 | -------------------------------------------------------------------------------- /src/core/EditableComponent.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 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 | import React from 'react'; 13 | import { Properties } from '../constants'; 14 | import { Utils } from '../utils/Utils'; 15 | import { AuthoringUtils, ModelManager } from '@adobe/aem-spa-page-model-manager'; 16 | import { ModelProps } from '../types/AEMModel'; 17 | import { Config, MappedComponentProperties } from '../types/EditConfig'; 18 | import { useEditor } from '../hooks/useEditor'; 19 | import Placeholder from '../components/Placeholder'; 20 | 21 | export type EditableComponentProps = { 22 | config?: Config; 23 | children?: React.ReactNode; 24 | className?: string; 25 | appliedCssClassNames?: string; 26 | containerProps?: { className?: string }; 27 | model?: ModelProps; 28 | pagePath?: string; 29 | itemPath?: string; 30 | removeDefaultStyles?: boolean; 31 | } & MappedComponentProperties; 32 | 33 | const addPropsToComponent = (component: React.ReactNode, props: MappedComponentProperties) => { 34 | if (React.isValidElement(component)) { 35 | return React.cloneElement(component, props); 36 | } 37 | return component; 38 | }; 39 | 40 | export const EditableComponent = (editableProps: EditableComponentProps): JSX.Element => { 41 | const { 42 | config = { isEmpty: () => false }, 43 | children, 44 | model: userModel, 45 | cqPath, 46 | pagePath, 47 | itemPath, 48 | isInEditor = AuthoringUtils.isInEditor(), 49 | className = '', 50 | ...props 51 | } = editableProps; 52 | const { updateModel } = useEditor(); 53 | const { forceReload, resourceType = '' } = config || {}; 54 | const path = cqPath || Utils.getCQPath({ cqPath, pagePath, itemPath }); 55 | const [model, setModel] = React.useState(() => userModel || {}); 56 | 57 | React.useEffect(() => { 58 | setModel(userModel ?? {}); 59 | }, [path, userModel]); 60 | 61 | React.useEffect(() => { 62 | const renderContent = () => updateModel({ path, forceReload, setModel, isInEditor, pagePath }); 63 | !Object.keys(model)?.length && renderContent(); 64 | ModelManager.addListener(path, renderContent); 65 | return () => { 66 | ModelManager.removeListener(path, renderContent); 67 | }; 68 | }, [path, model, updateModel, isInEditor, pagePath, forceReload]); 69 | 70 | if (!editableProps) return <>; 71 | 72 | const componentProps = { 73 | cqPath: path, 74 | ...model, 75 | }; 76 | 77 | const dataAttr = 78 | (isInEditor && { 79 | [Properties.DATA_PATH_ATTR]: path, 80 | [Properties.DATA_CQ_RESOURCE_TYPE_ATTR]: resourceType, 81 | }) || 82 | {}; 83 | 84 | const { appliedCssClassNames = '' } = model; 85 | const componentClassName = `${className} ${props.containerProps?.className || ''} ${appliedCssClassNames}`.trim(); 86 | const updatedComponent = addPropsToComponent(children, pagePath ? componentProps : model); 87 | return isInEditor || (!props.removeDefaultStyles && componentClassName) ? ( 88 |

89 | {updatedComponent} 90 | {isInEditor && } 91 |
92 | ) : ( 93 | <>{updatedComponent} 94 | ); 95 | }; 96 | -------------------------------------------------------------------------------- /src/core/README.md: -------------------------------------------------------------------------------- 1 | # Integration with AEM 2 | 3 | ## EditableComponent 4 | 5 | Wrapper component to enable AEM authoring and content fetch for a React component. 6 | 7 | ### Sample 8 | 9 | Existing simple React component: 10 | 11 | ``` 12 | const Text = (props) =>
{props.text}
13 | ``` 14 | 15 | To make this editable on AEM and fetch content to be rendered on AEM : 16 | 17 | 1. Create a config object as illustrated [in the wknd sample](https://github.com/adobe/aem-guides-wknd-spa/blob/React/latest/ui.frontend/src/components/Text/Text.js#L29). 18 | 19 | ``` 20 | const TextEditConfig = { 21 | emptyLabel: 'Text', 22 | isEmpty: () => {}, 23 | resourceType: "wknd-app/components/text" 24 | }; 25 | ``` 26 | where _emptyLabel_ is the label to be displayed for empty overlay in AEM, _isEmpty_ the method to check if no content is present and empty overlay is needed, and _resourceType_ the resourcetype of the component on AEM. 27 | 28 | _resourceType_ in config is essential for supporting [virtual component](https://experienceleague.adobe.com/docs/experience-manager-cloud-service/content/implementing/developing/hybrid/editing-external-spa.html?lang=en#virtual-leaf-components) usecases. 29 | 30 | 31 | 2. Create an editable version of the component using the _EditableComponent_ wrapper and passing in the config. 32 | 33 | ``` 34 | export const AEMText = (props) => ( 35 | 38 | 39 | 40 | ); 41 | ``` 42 | 43 | 44 | 3. Use this component - 45 | - By passing in the appropriate props if using it as a standalone component within your SPA 46 | 47 | ``` 48 | ; 24 | }; 25 | 26 | export const useEditor = () => { 27 | const updateModel = useCallback(async ({ path, forceReload, pagePath, setModel, isInEditor }: Props) => { 28 | const model = await fetchModel({ cqPath: path, forceReload, pagePath }).catch((err) => console.error(err)); 29 | if (model && Object.keys(model).length) { 30 | setModel(model); 31 | if (isInEditor) { 32 | PathUtils.dispatchGlobalCustomEvent(Events.ASYNC_CONTENT_LOADED_EVENT, {}); 33 | } 34 | } 35 | }, []); 36 | 37 | return { updateModel }; 38 | }; 39 | -------------------------------------------------------------------------------- /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 | export { MapTo } from './core/ComponentMapping'; 13 | export * from './components/Container'; 14 | export * from './core/EditableComponent'; 15 | export * from './components/Page'; 16 | export * from './components/ResponsiveGrid'; 17 | export * from './api/fetchModel'; 18 | 19 | export * from './types/AEMModel'; 20 | export * from './types/EditConfig'; 21 | -------------------------------------------------------------------------------- /src/types/AEMModel.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 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 | import { Model } from '@adobe/aem-spa-page-model-manager'; 13 | 14 | /** 15 | * Component that is allowed to be used on the page by the editor. 16 | */ 17 | export type AllowedComponent = { 18 | path: string; 19 | title: string; 20 | }; 21 | 22 | export type AllowedComponentList = { 23 | /** 24 | * Should AllowedComponents list be applied. 25 | */ 26 | applicable: boolean; 27 | components: AllowedComponent[]; 28 | }; 29 | 30 | export interface PageModel extends Model { 31 | ':type': string; 32 | id: string; 33 | ':path': string; 34 | ':children'?: { [key: string]: PageModel }; 35 | } 36 | 37 | export type ModelProps = { 38 | cqPath?: string; 39 | cqItems?: { [key: string]: Model }; 40 | cqItemsOrder?: string[]; 41 | cqType?: string; 42 | cqChildren?: { [key: string]: PageModel }; 43 | appliedCssClassNames?: string; 44 | }; 45 | 46 | export type ResponsiveGridProps = { 47 | gridClassNames: string; 48 | columnClassNames: { [key: string]: string }; 49 | allowedComponents: AllowedComponentList; 50 | columnCount?: string; 51 | } & ModelProps; 52 | -------------------------------------------------------------------------------- /src/types/EditConfig.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 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 | * Hold force reload state. 15 | */ 16 | export interface ReloadForceAble { 17 | /* 18 | * Should the model cache be ignored when processing the component. 19 | */ 20 | cqForceReload?: boolean; 21 | } 22 | 23 | /** 24 | * Properties given to every component runtime by the SPA editor. 25 | */ 26 | export interface MappedComponentProperties extends ReloadForceAble { 27 | isInEditor?: boolean; 28 | cqPath?: string; 29 | appliedCssClassNames?: string; 30 | aemNoDecoration?: boolean; 31 | } 32 | 33 | export interface Config

{ 34 | emptyLabel?: string; 35 | isEmpty(_props: P): boolean; 36 | resourceType?: string; 37 | forceReload?: boolean; 38 | } 39 | -------------------------------------------------------------------------------- /src/utils/Utils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 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 { sanitizeUrl } from '@braintree/sanitize-url'; 14 | import { Model } from '@adobe/aem-spa-page-model-manager'; 15 | import { ModelProps } from '../types/AEMModel'; 16 | 17 | interface ComponentProps { 18 | pagePath?: string; 19 | itemPath?: string; 20 | cqPath?: string; 21 | } 22 | 23 | /** 24 | * Transformation of internal properties namespaced with [:] to [cq] 25 | * :myProperty => cqMyProperty 26 | * @param propKey 27 | */ 28 | function transformToCQ(propKey: string) { 29 | const tempKey = propKey.substring(1); 30 | 31 | return 'cq' + tempKey.substring(0, 1).toUpperCase() + tempKey.substring(1); 32 | } 33 | 34 | /** 35 | * Helper functions for interacting with the AEM environment. 36 | */ 37 | const Utils = { 38 | /** 39 | * Transforms the item data to component properties. 40 | * It will replace ':' with 'cq' and convert the name to CameCase. 41 | * 42 | * @param item - the item data 43 | * @returns the transformed data 44 | */ 45 | modelToProps(item: Model): ModelProps { 46 | if (!item || !Object.keys(item).length) { 47 | return { cqPath: '' }; 48 | } 49 | 50 | const keys = Object.getOwnPropertyNames(item); 51 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 52 | const props: any = {}; 53 | 54 | keys.forEach((key: string) => { 55 | const propKey = (key.startsWith(':') ? transformToCQ(key) : key) as keyof ModelProps; 56 | props[propKey] = item[key as keyof Model] || ''; 57 | }); 58 | 59 | return props; 60 | }, 61 | 62 | /** 63 | * Determines the cqPath of a component given its props 64 | * 65 | * @returns cqPath of the component 66 | */ 67 | getCQPath(componentProps: ComponentProps): string { 68 | const { pagePath = '', itemPath = '', cqPath = '' } = componentProps; 69 | if (pagePath && !cqPath) { 70 | const path = sanitizeUrl(itemPath ? `${pagePath}/jcr:content/${itemPath}` : pagePath); 71 | return path.replace(/\/+/g, '/').replace(/\/$/, ''); 72 | } 73 | return cqPath; 74 | }, 75 | }; 76 | 77 | export { Utils }; 78 | -------------------------------------------------------------------------------- /test/ComponentMapping.test.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 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 React from 'react'; 14 | import { ComponentMapping, MapTo } from '../src/core/ComponentMapping'; 15 | 16 | describe('ComponentMapping', () => { 17 | const COMPONENT_RESOURCE_TYPE = 'test/component/resource/type'; 18 | 19 | const TestComponent = () =>

; 20 | 21 | it('should store and retrieve component', () => { 22 | const WrappedReturnType = MapTo(COMPONENT_RESOURCE_TYPE)(TestComponent); 23 | 24 | const WrappedComponentFromGet = ComponentMapping.get(COMPONENT_RESOURCE_TYPE); 25 | 26 | expect(WrappedComponentFromGet).toBeDefined(); 27 | expect(WrappedReturnType).toBeDefined(); 28 | 29 | expect(WrappedReturnType).toBe(WrappedComponentFromGet); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /test/EditableComponent.test.tsx: -------------------------------------------------------------------------------- 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 React from 'react'; 14 | import { act } from 'react-dom/test-utils'; 15 | import { waitFor, render, screen } from '@testing-library/react'; 16 | import { ModelManager, PathUtils } from '@adobe/aem-spa-page-model-manager'; 17 | import { EditableComponent } from '../src/core/EditableComponent'; 18 | import { ClassNames, Events } from '../src/constants'; 19 | 20 | describe('EditableComponent ->', () => { 21 | const COMPONENT_RESOURCE_TYPE = '/component/resource/type'; 22 | const COMPONENT_PATH = '/path/to/component'; 23 | const CHILD_COMPONENT_CLASS_NAME = 'child-class'; 24 | const CHILD_COMPONENT_APPLIED_STYLE_CLASS_NAME = 'my_custom_style'; 25 | const IN_EDITOR_CLASS_NAME = 'in-editor-class'; 26 | const GRID_CLASS_NAME = 'aem-grid-column-x'; 27 | const EMPTY_LABEL = 'Empty Label'; 28 | const EMPTY_TEXT_SELECTOR = '[data-emptytext="' + EMPTY_LABEL + '"]'; 29 | const INNER_COMPONENT_ID = 'innerContent'; 30 | const TEST_COMPONENT_MODEL = { ':type': 'test/components/componentchild' }; 31 | 32 | const CQ_PROPS = { 33 | cqType: COMPONENT_RESOURCE_TYPE, 34 | cqPath: COMPONENT_PATH, 35 | model: { 36 | appliedCssClassNames: CHILD_COMPONENT_APPLIED_STYLE_CLASS_NAME, 37 | }, 38 | containerProps: { 39 | className: GRID_CLASS_NAME, 40 | }, 41 | }; 42 | 43 | let addListenerSpy: jest.SpyInstance; 44 | let removeListenerSpy: jest.SpyInstance; 45 | let getDataSpy: jest.SpyInstance; 46 | 47 | const ChildComponent = ({ className = '' }) => { 48 | return
; 49 | }; 50 | 51 | beforeAll(() => { 52 | jest.useFakeTimers(); 53 | }); 54 | 55 | beforeEach(() => { 56 | getDataSpy = jest.spyOn(ModelManager, 'getData').mockResolvedValue({}); 57 | addListenerSpy = jest.spyOn(ModelManager, 'addListener').mockImplementation(); 58 | removeListenerSpy = jest.spyOn(ModelManager, 'removeListener').mockImplementation(); 59 | }); 60 | 61 | afterEach(() => { 62 | getDataSpy.mockReset(); 63 | addListenerSpy.mockReset(); 64 | removeListenerSpy.mockReset(); 65 | }); 66 | 67 | afterAll(() => { 68 | jest.useRealTimers(); 69 | }); 70 | 71 | type Props = { 72 | cqType: string; 73 | cqPath: string; 74 | model?: { 75 | appliedCssClassNames?: string; 76 | }; 77 | containerProps: { 78 | className?: string; 79 | }; 80 | }; 81 | 82 | const createEditableComponent = (config, isInEditor = true, props: Props = CQ_PROPS) => { 83 | const editorClassNames = isInEditor ? IN_EDITOR_CLASS_NAME : ''; 84 | return ( 85 | 86 | 87 | 88 | ); 89 | }; 90 | 91 | describe('Component emptiness ->', () => { 92 | it('should declare the component to be empty', () => { 93 | const EDIT_CONFIG = { 94 | isEmpty: function () { 95 | return true; 96 | }, 97 | emptyLabel: EMPTY_LABEL, 98 | }; 99 | act(() => { 100 | render(createEditableComponent(EDIT_CONFIG)); 101 | }); 102 | 103 | const node = screen.getByTestId('childComponent').parentElement; 104 | expect(node.querySelector('.' + ClassNames.DEFAULT_PLACEHOLDER + EMPTY_TEXT_SELECTOR)).toBeTruthy(); 105 | expect(node).not.toBeNull(); 106 | }); 107 | 108 | it('should declare the component to be empty without providing a label', () => { 109 | const EDIT_CONFIG = { 110 | isEmpty: function () { 111 | return true; 112 | }, 113 | }; 114 | act(() => { 115 | render(createEditableComponent(EDIT_CONFIG)); 116 | }); 117 | 118 | const node = screen.getByTestId('childComponent').parentElement; 119 | expect(node.querySelector(EMPTY_TEXT_SELECTOR)).toBeFalsy(); 120 | expect(node.querySelector('.' + ClassNames.DEFAULT_PLACEHOLDER)).toBeTruthy(); 121 | }); 122 | 123 | it('should declare the component as not being in the editor', () => { 124 | const EDIT_CONFIG = { isEmpty: () => true }; 125 | act(() => { 126 | render(createEditableComponent(EDIT_CONFIG, false)); 127 | }); 128 | const node = screen.getByTestId('childComponent').parentElement; 129 | expect(node.querySelector(EMPTY_TEXT_SELECTOR)).toBeFalsy(); 130 | expect(node.querySelector('.' + ClassNames.DEFAULT_PLACEHOLDER)).toBeFalsy(); 131 | }); 132 | 133 | it('should declare the component not to be empty', () => { 134 | const EDIT_CONFIG = { 135 | isEmpty: function () { 136 | return false; 137 | }, 138 | emptyLabel: EMPTY_LABEL, 139 | }; 140 | act(() => { 141 | render(createEditableComponent(EDIT_CONFIG)); 142 | }); 143 | const node = screen.getByTestId('childComponent'); 144 | expect(node).toBeDefined(); 145 | expect(node.parentElement.querySelector('.' + ClassNames.DEFAULT_PLACEHOLDER)).toBeFalsy(); 146 | }); 147 | }); 148 | 149 | describe('resouceType attribute ->', () => { 150 | it('should have the data-cq-resource-type attribute set when passing this via the Editconfig', () => { 151 | const EDIT_CONFIG = { 152 | isEmpty: function () { 153 | return false; 154 | }, 155 | emptyLabel: EMPTY_LABEL, 156 | resourceType: COMPONENT_RESOURCE_TYPE, 157 | }; 158 | act(() => { 159 | render(createEditableComponent(EDIT_CONFIG)); 160 | }); 161 | const node = screen.getByTestId('childComponent').parentElement; 162 | expect(node.dataset.cqResourceType).toEqual(COMPONENT_RESOURCE_TYPE); 163 | }); 164 | 165 | it('should NOT have the data-cq-resource-type attribute set when NOT passing it via the Editconfig', () => { 166 | const EDIT_CONFIG = { 167 | isEmpty: function () { 168 | return false; 169 | }, 170 | emptyLabel: EMPTY_LABEL, 171 | }; 172 | act(() => { 173 | render(createEditableComponent(EDIT_CONFIG)); 174 | }); 175 | const node = screen.getByTestId('childComponent').parentElement; 176 | expect(node.dataset.cqResourceType).toEqual(''); 177 | }); 178 | }); 179 | 180 | describe('resouceType attribute (className) ->', () => { 181 | it('should have the className attribute containing appliedCssClasses value appended/set to pre-existing className if any set', () => { 182 | const EDIT_CONFIG = { 183 | isEmpty: function () { 184 | return false; 185 | }, 186 | emptyLabel: EMPTY_LABEL, 187 | resourceType: COMPONENT_RESOURCE_TYPE, 188 | }; 189 | act(() => { 190 | render(createEditableComponent(EDIT_CONFIG)); 191 | }); 192 | const node = screen.getByTestId('childComponent').parentElement; 193 | console.error(node?.classList); 194 | expect(node.classList.contains(CQ_PROPS.model?.appliedCssClassNames)).toBeTruthy(); 195 | }); 196 | 197 | it('should not have any custom CSS classes if appliedCssClasses is empty or not set', () => { 198 | const EDIT_CONFIG = { 199 | isEmpty: function () { 200 | return false; 201 | }, 202 | emptyLabel: EMPTY_LABEL, 203 | resourceType: COMPONENT_RESOURCE_TYPE, 204 | }; 205 | const { 206 | model: { appliedCssClassNames }, 207 | ...otherCQProps 208 | } = CQ_PROPS; 209 | act(() => { 210 | render(createEditableComponent(EDIT_CONFIG, true, otherCQProps)); 211 | }); 212 | const node = screen.getByTestId('childComponent').parentElement; 213 | expect(node.classList.contains(appliedCssClassNames)).toBeFalsy(); 214 | }); 215 | 216 | it('if aem grid column styles set, appliedCssClassNames should not override the grid styles', () => { 217 | const EDIT_CONFIG = { 218 | isEmpty: function () { 219 | return false; 220 | }, 221 | emptyLabel: EMPTY_LABEL, 222 | resourceType: COMPONENT_RESOURCE_TYPE, 223 | }; 224 | act(() => { 225 | render(createEditableComponent(EDIT_CONFIG)); 226 | }); 227 | const node = screen.getByTestId('childComponent').parentElement; 228 | expect(node.classList.contains(GRID_CLASS_NAME)).toBeTruthy(); 229 | }); 230 | }); 231 | 232 | describe('Model events ->', () => { 233 | it('should initialize properly without parameter', () => { 234 | act(() => { 235 | render( 236 | 237 | 238 | , 239 | ); 240 | }); 241 | expect(addListenerSpy).toHaveBeenCalledWith('', expect.any(Function)); 242 | const node = screen.getByTestId('childComponent'); 243 | expect(node).toBeDefined(); 244 | }); 245 | 246 | it('should render components with a cqpath parameter', () => { 247 | act(() => { 248 | render( 249 | 250 | 251 | , 252 | ); 253 | }); 254 | expect(addListenerSpy).toHaveBeenCalledWith(COMPONENT_PATH, expect.any(Function)); 255 | expect(getDataSpy).toHaveBeenCalledWith({ path: COMPONENT_PATH, forceReload: false }); 256 | const node = screen.getByTestId('childComponent'); 257 | expect(node).toBeDefined(); 258 | }); 259 | 260 | it('should render components with a pagepath parameter', () => { 261 | act(() => { 262 | render( 263 | 264 | 265 | , 266 | ); 267 | }); 268 | expect(addListenerSpy).toHaveBeenCalledWith(COMPONENT_PATH, expect.any(Function)); 269 | expect(getDataSpy).toHaveBeenCalledWith({ path: COMPONENT_PATH, forceReload: false }); 270 | const node = screen.getByTestId('childComponent'); 271 | expect(node).toBeDefined(); 272 | }); 273 | 274 | it('should render components when pagepath and path to item is provided', () => { 275 | const PAGE_PATH = '/page/subpage'; 276 | const ITEM_PATH = 'root/paragraph'; 277 | const PATH = `${PAGE_PATH}/jcr:content/${ITEM_PATH}`; 278 | 279 | act(() => { 280 | render( 281 | 282 | 283 | , 284 | ); 285 | }); 286 | 287 | expect(addListenerSpy).toHaveBeenCalledWith(PATH, expect.any(Function)); 288 | expect(getDataSpy).toHaveBeenCalledWith({ path: PATH, forceReload: false }); 289 | const node = screen.getByTestId('childComponent'); 290 | expect(node).toBeDefined(); 291 | }); 292 | 293 | it('should fire event to reload editables when in editor for remote app', async () => { 294 | const dispatchEventSpy: jest.SpyInstance = jest 295 | .spyOn(PathUtils, 'dispatchGlobalCustomEvent') 296 | .mockImplementation(); 297 | getDataSpy = jest.spyOn(ModelManager, 'getData').mockResolvedValue(TEST_COMPONENT_MODEL); 298 | act(() => { 299 | render( 300 | 301 | 302 | , 303 | ); 304 | }); 305 | 306 | expect(getDataSpy).toHaveBeenCalledWith({ path: COMPONENT_PATH, forceReload: false }); 307 | const node = screen.getByTestId('childComponent'); 308 | expect(node).toBeDefined(); 309 | await waitFor(() => expect(dispatchEventSpy).toHaveBeenCalledWith(Events.ASYNC_CONTENT_LOADED_EVENT, {})); 310 | dispatchEventSpy.mockReset(); 311 | }); 312 | it('should log error when there is no data', async () => { 313 | const error = new Error('404 - Not found'); 314 | getDataSpy = jest.spyOn(ModelManager, 'getData').mockImplementation(() => { 315 | throw error; 316 | }); 317 | // eslint-disable-next-line @typescript-eslint/no-empty-function 318 | const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); 319 | 320 | act(() => { 321 | render( 322 | 323 | 324 | , 325 | ); 326 | }); 327 | await waitFor(() => expect(consoleSpy).toHaveBeenCalledWith(error)); 328 | consoleSpy.mockRestore(); 329 | }); 330 | it('should remove listeners on unmount', () => { 331 | act(() => { 332 | const { unmount } = render( 333 | 334 | 335 | , 336 | ); 337 | unmount(); 338 | }); 339 | expect(removeListenerSpy).toHaveBeenCalledWith(COMPONENT_PATH, expect.any(Function)); 340 | }); 341 | }); 342 | 343 | it('should render components even if the children are not valid React elements', () => { 344 | const isElementValidSpy = jest.spyOn(React, 'isValidElement').mockReturnValue(false); 345 | act(() => { 346 | render( 347 | 348 | 349 | , 350 | ); 351 | }); 352 | expect(addListenerSpy).toHaveBeenCalledWith(COMPONENT_PATH, expect.any(Function)); 353 | expect(getDataSpy).toHaveBeenCalledWith({ path: COMPONENT_PATH, forceReload: false }); 354 | const node = screen.getByTestId('childComponent'); 355 | expect(node).toBeDefined(); 356 | isElementValidSpy.mockReset(); 357 | }); 358 | }); 359 | -------------------------------------------------------------------------------- /test/api/fetchModel.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 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, ModelManager } from '@adobe/aem-spa-page-model-manager'; 14 | import { fetchModel } from '../../src/api/fetchModel'; 15 | import { waitFor } from '@testing-library/react'; 16 | 17 | describe('fetchModel ->', () => { 18 | const MODEL = { data: 'testModel' } as Model; 19 | const MODEL_OK_RESPONSE = { data: 'testModel', ok: true, json: () => ' ' } as Model; 20 | const CQ_PATH = 'testPage/jcr:content/componentPath'; 21 | 22 | let getDataSpy: jest.SpyInstance; 23 | 24 | beforeEach(() => { 25 | getDataSpy = jest.spyOn(ModelManager, 'getData').mockResolvedValue(MODEL); 26 | }); 27 | 28 | afterEach(() => { 29 | getDataSpy.mockReset(); 30 | }); 31 | it('should fetch model using modelmanager for the given cqPath', async () => { 32 | const response = await fetchModel({ cqPath: CQ_PATH }); 33 | expect(getDataSpy).toHaveBeenCalledWith(expect.objectContaining({ path: CQ_PATH })); 34 | expect(response).toEqual(MODEL); 35 | }); 36 | 37 | it('should fetch model using modelmanager for the given page and item paths', async () => { 38 | const response = await fetchModel({ pagePath: 'testPage', itemPath: 'componentPath' }); 39 | expect(getDataSpy).toHaveBeenCalledWith(expect.objectContaining({ path: CQ_PATH })); 40 | expect(response).toEqual(MODEL); 41 | }); 42 | 43 | it('should fetch model using fetch api if remote host if provided', async () => { 44 | global.fetch = jest.fn().mockImplementation(() => MODEL_OK_RESPONSE as Response); 45 | fetchModel({ cqPath: CQ_PATH, host: 'test.com/', options: {} }); 46 | expect(fetch).toHaveBeenCalledWith(`test.com/${CQ_PATH}.model.json`, {}); 47 | delete global.fetch; 48 | }); 49 | 50 | it('should log error when there is no remote host and no data', async () => { 51 | const error = new Error('404 - Not found'); 52 | getDataSpy = jest.spyOn(ModelManager, 'getData').mockRejectedValue(error); 53 | // eslint-disable-next-line @typescript-eslint/no-empty-function 54 | const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); 55 | await fetchModel({ cqPath: CQ_PATH }); 56 | await waitFor(() => expect(consoleSpy).toHaveBeenCalledWith(error)); 57 | consoleSpy.mockRestore(); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /test/components/AllowedComponentsContainer.test.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 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 React from 'react'; 14 | import { render, screen } from '@testing-library/react'; 15 | import AllowedComponentsContainer from '../../src/components/AllowedComponentsContainer'; 16 | import { AllowedComponentList } from '../../src/types/AEMModel'; 17 | import { Texts } from '../../src/constants'; 18 | 19 | describe('AllowedComponentsContainer ->', () => { 20 | const DEFAULT_TITLE = 'Layout Container'; 21 | const ALLOWED_PLACEHOLDER_SELECTOR = '.aem-AllowedComponent--list'; 22 | const ALLOWED_COMPONENT_TITLE_SELECTOR = '.aem-AllowedComponent--title'; 23 | const ALLOWED_COMPONENT_PLACEHOLDER_SELECTOR = '.aem-AllowedComponent--component.cq-placeholder.placeholder'; 24 | const COMPONENT_TEXT_PATH = '/content/page/jcr:content/root/text'; 25 | const COMPONENT_TEXT_TITLE = 'Text'; 26 | const COMPONENT_IMAGE_PATH = '/content/page/jcr:content/root/image'; 27 | const COMPONENT_IMAGE_TITLE = 'Image'; 28 | 29 | const ALLOWED_COMPONENTS_EMPTY_DATA: AllowedComponentList = { 30 | applicable: true, 31 | components: [], 32 | }; 33 | 34 | const ALLOWED_COMPONENTS_DATA: AllowedComponentList = { 35 | applicable: true, 36 | components: [ 37 | { 38 | path: COMPONENT_TEXT_PATH, 39 | title: COMPONENT_TEXT_TITLE, 40 | }, 41 | { 42 | path: COMPONENT_IMAGE_PATH, 43 | title: COMPONENT_IMAGE_TITLE, 44 | }, 45 | ], 46 | }; 47 | 48 | function generateAllowedComponentsContainer(allowedComponents: AllowedComponentList, title?: string): JSX.Element { 49 | const props = { 50 | title: title || '', 51 | allowedComponents: allowedComponents, 52 | cqPath: '', 53 | }; 54 | return ( 55 |
56 | 57 |
58 | ); 59 | } 60 | 61 | describe('applicable ->', () => { 62 | it('should be applicable with an empty list of allowed components', () => { 63 | render(generateAllowedComponentsContainer(ALLOWED_COMPONENTS_EMPTY_DATA)); 64 | const node = screen.getByTestId('testcontainer'); 65 | const allowedComponentsContainer = node.querySelector(ALLOWED_PLACEHOLDER_SELECTOR); 66 | expect(allowedComponentsContainer).toBeDefined(); 67 | const allowedComponentsTitle = allowedComponentsContainer.querySelector( 68 | ALLOWED_COMPONENT_TITLE_SELECTOR, 69 | ) as HTMLElement; 70 | expect(allowedComponentsTitle).toBeDefined(); 71 | expect(allowedComponentsTitle.dataset.text).toEqual('No allowed components'); 72 | expect(allowedComponentsContainer.querySelector(ALLOWED_COMPONENT_PLACEHOLDER_SELECTOR)).toBeFalsy(); 73 | }); 74 | 75 | it('should be applicable with a list of allowed components', () => { 76 | render(generateAllowedComponentsContainer(ALLOWED_COMPONENTS_DATA, DEFAULT_TITLE)); 77 | const node = screen.getByTestId('testcontainer'); 78 | const allowedComponentsContainer = node.querySelector(ALLOWED_PLACEHOLDER_SELECTOR); 79 | expect(allowedComponentsContainer).toBeDefined(); 80 | const allowedComponentsTitle = allowedComponentsContainer.querySelector( 81 | ALLOWED_COMPONENT_TITLE_SELECTOR, 82 | ) as HTMLElement; 83 | expect(allowedComponentsTitle).toBeDefined(); 84 | expect(allowedComponentsTitle.dataset.text).toEqual(DEFAULT_TITLE); 85 | expect(allowedComponentsContainer.querySelectorAll(ALLOWED_COMPONENT_PLACEHOLDER_SELECTOR).length).toEqual(2); 86 | }); 87 | }); 88 | 89 | describe('AllowedComponentPlaceholder ->', () => { 90 | const renderAllowedComponentPlaceholder = ({ title = null, path = '' }) => { 91 | const allowedComponents: AllowedComponentList = { 92 | applicable: true, 93 | components: [ 94 | { 95 | path, 96 | title, 97 | }, 98 | ], 99 | }; 100 | render(generateAllowedComponentsContainer(allowedComponents)); 101 | const node = screen.getByTestId('testcontainer'); 102 | return node.querySelector(ALLOWED_COMPONENT_PLACEHOLDER_SELECTOR) as HTMLElement; 103 | }; 104 | 105 | it('should contain title', () => { 106 | const placeholder = renderAllowedComponentPlaceholder({ title: COMPONENT_TEXT_TITLE }); 107 | expect(placeholder.dataset.emptytext).toEqual(COMPONENT_TEXT_TITLE); 108 | }); 109 | 110 | it('should contain empty title when none provided', () => { 111 | const placeholder = renderAllowedComponentPlaceholder({}); 112 | expect(placeholder.dataset.emptytext).toEqual(Texts.EMPTY_COMPONENT_TITLE); 113 | }); 114 | 115 | it('should contain path', () => { 116 | const placeholder = renderAllowedComponentPlaceholder({ path: COMPONENT_TEXT_PATH }); 117 | expect(placeholder.dataset.cqDataPath).toEqual(COMPONENT_TEXT_PATH); 118 | }); 119 | }); 120 | }); 121 | -------------------------------------------------------------------------------- /test/components/Container.test.tsx: -------------------------------------------------------------------------------- 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 React, { ReactElement } from 'react'; 14 | import { render, screen } from '@testing-library/react'; 15 | import { ComponentMapping } from '../../src/core/ComponentMapping'; 16 | import { Container, getChildComponents } from '../../src/components/Container'; 17 | 18 | describe('Container ->', () => { 19 | const CONTAINER_PLACEHOLDER_SELECTOR = '.new.section'; 20 | const CONTAINER_PLACEHOLDER_DATA_ATTRIBUTE_SELECTOR = '[data-cq-data-path="/container/*"]'; 21 | const CONTAINER_PATH = '/container'; 22 | const ITEM1_DATA_ATTRIBUTE_SELECTOR = '[data-cq-data-path="/container/component1"]'; 23 | const ITEM2_DATA_ATTRIBUTE_SELECTOR = '[data-cq-data-path="/container/component2"]'; 24 | const COMPONENT_TYPE1 = 'components/c1'; 25 | const COMPONENT_TYPE2 = 'components/c2'; 26 | const COMPONENT_1_CLASS_NAMES = 'component1-class'; 27 | const COMPONENT_2_CLASS_NAMES = 'component2-class'; 28 | 29 | const ITEMS = { 30 | component1: { 31 | ':type': COMPONENT_TYPE1, 32 | id: 'c1', 33 | }, 34 | component2: { 35 | ':type': COMPONENT_TYPE2, 36 | id: 'c2', 37 | }, 38 | }; 39 | 40 | const ITEMS_NO_TYPE = { 41 | component1: { 42 | id: 'c1', 43 | }, 44 | component2: { 45 | id: 'c2', 46 | }, 47 | }; 48 | 49 | const ITEM_CLASSES = { 50 | component1: COMPONENT_1_CLASS_NAMES, 51 | component2: COMPONENT_2_CLASS_NAMES, 52 | }; 53 | 54 | const getItemClassNames = (itemKey: string) => { 55 | return ITEM_CLASSES[itemKey]; 56 | }; 57 | 58 | const ITEMS_ORDER = ['component1', 'component2']; 59 | 60 | const ComponentChild = ({ model, className, cqPath }) => { 61 | const { id = '' } = model || {}; 62 | return
; 63 | }; 64 | 65 | let rootNode: HTMLElement; 66 | let ComponentMappingSpy: jest.SpyInstance; 67 | 68 | beforeEach(() => { 69 | ComponentMappingSpy = jest.spyOn(ComponentMapping, 'get'); 70 | }); 71 | 72 | afterEach(() => { 73 | ComponentMappingSpy.mockRestore(); 74 | 75 | if (rootNode) { 76 | document.body.appendChild(rootNode); 77 | rootNode = undefined; 78 | } 79 | }); 80 | 81 | function generateContainerComponent({ isInEditor = true, cqPath = CONTAINER_PATH, ...rest }): JSX.Element { 82 | const props = { 83 | componentMapping: ComponentMapping, 84 | isInEditor, 85 | cqPath, 86 | ...rest, 87 | }; 88 | return ( 89 |
90 | 91 |
92 | ); 93 | } 94 | 95 | describe('childComponents ->', () => { 96 | it('should not render components without type', () => { 97 | ComponentMappingSpy.mockReturnValue(ComponentChild); 98 | render( 99 | generateContainerComponent({ 100 | isInEditor: false, 101 | cqPath: '', 102 | cqItems: ITEMS_NO_TYPE, 103 | cqItemsOrder: ITEMS_ORDER, 104 | }), 105 | ); 106 | const node = screen.getByTestId('testcontainer'); 107 | expect(node.querySelector('#c1')).toBeNull(); 108 | expect(node.querySelector('#c2')).toBeNull(); 109 | }); 110 | 111 | it('should return an empty array of JSX if incoming model json doesnt meet container props', () => { 112 | ComponentMappingSpy.mockReturnValue(ComponentChild); 113 | 114 | const jsxList: ReactElement[] = getChildComponents({ 115 | cqType: '/some/type', 116 | removeDefaultStyles: true, 117 | }); 118 | 119 | expect(jsxList.length).toBe(0); 120 | }); 121 | 122 | it('should return an empty JSX if incoming model json has empty items', () => { 123 | ComponentMappingSpy.mockReturnValue(ComponentChild); 124 | render( 125 |
126 | 135 |
, 136 | ); 137 | const node = screen.getByTestId('testcontainer'); 138 | expect(node).not.toBeNull(); 139 | expect(node.getElementsByClassName('aem-container').length).toBe(0); 140 | }); 141 | 142 | it('should return an empty JSX if incoming model json has empty items when isInEditor is set', () => { 143 | ComponentMappingSpy.mockReturnValue(ComponentChild); 144 | render( 145 |
146 | 156 |
, 157 | ); 158 | const node = screen.getByTestId('testcontainer'); 159 | expect(node).not.toBeNull(); 160 | expect(node.getElementsByClassName('aem-container').length).toBe(1); 161 | }); 162 | 163 | it('should render available components if some are unmapped', () => { 164 | // eslint-disable-next-line @typescript-eslint/no-empty-function 165 | const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); 166 | ComponentMappingSpy.mockReturnValueOnce(ComponentChild); 167 | render( 168 | generateContainerComponent({ 169 | isInEditor: false, 170 | cqPath: '', 171 | cqItems: ITEMS, 172 | cqItemsOrder: ITEMS_ORDER, 173 | }), 174 | ); 175 | const node = screen.getByTestId('testcontainer'); 176 | expect(node.querySelector('#c1')).toBeTruthy(); 177 | expect(node.querySelector('#c2')).toBeNull(); 178 | expect(consoleSpy).toHaveBeenCalled(); 179 | expect(consoleSpy.mock.calls[0]).toContain('components/c2'); 180 | consoleSpy.mockRestore(); 181 | }); 182 | it('should add the expected components', () => { 183 | ComponentMappingSpy.mockReturnValue(ComponentChild); 184 | render(generateContainerComponent({ isInEditor: false, cqPath: '', cqItems: ITEMS, cqItemsOrder: ITEMS_ORDER })); 185 | expect(ComponentMappingSpy).toHaveBeenCalledWith(COMPONENT_TYPE1); 186 | expect(ComponentMappingSpy).toHaveBeenCalledWith(COMPONENT_TYPE2); 187 | const node = screen.getByTestId('testcontainer'); 188 | expect(node.querySelector('#c1')).toBeTruthy(); 189 | expect(node.querySelector('#c2')).toBeTruthy(); 190 | }); 191 | 192 | it('should render components if container cqpath is undefined', () => { 193 | ComponentMappingSpy.mockReturnValue(ComponentChild); 194 | render( 195 |
196 | 197 |
, 198 | ); 199 | const node = screen.getByTestId('testcontainer'); 200 | expect(node.querySelector('#c1')).toBeTruthy(); 201 | expect(node.querySelector('#c2')).toBeTruthy(); 202 | }); 203 | 204 | it('should add a placeholder with data attribute when in WCM edit mode', () => { 205 | render(generateContainerComponent({})); 206 | const node = screen.getByTestId('testcontainer'); 207 | expect( 208 | node.querySelector(CONTAINER_PLACEHOLDER_DATA_ATTRIBUTE_SELECTOR + CONTAINER_PLACEHOLDER_SELECTOR), 209 | ).toBeTruthy(); 210 | }); 211 | 212 | it('should not add a placeholder when not in WCM edit mode', () => { 213 | render(generateContainerComponent({ isInEditor: false })); 214 | const node = screen.getByTestId('testcontainer'); 215 | expect(node.querySelector(CONTAINER_PLACEHOLDER_SELECTOR)).toBeNull(); 216 | }); 217 | 218 | it('should add a data attribute on the children when in WCM edit mode', () => { 219 | ComponentMappingSpy.mockReturnValue(ComponentChild); 220 | render( 221 | generateContainerComponent({ 222 | cqItems: ITEMS, 223 | cqItemsOrder: ITEMS_ORDER, 224 | }), 225 | ); 226 | expect(ComponentMappingSpy).toHaveBeenCalledWith(COMPONENT_TYPE1); 227 | expect(ComponentMappingSpy).toHaveBeenCalledWith(COMPONENT_TYPE2); 228 | const node = screen.getByTestId('testcontainer'); 229 | const containerPlaceholder = node.querySelector(CONTAINER_PLACEHOLDER_SELECTOR); 230 | expect(containerPlaceholder).toBeTruthy(); 231 | const childItem1 = node.querySelector(ITEM1_DATA_ATTRIBUTE_SELECTOR); 232 | const childItem2 = node.querySelector(ITEM2_DATA_ATTRIBUTE_SELECTOR); 233 | expect(childItem1).toBeTruthy(); 234 | expect(childItem2).toBeTruthy(); 235 | }); 236 | 237 | it('should add the expected item classes when passed', () => { 238 | ComponentMappingSpy.mockReturnValue(ComponentChild); 239 | render( 240 | generateContainerComponent({ 241 | isInEditor: false, 242 | cqPath: '', 243 | cqItems: ITEMS, 244 | cqItemsOrder: ITEMS_ORDER, 245 | getItemClassNames, 246 | }), 247 | ); 248 | const node = screen.getByTestId('testcontainer'); 249 | const childItem1 = node.querySelector('#c1.' + COMPONENT_1_CLASS_NAMES); 250 | const childItem2 = node.querySelector('#c2.' + COMPONENT_2_CLASS_NAMES); 251 | expect(childItem1).toBeTruthy(); 252 | expect(childItem2).toBeTruthy(); 253 | }); 254 | 255 | it('should support component mapping via prop', () => { 256 | const MapComponentsSpy = jest.spyOn(ComponentMapping, 'map'); 257 | render( 258 | generateContainerComponent({ 259 | components: { [COMPONENT_TYPE2]: ComponentChild }, 260 | }), 261 | ); 262 | expect(MapComponentsSpy).toBeCalledWith(COMPONENT_TYPE2, ComponentChild); 263 | }); 264 | }); 265 | 266 | describe('Data attributes ->', () => { 267 | it('should not add a the cq-data-path attribute if not in WCM edit mode', () => { 268 | render(generateContainerComponent({ isInEditor: false })); 269 | const node = screen.getByTestId('testcontainer'); 270 | const container = node.querySelector('[data-cq-data-path="/container"]'); 271 | expect(container).toBeNull(); 272 | }); 273 | 274 | it('should add a the cq-data-path attribute if in WCM edit mode', () => { 275 | render(generateContainerComponent({})); 276 | const node = screen.getByTestId('testcontainer'); 277 | const container = node.querySelector('[data-cq-data-path="/container"]'); 278 | expect(container).toBeTruthy(); 279 | }); 280 | }); 281 | }); 282 | -------------------------------------------------------------------------------- /test/components/Page.test.tsx: -------------------------------------------------------------------------------- 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 React from 'react'; 14 | import { render, screen } from '@testing-library/react'; 15 | import { Model, ModelManager } from '@adobe/aem-spa-page-model-manager'; 16 | import { ComponentMapping } from '../../src/core/ComponentMapping'; 17 | import { Page } from '../../src/components/Page'; 18 | 19 | describe('Page ->', () => { 20 | const CHILD_COMPONENT_CLASS_NAME = 'child-class'; 21 | const PAGE_PATH = '/page'; 22 | const ITEM1_DATA_ATTRIBUTE_SELECTOR = '[data-cq-data-path="/page/jcr:content/component1"]'; 23 | const ITEM2_DATA_ATTRIBUTE_SELECTOR = '[data-cq-data-path="/page/jcr:content/component2"]'; 24 | const CHILD_PAGE_1_DATA_ATTRIBUTE_SELECTOR = '[data-cq-data-path="child/page1"]'; 25 | const CHILD_PAGE_2_DATA_ATTRIBUTE_SELECTOR = '[data-cq-data-path="child/page2"]'; 26 | const COMPONENT_TYPE1 = 'components/c1'; 27 | const COMPONENT_TYPE2 = 'components/c2'; 28 | const PAGE_TYPE1 = 'components/p1'; 29 | const PAGE_TYPE2 = 'components/p2'; 30 | 31 | const ITEMS = { 32 | component1: { 33 | ':type': COMPONENT_TYPE1, 34 | id: 'c1', 35 | }, 36 | component2: { 37 | ':type': COMPONENT_TYPE2, 38 | id: 'c2', 39 | }, 40 | }; 41 | 42 | const ITEMS_ORDER = ['component1', 'component2']; 43 | 44 | type ChildrenProps = { 45 | id: string; 46 | } & Model; 47 | 48 | const CHILDREN: { [key: string]: ChildrenProps } = { 49 | page1: { 50 | ':children': {}, 51 | ':items': {}, 52 | ':itemsOrder': [], 53 | ':type': PAGE_TYPE1, 54 | id: 'p1', 55 | ':path': 'child/page1', 56 | }, 57 | page2: { 58 | ':children': {}, 59 | ':items': {}, 60 | ':itemsOrder': [], 61 | ':type': PAGE_TYPE2, 62 | id: 'p2', 63 | ':path': 'child/page2', 64 | }, 65 | }; 66 | 67 | let ComponentMappingSpy: jest.SpyInstance; 68 | let addListenerSpy: jest.SpyInstance; 69 | let removeListenerSpy: jest.SpyInstance; 70 | let getDataSpy: jest.SpyInstance; 71 | 72 | const ChildComponent = ({ model, cqPath = '' }) => { 73 | const { id = '' } = model || {}; 74 | return
; 75 | }; 76 | 77 | function generatePage({ cqChildren = {} }) { 78 | const props = { 79 | isInEditor: true, 80 | componentMapping: ComponentMapping, 81 | cqPath: PAGE_PATH, 82 | cqItems: ITEMS, 83 | cqItemsOrder: ITEMS_ORDER, 84 | cqChildren, 85 | }; 86 | return ( 87 |
88 | 89 |
90 | ); 91 | } 92 | 93 | beforeEach(() => { 94 | ComponentMappingSpy = jest.spyOn(ComponentMapping, 'get'); 95 | getDataSpy = jest.spyOn(ModelManager, 'getData').mockResolvedValue({}); 96 | addListenerSpy = jest.spyOn(ModelManager, 'addListener').mockImplementation(); 97 | removeListenerSpy = jest.spyOn(ModelManager, 'removeListener').mockImplementation(); 98 | }); 99 | 100 | afterEach(() => { 101 | ComponentMappingSpy.mockRestore(); 102 | addListenerSpy.mockReset(); 103 | removeListenerSpy.mockReset(); 104 | getDataSpy.mockReset(); 105 | }); 106 | 107 | describe('child pages ->', () => { 108 | it('should render only components if no children', () => { 109 | ComponentMappingSpy.mockReturnValue(ChildComponent); 110 | render(generatePage({})); 111 | const node = screen.getByTestId('pageComponent'); 112 | expect(node.querySelector('#c1')).toBeTruthy(); 113 | expect(node.querySelector('#c2')).toBeTruthy(); 114 | expect(node.querySelector('#p1')).toBeNull(); 115 | expect(node.querySelector('#p2')).toBeNull(); 116 | }); 117 | it('should render available pages if some are unmapped', () => { 118 | // eslint-disable-next-line @typescript-eslint/no-empty-function 119 | const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); 120 | ComponentMappingSpy.mockImplementation((type) => type !== PAGE_TYPE2 && ChildComponent); 121 | render(generatePage({ cqChildren: CHILDREN })); 122 | const node = screen.getByTestId('pageComponent'); 123 | expect(node.querySelector('#p1')).toBeTruthy(); 124 | expect(node.querySelector('#p2')).toBeNull(); 125 | expect(consoleSpy).toHaveBeenCalled(); 126 | expect(consoleSpy.mock.calls[0]).toContain(PAGE_TYPE2); 127 | consoleSpy.mockRestore(); 128 | }); 129 | it('should add the expected children', () => { 130 | ComponentMappingSpy.mockReturnValue(ChildComponent); 131 | render(generatePage({ cqChildren: CHILDREN })); 132 | expect(ComponentMappingSpy).toHaveBeenCalledWith(COMPONENT_TYPE1); 133 | expect(ComponentMappingSpy).toHaveBeenCalledWith(COMPONENT_TYPE2); 134 | const node = screen.getByTestId('pageComponent'); 135 | const childItem1 = node.querySelector('#c1'); 136 | const childItem2 = node.querySelector('#c1'); 137 | expect(childItem1).toBeTruthy(); 138 | expect(childItem2).toBeTruthy(); 139 | 140 | const childPage1 = node.querySelector('#p1'); 141 | const childPage2 = node.querySelector('#p2'); 142 | expect(childPage1).toBeTruthy(); 143 | expect(childPage2).toBeTruthy(); 144 | }); 145 | 146 | it('should add the expected children with data attributes when in WCM edit mode', () => { 147 | ComponentMappingSpy.mockImplementation((key: string) => { 148 | switch (key) { 149 | case COMPONENT_TYPE1: 150 | case COMPONENT_TYPE2: 151 | return ChildComponent; 152 | 153 | case PAGE_TYPE1: 154 | case PAGE_TYPE2: 155 | return Page; 156 | 157 | default: 158 | return null; 159 | } 160 | }); 161 | 162 | render(generatePage({ cqChildren: CHILDREN })); 163 | expect(ComponentMappingSpy).toHaveBeenCalledWith(COMPONENT_TYPE1); 164 | expect(ComponentMappingSpy).toHaveBeenCalledWith(COMPONENT_TYPE2); 165 | const node = screen.getByTestId('pageComponent'); 166 | const childItem1 = node.querySelector(ITEM1_DATA_ATTRIBUTE_SELECTOR); 167 | const childItem2 = node.querySelector(ITEM2_DATA_ATTRIBUTE_SELECTOR); 168 | expect(childItem1).toBeTruthy(); 169 | expect(childItem2).toBeTruthy(); 170 | 171 | const childPage1 = node.querySelector(CHILD_PAGE_1_DATA_ATTRIBUTE_SELECTOR); 172 | const childPage2 = node.querySelector(CHILD_PAGE_2_DATA_ATTRIBUTE_SELECTOR); 173 | expect(childPage1).toBeTruthy(); 174 | expect(childPage2).toBeTruthy(); 175 | }); 176 | }); 177 | }); 178 | -------------------------------------------------------------------------------- /test/components/ResponsiveGrid.test.tsx: -------------------------------------------------------------------------------- 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 React from 'react'; 14 | import { render, screen } from '@testing-library/react'; 15 | import { AuthoringUtils, ModelManager } from '@adobe/aem-spa-page-model-manager'; 16 | import { ComponentMapping } from '../../src/core/ComponentMapping'; 17 | import { ResponsiveGrid } from '../../src/components/ResponsiveGrid'; 18 | import { AllowedComponentList } from '../../src/types/AEMModel'; 19 | 20 | describe('ResponsiveGrid ->', () => { 21 | const CONTAINER_PLACEHOLDER_SELECTOR = '.new.section'; 22 | const CONTAINER_PLACEHOLDER_DATA_ATTRIBUTE_SELECTOR = '[data-cq-data-path="/container/*"]'; 23 | const ITEM_CLASS_NAME = 'item-class'; 24 | const CONTAINER_PATH = '/container'; 25 | const ITEM1_DATA_ATTRIBUTE_SELECTOR = '[data-cq-data-path="/container/component1"]'; 26 | const ITEM2_DATA_ATTRIBUTE_SELECTOR = '[data-cq-data-path="/container/component2"]'; 27 | const GRID_CLASS_NAMES = 'grid-class-names'; 28 | const COLUMN_1_CLASS_NAMES = 'column-class-names-1'; 29 | const COLUMN_2_CLASS_NAMES = 'column-class-names-2'; 30 | const PLACEHOLDER_CLASS_NAMES = 'aem-Grid-newComponent'; 31 | const COMPONENT_TYPE1 = 'components/c1'; 32 | const COMPONENT_TYPE2 = 'components/c2'; 33 | const COMPONENT_TEXT_PATH = '/content/page/jcr:content/root/text'; 34 | const COMPONENT_TEXT_TITLE = 'Text'; 35 | const CUSTOM_TITLE = 'Custom Container'; 36 | const ALLOWED_PLACEHOLDER_SELECTOR = '.aem-AllowedComponent--list'; 37 | const ALLOWED_COMPONENT_TITLE_SELECTOR = '.aem-AllowedComponent--title'; 38 | const ALLOWED_TEXT_COMPONENT_PLACEHOLDER_SELECTOR = 39 | '.aem-AllowedComponent--component.cq-placeholder.placeholder[data-cq-data-path="/content/page/jcr:content/root/text"]'; 40 | const DEFAULT_PLACEHOLDER_SELECTOR = '.cq-placeholder'; 41 | 42 | const ITEMS = { 43 | component1: { 44 | ':type': COMPONENT_TYPE1, 45 | id: 'c1', 46 | }, 47 | component2: { 48 | ':type': COMPONENT_TYPE2, 49 | id: 'c2', 50 | }, 51 | }; 52 | 53 | const ITEMS_ORDER = ['component1', 'component2']; 54 | 55 | const COLUMN_CLASS_NAMES = { 56 | component1: COLUMN_1_CLASS_NAMES, 57 | component2: COLUMN_2_CLASS_NAMES, 58 | }; 59 | 60 | const ComponentChild = ({ id = '' }) =>
; 61 | 62 | const allowedComp: AllowedComponentList = { 63 | applicable: false, 64 | components: [], 65 | }; 66 | 67 | const allowedCompTrue: AllowedComponentList = { 68 | applicable: true, 69 | components: [ 70 | { 71 | path: COMPONENT_TEXT_PATH, 72 | title: COMPONENT_TEXT_TITLE, 73 | }, 74 | ], 75 | }; 76 | 77 | const STANDARD_GRID_PROPS = { 78 | cqPath: CONTAINER_PATH, 79 | gridClassNames: GRID_CLASS_NAMES, 80 | columnClassNames: {}, 81 | allowedComponents: allowedComp, 82 | componentMapping: ComponentMapping, 83 | cqItems: {}, 84 | cqItemsOrder: [], 85 | isInEditor: false, 86 | cqType: '', 87 | }; 88 | 89 | let ComponentMappingSpy: jest.SpyInstance; 90 | let addListenerSpy: jest.SpyInstance; 91 | let removeListenerSpy: jest.SpyInstance; 92 | let getDataSpy: jest.SpyInstance; 93 | let isInEditorSpy: jest.SpyInstance; 94 | 95 | function generateResponsiveGrid(customProps) { 96 | const props = { 97 | ...STANDARD_GRID_PROPS, 98 | ...customProps, 99 | }; 100 | return ( 101 |
102 | 103 |
104 | ); 105 | } 106 | beforeEach(() => { 107 | ComponentMappingSpy = jest.spyOn(ComponentMapping, 'get'); 108 | getDataSpy = jest.spyOn(ModelManager, 'getData').mockResolvedValue({}); 109 | addListenerSpy = jest.spyOn(ModelManager, 'addListener').mockImplementation(); 110 | removeListenerSpy = jest.spyOn(ModelManager, 'removeListener').mockImplementation(); 111 | isInEditorSpy = jest.spyOn(AuthoringUtils, 'isInEditor').mockReturnValue(false); 112 | }); 113 | 114 | afterEach(() => { 115 | ComponentMappingSpy.mockRestore(); 116 | addListenerSpy.mockReset(); 117 | removeListenerSpy.mockReset(); 118 | getDataSpy.mockReset(); 119 | isInEditorSpy.mockReset(); 120 | }); 121 | 122 | describe('Grid class names ->', () => { 123 | it('should add the grid class names', () => { 124 | render(generateResponsiveGrid({})); 125 | const node = screen.getByTestId('gridComponent'); 126 | expect(node.querySelector('.' + GRID_CLASS_NAMES)).toBeTruthy(); 127 | }); 128 | }); 129 | 130 | describe('Placeholder ->', () => { 131 | it('should add the expected placeholder class names', () => { 132 | render(generateResponsiveGrid({})); 133 | const node = screen.getByTestId('gridComponent'); 134 | const gridPlaceholder = node.querySelector( 135 | '.' + PLACEHOLDER_CLASS_NAMES + CONTAINER_PLACEHOLDER_SELECTOR + CONTAINER_PLACEHOLDER_DATA_ATTRIBUTE_SELECTOR, 136 | ); 137 | expect(gridPlaceholder).toBeDefined(); 138 | }); 139 | }); 140 | 141 | describe('Column class names ->', () => { 142 | it('should add the expected column class names', () => { 143 | ComponentMappingSpy.mockReturnValue(ComponentChild); 144 | render( 145 | generateResponsiveGrid({ columnClassNames: COLUMN_CLASS_NAMES, cqItems: ITEMS, cqItemsOrder: ITEMS_ORDER }), 146 | ); 147 | const node = screen.getByTestId('gridComponent'); 148 | const childItem1 = node.querySelector('.' + COLUMN_1_CLASS_NAMES + ITEM1_DATA_ATTRIBUTE_SELECTOR); 149 | const childItem2 = node.querySelector('.' + COLUMN_2_CLASS_NAMES + ITEM2_DATA_ATTRIBUTE_SELECTOR); 150 | expect(childItem1).toBeDefined(); 151 | expect(childItem2).toBeDefined(); 152 | }); 153 | }); 154 | 155 | describe('Allowed Component Container ->', () => { 156 | it('should add the Allowed Component Container', () => { 157 | ComponentMappingSpy.mockReturnValue(ComponentChild); 158 | render( 159 | generateResponsiveGrid({ 160 | allowedComponents: allowedCompTrue, 161 | isInEditor: true, 162 | title: 'Custom Container', 163 | }), 164 | ); 165 | const node = screen.getByTestId('gridComponent'); 166 | 167 | const allowedComponentsContainer = node.querySelector(ALLOWED_PLACEHOLDER_SELECTOR); 168 | const allowedComponentsTitle = allowedComponentsContainer.querySelector( 169 | ALLOWED_COMPONENT_TITLE_SELECTOR, 170 | ) as HTMLElement; 171 | expect(allowedComponentsTitle).toBeDefined(); 172 | expect(allowedComponentsTitle.dataset.text).toEqual(CUSTOM_TITLE); 173 | expect(allowedComponentsContainer.querySelector(ALLOWED_TEXT_COMPONENT_PLACEHOLDER_SELECTOR)).toBeTruthy(); 174 | }); 175 | }); 176 | 177 | describe('No Default Placeholder ->', () => { 178 | it('should not add default placeholder', () => { 179 | isInEditorSpy = jest.spyOn(AuthoringUtils, 'isInEditor').mockReturnValue(true); 180 | ComponentMappingSpy.mockReturnValue(ComponentChild); 181 | render(generateResponsiveGrid({ cqItems: ITEMS, cqItemsOrder: ITEMS_ORDER })); 182 | const node = screen.getByTestId('gridComponent'); 183 | const defaultPlaceholderSelector = node.querySelector(DEFAULT_PLACEHOLDER_SELECTOR); 184 | expect(defaultPlaceholderSelector).toBeNull(); 185 | }); 186 | }); 187 | }); 188 | -------------------------------------------------------------------------------- /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 '@adobe/aem-spa-page-model-manager'; 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 | -------------------------------------------------------------------------------- /test/specs/composition_attribute_propagation.test.tsx: -------------------------------------------------------------------------------- 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 { AuthoringUtils, ModelManager } from '@adobe/aem-spa-page-model-manager'; 14 | import React, { Component } from 'react'; 15 | import ReactDOM from 'react-dom'; 16 | import { MappedComponentProperties } from '../../src/core/ComponentMapping'; 17 | 18 | describe('Composition and attribute propagation ->', () => { 19 | const ROOT_CLASS_NAME = 'root-class'; 20 | const COMPONENT_RESOURCE_TYPE = '/component/resource/type'; 21 | const COMPONENT_PATH = '/path/to/component'; 22 | const CHILD_COMPONENT_CLASS_NAME = 'child-class'; 23 | const DATA_ATTR_TO_PROPS = 'data-attr-to-props'; 24 | 25 | interface DummyProps extends MappedComponentProperties { 26 | cqType: string; 27 | attrToProps?: string; 28 | id: string; 29 | } 30 | 31 | const CQ_PROPS: DummyProps = { 32 | cqType: COMPONENT_RESOURCE_TYPE, 33 | cqPath: COMPONENT_PATH, 34 | isInEditor: false, 35 | id: '', 36 | }; 37 | 38 | class ChildComponent extends Component { 39 | render() { 40 | const attr = { 41 | [DATA_ATTR_TO_PROPS]: this.props.attrToProps, 42 | }; 43 | 44 | return
; 45 | } 46 | } 47 | 48 | let rootNode: HTMLElement; 49 | let isInEditorSpy: jest.SpyInstance; 50 | let addListenerSpy: jest.SpyInstance; 51 | let getDataSpy: jest.SpyInstance; 52 | 53 | beforeEach(() => { 54 | rootNode = document.createElement('div'); 55 | rootNode.className = ROOT_CLASS_NAME; 56 | document.body.appendChild(rootNode); 57 | isInEditorSpy = jest.spyOn(AuthoringUtils, 'isInEditor').mockReturnValue(false); 58 | addListenerSpy = jest.spyOn(ModelManager, 'addListener').mockImplementation(); 59 | getDataSpy = jest.spyOn(ModelManager, 'getData').mockResolvedValue({}); 60 | }); 61 | 62 | afterEach(() => { 63 | isInEditorSpy.mockRestore(); 64 | addListenerSpy.mockRestore(); 65 | getDataSpy.mockRestore(); 66 | 67 | if (rootNode) { 68 | document.body.appendChild(rootNode); 69 | rootNode = undefined; 70 | } 71 | }); 72 | it('should propagate attributes to properties', () => { 73 | ReactDOM.render(, rootNode); 74 | let node = rootNode.querySelector('[' + DATA_ATTR_TO_PROPS + ']') as HTMLElement; 75 | 76 | expect(node).toBeDefined(); 77 | expect(node.dataset.attrToProps).toEqual('true'); 78 | 79 | // Update the component with new properties 80 | ReactDOM.render(, rootNode); 81 | 82 | node = rootNode.querySelector('[' + DATA_ATTR_TO_PROPS + ']'); 83 | expect(node).toBeDefined(); 84 | expect(node.dataset.attrToProps).toEqual('false'); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": false, 5 | "baseUrl": ".", 6 | "paths": { 7 | "@adobe/aem-react-editable-components": ["./"], 8 | "@adobe/aem-react-editable-components/*": ["./*"] 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /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 | "jsx": "react", 11 | "lib": ["dom", "dom.iterable", "esnext"], 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "outDir": "./dist", 15 | "resolveJsonModule": true, 16 | "skipLibCheck": true, 17 | "strict": true, 18 | "target": "ES6", 19 | "declaration": true 20 | }, 21 | "include": ["src/**/*"] 22 | } 23 | -------------------------------------------------------------------------------- /webpack.config.babel.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 path from 'path'; 14 | import webpack from 'webpack'; 15 | import nodeExternals from 'webpack-node-externals'; 16 | import { CleanWebpackPlugin } from 'clean-webpack-plugin'; 17 | import CircularDependencyPlugin from 'circular-dependency-plugin'; 18 | 19 | const isProduction = process.env.NODE_ENV === 'production'; 20 | const mode = isProduction ? 'production' : 'development'; 21 | const devtool = isProduction ? false : 'source-map'; 22 | 23 | console.log('Building for:', mode); 24 | 25 | const config = { 26 | ...(process.argv.includes('--watch') && { 27 | watchOptions: { 28 | poll: 1000, 29 | ignored: ['**/node_modules'], 30 | }, 31 | }), 32 | 33 | entry: './src/types.ts', 34 | mode, 35 | devtool, 36 | 37 | output: { 38 | globalObject: `(function(){ try{ return typeof self !== 'undefined';}catch(err){return false;}})() ? self : this`, 39 | path: path.resolve(__dirname, 'dist'), 40 | filename: 'aem-react-editable-components.js', 41 | library: 'aemReactEditableComponents', 42 | libraryTarget: 'umd', 43 | }, 44 | 45 | module: { 46 | rules: [ 47 | { 48 | test: /\.ts$|\.tsx$/, 49 | exclude: /(node_modules|dist)/, 50 | use: { 51 | loader: 'ts-loader', 52 | }, 53 | enforce: 'post', 54 | }, 55 | ], 56 | }, 57 | 58 | resolve: { 59 | extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'], 60 | }, 61 | 62 | externals: [ 63 | nodeExternals({ 64 | modulesFromFile: { exclude: ['dependencies'] }, 65 | }), 66 | ], 67 | 68 | plugins: [ 69 | new webpack.ProgressPlugin(), 70 | new CleanWebpackPlugin(), 71 | new CircularDependencyPlugin({ 72 | exclude: /node_modules/, 73 | failOnError: true, 74 | allowAsyncCycles: false, 75 | cwd: process.cwd(), 76 | }), 77 | ], 78 | }; 79 | 80 | export default config; 81 | --------------------------------------------------------------------------------