├── .codeclimate.yml ├── .editorconfig ├── .ember-cli ├── .eslintignore ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── ci.yml │ ├── plan-release.yml │ └── publish.yml ├── .gitignore ├── .npmignore ├── .npmrc ├── .prettierignore ├── .prettierrc.js ├── .release-plan.json ├── .stylelintignore ├── .stylelintrc.js ├── .template-lintrc.js ├── .tool-versions ├── .watchmanconfig ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── RELEASE.md ├── UPGRADING.md ├── addon ├── -private │ ├── ember-internals.js │ ├── ember-validator.js │ ├── internal-result-object.js │ ├── options.js │ ├── result.js │ └── symbols.js ├── index.js ├── utils │ ├── array.js │ ├── cycle-breaker.js │ ├── deep-set.js │ ├── get-with-default.js │ ├── lookup-validator.js │ ├── meta-data.js │ ├── should-call-super.js │ └── utils.js ├── validations │ ├── error.js │ ├── factory.js │ ├── result-collection.js │ ├── validator.js │ └── warning-result-collection.js └── validators │ ├── alias.js │ ├── base.js │ ├── belongs-to.js │ ├── collection.js │ ├── confirmation.js │ ├── date.js │ ├── dependent.js │ ├── ds-error.js │ ├── exclusion.js │ ├── format.js │ ├── has-many.js │ ├── inclusion.js │ ├── inline.js │ ├── length.js │ ├── messages.js │ ├── number.js │ └── presence.js ├── app ├── .gitkeep └── validators │ ├── alias.js │ ├── belongs-to.js │ ├── collection.js │ ├── confirmation.js │ ├── date.js │ ├── dependent.js │ ├── ds-error.js │ ├── exclusion.js │ ├── format.js │ ├── has-many.js │ ├── inclusion.js │ ├── inline.js │ ├── length.js │ ├── messages.js │ ├── number.js │ └── presence.js ├── blueprints ├── validator-test │ ├── index.js │ ├── mocha-files │ │ └── tests │ │ │ └── unit │ │ │ └── validators │ │ │ └── __name__-test.js │ └── qunit-files │ │ └── tests │ │ └── unit │ │ └── validators │ │ └── __name__-test.js └── validator │ ├── files │ └── __root__ │ │ └── validators │ │ └── __name__.js │ └── index.js ├── ember-cli-build.js ├── htmlbars-plugins └── v-get.js ├── index.js ├── jsconfig.json ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── scripts └── write-snippets.mjs ├── stderr.log ├── testem.js ├── tests ├── acceptance │ └── dummy-index-test.js ├── dummy │ ├── app │ │ ├── adapters │ │ │ └── application.js │ │ ├── app.js │ │ ├── components │ │ │ ├── .gitkeep │ │ │ ├── code-snippet.hbs │ │ │ ├── code-snippet.js │ │ │ ├── named-v-get.hbs │ │ │ ├── validated-input.hbs │ │ │ └── validated-input.js │ │ ├── controllers │ │ │ ├── .gitkeep │ │ │ └── index.js │ │ ├── helpers │ │ │ └── .gitkeep │ │ ├── index.html │ │ ├── models │ │ │ ├── company.js │ │ │ ├── order-line.js │ │ │ ├── order-selection-question.js │ │ │ ├── order-selection.js │ │ │ ├── order.js │ │ │ ├── signup.js │ │ │ ├── user-detail.js │ │ │ └── user.js │ │ ├── router.js │ │ ├── routes │ │ │ ├── .gitkeep │ │ │ └── index.js │ │ ├── styles │ │ │ ├── app.scss │ │ │ ├── code-snippet.scss │ │ │ ├── form.scss │ │ │ └── navbar.scss │ │ ├── templates │ │ │ ├── application.hbs │ │ │ └── index.hbs │ │ └── validators │ │ │ └── messages.js │ ├── config │ │ ├── ember-cli-update.json │ │ ├── ember-try.js │ │ ├── environment.js │ │ ├── icons.js │ │ ├── optional-features.json │ │ └── targets.js │ └── public │ │ ├── images │ │ ├── ember-logo.png │ │ └── tomsterzilla.jpeg │ │ └── robots.txt ├── helpers │ ├── index.js │ └── setup-object.js ├── index.html ├── integration │ ├── helpers │ │ └── v-get-test.js │ ├── validations │ │ ├── factory-dependent-keys-test.js │ │ ├── factory-general-test.js │ │ └── model-relationships-test.js │ └── validators │ │ └── composable-test.js ├── test-helper.js └── unit │ ├── .gitkeep │ ├── utils │ ├── assign-test.js │ ├── flatten-test.js │ ├── get-with-default-test.js │ └── should-call-super-test.js │ ├── validations │ ├── ds-model-test.js │ ├── ember-proxy-test.js │ └── nested-model-relationship-test.js │ └── validators │ ├── base-test.js │ ├── collection-test.js │ ├── confirmation-test.js │ ├── date-test.js │ ├── dependent-test.js │ ├── ds-error-test.js │ ├── exclusion-test.js │ ├── format-test.js │ ├── inclusion-test.js │ ├── inline-test.js │ ├── length-test.js │ ├── messages-test.js │ ├── number-test.js │ └── presence-test.js └── yuidoc.json /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | languages: 2 | JavaScript: true 3 | exclude_paths: 4 | - "tests/*" 5 | - "config/*" 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.hbs] 16 | insert_final_newline = false 17 | 18 | [*.{diff,md}] 19 | trim_trailing_whitespace = false 20 | -------------------------------------------------------------------------------- /.ember-cli: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | Setting `isTypeScriptProject` to true will force the blueprint generators to generate TypeScript 4 | rather than JavaScript by default, when a TypeScript version of a given blueprint is available. 5 | */ 6 | "isTypeScriptProject": false 7 | } 8 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # unconventional js 2 | /blueprints/*/files/ 3 | 4 | # compiled output 5 | /declarations/ 6 | /dist/ 7 | 8 | # misc 9 | /coverage/ 10 | !.* 11 | .*/ 12 | 13 | # ember-try 14 | /.node_modules.ember-try/ 15 | 16 | tests/dummy/app/snippets.js 17 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | root: true, 5 | parser: '@babel/eslint-parser', 6 | parserOptions: { 7 | ecmaVersion: 'latest', 8 | sourceType: 'module', 9 | requireConfigFile: false, 10 | babelOptions: { 11 | plugins: [ 12 | ['@babel/plugin-proposal-decorators', { decoratorsBeforeExport: true }], 13 | ], 14 | }, 15 | }, 16 | plugins: ['ember'], 17 | extends: [ 18 | 'eslint:recommended', 19 | 'plugin:ember/recommended', 20 | 'plugin:prettier/recommended', 21 | ], 22 | env: { 23 | browser: true, 24 | }, 25 | rules: { 26 | 'ember/no-classic-classes': 'off', 27 | 'ember/no-new-mixins': 'off', 28 | 'ember/no-runloop': 'off', 29 | 'ember/use-ember-data-rfc-395-imports': 'off', 30 | }, 31 | overrides: [ 32 | // node files 33 | { 34 | files: [ 35 | './.eslintrc.js', 36 | './.prettierrc.js', 37 | './.stylelintrc.js', 38 | './.template-lintrc.js', 39 | './ember-cli-build.js', 40 | './index.js', 41 | './testem.js', 42 | './blueprints/*/index.js', 43 | './config/**/*.js', 44 | './tests/dummy/config/**/*.js', 45 | ], 46 | parserOptions: { 47 | sourceType: 'script', 48 | }, 49 | env: { 50 | browser: false, 51 | node: true, 52 | }, 53 | extends: ['plugin:n/recommended'], 54 | }, 55 | { 56 | // test files 57 | files: ['tests/**/*-test.{js,ts}'], 58 | extends: ['plugin:qunit/recommended'], 59 | rules: { 60 | 'qunit/no-conditional-assertions': 'off', 61 | 'qunit/require-expect': 'off', 62 | }, 63 | }, 64 | ], 65 | }; 66 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Environment 2 | 3 | - Ember Version: 4 | - Ember CLI Version: 5 | - Ember CP Validations Version: 6 | 7 | ### Steps to Reproduce 8 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Resolves # . 2 | 3 | Changes proposed: 4 | 5 | - 6 | - 7 | - 8 | 9 | Tasks: 10 | 11 | - [ ] Added test case(s) 12 | - [ ] Updated documentation 13 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - master 8 | pull_request: {} 9 | 10 | concurrency: 11 | group: ci-${{ github.head_ref || github.ref }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | test: 16 | name: "Tests" 17 | runs-on: ubuntu-latest 18 | timeout-minutes: 10 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: pnpm/action-setup@v4 23 | with: 24 | version: 10 25 | - name: Install Node 26 | uses: actions/setup-node@v4 27 | with: 28 | node-version: 18 29 | cache: pnpm 30 | - name: Install Dependencies 31 | run: pnpm install --frozen-lockfile 32 | - name: Lint 33 | run: pnpm lint 34 | - name: Run Tests 35 | run: pnpm test:ember 36 | 37 | floating: 38 | name: "Floating Dependencies" 39 | runs-on: ubuntu-latest 40 | timeout-minutes: 10 41 | 42 | steps: 43 | - uses: actions/checkout@v4 44 | - uses: pnpm/action-setup@v4 45 | with: 46 | version: 10 47 | - uses: actions/setup-node@v4 48 | with: 49 | node-version: 18 50 | cache: pnpm 51 | - name: Install Dependencies 52 | run: pnpm install --no-lockfile 53 | - name: Run Tests 54 | run: pnpm test:ember 55 | 56 | try-scenarios: 57 | name: ${{ matrix.try-scenario }} 58 | runs-on: ubuntu-latest 59 | needs: "test" 60 | timeout-minutes: 10 61 | 62 | strategy: 63 | fail-fast: false 64 | matrix: 65 | try-scenario: 66 | - ember-lts-4.4 67 | - ember-lts-4.8 68 | - ember-lts-4.12 69 | - ember-lts-5.4 70 | - ember-lts-5.8 71 | - ember-lts-5.12 72 | - ember-release 73 | - ember-beta 74 | - ember-canary 75 | - embroider-safe 76 | - embroider-optimized 77 | 78 | steps: 79 | - uses: actions/checkout@v4 80 | - uses: pnpm/action-setup@v4 81 | with: 82 | version: 10 83 | - name: Install Node 84 | uses: actions/setup-node@v4 85 | with: 86 | node-version: 18 87 | cache: pnpm 88 | - name: Install Dependencies 89 | run: pnpm install --frozen-lockfile 90 | - name: Run Tests 91 | run: ./node_modules/.bin/ember try:one ${{ matrix.try-scenario }} 92 | -------------------------------------------------------------------------------- /.github/workflows/plan-release.yml: -------------------------------------------------------------------------------- 1 | name: Release Plan Review 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - master 7 | pull_request: 8 | types: 9 | - labeled 10 | 11 | concurrency: 12 | group: plan-release # only the latest one of these should ever be running 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | check-plan: 17 | name: "Check Release Plan" 18 | runs-on: ubuntu-latest 19 | outputs: 20 | command: ${{ steps.check-release.outputs.command }} 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | with: 25 | fetch-depth: 0 26 | ref: 'main' 27 | # This will only cause the `check-plan` job to have a "command" of `release` 28 | # when the .release-plan.json file was changed on the last commit. 29 | - id: check-release 30 | run: if git diff --name-only HEAD HEAD~1 | grep -w -q ".release-plan.json"; then echo "command=release"; fi >> $GITHUB_OUTPUT 31 | 32 | prepare_release_notes: 33 | name: Prepare Release Notes 34 | runs-on: ubuntu-latest 35 | timeout-minutes: 5 36 | needs: check-plan 37 | permissions: 38 | contents: write 39 | pull-requests: write 40 | outputs: 41 | explanation: ${{ steps.explanation.outputs.text }} 42 | # only run on push event if plan wasn't updated (don't create a release plan when we're releasing) 43 | # only run on labeled event if the PR has already been merged 44 | if: (github.event_name == 'push' && needs.check-plan.outputs.command != 'release') || (github.event_name == 'pull_request' && github.event.pull_request.merged == true) 45 | 46 | steps: 47 | - uses: actions/checkout@v4 48 | # We need to download lots of history so that 49 | # github-changelog can discover what's changed since the last release 50 | with: 51 | fetch-depth: 0 52 | ref: 'main' 53 | - uses: actions/setup-node@v4 54 | with: 55 | node-version: 18 56 | 57 | - uses: pnpm/action-setup@v4 58 | with: 59 | version: 10 60 | - run: pnpm install --frozen-lockfile 61 | 62 | - name: "Generate Explanation and Prep Changelogs" 63 | id: explanation 64 | run: | 65 | set +e 66 | 67 | pnpm release-plan prepare 2> >(tee -a stderr.log >&2) 68 | 69 | 70 | if [ $? -ne 0 ]; then 71 | echo 'text<> $GITHUB_OUTPUT 72 | cat stderr.log >> $GITHUB_OUTPUT 73 | echo 'EOF' >> $GITHUB_OUTPUT 74 | else 75 | echo 'text<> $GITHUB_OUTPUT 76 | jq .description .release-plan.json -r >> $GITHUB_OUTPUT 77 | echo 'EOF' >> $GITHUB_OUTPUT 78 | fi 79 | env: 80 | GITHUB_AUTH: ${{ secrets.GITHUB_TOKEN }} 81 | 82 | - uses: peter-evans/create-pull-request@v6 83 | with: 84 | commit-message: "Prepare Release using 'release-plan'" 85 | labels: "internal" 86 | branch: release-preview 87 | title: Prepare Release 88 | body: | 89 | This PR is a preview of the release that [release-plan](https://github.com/embroider-build/release-plan) has prepared. To release you should just merge this PR 👍 90 | 91 | ----------------------------------------- 92 | 93 | ${{ steps.explanation.outputs.text }} 94 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # For every push to the master branch, this checks if the release-plan was 2 | # updated and if it was it will publish stable npm packages based on the 3 | # release plan 4 | 5 | name: Publish Stable 6 | 7 | on: 8 | workflow_dispatch: 9 | push: 10 | branches: 11 | - main 12 | - master 13 | 14 | concurrency: 15 | group: publish-${{ github.head_ref || github.ref }} 16 | cancel-in-progress: true 17 | 18 | jobs: 19 | check-plan: 20 | name: "Check Release Plan" 21 | runs-on: ubuntu-latest 22 | outputs: 23 | command: ${{ steps.check-release.outputs.command }} 24 | 25 | steps: 26 | - uses: actions/checkout@v4 27 | with: 28 | fetch-depth: 0 29 | ref: 'main' 30 | # This will only cause the `check-plan` job to have a result of `success` 31 | # when the .release-plan.json file was changed on the last commit. This 32 | # plus the fact that this action only runs on main will be enough of a guard 33 | - id: check-release 34 | run: if git diff --name-only HEAD HEAD~1 | grep -w -q ".release-plan.json"; then echo "command=release"; fi >> $GITHUB_OUTPUT 35 | 36 | publish: 37 | name: "NPM Publish" 38 | runs-on: ubuntu-latest 39 | needs: check-plan 40 | if: needs.check-plan.outputs.command == 'release' 41 | permissions: 42 | contents: write 43 | pull-requests: write 44 | 45 | steps: 46 | - uses: actions/checkout@v4 47 | - uses: actions/setup-node@v4 48 | with: 49 | node-version: 18 50 | # This creates an .npmrc that reads the NODE_AUTH_TOKEN environment variable 51 | registry-url: 'https://registry.npmjs.org' 52 | 53 | - uses: pnpm/action-setup@v4 54 | with: 55 | version: 10 56 | - run: pnpm install --frozen-lockfile 57 | - name: npm publish 58 | run: pnpm release-plan publish 59 | 60 | env: 61 | GITHUB_AUTH: ${{ secrets.GITHUB_TOKEN }} 62 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist/ 3 | /declarations/ 4 | 5 | # dependencies 6 | /node_modules/ 7 | 8 | # misc 9 | /.env* 10 | /.pnp* 11 | /.eslintcache 12 | /coverage/ 13 | /npm-debug.log* 14 | /testem.log 15 | /yarn-error.log 16 | 17 | # ember-try 18 | /.node_modules.ember-try/ 19 | /npm-shrinkwrap.json.ember-try 20 | /package.json.ember-try 21 | /package-lock.json.ember-try 22 | /yarn.lock.ember-try 23 | 24 | # broccoli-debug 25 | /DEBUG/ 26 | 27 | tests/dummy/app/snippets.js 28 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist/ 3 | /tmp/ 4 | 5 | # misc 6 | /.editorconfig 7 | /.ember-cli 8 | /.env* 9 | /.eslintcache 10 | /.eslintignore 11 | /.eslintrc.js 12 | /.git/ 13 | /.github/ 14 | /.gitignore 15 | /.prettierignore 16 | /.prettierrc.js 17 | /.stylelintignore 18 | /.stylelintrc.js 19 | /.template-lintrc.js 20 | /.travis.yml 21 | /.watchmanconfig 22 | /CONTRIBUTING.md 23 | /ember-cli-build.js 24 | /testem.js 25 | /tests/ 26 | /tsconfig.declarations.json 27 | /tsconfig.json 28 | /yarn-error.log 29 | /yarn.lock 30 | .gitkeep 31 | 32 | # ember-try 33 | /.node_modules.ember-try/ 34 | /npm-shrinkwrap.json.ember-try 35 | /package.json.ember-try 36 | /package-lock.json.ember-try 37 | /yarn.lock.ember-try 38 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | #################### 2 | # super strict mode 3 | #################### 4 | auto-install-peers=false 5 | strict-peer-dependents=true 6 | resolve-peers-from-workspace-root=false 7 | 8 | ################ 9 | # Optimizations 10 | ################ 11 | # Less strict, but required for tooling to not barf on duplicate peer trees. 12 | # (many libraries declare the same peers, which resolve to the same 13 | # versions) 14 | peers-suffix-max-length=40 15 | dedupe-injected-deps=true 16 | dedupe-peer-dependents=true 17 | public-hoist-pattern[]=ember-source 18 | sync-injected-deps-after-scripts[]=build 19 | sync-injected-deps-after-scripts[]=sync 20 | inject-workspace-packages=true 21 | 22 | ################ 23 | # Compatibility 24 | ################ 25 | # highest is what everyone is used to, but 26 | # not ensuring folks are actually compatible with declared ranges. 27 | resolution-mode=highest -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # unconventional js 2 | /blueprints/*/files/ 3 | 4 | # compiled output 5 | /dist/ 6 | 7 | # misc 8 | /coverage/ 9 | !.* 10 | .*/ 11 | 12 | # ember-try 13 | /.node_modules.ember-try/ 14 | 15 | tests/dummy/app/snippets.js 16 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | overrides: [ 5 | { 6 | files: '*.{js,ts}', 7 | options: { 8 | singleQuote: true, 9 | }, 10 | }, 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /.release-plan.json: -------------------------------------------------------------------------------- 1 | { 2 | "solution": { 3 | "ember-cp-validations": { 4 | "impact": "major", 5 | "oldVersion": "6.0.1", 6 | "newVersion": "7.0.0", 7 | "constraints": [ 8 | { 9 | "impact": "major", 10 | "reason": "Appears in changelog section :boom: Breaking Change" 11 | }, 12 | { 13 | "impact": "patch", 14 | "reason": "Appears in changelog section :bug: Bug Fix" 15 | }, 16 | { 17 | "impact": "patch", 18 | "reason": "Appears in changelog section :house: Internal" 19 | } 20 | ], 21 | "pkgJSONPath": "./package.json" 22 | } 23 | }, 24 | "description": "## Release (2025-04-17)\n\nember-cp-validations 7.0.0 (major)\n\n#### :boom: Breaking Change\n* `ember-cp-validations`\n * [#757](https://github.com/adopted-ember-addons/ember-cp-validations/pull/757) Drop support for node < 18, Ember < 4.4 ([@RobbieTheWagner](https://github.com/RobbieTheWagner))\n\n#### :bug: Bug Fix\n* `ember-cp-validations`\n * [#756](https://github.com/adopted-ember-addons/ember-cp-validations/pull/756) Fix deprecation with path.original ([@RobbieTheWagner](https://github.com/RobbieTheWagner))\n\n#### :house: Internal\n* `ember-cp-validations`\n * [#749](https://github.com/adopted-ember-addons/ember-cp-validations/pull/749) update @embroider/test-setup to fix CI ([@mansona](https://github.com/mansona))\n\n#### Committers: 2\n- Chris Manson ([@mansona](https://github.com/mansona))\n- Robbie Wagner ([@RobbieTheWagner](https://github.com/RobbieTheWagner))\n" 25 | } 26 | -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | # unconventional files 2 | /blueprints/*/files/ 3 | 4 | # compiled output 5 | /dist/ 6 | 7 | # addons 8 | /.node_modules.ember-try/ 9 | -------------------------------------------------------------------------------- /.stylelintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | extends: ['stylelint-config-standard', 'stylelint-prettier/recommended'], 5 | }; 6 | -------------------------------------------------------------------------------- /.template-lintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | extends: 'recommended', 5 | }; 6 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | pnpm 10.7.1 -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": ["dist"] 3 | } 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How To Contribute 2 | 3 | ## Installation 4 | 5 | - `git clone ` 6 | - `cd ember-cp-validations` 7 | - `pnpm install` 8 | 9 | ## Linting 10 | 11 | - `pnpm lint` 12 | - `pnpm lint:fix` 13 | 14 | ## Running tests 15 | 16 | - `pnpm test` – Runs the test suite on the current Ember version 17 | - `pnpm test:ember --server` – Runs the test suite in "watch mode" 18 | - `pnpm test:ember-compatibility` – Runs the test suite against multiple Ember versions 19 | 20 | ## Running the dummy application 21 | 22 | - `pnpm start` 23 | - Visit the dummy application at [http://localhost:4200](http://localhost:4200). 24 | 25 | For more information on using ember-cli, visit [https://cli.emberjs.com/release/](https://cli.emberjs.com/release/). 26 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2016 Yahoo Inc. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | * Neither the name of the Yahoo Inc. nor the 15 | names of its contributors may be used to endorse or promote products 16 | derived from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL YAHOO! INC. BE LIABLE FOR ANY 22 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 25 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ember CP Validations 2 | 3 | [![Build Status](https://github.com/adopted-ember-addons/ember-cp-validations/actions/workflows/ci.yml/badge.svg)](https://github.com/adopted-ember-addons/ember-cp-validations/actions/workflows/ci.yml) 4 | [![npm version](https://badge.fury.io/js/ember-cp-validations.svg)](http://badge.fury.io/js/ember-cp-validations) 5 | [![Download Total](https://img.shields.io/npm/dt/ember-cp-validations.svg)](http://badge.fury.io/js/ember-cp-validations) 6 | [![Ember Observer Score](http://emberobserver.com/badges/ember-cp-validations.svg)](http://emberobserver.com/addons/ember-cp-validations) 7 | 8 | An EmberJS validation framework that is completely and utterly computed property based. 9 | 10 | ## Compatibility 11 | 12 | | Addon | Ember | Node | 13 | |-------|---------|---------| 14 | | 7.x | >= 4.4 | >= 18.x | 15 | | 6.x | >= 3.28 | >= 14.x | 16 | | 5.x | >= 3.28 | >= 12.x | 17 | | 4.x | <= 3.28 | >= 12.x | 18 | 19 | 20 | ## Features 21 | 22 | **No observers were used nor harmed while developing and testing this addon.** 23 | 24 | - Lazily computed validations 25 | - Ruby on rails inspired validators 26 | - Support for Ember Data Models, Objects, Components, Services, etc. 27 | - Support for nested models and objects 28 | - Synchronous and asynchronous support 29 | - Easily integrated with Ember Data 30 | - No observers. Seriously... there are none. Like absolutely zero.... 31 | - Custom validators 32 | - I18n support 33 | - Debounceable validations 34 | - Warning validations 35 | 36 | [![Introduction to Ember CP Validations](https://cloud.githubusercontent.com/assets/2922250/21854491/ebda55b8-d7e8-11e6-8d13-00dff93be8d8.png)](https://embermap.com/video/ember-cp-validations) 37 | 38 | You can also learn more by watching this Global Ember Meetup talk: 39 | 40 | [![Introduction to ember-cp-validations](https://i.vimeocdn.com/video/545445254.png?mw=1920&mh=1080&q=70)](https://vimeo.com/146857699) 41 | 42 | ## Installation 43 | 44 | ```shell 45 | ember install ember-cp-validations 46 | ``` 47 | 48 | ## Upgrading to 4.x 49 | 50 | If you are upgrading from 3.x to 4.x, please checkout the [upgrading documentation](UPGRADING.md). 51 | 52 | ## Helpful Links 53 | 54 | - ### [Live Demo](http://adopted-ember-addons.github.io/ember-cp-validations) 55 | 56 | - ### Documentation 57 | 58 | - [4.x](http://adopted-ember-addons.github.io/ember-cp-validations/docs) 59 | - [3.x](https://rawgit.com/adopted-ember-addons/ember-cp-validations/c4123c983e54f24dd790ffa1bad66cfdf2f47ec6/docs/index.html) 60 | 61 | - ### [Changelog](CHANGELOG.md) 62 | 63 | ## Looking for help? 64 | 65 | - If it is a bug [please open an issue on GitHub](http://github.com/adopted-ember-addons/ember-cp-validations/issues). 66 | - Ask a question in the `#e-cp-validations` channel at the [Ember.js Community Slack](https://embercommunity.slack.com) 67 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release Process 2 | 3 | Releases in this repo are mostly automated using [release-plan](https://github.com/embroider-build/release-plan/). Once you label all your PRs correctly (see below) you will have an automatically generated PR that updates your CHANGELOG.md file and a `.release-plan.json` that is used to prepare the release once the PR is merged. 4 | 5 | ## Preparation 6 | 7 | Since the majority of the actual release process is automated, the remaining tasks before releasing are: 8 | 9 | - correctly labeling **all** pull requests that have been merged since the last release 10 | - updating pull request titles so they make sense to our users 11 | 12 | Some great information on why this is important can be found at [keepachangelog.com](https://keepachangelog.com/en/1.1.0/), but the overall 13 | guiding principle here is that changelogs are for humans, not machines. 14 | 15 | When reviewing merged PR's the labels to be used are: 16 | 17 | * breaking - Used when the PR is considered a breaking change. 18 | * enhancement - Used when the PR adds a new feature or enhancement. 19 | * bug - Used when the PR fixes a bug included in a previous release. 20 | * documentation - Used when the PR adds or updates documentation. 21 | * internal - Internal changes or things that don't fit in any other category. 22 | 23 | **Note:** `release-plan` requires that **all** PRs are labeled. If a PR doesn't fit in a category it's fine to label it as `internal` 24 | 25 | ## Release 26 | 27 | Once the prep work is completed, the actual release is straight forward: you just need to merge the open [Plan Release](https://github.com/adopted-ember-addons/ember-cp-validations/pulls?q=is%3Apr+is%3Aopen+%22Prepare+Release%22+in%3Atitle) PR 28 | -------------------------------------------------------------------------------- /UPGRADING.md: -------------------------------------------------------------------------------- 1 | # Upgrading v3.x to 4.x 2 | 3 | This document is here to show breaking changes when upgrading from v3.x to v4.x. 4 | 5 | ## Support Latest 2 LTS Releases 6 | 7 | As Ember is evolving, we have to be able to keep up. v3.x supported Ember versions as old 8 | as 1.11 which not only made this addon difficult to maintain, but also added a 9 | lot of bloat. Going forward, this addon will target and test against only the 2 10 | latest LTS releases. 11 | 12 | ## Inline Validator 13 | 14 | This library has always supported the ability to pass in a custom validate function 15 | to the `validator` but it didn't feel consistent with the rest of the API. To normalize 16 | this, we created a new `inline` validator that you can pass a function to via 17 | the `validate` option. 18 | 19 | **Before (3.x)** 20 | 21 | ```javascript 22 | validator(function(value, options, model, attribute) { 23 | return value === options.username ? 24 | true : 25 | `Username must be ${options.username}`; 26 | }, { 27 | username: 'offirgolan' 28 | }); 29 | ``` 30 | 31 | **After (4.x)** 32 | 33 | ```javascript 34 | validator('inline', { 35 | username: 'offirgolan', 36 | validate(value, options, model, attribute) { 37 | return value === options.username ? 38 | true : 39 | `Username must be ${options.username}`; 40 | } 41 | }); 42 | ``` 43 | -------------------------------------------------------------------------------- /addon/-private/ember-internals.js: -------------------------------------------------------------------------------- 1 | import { 2 | isClassicDecorator, 3 | descriptorForDecorator, 4 | } from '@ember/-internals/metal'; 5 | 6 | export function getDependentKeys(descriptorOrDecorator) { 7 | if (descriptorForDecorator) { 8 | let descriptor = descriptorForDecorator(descriptorOrDecorator); 9 | return descriptor._dependentKeys || [descriptor.altKey]; 10 | } else { 11 | return descriptorOrDecorator._dependentKeys; 12 | } 13 | } 14 | 15 | export function isDescriptor(o) { 16 | if (isClassicDecorator) { 17 | return isClassicDecorator(o); 18 | } else { 19 | return ( 20 | o && (typeof o === 'object' || typeof o === 'function') && o.isDescriptor 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /addon/-private/ember-validator.js: -------------------------------------------------------------------------------- 1 | import Base from 'ember-cp-validations/validators/base'; 2 | import { validate as _validate } from 'ember-validators'; 3 | 4 | export default Base.extend({ 5 | validate() { 6 | let result = _validate(this._evType, ...arguments); 7 | 8 | if (result && typeof result === 'object') { 9 | return result.message 10 | ? result.message 11 | : this.createErrorMessage(result.type, result.value, result.context); 12 | } 13 | 14 | return result; 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /addon/-private/internal-result-object.js: -------------------------------------------------------------------------------- 1 | import EmberObject, { computed, set } from '@ember/object'; 2 | import { and, not, readOnly } from '@ember/object/computed'; 3 | import { isNone } from '@ember/utils'; 4 | import { makeArray } from '@ember/array'; 5 | import ValidationError from '../validations/error'; 6 | import { isPromise } from '../utils/utils'; 7 | 8 | export default EmberObject.extend({ 9 | model: null, 10 | isValid: true, 11 | isValidating: false, 12 | message: null, 13 | warningMessage: null, 14 | attribute: '', 15 | 16 | _promise: null, 17 | _validator: null, 18 | _type: readOnly('_validator._type'), 19 | 20 | init() { 21 | this._super(...arguments); 22 | 23 | if (this.isAsync) { 24 | this._handlePromise(); 25 | } 26 | }, 27 | 28 | isWarning: readOnly('_validator.isWarning'), 29 | isInvalid: not('isValid'), 30 | isNotValidating: not('isValidating'), 31 | isTruelyValid: and('isNotValidating', 'isValid'), 32 | isTruelyInvalid: and('isNotValidating', 'isInvalid'), 33 | 34 | isAsync: computed('_promise', function () { 35 | return isPromise(this._promise); 36 | }), 37 | 38 | messages: computed('message', function () { 39 | return makeArray(this.message); 40 | }), 41 | 42 | error: computed( 43 | '_type', 44 | 'attribute', 45 | 'isInvalid', 46 | 'message', 47 | 'type', 48 | function () { 49 | if (this.isInvalid) { 50 | return ValidationError.create({ 51 | type: this._type, 52 | message: this.message, 53 | attribute: this.attribute, 54 | }); 55 | } 56 | 57 | return null; 58 | }, 59 | ), 60 | 61 | errors: computed('error', function () { 62 | return makeArray(this.error); 63 | }), 64 | 65 | warningMessages: computed('warningMessage', function () { 66 | return makeArray(this.warningMessage); 67 | }), 68 | 69 | warning: computed( 70 | '_type', 71 | 'attribute', 72 | 'isWarning', 73 | 'type', 74 | 'warningMessage', 75 | function () { 76 | if (this.isWarning && !isNone(this.warningMessage)) { 77 | return ValidationError.create({ 78 | type: this._type, 79 | message: this.warningMessage, 80 | attribute: this.attribute, 81 | }); 82 | } 83 | 84 | return null; 85 | }, 86 | ), 87 | 88 | warnings: computed('warning', function () { 89 | return makeArray(this.warning); 90 | }), 91 | 92 | _handlePromise() { 93 | set(this, 'isValidating', true); 94 | 95 | this._promise.finally(() => { 96 | set(this, 'isValidating', false); 97 | }); 98 | }, 99 | }); 100 | -------------------------------------------------------------------------------- /addon/-private/options.js: -------------------------------------------------------------------------------- 1 | import EmberObject, { get } from '@ember/object'; 2 | 3 | const { keys } = Object; 4 | const OPTION_KEYS = '__option_keys__'; 5 | 6 | const OptionsObject = EmberObject.extend({ 7 | toObject() { 8 | return this[OPTION_KEYS].reduce((obj, key) => { 9 | obj[key] = get(this, key); 10 | return obj; 11 | }, {}); 12 | }, 13 | }); 14 | 15 | export default class Options { 16 | constructor({ model, attribute, options = {} }) { 17 | const optionKeys = keys(options); 18 | const createParams = { [OPTION_KEYS]: optionKeys, model, attribute }; 19 | 20 | // we have to extend here in case anyone passes options that have computedProperties. 21 | return OptionsObject.extend(options).create(createParams); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /addon/-private/result.js: -------------------------------------------------------------------------------- 1 | import { isNone } from '@ember/utils'; 2 | 3 | import { isArray } from '@ember/array'; 4 | import EmberObject, { setProperties, computed, set } from '@ember/object'; 5 | import { readOnly } from '@ember/object/computed'; 6 | import ResultCollection from '../validations/result-collection'; 7 | import WarningResultCollection from '../validations/warning-result-collection'; 8 | import InternalResultObject from './internal-result-object'; 9 | 10 | /** 11 | * __PRIVATE__ 12 | * 13 | * @module Validations 14 | * @class Result 15 | * @private 16 | */ 17 | 18 | const Result = EmberObject.extend({ 19 | /** 20 | * @property model 21 | * @type {Object} 22 | */ 23 | model: null, 24 | 25 | /** 26 | * @property attribute 27 | * @type {String} 28 | */ 29 | attribute: '', 30 | 31 | /** 32 | * @property _promise 33 | * @async 34 | * @private 35 | * @type {Promise} 36 | */ 37 | _promise: null, 38 | 39 | /** 40 | * The validator that returned this result 41 | * @property _validator 42 | * @private 43 | * @type {Validator} 44 | */ 45 | _validator: null, 46 | 47 | /** 48 | * Determines if the _result object is readOnly. 49 | * 50 | * This is needed because ResultCollections and global validation objects control their own 51 | * state via CPs 52 | * 53 | * @property _isReadOnly 54 | * @private 55 | * @readOnly 56 | * @type {Boolean} 57 | */ 58 | _isReadOnly: computed('_result', function () { 59 | let validations = this._result; 60 | return validations instanceof ResultCollection || validations.isValidations; 61 | }).readOnly(), 62 | 63 | /** 64 | * @property isWarning 65 | * @readOnly 66 | * @type {Boolean} 67 | */ 68 | isWarning: readOnly('_validator.isWarning'), 69 | 70 | /** 71 | * @property isValid 72 | * @readOnly 73 | * @type {Boolean} 74 | */ 75 | isValid: readOnly('_result.isValid'), 76 | 77 | /** 78 | * @property isInvalid 79 | * @readOnly 80 | * @type {Boolean} 81 | */ 82 | isInvalid: readOnly('_result.isInvalid'), 83 | 84 | /** 85 | * @property isValidating 86 | * @readOnly 87 | * @type {Boolean} 88 | */ 89 | isValidating: readOnly('_result.isValidating'), 90 | 91 | /** 92 | * @property isTruelyValid 93 | * @readOnly 94 | * @type {Boolean} 95 | */ 96 | isTruelyValid: readOnly('_result.isTruelyValid'), 97 | 98 | /** 99 | * @property isTruelyInvalid 100 | * @readOnly 101 | * @type {Boolean} 102 | */ 103 | isTruelyInvalid: readOnly('_result.isTruelyInvalid'), 104 | 105 | /** 106 | * @property isAsync 107 | * @readOnly 108 | * @type {Boolean} 109 | */ 110 | isAsync: readOnly('_result.isAsync'), 111 | 112 | /** 113 | * @property message 114 | * @readOnly 115 | * @type {String} 116 | */ 117 | message: readOnly('_result.message'), 118 | 119 | /** 120 | * @property messages 121 | * @readOnly 122 | * @type {Array} 123 | */ 124 | messages: readOnly('_result.messages'), 125 | 126 | /** 127 | * @property error 128 | * @readOnly 129 | * @type {Object} 130 | */ 131 | error: readOnly('_result.error'), 132 | 133 | /** 134 | * @property errors 135 | * @readOnly 136 | * @type {Array} 137 | */ 138 | errors: readOnly('_result.errors'), 139 | 140 | /** 141 | * @property warningMessage 142 | * @readOnly 143 | * @type {String} 144 | */ 145 | warningMessage: readOnly('_result.warningMessage'), 146 | 147 | /** 148 | * @property warningMessages 149 | * @readOnly 150 | * @type {Array} 151 | */ 152 | warningMessages: readOnly('_result.warningMessages'), 153 | 154 | /** 155 | * @property warning 156 | * @readOnly 157 | * @type {Object} 158 | */ 159 | warning: readOnly('_result.warning'), 160 | 161 | /** 162 | * @property warnings 163 | * @readOnly 164 | * @type {Array} 165 | */ 166 | warnings: readOnly('_result.warnings'), 167 | 168 | /** 169 | * This hold all the logic for the above CPs. We do this so we can easily switch this object out with a different validations object 170 | * @property _result 171 | * @private 172 | * @type {Result} 173 | */ 174 | _result: computed( 175 | 'model', 176 | 'attribute', 177 | '_promise', 178 | '_validator', 179 | '_resultOverride', 180 | function () { 181 | let { model, attribute, _promise, _validator } = this; 182 | 183 | return ( 184 | this._resultOverride || 185 | InternalResultObject.create({ model, attribute, _promise, _validator }) 186 | ); 187 | }, 188 | ), 189 | 190 | init() { 191 | this._super(...arguments); 192 | 193 | if (this.isAsync && !this._isReadOnly) { 194 | this._handlePromise(); 195 | } 196 | }, 197 | 198 | /** 199 | * Update the current validation result object with the given value 200 | * - If value is undefined or null, set isValid to false 201 | * - If value is a validations object from a different model/object, set the _result object to the one given (belongs-to) 202 | * - If value is a collection of result objects, create a Validation Result Collection and set that to the _result object (has-many) 203 | * - If value is a string, set the message to the given string and set isValid to false 204 | * - If value is a boolean, set isValid to result 205 | * - If value is a pojo, update _result object with the information given 206 | * 207 | * @method update 208 | * @private 209 | * @param value 210 | */ 211 | update(value) { 212 | let result = this._result; 213 | let attribute = this.attribute; 214 | let isWarning = this.isWarning; 215 | let Collection = isWarning ? WarningResultCollection : ResultCollection; 216 | 217 | if (isNone(value)) { 218 | return this.update(false); 219 | } else if (value.isValidations) { 220 | this._overrideResult(Collection.create({ attribute, content: [value] })); 221 | } else if (isArray(value)) { 222 | this._overrideResult(Collection.create({ attribute, content: value })); 223 | } else if (!this._isReadOnly) { 224 | this._overrideResult(undefined); 225 | 226 | if (typeof value === 'string') { 227 | setProperties(this._result, { 228 | [isWarning ? 'warningMessage' : 'message']: value, 229 | isValid: isWarning ? true : false, 230 | }); 231 | } else if (typeof value === 'boolean') { 232 | set(result, 'isValid', value); 233 | } else if (typeof value === 'object') { 234 | setProperties(result, value); 235 | } 236 | } 237 | }, 238 | 239 | /** 240 | * Override the internal _result property. 241 | * @method _overrideResult 242 | * @param result 243 | * @private 244 | */ 245 | _overrideResult(result) { 246 | set(this, '_resultOverride', result); 247 | }, 248 | 249 | /** 250 | * Promise handler 251 | * @method _handlePromise 252 | * @private 253 | */ 254 | _handlePromise() { 255 | this._promise 256 | .then( 257 | (value) => this.update(value), 258 | (value) => this.update(value), 259 | ) 260 | .catch((reason) => { 261 | // TODO: send into error state 262 | throw reason; 263 | }); 264 | }, 265 | }); 266 | 267 | export default Result; 268 | -------------------------------------------------------------------------------- /addon/-private/symbols.js: -------------------------------------------------------------------------------- 1 | export const VALIDATIONS_CLASS = '__VALIDATIONS_CLASS__'; 2 | export const IS_VALIDATIONS_CLASS = '__IS_VALIDATIONS_CLASS__'; 3 | export const ATTRS_MODEL = '__ATTRS_MODEL__'; 4 | export const ATTRS_PATH = '__ATTRS_PATH__'; 5 | export const ATTRS_RESULT_COLLECTION = '__ATTRS_RESULT_COLLECTION__'; 6 | -------------------------------------------------------------------------------- /addon/utils/array.js: -------------------------------------------------------------------------------- 1 | import { A as emberArray } from '@ember/array'; 2 | 3 | const A = emberArray(); 4 | 5 | export function callable(method) { 6 | return function (collection) { 7 | return A[method].apply(collection, arguments); 8 | }; 9 | } 10 | 11 | export const uniq = callable('uniq'); 12 | export const compact = callable('compact'); 13 | 14 | export function flatten(array = []) { 15 | let result = []; 16 | 17 | for (let i = 0, l = array.length; i < l; i++) { 18 | let item = array[i]; 19 | 20 | if (Array.isArray(item)) { 21 | result = result.concat(flatten(item)); 22 | } else { 23 | result.push(item); 24 | } 25 | } 26 | 27 | return result; 28 | } 29 | -------------------------------------------------------------------------------- /addon/utils/cycle-breaker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Use Ember Meta to break cycles in the CP chains. Lets say we have a User model with a `friends` property that is a hasMany 3 | * relationship. If we have a user John and he has a friend Jane, that creates a two-way relationship. John is Jane's friends and vise 4 | * versa. If we were to go down the CP chain and get validations for John's friends, it would go to Jane, then to Jane's friends, which 5 | * would point back to John. This method tracks which models have been already visited and breaks the cycle. 6 | */ 7 | 8 | import MetaData from './meta-data'; 9 | 10 | export default function cycleBreaker(fn, value) { 11 | let key = MetaData.symbol('cycle'); 12 | 13 | return function () { 14 | if (MetaData.getData(this, key)) { 15 | return value; 16 | } 17 | MetaData.setData(this, key, true); 18 | try { 19 | return fn.apply(this, arguments); 20 | } finally { 21 | MetaData.setData(this, key, false); 22 | } 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /addon/utils/deep-set.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Assigns a value to an object via the given path while creating new objects if 3 | * the pathing requires it. If the given path is `foo.bar`, it will create a new object (obj.foo) 4 | * and assign value to obj.foo.bar. If the given object is an EmberObject, it will create new EmberObjects. 5 | */ 6 | import { isDescriptor } from './utils'; 7 | import { isNone } from '@ember/utils'; 8 | import EmberObject, { defineProperty, set, get } from '@ember/object'; 9 | 10 | export default function deepSet( 11 | obj, 12 | path, 13 | value, 14 | useEmberObject = false, 15 | delimiter = '.', 16 | ) { 17 | let keyPath = path.split(delimiter); 18 | let lastKeyIndex = keyPath.length - 1; 19 | let currObj = obj; 20 | 21 | // Iterate over each key in the path (minus the last one which is the property to be assigned) 22 | for (let i = 0; i < lastKeyIndex; ++i) { 23 | let key = keyPath[i]; 24 | 25 | // Create a new object if it doesnt exist 26 | if (isNone(get(currObj, key))) { 27 | set(currObj, key, useEmberObject ? EmberObject.create() : {}); 28 | } 29 | currObj = get(currObj, key); 30 | } 31 | 32 | if (isDescriptor(value)) { 33 | defineProperty(currObj, keyPath[lastKeyIndex], value); 34 | } else { 35 | set(currObj, keyPath[lastKeyIndex], value); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /addon/utils/get-with-default.js: -------------------------------------------------------------------------------- 1 | import { get } from '@ember/object'; 2 | 3 | export default function getWithDefault(obj, key, defaultValue) { 4 | let result = get(obj, key); 5 | 6 | if (result === undefined) { 7 | result = defaultValue; 8 | } 9 | return result; 10 | } 11 | -------------------------------------------------------------------------------- /addon/utils/lookup-validator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Lookup a validator of a specific type on the owner 3 | * 4 | * @param {Ember.Owner} owner 5 | * @param {String} type 6 | * @throws {Error} Validator not found 7 | * @return {Class} Validator class 8 | */ 9 | export default function lookupValidator(owner, type) { 10 | if (!owner) { 11 | throw new Error( 12 | `[ember-cp-validations] \`lookupValidator\` requires owner/container access.`, 13 | ); 14 | } 15 | 16 | const validatorClass = owner.factoryFor(`validator:${type}`); 17 | 18 | if (!validatorClass) { 19 | throw new Error( 20 | `[ember-cp-validations] Validator not found of type: ${type}.`, 21 | ); 22 | } 23 | 24 | return validatorClass; 25 | } 26 | -------------------------------------------------------------------------------- /addon/utils/meta-data.js: -------------------------------------------------------------------------------- 1 | import { meta } from '@ember/-internals/meta'; 2 | 3 | let id = 0; 4 | const dataKey = symbol('data'); 5 | 6 | function symbol(key) { 7 | return `_${key}_${new Date().getTime()}_${id++}`; 8 | } 9 | 10 | function getData(obj, s) { 11 | let m = meta(obj); 12 | let data = m[dataKey]; 13 | 14 | if (data) { 15 | return data[s]; 16 | } 17 | } 18 | 19 | function setData(obj, s, value) { 20 | let m = meta(obj); 21 | let data = (m[dataKey] = m[dataKey] || {}); 22 | 23 | data[s] = value; 24 | } 25 | 26 | export default { symbol, getData, setData }; 27 | -------------------------------------------------------------------------------- /addon/utils/should-call-super.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Checks if the give key exists on the object's super. 3 | * If so, we can successfully call the obj[key] _super 4 | * 5 | * Created by @rwjblue 6 | */ 7 | export default function shouldCallSuper(obj, key) { 8 | let current = Object.getPrototypeOf(obj); 9 | current = Object.getPrototypeOf(current); 10 | 11 | while (current) { 12 | let descriptor = Object.getOwnPropertyDescriptor(current, key); 13 | 14 | if (descriptor) { 15 | return true; 16 | } 17 | 18 | current = Object.getPrototypeOf(current); 19 | } 20 | 21 | return false; 22 | } 23 | -------------------------------------------------------------------------------- /addon/utils/utils.js: -------------------------------------------------------------------------------- 1 | import ArrayProxy from '@ember/array/proxy'; 2 | import ObjectProxy from '@ember/object/proxy'; 3 | import { isHTMLSafe } from '@ember/template'; 4 | import EmberObject from '@ember/object'; 5 | import { typeOf } from '@ember/utils'; 6 | import { A as emberArray } from '@ember/array'; 7 | import { 8 | macroCondition, 9 | dependencySatisfies, 10 | importSync, 11 | } from '@embroider/macros'; 12 | 13 | let DS; 14 | if (macroCondition(dependencySatisfies('ember-data', '*'))) { 15 | DS = importSync('ember-data').default; 16 | } 17 | 18 | export { getDependentKeys, isDescriptor } from '../-private/ember-internals'; 19 | 20 | export function unwrapString(s) { 21 | if (isHTMLSafe(s)) { 22 | return s.toString(); 23 | } 24 | 25 | return s; 26 | } 27 | 28 | export function unwrapProxy(o) { 29 | return isProxy(o) ? unwrapProxy(o.content) : o; 30 | } 31 | 32 | export function isProxy(o) { 33 | return !!(o && (o instanceof ObjectProxy || o instanceof ArrayProxy)); 34 | } 35 | 36 | function canInvoke(obj, methodName) { 37 | return ( 38 | obj !== null && obj !== undefined && typeof obj[methodName] === 'function' 39 | ); 40 | } 41 | 42 | export function isPromise(p) { 43 | return !!(p && canInvoke(p, 'then')); 44 | } 45 | 46 | export function isDsModel(o) { 47 | return !!(DS && o && o instanceof DS.Model); 48 | } 49 | 50 | export function isDSManyArray(o) { 51 | return !!( 52 | DS && 53 | o && 54 | (o instanceof DS.PromiseManyArray || o instanceof DS.ManyArray) 55 | ); 56 | } 57 | 58 | export function isEmberObject(o) { 59 | return !!(o && o instanceof EmberObject); 60 | } 61 | 62 | export function isObject(o) { 63 | return typeOf(o) === 'object' || typeOf(o) === 'instance'; 64 | } 65 | 66 | export function isValidatable(value) { 67 | let v = unwrapProxy(value); 68 | return isDsModel(v) ? !v.isDeleted : true; 69 | } 70 | 71 | export function getValidatableValue(value) { 72 | if (!value) { 73 | return value; 74 | } 75 | 76 | if (isDSManyArray(value)) { 77 | return emberArray(value.filter((v) => isValidatable(v))); 78 | } 79 | 80 | return isValidatable(value) ? value : undefined; 81 | } 82 | 83 | export function mergeOptions(...options) { 84 | let o = {}; 85 | 86 | for (let i = options.length - 1; i >= 0; i--) { 87 | let _o = options[i]; 88 | Object.assign(o, isObject(_o) ? _o : {}); 89 | } 90 | 91 | return o; 92 | } 93 | -------------------------------------------------------------------------------- /addon/validations/error.js: -------------------------------------------------------------------------------- 1 | import EmberObject from '@ember/object'; 2 | 3 | /** 4 | * @module Validations 5 | * @class Error 6 | */ 7 | 8 | export default EmberObject.extend({ 9 | /** 10 | * The error validator type 11 | * @property type 12 | * @type {String} 13 | */ 14 | type: null, 15 | 16 | /** 17 | * The error message 18 | * @property message 19 | * @type {String} 20 | */ 21 | message: null, 22 | 23 | /** 24 | * The attribute that the error belongs to 25 | * @property attribute 26 | * @type {String} 27 | */ 28 | attribute: null, 29 | 30 | /** 31 | * The parent attribute that the error belongs to 32 | * @property parentAttribute 33 | * @type {String} 34 | */ 35 | parentAttribute: null, 36 | }); 37 | -------------------------------------------------------------------------------- /addon/validations/validator.js: -------------------------------------------------------------------------------- 1 | import { isNone } from '@ember/utils'; 2 | import { deprecate } from '@ember/debug'; 3 | 4 | /** 5 | * @module Validators 6 | * @main Validators 7 | */ 8 | 9 | /** 10 | * ### description 11 | * 12 | * Default: __'This field'__ 13 | * 14 | * A descriptor for your attribute used in the error message strings. 15 | * You can overwrite this value in your `validators/messages.js` file by changing the `defaultDescription` property. 16 | * 17 | * ```javascript 18 | * // Examples 19 | * validator('date', { 20 | * description: 'Date of birth' 21 | * }) 22 | * // If validation is run and the attribute is empty, the error returned will be: 23 | * // 'Date of birth can't be blank' 24 | * ``` 25 | * 26 | * ### lazy 27 | * 28 | * Default: __true__ 29 | * 30 | * Only validate the given validator if the attribute is not already in an invalid 31 | * state. When you have multiple validators on an attribute, it will only validate subsequent 32 | * validators if the preceding validators have passed. When set to __false__, the validator 33 | * will always be executed, even if its preceding validators are invalid. 34 | * 35 | * ```javascript 36 | * // Examples 37 | * buildValidations({ 38 | * username: [ 39 | * validator('presence', true), 40 | * validator('length', { min: 5 }), 41 | * validator('custom-promise-based-validator') // Will only be executed if the above two have passed 42 | * ] 43 | * }); 44 | * 45 | * validator('custom-validator-that-must-executed', { 46 | * lazy: false 47 | * }) 48 | * ``` 49 | * 50 | * ### dependentKeys 51 | * 52 | * A list of other model specific dependents for you validator. 53 | * 54 | * ```javascript 55 | * // Examples 56 | * validator('has-friends', { 57 | * dependentKeys: ['model.friends.[]'] 58 | * }) 59 | * validator('has-valid-friends', { 60 | * dependentKeys: ['model.friends.@each.username'] 61 | * }) 62 | * validator('x-validator', { 63 | * dependentKeys: ['model.username', 'model.email', 'model.meta.foo.bar'] 64 | * }) 65 | * ``` 66 | * 67 | * ### disabled 68 | * 69 | * Default: __false__ 70 | * 71 | * If set to __true__, disables the given validator. 72 | * 73 | * ```js 74 | * // Examples 75 | * validator('presence', { 76 | * presence: true, 77 | * disabled: true 78 | * }) 79 | * validator('presence', { 80 | * presence: true, 81 | * disabled: computed.not('model.shouldValidate') 82 | * }) 83 | * ``` 84 | * 85 | * ### debounce 86 | * 87 | * Default: __0__ 88 | * 89 | * Debounces the validation with the given time in `milliseconds`. All debounced validations will 90 | * be handled asynchronously (wrapped in a promise). 91 | * 92 | * ```javascript 93 | * // Examples 94 | * validator('length', { 95 | * debounce: 500 96 | * }) 97 | * validator('x-validator', { 98 | * debounce: 250 99 | * }) 100 | * ``` 101 | * 102 | * ### isWarning 103 | * 104 | * Default: __false__ 105 | * 106 | * Any validator can be declared as a warning validator by setting `isWarning` to true. These validators will act as 107 | * assertions that when return a message, will be placed under `warnings` and `warningMessages` collections. What this means, 108 | * is that these validators will not have any affect on the valid state of the attribute allowing you to display warning messages 109 | * even when the attribute is valid. 110 | * 111 | * ```javascript 112 | * // Examples 113 | * validator('length', { 114 | * isWarning: true, 115 | * min: 6, 116 | * message: 'Password is weak' 117 | * }) 118 | * ``` 119 | * 120 | * ### value 121 | * 122 | * Used to retrieve the value to validate. This will overwrite the validator's default `value` method. 123 | * By default this returns `model[attribute]`. If you are dependent on other model attributes, you will 124 | * need to add them as `dependentKeys`. 125 | * 126 | * ```javascript 127 | * // Examples 128 | * validator('date', { 129 | * value(model, attribute) { 130 | * // Format the original value before passing it into the validator 131 | * return moment().utc(model.get(attribute)).format('DD/MM/YYY'); 132 | * } 133 | * }) 134 | * validator('number', { 135 | * dependentKeys: ['someOtherAttr'], 136 | * value(model, attribute) { 137 | * // Validate a value that is not the current attribute 138 | * return this.get('model').get('someOtherAttr'); 139 | * } 140 | * }) 141 | * ``` 142 | * 143 | * ### message 144 | * 145 | * This option can take two forms. It can either be a `string` (a CP that returns a string is also valid), or a `function`. 146 | * If a string is used, then it will overwrite all error message types for the specified validator. 147 | * 148 | * ```javascript 149 | * // Example: String 150 | * validator('confirmation', { 151 | * message: 'Email does not match {attribute}. What are you even thinking?!' 152 | * }) 153 | * ``` 154 | * 155 | * We can pass a `function` into our message option for even more customization capabilities. 156 | * 157 | * ```javascript 158 | * // Example: Function 159 | * validator('date', { 160 | * message(type, options, value, context) { 161 | * if (type === 'before') { 162 | * return '{description} should really be before {date}'; 163 | * } 164 | * if (type === 'after') { 165 | * return '{description} should really be after {date}'; 166 | * } 167 | * } 168 | * }) 169 | * ``` 170 | * The message function is given the following arguments: 171 | * 172 | * - `type` (**String**): The error message type 173 | * - `options` (**Object**): The validator options that were defined in the model 174 | * - `value`: The current value being evaluated 175 | * - `context` (**Object**): Context for string replacement 176 | * 177 | * The return value must be a `string`. If nothing is returned (`undefined`), 178 | * defaults to the default error message of the specified type. 179 | * 180 | * Within this function, the context is set to that of the current validator. 181 | * This gives you access to the model, defaultMessages, options and more. 182 | * 183 | * 184 | * @module Validators 185 | * @submodule Common Options 186 | */ 187 | 188 | export default function (arg1, options) { 189 | let props = { 190 | options: isNone(options) ? {} : options, 191 | }; 192 | 193 | if (typeof arg1 === 'function') { 194 | deprecate( 195 | '[ember-cp-validations] `validator` no longer directly accepts ' + 196 | 'a function. Please use the inline validator syntax:' + 197 | "\n\nvalidator('inline', { validate() {} )\n\n", 198 | false, 199 | { id: 'ember-cp-validations.inline-validator', until: '4.2.0' }, 200 | ); 201 | props.options.validate = arg1; 202 | props._type = 'inline'; 203 | } else if (typeof arg1 === 'string') { 204 | props._type = arg1; 205 | } else { 206 | throw new TypeError( 207 | '[ember-cp-validations] Unexpected type for first validator argument — It must be a string.', 208 | ); 209 | } 210 | 211 | return props; 212 | } 213 | -------------------------------------------------------------------------------- /addon/validations/warning-result-collection.js: -------------------------------------------------------------------------------- 1 | import { not } from '@ember/object/computed'; 2 | import { computed } from '@ember/object'; 3 | import ResultCollection from './result-collection'; 4 | import cycleBreaker from '../utils/cycle-breaker'; 5 | import { flatten, uniq, compact } from '../utils/array'; 6 | 7 | export default ResultCollection.extend({ 8 | isValid: computed(function () { 9 | return true; 10 | }).readOnly(), 11 | isTruelyValid: not('isValidating').readOnly(), 12 | 13 | messages: computed(function () { 14 | return []; 15 | }).readOnly(), 16 | errors: computed(function () { 17 | return []; 18 | }).readOnly(), 19 | 20 | warningMessages: computed( 21 | 'content.@each.{messages,warningMessages}', 22 | cycleBreaker(function () { 23 | return uniq( 24 | compact( 25 | flatten([this.getEach('messages'), this.getEach('warningMessages')]), 26 | ), 27 | ); 28 | }), 29 | ).readOnly(), 30 | 31 | warnings: computed( 32 | 'attribute', 33 | 'content.@each.{errors,warnings}', 34 | cycleBreaker(function () { 35 | return this._computeErrorCollection( 36 | flatten([this.getEach('errors'), this.getEach('warnings')]), 37 | ); 38 | }), 39 | ).readOnly(), 40 | }); 41 | -------------------------------------------------------------------------------- /addon/validators/alias.js: -------------------------------------------------------------------------------- 1 | import { assert } from '@ember/debug'; 2 | 3 | import { isPresent } from '@ember/utils'; 4 | import { get } from '@ember/object'; 5 | import Base from 'ember-cp-validations/validators/base'; 6 | 7 | /** 8 | * [See All Options](#method_validate) 9 | * 10 | * Creates an alias between a single attribute's validations to another. 11 | * This copies all messages, errors, etc., to the current attribute as well as 12 | * its validation state (isValid, isValidating, etc.) 13 | * 14 | * ## Examples 15 | * 16 | * ```javascript 17 | * validator('alias', 'attribute') 18 | * validator('alias', { 19 | * alias: 'attribute', 20 | * firstMessageOnly: true 21 | * }) 22 | * ``` 23 | * 24 | * @class Alias 25 | * @module Validators 26 | * @extends Base 27 | */ 28 | const Alias = Base.extend({ 29 | /** 30 | * Normalized options passed in. 31 | * ```js 32 | * validator('alias', 'attribute') 33 | * // Becomes 34 | * validator('alias', { 35 | * alias: 'attribute' 36 | * }) 37 | * ``` 38 | * 39 | * @method buildOptions 40 | * @param {Object} options 41 | * @param {Object} defaultOptions 42 | * @param {Object} globalOptions 43 | * @return {Object} 44 | */ 45 | buildOptions(options = {}, defaultOptions = {}, globalOptions = {}) { 46 | let opts = options; 47 | 48 | if (typeof options === 'string') { 49 | opts = { 50 | alias: options, 51 | }; 52 | } 53 | return this._super(opts, defaultOptions, globalOptions); 54 | }, 55 | 56 | /** 57 | * @method validate 58 | * @param {Any} value 59 | * @param {Object} options 60 | * @param {String} options.alias The attribute to alias 61 | * @param {Boolean} options.firstMessageOnly If true, only returns the first error message of the 62 | * aliased attribute and will not include validation state 63 | * @param {Object} model 64 | * @param {String} attribute 65 | */ 66 | validate(value, options, model, attribute) { 67 | let { alias, firstMessageOnly } = options; 68 | 69 | assert( 70 | `[validator:alias] [${attribute}] option 'alias' is required`, 71 | isPresent(alias), 72 | ); 73 | 74 | let aliasValidation = get(model, `validations.attrs.${alias}`); 75 | 76 | return firstMessageOnly ? aliasValidation.message : aliasValidation.content; 77 | }, 78 | }); 79 | 80 | Alias.reopenClass({ 81 | getDependentsFor(attribute, options) { 82 | let alias = typeof options === 'string' ? options : options.alias; 83 | 84 | assert( 85 | `[validator:alias] [${attribute}] 'alias' must be a string`, 86 | typeof alias === 'string', 87 | ); 88 | 89 | return [`${alias}.messages.[]`, `${alias}.isTruelyValid`]; 90 | }, 91 | }); 92 | 93 | export default Alias; 94 | -------------------------------------------------------------------------------- /addon/validators/belongs-to.js: -------------------------------------------------------------------------------- 1 | import Base from 'ember-cp-validations/validators/base'; 2 | import { isPromise } from 'ember-cp-validations/utils/utils'; 3 | 4 | /** 5 | * [See All Options](#method_validate) 6 | * 7 | * Identifies a `belongs-to` relationship in an Ember Data Model or EmberObject. 8 | * This is used to create a link to the validations object of the child model. 9 | * 10 | * _**Note:** Validations must exist on **both** models/objects_ 11 | * 12 | * ### Ember Model 13 | * 14 | * ```javascript 15 | * // model/users.js 16 | * 17 | * const Validations = buildValidations({ 18 | * details: validator('belongs-to') 19 | * }); 20 | * 21 | * export default Model.extend(Validations, { 22 | * 'details': belongsTo('user-detail') 23 | * }); 24 | * ``` 25 | * 26 | * ```javascript 27 | * // model/user-details.js 28 | * 29 | * const Validations = buildValidations({ 30 | * firstName: validator('presence', true), 31 | * lastName: validator('presence', true) 32 | * }); 33 | * 34 | * export default Model.extend(Validations, { 35 | * "firstName": attr('string'), 36 | * "lastName": attr('string'), 37 | * }); 38 | * ``` 39 | * 40 | * ### Ember Object 41 | * 42 | * ```javascript 43 | * // model/users.js 44 | * 45 | * import { getOwner } from '@ember/application'; 46 | * import EmberObject from '@ember/object'; 47 | * import UserDetails from '../user-details'; 48 | * 49 | * const Validations = buildValidations({ 50 | * details: validator('belongs-to') 51 | * }); 52 | * 53 | * export default EmberObject.extend(Validations, { 54 | * details: null, 55 | * 56 | * init() { 57 | * this._super(...arguments); 58 | * let owner = getOwner(this); 59 | * this.set('details', UserDetails.create(owner.ownerInjection())); 60 | * } 61 | * }); 62 | * ``` 63 | * 64 | * From our `user` model, we can now check any validation property on the `user-details` model. 65 | * 66 | * ```javascript 67 | * get(model, 'validations.attrs.details.isValid') 68 | * get(model, 'validations.attrs.details.messages') 69 | * ``` 70 | * 71 | * @class Belongs To 72 | * @module Validators 73 | * @extends Base 74 | */ 75 | const BelongsTo = Base.extend({ 76 | validate(value, ...args) { 77 | if (value) { 78 | if (isPromise(value)) { 79 | return value.then((model) => this.validate(model, ...args)); 80 | } 81 | 82 | return value.validations; 83 | } 84 | 85 | return true; 86 | }, 87 | }); 88 | 89 | BelongsTo.reopenClass({ 90 | getDependentsFor(attribute) { 91 | return [ 92 | `model.${attribute}.isDeleted`, 93 | `model.${attribute}.content.isDeleted`, 94 | `model.${attribute}.validations`, 95 | `model.${attribute}.content.validations`, 96 | ]; 97 | }, 98 | }); 99 | 100 | export default BelongsTo; 101 | -------------------------------------------------------------------------------- /addon/validators/collection.js: -------------------------------------------------------------------------------- 1 | import EmberValidator from 'ember-cp-validations/-private/ember-validator'; 2 | 3 | /** 4 | * [See All Options](#method_validate) 5 | * 6 | * If `true` validates that the given value is a valid collection and will add `.[]` as a dependent key to the CP. 7 | * If `false`, validates that the given value is singular. Use this validator if you want validation to occur when the content of your collection changes. 8 | * 9 | * ## Examples 10 | * 11 | * ```javascript 12 | * validator('collection', true) 13 | * validator('collection', false) 14 | * validator('collection', { 15 | * collection: true, 16 | * message: 'must be a collection' 17 | * }) 18 | * ``` 19 | * 20 | * @class Collection 21 | * @module Validators 22 | * @extends Base 23 | */ 24 | const Collection = EmberValidator.extend({ 25 | _evType: 'collection', 26 | 27 | /** 28 | * Normalized options passed in. 29 | * ```js 30 | * validator('collection', true) 31 | * // Becomes 32 | * validator('collection', { 33 | * collection: true 34 | * }) 35 | * ``` 36 | * 37 | * @method buildOptions 38 | * @param {Object} options 39 | * @param {Object} defaultOptions 40 | * @param {Object} globalOptions 41 | * @return {Object} 42 | */ 43 | buildOptions(options = {}, defaultOptions = {}, globalOptions = {}) { 44 | let opts = options; 45 | 46 | if (typeof options === 'boolean') { 47 | opts = { 48 | collection: options, 49 | }; 50 | } 51 | return this._super(opts, defaultOptions, globalOptions); 52 | }, 53 | }); 54 | 55 | Collection.reopenClass({ 56 | getDependentsFor(attribute, options) { 57 | return options === true || options.collection === true 58 | ? [`model.${attribute}.[]`] 59 | : []; 60 | }, 61 | }); 62 | 63 | export default Collection; 64 | -------------------------------------------------------------------------------- /addon/validators/confirmation.js: -------------------------------------------------------------------------------- 1 | import { assert } from '@ember/debug'; 2 | import EmberValidator from 'ember-cp-validations/-private/ember-validator'; 3 | 4 | /** 5 | * [See All Options](#method_validate) 6 | * 7 | * Validates that the attribute has the same value as the one of the declared attribute. 8 | * 9 | * ## Examples 10 | * 11 | * ```javascript 12 | * email: validator('format', { 13 | * type: 'email' 14 | * }) 15 | * verifiedEmail: validator('confirmation', { 16 | * on: 'email', 17 | * message: 'Email addresses do not match' 18 | * }) 19 | * ``` 20 | * 21 | * @class Confirmation 22 | * @module Validators 23 | * @extends Base 24 | */ 25 | const Confirmation = EmberValidator.extend({ 26 | _evType: 'confirmation', 27 | }); 28 | 29 | Confirmation.reopenClass({ 30 | getDependentsFor(attribute, options) { 31 | let on = options.on; 32 | 33 | assert( 34 | `[validator:confirmation] [${attribute}] 'on' must be a string`, 35 | typeof on === 'string', 36 | ); 37 | 38 | return on ? [`model.${on}`] : []; 39 | }, 40 | }); 41 | 42 | export default Confirmation; 43 | -------------------------------------------------------------------------------- /addon/validators/date.js: -------------------------------------------------------------------------------- 1 | import EmberValidator from 'ember-cp-validations/-private/ember-validator'; 2 | 3 | /** 4 | * [See All Options](#method_validate) 5 | * 6 | * Validate over a date range. Uses [MomentJS](http://momentjs.com/) for date mathematics and calculations. 7 | * 8 | * **Note**: MomentJS must be installed to be able to use this validator. The easiest way to do this is to install [ember-moment](https://github.com/stefanpenner/ember-moment) 9 | * 10 | * ## Examples 11 | * 12 | * If `before`, `onOrBefore`, `after`, or `onOrAfter` is set to **now**, the value given to the validator will be tested against the current date and time. 13 | * 14 | * ```javascript 15 | * validator('date', { 16 | * after: 'now', 17 | * before: '1/1/2020', 18 | * precision: 'day', 19 | * format: 'M/D/YYY', 20 | * errorFormat: 'M/D/YYY' 21 | * }) 22 | * ``` 23 | * 24 | * @class Date 25 | * @module Validators 26 | * @extends Base 27 | */ 28 | export default EmberValidator.extend({ 29 | _evType: 'date', 30 | }); 31 | -------------------------------------------------------------------------------- /addon/validators/dependent.js: -------------------------------------------------------------------------------- 1 | import { get } from '@ember/object'; 2 | import { assert } from '@ember/debug'; 3 | import { isPresent, isEmpty, isNone } from '@ember/utils'; 4 | import { isArray, A } from '@ember/array'; 5 | import Base from 'ember-cp-validations/validators/base'; 6 | import getWithDefault from '../utils/get-with-default'; 7 | 8 | /** 9 | * [See All Options](#method_validate) 10 | * 11 | * Defines an attribute as valid only if its dependents are valid. 12 | * 13 | * ## Example 14 | * 15 | * ```javascript 16 | * // Full name will only be valid if firstName and lastName are filled in 17 | * validator('dependent', { 18 | * on: ['firstName', 'lastName'] 19 | * }) 20 | * ``` 21 | * 22 | * @class Dependent 23 | * @module Validators 24 | * @extends Base 25 | */ 26 | const Dependent = Base.extend({ 27 | /** 28 | * @method validate 29 | * @param {Any} value 30 | * @param {Object} options 31 | * @param {Array} options.on Attributes this field is dependent on 32 | * @param {Object} model 33 | * @param {String} attribute 34 | */ 35 | validate(value, options, model, attribute) { 36 | let { on, allowBlank } = options; 37 | 38 | assert( 39 | `[validator:dependent] [${attribute}] option 'on' is required`, 40 | isPresent(on), 41 | ); 42 | 43 | if (isNone(model)) { 44 | return true; 45 | } 46 | 47 | if (allowBlank && isEmpty(value)) { 48 | return true; 49 | } 50 | 51 | let dependentValidations = getWithDefault(options, 'on', A()).map( 52 | (dependent) => get(model, `validations.attrs.${dependent}`), 53 | ); 54 | 55 | if (!isEmpty(dependentValidations.filter((v) => v.isTruelyInvalid))) { 56 | return this.createErrorMessage('invalid', value, options); 57 | } 58 | 59 | return true; 60 | }, 61 | }); 62 | 63 | Dependent.reopenClass({ 64 | getDependentsFor(attribute, options) { 65 | let dependents = options.on; 66 | 67 | assert( 68 | `[validator:dependent] [${attribute}] 'on' must be an array`, 69 | isArray(dependents), 70 | ); 71 | 72 | if (!isEmpty(dependents)) { 73 | return dependents.map((dependent) => `${dependent}.isTruelyValid`); 74 | } 75 | 76 | return []; 77 | }, 78 | }); 79 | 80 | export default Dependent; 81 | -------------------------------------------------------------------------------- /addon/validators/ds-error.js: -------------------------------------------------------------------------------- 1 | import EmberValidator from 'ember-cp-validations/-private/ember-validator'; 2 | import { getPathAndKey } from 'ember-validators/ds-error'; 3 | 4 | /** 5 | * [See All Options](#method_validate) 6 | * 7 | * Creates a link between this library and Ember-Data 8 | * to fetch the latest message for the given attribute. 9 | * 10 | * ## Examples 11 | * 12 | * ```javascript 13 | * validator('ds-error') 14 | * ``` 15 | * 16 | * @class DS Error 17 | * @module Validators 18 | * @extends Base 19 | */ 20 | const DSError = EmberValidator.extend({ 21 | _evType: 'ds-error', 22 | }); 23 | 24 | DSError.reopenClass({ 25 | getDependentsFor(attribute) { 26 | let { path, key } = getPathAndKey(attribute); 27 | 28 | return [`model.${path}.${key}.[]`]; 29 | }, 30 | }); 31 | 32 | export default DSError; 33 | -------------------------------------------------------------------------------- /addon/validators/exclusion.js: -------------------------------------------------------------------------------- 1 | import EmberValidator from 'ember-cp-validations/-private/ember-validator'; 2 | 3 | /** 4 | * [See All Options](#method_validate) 5 | * 6 | * Validates that the attributes’ values are not included in a given list. All comparisons are done using strict equality so type matters! For range, the value type is checked against both lower and upper bounds for type equality. 7 | * 8 | * ## Examples: 9 | * 10 | * ```javascript 11 | * validator('exclusion', { 12 | * in: ['Admin', 'Super Admin'] 13 | * }) 14 | * validator('exclusion', { 15 | * range: [0, 5] // Cannot be between 0 (inclusive) to 5 (inclusive) 16 | * }) 17 | * ``` 18 | * 19 | * @class Exclusion 20 | * @module Validators 21 | * @extends Base 22 | */ 23 | export default EmberValidator.extend({ 24 | _evType: 'exclusion', 25 | }); 26 | -------------------------------------------------------------------------------- /addon/validators/format.js: -------------------------------------------------------------------------------- 1 | import EmberValidator from 'ember-cp-validations/-private/ember-validator'; 2 | import { regularExpressions } from 'ember-validators/format'; 3 | 4 | /** 5 | * [See All Options](#method_validate) 6 | * 7 | * Validate over a predefined or custom regular expression. 8 | * 9 | * ## Examples 10 | * 11 | * ```javascript 12 | * validator('format', { 13 | * type: 'email', 14 | * allowNonTld: true 15 | * }) 16 | * validator('format', { 17 | * allowBlank: true, 18 | * type: 'phone' 19 | * }) 20 | * validator('format', { 21 | * type: 'url' 22 | * }) 23 | * validator('format', { 24 | * regex: /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{4,8}$/, 25 | * message: 'Password must include at least one upper case letter, one lower case letter, and a number' 26 | * }) 27 | * ``` 28 | * 29 | * If you do not want to use the predefined regex for a specific type, you can do something like this 30 | * 31 | * ```javascript 32 | * validator('format', { 33 | * type: 'email', 34 | * regex: /My Better Email Regexp/ 35 | * }) 36 | * ``` 37 | * 38 | * This allows you to still keep the email error message but with your own custom regex. 39 | * 40 | * @class Format 41 | * @module Validators 42 | * @extends Base 43 | */ 44 | export default EmberValidator.extend({ 45 | _evType: 'format', 46 | regularExpressions, 47 | }); 48 | -------------------------------------------------------------------------------- /addon/validators/has-many.js: -------------------------------------------------------------------------------- 1 | import Base from 'ember-cp-validations/validators/base'; 2 | import { isPromise } from 'ember-cp-validations/utils/utils'; 3 | 4 | /** 5 | * [See All Options](#method_validate) 6 | * 7 | * Identifies a `has-many` relationship in an Ember Data Model or EmberObject. 8 | * This is used to create a validation collection of the `has-many` validations. 9 | * 10 | * _**Note:** Validations must exist on **all** models/objects_ 11 | * 12 | * ### Ember Models 13 | * 14 | * ```javascript 15 | * // model/users.js 16 | * 17 | * const Validations = buildValidations({ 18 | * friends: validator('has-many') 19 | * }); 20 | * 21 | * export default Model.extend(Validations, { 22 | * friends: hasMany('user') 23 | * }); 24 | * ``` 25 | * 26 | * ### Ember Objects 27 | * 28 | * ```javascript 29 | * // model/users.js 30 | * 31 | * import EmberObject from '@ember/object'; 32 | * 33 | * const Validations = buildValidations({ 34 | * friends: validator('has-many') 35 | * }); 36 | * 37 | * export default EmberObject.extend(Validations, { 38 | * friends: null 39 | * }); 40 | * ``` 41 | * 42 | * From our `user` model, we can now check validation properties on the `friends` attribute. 43 | * 44 | * ```javascript 45 | * get(model, 'validations.attrs.friends.isValid') 46 | * get(model, 'validations.attrs.friends.messages') 47 | * ``` 48 | * 49 | * @class Has Many 50 | * @module Validators 51 | * @extends Base 52 | */ 53 | const HasMany = Base.extend({ 54 | validate(value, ...args) { 55 | if (value) { 56 | if (isPromise(value)) { 57 | return value.then((models) => this.validate(models, ...args)); 58 | } 59 | 60 | return value.map((m) => m.validations); 61 | } 62 | 63 | return true; 64 | }, 65 | }); 66 | 67 | HasMany.reopenClass({ 68 | getDependentsFor(attribute) { 69 | /* 70 | The content.@each.isDeleted must be added for older ember-data versions 71 | */ 72 | return [ 73 | `model.${attribute}.[]`, 74 | `model.${attribute}.@each.isDeleted`, 75 | `model.${attribute}.content.@each.isDeleted`, 76 | `model.${attribute}.@each.validations`, 77 | `model.${attribute}.content.@each.validations`, 78 | ]; 79 | }, 80 | }); 81 | 82 | export default HasMany; 83 | -------------------------------------------------------------------------------- /addon/validators/inclusion.js: -------------------------------------------------------------------------------- 1 | import EmberValidator from 'ember-cp-validations/-private/ember-validator'; 2 | 3 | /** 4 | * [See All Options](#method_validate) 5 | * 6 | * Validates that the attributes’ values are included in a given list. All comparisons are done using strict equality so type matters! 7 | * For range, the value type is checked against both lower and upper bounds for type equality. 8 | * 9 | * ## Examples 10 | * 11 | * ```javascript 12 | * validator('inclusion', { 13 | * in: ['User', 'Admin'] 14 | * }) 15 | * validator('inclusion', { 16 | * range: [0, 5] // Must be between 0 (inclusive) to 5 (inclusive) 17 | * }) 18 | * ``` 19 | * 20 | * Because of the strict equality comparisons, you can use this validator in many different ways. 21 | * 22 | * ```javascript 23 | * validator('inclusion', { 24 | * in: ['Admin'] // Input must be equal to 'Admin' 25 | * }) 26 | * validator('inclusion', { 27 | * range: [0, Infinity] // Input must be positive number 28 | * }) 29 | * validator('inclusion', { 30 | * range: [-Infinity, Infinity] // Input must be a number 31 | * }) 32 | * ``` 33 | * 34 | * @class Inclusion 35 | * @module Validators 36 | * @extends Base 37 | */ 38 | export default EmberValidator.extend({ 39 | _evType: 'inclusion', 40 | }); 41 | -------------------------------------------------------------------------------- /addon/validators/inline.js: -------------------------------------------------------------------------------- 1 | import Base from 'ember-cp-validations/validators/base'; 2 | import { assert } from '@ember/debug'; 3 | 4 | /** 5 | * Accepts a custom `validate` function. 6 | * 7 | * ## Examples 8 | * 9 | * ```javascript 10 | * validator('inline', { 11 | * username: 'offirgolan', 12 | * validate(value, options, model, attribute) { 13 | * return value === options.username ? 14 | * true : 15 | * `Username must be ${options.username}`; 16 | * } 17 | * }); 18 | * ``` 19 | * 20 | * @class Inline 21 | * @module Validators 22 | * @extends Base 23 | */ 24 | export default Base.extend({ 25 | /** 26 | * Override the validator's `validate` method with the one that was 27 | * passed in via the options. 28 | * 29 | * @method buildOptions 30 | * @param {Object} options 31 | * @param {Object} defaultOptions 32 | * @param {Object} globalOptions 33 | * @return {Object} 34 | */ 35 | buildOptions(options = {}, ...args) { 36 | assert( 37 | `[validator:inline] You must pass in a validate function`, 38 | options && typeof options.validate === 'function', 39 | ); 40 | 41 | const opts = Object.assign({}, options); 42 | 43 | this.validate = opts.validate; 44 | delete opts.validate; 45 | 46 | return this._super(opts, ...args); 47 | }, 48 | }); 49 | -------------------------------------------------------------------------------- /addon/validators/length.js: -------------------------------------------------------------------------------- 1 | import EmberValidator from 'ember-cp-validations/-private/ember-validator'; 2 | 3 | /** 4 | * [See All Options](#method_validate) 5 | * 6 | * Validates the length of the attributes’ values. 7 | * 8 | * ## Examples 9 | * 10 | * ```javascript 11 | * validator('length', { 12 | * is: 15 13 | * }) 14 | * validator('length', { 15 | * min: 5, 16 | * max: 10 17 | * }) 18 | * ``` 19 | * 20 | * @class Length 21 | * @module Validators 22 | * @extends Base 23 | */ 24 | export default EmberValidator.extend({ 25 | _evType: 'length', 26 | }); 27 | -------------------------------------------------------------------------------- /addon/validators/messages.js: -------------------------------------------------------------------------------- 1 | import EmberObject from '@ember/object'; 2 | import Messages from 'ember-validators/messages'; 3 | 4 | /** 5 | * The default validation error messages are imported in your app's `validators` folder. 6 | * If you want to change or extend them, all you need to do is create a `messages.js` file under `app/validators`. 7 | * 8 | * ```javascript 9 | * // app/validators/messages.js 10 | * 11 | * import Messages from 'ember-cp-validations/validators/messages'; 12 | * 13 | * export default Messages.extend({ 14 | * uniqueUsername: '{description} {username} already exists' 15 | * }); 16 | * ``` 17 | * 18 | * Within this object, you can overwrite the [default messages](https://github.com/adopted-ember-addons/ember-cp-validations/blob/master/addon/validators/messages.js) or create new messages just like in the example above. 19 | * If a message of a given type is not found, it will default to the `invalid` message. 20 | * Usage examples can be found {{#crossLink "Base/createErrorMessage:method"}}here{{/crossLink}} 21 | * 22 | * @class Messages 23 | * @module Validators 24 | */ 25 | export default EmberObject.extend(Messages); 26 | -------------------------------------------------------------------------------- /addon/validators/number.js: -------------------------------------------------------------------------------- 1 | import EmberValidator from 'ember-cp-validations/-private/ember-validator'; 2 | 3 | /** 4 | * [See All Options](#method_validate) 5 | * 6 | * Validates that your attributes have only numeric values. 7 | * 8 | * ## Examples 9 | * 10 | * ```javascript 11 | * validator('number') // Simple check if the value is a number 12 | * validator('number', { 13 | * allowString: true, 14 | * integer: true, 15 | * gt: 5, 16 | * lte: 100 17 | * }) 18 | * ``` 19 | * 20 | * @class Number 21 | * @module Validators 22 | * @extends Base 23 | */ 24 | export default EmberValidator.extend({ 25 | _evType: 'number', 26 | }); 27 | -------------------------------------------------------------------------------- /addon/validators/presence.js: -------------------------------------------------------------------------------- 1 | import EmberValidator from 'ember-cp-validations/-private/ember-validator'; 2 | 3 | /** 4 | * [See All Options](#method_validate) 5 | * 6 | * If `true` validates that the given value is not empty, if `false`, validates that the given value is empty. 7 | * 8 | * ## Examples 9 | * 10 | * ```javascript 11 | * validator('presence', true) 12 | * validator('presence', false) 13 | * validator('presence', { 14 | * presence: true, 15 | * message: 'should not be empty' 16 | * }) 17 | * 18 | * validator('presence', { 19 | * presence: true, 20 | * ignoreBlank: true, 21 | * message: 'should not be empty or consist only of spaces' 22 | * }) 23 | * ``` 24 | * 25 | * @class Presence 26 | * @module Validators 27 | * @extends Base 28 | */ 29 | export default EmberValidator.extend({ 30 | _evType: 'presence', 31 | 32 | /** 33 | * Normalized options passed in. 34 | * ```js 35 | * validator('presence', true) 36 | * // Becomes 37 | * validator('presence', { 38 | * presence: true 39 | * }) 40 | * ``` 41 | * 42 | * @method buildOptions 43 | * @param {Object} options 44 | * @param {Object} defaultOptions 45 | * @param {Object} globalOptions 46 | * @return {Object} 47 | */ 48 | buildOptions(options = {}, defaultOptions = {}, globalOptions = {}) { 49 | let opts = options; 50 | 51 | if (typeof options === 'boolean') { 52 | opts = { 53 | presence: options, 54 | }; 55 | } 56 | return this._super(opts, defaultOptions, globalOptions); 57 | }, 58 | }); 59 | -------------------------------------------------------------------------------- /app/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adopted-ember-addons/ember-cp-validations/13d8f8457ac9a50370050205bff189f20f526e01/app/.gitkeep -------------------------------------------------------------------------------- /app/validators/alias.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-cp-validations/validators/alias'; 2 | -------------------------------------------------------------------------------- /app/validators/belongs-to.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-cp-validations/validators/belongs-to'; 2 | -------------------------------------------------------------------------------- /app/validators/collection.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-cp-validations/validators/collection'; 2 | -------------------------------------------------------------------------------- /app/validators/confirmation.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-cp-validations/validators/confirmation'; 2 | -------------------------------------------------------------------------------- /app/validators/date.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-cp-validations/validators/date'; 2 | -------------------------------------------------------------------------------- /app/validators/dependent.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-cp-validations/validators/dependent'; 2 | -------------------------------------------------------------------------------- /app/validators/ds-error.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-cp-validations/validators/ds-error'; 2 | -------------------------------------------------------------------------------- /app/validators/exclusion.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-cp-validations/validators/exclusion'; 2 | -------------------------------------------------------------------------------- /app/validators/format.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-cp-validations/validators/format'; 2 | -------------------------------------------------------------------------------- /app/validators/has-many.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-cp-validations/validators/has-many'; 2 | -------------------------------------------------------------------------------- /app/validators/inclusion.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-cp-validations/validators/inclusion'; 2 | -------------------------------------------------------------------------------- /app/validators/inline.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-cp-validations/validators/inline'; 2 | -------------------------------------------------------------------------------- /app/validators/length.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-cp-validations/validators/length'; 2 | -------------------------------------------------------------------------------- /app/validators/messages.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-cp-validations/validators/messages'; 2 | -------------------------------------------------------------------------------- /app/validators/number.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-cp-validations/validators/number'; 2 | -------------------------------------------------------------------------------- /app/validators/presence.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-cp-validations/validators/presence'; 2 | -------------------------------------------------------------------------------- /blueprints/validator-test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | description: 'Generates a validator unit test', 7 | 8 | filesPath() { 9 | const dependencies = this.project.dependencies(); 10 | let type; 11 | 12 | if ('ember-mocha' in dependencies || 'ember-cli-mocha' in dependencies) { 13 | type = 'mocha'; 14 | } else if ('ember-cli-qunit' in dependencies) { 15 | type = 'qunit'; 16 | } else { 17 | this.ui.writeLine("Couldn't determine test style - using QUnit"); 18 | type = 'qunit'; 19 | } 20 | 21 | return path.join(this.path, type + '-files'); 22 | }, 23 | 24 | // workaround https://github.com/ember-cli/ember-cli/issues/5481 25 | supportsAddon() { 26 | return false; 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /blueprints/validator-test/mocha-files/tests/unit/validators/__name__-test.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | import { setupTest } from 'ember-mocha'; 4 | 5 | describe('Unit | Validator | <%= dasherizedModuleName %>', function () { 6 | setupTest(); 7 | 8 | // Replace this with your real tests. 9 | it('exists', function () { 10 | const validator = this.owner.lookup( 11 | 'validator:<%= dasherizedModuleName %>', 12 | ); 13 | expect(validator).to.be.ok; 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /blueprints/validator-test/qunit-files/tests/unit/validators/__name__-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { setupTest } from 'ember-qunit'; 3 | 4 | module('Unit | Validator | <%= dasherizedModuleName %>', function (hooks) { 5 | setupTest(hooks); 6 | 7 | // Replace this with your real tests. 8 | test('it exists', function (assert) { 9 | const validator = this.owner.lookup( 10 | 'validator:<%= dasherizedModuleName %>', 11 | ); 12 | assert.ok(validator); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /blueprints/validator/files/__root__/validators/__name__.js: -------------------------------------------------------------------------------- 1 | import BaseValidator from 'ember-cp-validations/validators/base'; 2 | 3 | const <%= classifiedModuleName %> = BaseValidator.extend({ 4 | validate(/*value, options, model, attribute*/) { 5 | return true; 6 | } 7 | }); 8 | 9 | <%= classifiedModuleName %>.reopenClass({ 10 | /** 11 | * Define attribute specific dependent keys for your validator 12 | * 13 | * [ 14 | * `model.array.@each.${attribute}` --> Dependent is created on the model's context 15 | * `${attribute}.isValid` --> Dependent is created on the `model.validations.attrs` context 16 | * ] 17 | * 18 | * @param {String} attribute The attribute being evaluated 19 | * @param {Unknown} options Options passed into your validator 20 | * @return {Array} 21 | */ 22 | getDependentsFor(/* attribute, options */) { 23 | return []; 24 | } 25 | }); 26 | 27 | export default <%= classifiedModuleName %>; 28 | -------------------------------------------------------------------------------- /blueprints/validator/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | description: 'Generates a validator and unit test', 3 | }; 4 | -------------------------------------------------------------------------------- /ember-cli-build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const EmberAddon = require('ember-cli/lib/broccoli/ember-addon'); 4 | 5 | module.exports = function (defaults) { 6 | const app = new EmberAddon(defaults, { 7 | snippetSearchPaths: ['addon', 'tests/dummy/app'], 8 | snippetPaths: ['snippets', 'tests/dummy/snippets'], 9 | 10 | 'ember-prism': { 11 | components: ['javascript', 'markup'], 12 | }, 13 | }); 14 | 15 | /* 16 | This build file specifies the options for the dummy test app of this 17 | addon, located in `/tests/dummy` 18 | This build file does *not* influence how the addon or the app using it 19 | behave. You most likely want to be modifying `./index.js` or app's build file 20 | */ 21 | 22 | const { maybeEmbroider } = require('@embroider/test-setup'); 23 | return maybeEmbroider(app, { 24 | skipBabel: [ 25 | { 26 | package: 'qunit', 27 | }, 28 | ], 29 | }); 30 | }; 31 | -------------------------------------------------------------------------------- /htmlbars-plugins/v-get.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module Templating 3 | * @main Templating 4 | */ 5 | 6 | /** 7 | * Accessing validation information in your templates is really simple but the pathing can be quite long. For example, if we want to display the error `message` for the `username` attribute, it would look something like this: 8 | * 9 | * ```handlebars 10 | * {{model.validations.attrs.username.message}} 11 | * ``` 12 | * 13 | * ## The V-Get Helper 14 | * To bypass such long pathing, you can use the `v-get` helper. 15 | * 16 | * _**Notice**: Ember v1.13.0 is not supported due to a bug. Please use Ember v1.13.1 and higher or Ember v1.12.* and lower_ 17 | * 18 | * **Access global model properties** 19 | * 20 | * ```handlebars 21 | * {{v-get model 'isValid'}} 22 | * ``` 23 | * 24 | * **Access attribute specific properties** 25 | * 26 | * ```handlebars 27 | * {{v-get model 'username' 'message'}} 28 | * ``` 29 | * 30 | * **Access model relationship validations** 31 | * 32 | * Say we have a `user` model with a `details` attribute that is a belongsTo relationship, to access validations on the `details` attribute/model we can access it as such. 33 | * 34 | * ```handlebars 35 | * {{v-get model.details 'isValid'}} 36 | * {{v-get model.details 'firstName' 'message'}} 37 | * ``` 38 | * 39 | * What's awesome about this is that you can pass in bound properties! 40 | * 41 | * ```handlebars 42 | * {{v-get model attr prop}} 43 | * {{v-get model prop}} 44 | * ``` 45 | * 46 | * Here is a more extensive example: 47 | * ```handlebars 48 | *
49 | * {{input value=model.username placeholder="Username"}} 50 | * {{#if (v-get model 'username' 'isInvalid')}} 51 | *
52 | * {{v-get model 'username' 'message'}} 53 | *
54 | * {{/if}} 55 | * 56 | * 57 | *
58 | * ``` 59 | * 60 | * @module Templating 61 | * @submodule V-Get Helper 62 | */ 63 | 64 | /* eslint-env node */ 65 | 66 | var plugin = function ({ syntax }) { 67 | return { 68 | visitor: { 69 | BlockStatement(node) { 70 | processNode(node, syntax); 71 | }, 72 | 73 | ElementNode(node) { 74 | processNode(node, syntax); 75 | }, 76 | 77 | MustacheStatement(node) { 78 | processNode(node, syntax); 79 | }, 80 | }, 81 | }; 82 | }; 83 | 84 | function processNode(node, syntax) { 85 | var type = node.type; 86 | 87 | // {{v-get model 'username' 'isValid'}} 88 | if (type === 'MustacheStatement' && getValue(node.path) === 'v-get') { 89 | transformToGet(node, syntax); 90 | } 91 | 92 | processNodeParams(node, syntax); 93 | processNodeHash(node, syntax); 94 | processNodeAttributes(node, syntax); 95 | } 96 | 97 | /** 98 | * {{#if (v-get model 'username' 'isValid')}} {{/if}} 99 | * @param {AST.Node} node 100 | */ 101 | function processNodeParams(node, syntax) { 102 | if (node.params) { 103 | for (var i = 0; i < node.params.length; i++) { 104 | var param = node.params[i]; 105 | if (param.type === 'SubExpression') { 106 | if (getValue(param.path) === 'v-get') { 107 | transformToGet(param, syntax); 108 | } else { 109 | processNode(param, syntax); 110 | } 111 | } 112 | } 113 | } 114 | } 115 | 116 | /** 117 | * {{x-component prop=(v-get model 'isValid')}} 118 | * @param {AST.Node} node 119 | */ 120 | function processNodeHash(node, syntax) { 121 | if (node.hash && node.hash.pairs) { 122 | for (var i = 0; i < node.hash.pairs.length; i++) { 123 | var pair = node.hash.pairs[i]; 124 | if (pair.value.type === 'SubExpression') { 125 | if (getValue(pair.value.path) === 'v-get') { 126 | transformToGet(pair.value, syntax); 127 | } else { 128 | processNode(pair.value, syntax); 129 | } 130 | } 131 | } 132 | } 133 | } 134 | 135 | /** 136 | * (node.attributes) 137 | *
138 | * @param {AST.Node} node 139 | */ 140 | function processNodeAttributes(node, syntax) { 141 | var i; 142 | if (node.attributes) { 143 | for (i = 0; i < node.attributes.length; i++) { 144 | var attr = node.attributes[i]; 145 | processNode(attr.value, syntax); 146 | } 147 | } 148 | 149 | if (node.parts) { 150 | for (i = 0; i < node.parts.length; i++) { 151 | processNode(node.parts[i], syntax); 152 | } 153 | } 154 | } 155 | 156 | /** 157 | * Transform: 158 | * (v-get model 'username' 'isValid') to (get (get (get (get model 'validations') 'attrs') 'username') 'isValid') 159 | * OR 160 | * (v-get model 'isValid') to (get (get model 'validations') 'isValid') 161 | * @param {AST.Node} node 162 | * @return {AST.Node} 163 | */ 164 | function transformToGet(node, syntax) { 165 | var params = node.params; 166 | var numParams = params.length; 167 | 168 | if (numParams < 2) { 169 | throw new Error('{{v-get}} requires at least two arguments'); 170 | } 171 | if (params[0].type !== 'PathExpression') { 172 | throw new Error('The first argument to {{v-get}} must be a stream'); 173 | } 174 | 175 | // (get model 'validations') 176 | var root = syntax.builders.sexpr(syntax.builders.path('get'), [ 177 | params[0], 178 | syntax.builders.string('validations'), 179 | ]); 180 | 181 | // (get (get (get model 'validations') 'attrs') 'username') 182 | if (numParams === 3) { 183 | root = syntax.builders.sexpr(syntax.builders.path('get'), [ 184 | root, 185 | syntax.builders.string('attrs'), 186 | ]); 187 | root = syntax.builders.sexpr(syntax.builders.path('get'), [ 188 | root, 189 | params[1], 190 | ]); 191 | } 192 | 193 | node.path = syntax.builders.path('get'); 194 | // (get root 'isValid') 195 | node.params = [root, params[numParams - 1]]; 196 | } 197 | 198 | function getValue(path) { 199 | if (!path) return; 200 | 201 | if ('value' in path) { 202 | return path.value; 203 | } 204 | 205 | /** 206 | * Deprecated in ember 5.9+ 207 | * (so we use the above for newer embers) 208 | */ 209 | return path.original; 210 | } 211 | 212 | module.exports = plugin; 213 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | name: require('./package').name, 5 | 6 | setupPreprocessorRegistry: function (type, registry) { 7 | registry.add('htmlbars-ast-plugin', this._buildPlugin()); 8 | }, 9 | 10 | _buildPlugin() { 11 | const vGetPlugin = require('./htmlbars-plugins/v-get'); 12 | 13 | return { 14 | name: 'v-get', 15 | baseDir: function () { 16 | return __dirname; 17 | }, 18 | parallelBabel: { 19 | requireFile: __filename, 20 | buildUsing: '_buildPlugin', 21 | params: {}, 22 | }, 23 | plugin: vGetPlugin, 24 | }; 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | {"compilerOptions":{"target":"es6","experimentalDecorators":true},"exclude":["node_modules","bower_components","tmp","vendor",".git","dist"]} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-cp-validations", 3 | "version": "7.0.0", 4 | "description": "Ember computed property based validation library", 5 | "keywords": [ 6 | "ember-addon", 7 | "ember-data", 8 | "validator", 9 | "validation", 10 | "model", 11 | "input", 12 | "form" 13 | ], 14 | "homepage": "https://github.com/adopted-ember-addons/ember-cp-validations", 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/adopted-ember-addons/ember-cp-validations.git" 18 | }, 19 | "license": "MIT", 20 | "author": "Offir Golan ", 21 | "contributors": [ 22 | { 23 | "name": "Stefan Penner", 24 | "email": "stefan.penner@gmail.com" 25 | }, 26 | { 27 | "name": "Ben Limmer", 28 | "email": "hello@benlimmer.com" 29 | }, 30 | { 31 | "name": "Robert Jackson", 32 | "email": "me@rwjblue.com" 33 | } 34 | ], 35 | "directories": { 36 | "doc": "doc", 37 | "test": "tests" 38 | }, 39 | "scripts": { 40 | "build": "ember build --environment=production", 41 | "lint": "concurrently \"pnpm:lint:*(!fix)\" --names \"lint:\"", 42 | "lint:fix": "concurrently \"pnpm:lint:*:fix\" --names \"fix:\"", 43 | "lint:hbs": "ember-template-lint .", 44 | "lint:hbs:fix": "ember-template-lint . --fix", 45 | "lint:js": "eslint . --cache", 46 | "lint:js:fix": "eslint . --fix", 47 | "start": "ember serve", 48 | "test": "concurrently \"pnpm:lint\" \"pnpm:test:*\" --names \"lint,test:\"", 49 | "test:ember": "ember test", 50 | "test:ember-compatibility": "ember try:each", 51 | "prepare": "node ./scripts/write-snippets.mjs" 52 | }, 53 | "dependencies": { 54 | "@babel/core": "^7.26.10", 55 | "@embroider/macros": "~1.16.13", 56 | "ember-cli-babel": "^8.2.0", 57 | "ember-validators": "^4.1.2" 58 | }, 59 | "devDependencies": { 60 | "@babel/eslint-parser": "^7.27.0", 61 | "@babel/plugin-proposal-decorators": "^7.25.9", 62 | "@ember/optional-features": "^2.2.0", 63 | "@ember/string": "^3.1.1", 64 | "@ember/test-helpers": "^3.3.1", 65 | "@embroider/test-setup": "^4.0.0", 66 | "@fortawesome/ember-fontawesome": "^1.0.3", 67 | "@fortawesome/fontawesome-svg-core": "^6.7.2", 68 | "@fortawesome/free-brands-svg-icons": "^6.7.2", 69 | "@fortawesome/free-solid-svg-icons": "^6.7.2", 70 | "@glimmer/component": "^1.1.2", 71 | "@glimmer/tracking": "^1.1.2", 72 | "@release-it-plugins/lerna-changelog": "^5.0.0", 73 | "broccoli-asset-rev": "^2.7.0", 74 | "concurrently": "^8.2.2", 75 | "ember-auto-import": "^2.10.0", 76 | "ember-cli": "~5.12.0", 77 | "ember-cli-app-version": "^5.0.0", 78 | "ember-cli-autoprefixer": "^2.0.0", 79 | "ember-cli-clean-css": "^3.0.0", 80 | "ember-cli-dependency-checker": "^3.3.3", 81 | "ember-cli-github-pages": "^0.2.2", 82 | "ember-cli-htmlbars": "^6.3.0", 83 | "ember-cli-inject-live-reload": "^2.1.0", 84 | "ember-cli-moment-shim": "^3.8.0", 85 | "ember-cli-sass": "^11.0.1", 86 | "ember-cli-sri": "^2.1.1", 87 | "ember-cli-terser": "^4.0.2", 88 | "ember-data": "~4.12.8", 89 | "ember-inflector": "^4.0.0", 90 | "ember-load-initializers": "^2.1.2", 91 | "ember-qunit": "^8.1.1", 92 | "ember-resolver": "^12.0.1", 93 | "ember-set-helper": "^3.0.1", 94 | "ember-shiki": "^0.3.0", 95 | "ember-source": "~5.12.0", 96 | "ember-source-channel-url": "^3.0.0", 97 | "ember-template-lint": "^6.1.0", 98 | "ember-truth-helpers": "^4.0.3", 99 | "ember-try": "^3.0.0", 100 | "eslint": "^8.57.1", 101 | "eslint-config-prettier": "^9.1.0", 102 | "eslint-plugin-ember": "^12.5.0", 103 | "eslint-plugin-n": "^16.6.2", 104 | "eslint-plugin-prettier": "^5.2.6", 105 | "eslint-plugin-qunit": "^8.1.2", 106 | "loader.js": "^4.7.0", 107 | "prettier": "^3.5.3", 108 | "qunit": "^2.24.1", 109 | "qunit-dom": "^3.4.0", 110 | "release-it": "^15.11.0", 111 | "release-plan": "^0.8.0", 112 | "sass": "^1.86.3", 113 | "stylelint": "^15.11.0", 114 | "stylelint-config-standard": "^34.0.0", 115 | "stylelint-prettier": "^4.1.0", 116 | "webpack": "^5.99.5" 117 | }, 118 | "peerDependencies": { 119 | "ember-data": "*", 120 | "ember-source": ">= 4.0.0" 121 | }, 122 | "peerDependenciesMeta": { 123 | "ember-data": { 124 | "optional": true 125 | } 126 | }, 127 | "engines": { 128 | "node": ">= 18" 129 | }, 130 | "publishConfig": { 131 | "registry": "https://registry.npmjs.org" 132 | }, 133 | "ember": { 134 | "edition": "octane" 135 | }, 136 | "ember-addon": { 137 | "configPath": "tests/dummy/config", 138 | "demoURL": "https://adopted-ember-addons.github.io/ember-cp-validations" 139 | }, 140 | "release-it": { 141 | "plugins": { 142 | "@release-it-plugins/lerna-changelog": { 143 | "infile": "CHANGELOG.md", 144 | "launchEditor": true 145 | } 146 | }, 147 | "git": { 148 | "tagName": "v${version}" 149 | }, 150 | "github": { 151 | "release": true, 152 | "tokenRef": "GITHUB_AUTH" 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | onlyBuiltDependencies: 2 | - '@parcel/watcher' 3 | - core-js 4 | -------------------------------------------------------------------------------- /scripts/write-snippets.mjs: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as path from "path"; 3 | 4 | const extractAndAppendSnippets = (filepath, snippetsData) => { 5 | let content = fs.readFileSync(filepath, "utf-8"); 6 | let rows = content.split("\n"); 7 | 8 | while (rows.length) { 9 | let row = rows.shift(); 10 | let match = row.match(/BEGIN-SNIPPET ([^\s*]+)/); 11 | if (match) { 12 | let snippetContent = []; 13 | while (rows.length) { 14 | row = rows.shift(); 15 | if (/END-SNIPPET/.test(row)) { 16 | break; 17 | } 18 | snippetContent.push(row); 19 | } 20 | snippetsData[match[1]] = { 21 | source: filepath, 22 | content: snippetContent.join("\n"), 23 | }; 24 | } 25 | } 26 | }; 27 | 28 | const buildSnippetsListData = (dir, snippetsData = {}) => { 29 | const files = fs.readdirSync(dir); 30 | 31 | for (let file of files) { 32 | const filepath = path.join(dir, file); 33 | const stat = fs.statSync(filepath); 34 | if (stat.isDirectory()) { 35 | buildSnippetsListData(filepath, snippetsData); 36 | } else if (file !== "snippets.js" && file.match(/\.(js|hbs)$/)) { 37 | extractAndAppendSnippets(filepath, snippetsData); 38 | } 39 | } 40 | }; 41 | 42 | let snippetsData = {}; 43 | buildSnippetsListData("tests/dummy/app/", snippetsData); 44 | 45 | fs.writeSync( 46 | fs.openSync("tests/dummy/app/snippets.js", "w"), 47 | `export default ${JSON.stringify(snippetsData)}`, 48 | 0, 49 | "utf8", 50 | ); 51 | -------------------------------------------------------------------------------- /stderr.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adopted-ember-addons/ember-cp-validations/13d8f8457ac9a50370050205bff189f20f526e01/stderr.log -------------------------------------------------------------------------------- /testem.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | test_page: 'tests/index.html?hidepassed', 5 | disable_watching: true, 6 | launch_in_ci: ['Chrome'], 7 | launch_in_dev: ['Chrome'], 8 | browser_start_timeout: 120, 9 | browser_args: { 10 | Chrome: { 11 | ci: [ 12 | // --no-sandbox is needed when running Chrome inside a container 13 | process.env.CI ? '--no-sandbox' : null, 14 | '--headless', 15 | '--disable-dev-shm-usage', 16 | '--disable-software-rasterizer', 17 | '--mute-audio', 18 | '--remote-debugging-port=0', 19 | '--window-size=1440,900', 20 | ].filter(Boolean), 21 | }, 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /tests/acceptance/dummy-index-test.js: -------------------------------------------------------------------------------- 1 | import { click, fillIn, find, visit } from '@ember/test-helpers'; 2 | import { setupApplicationTest } from 'ember-qunit'; 3 | import { module, test } from 'qunit'; 4 | import { dasherize } from '@ember/string'; 5 | 6 | const validInputValues = { 7 | username: 'offirgolan', 8 | password: 'Pass123', 9 | email: 'offirgolan@gmail.com', 10 | emailConfirmation: 'offirgolan@gmail.com', 11 | firstName: 'Offir', 12 | lastName: 'Golan', 13 | dob: '1/1/1930', 14 | }; 15 | 16 | module('Acceptance | Dummy | index', function (hooks) { 17 | setupApplicationTest(hooks); 18 | 19 | test('Page Loads', async function (assert) { 20 | assert.expect(2); 21 | await visit('/'); 22 | 23 | assert.dom('a.navbar-brand').hasText('CP Validations'); 24 | assert.dom('.form .register h2').hasText('Create an Account'); 25 | }); 26 | 27 | test('Helper tooltips', async function (assert) { 28 | assert.expect(2); 29 | await visit('/'); 30 | 31 | assert.dom('.section .section-info').exists({ count: 3 }); 32 | assert 33 | .dom(find('.section .section-info')) 34 | .includesText('These form inputs are bound to the User model'); 35 | }); 36 | 37 | test('Invalid form submit', async function (assert) { 38 | await visit('/'); 39 | await click('[data-test-sign-up]'); 40 | 41 | assert 42 | .dom('.form .alert') 43 | .hasText('Please fix all the errors below before continuing.'); 44 | }); 45 | 46 | test('Valid form submit', async function (assert) { 47 | await visit('/'); 48 | 49 | for (let key in validInputValues) { 50 | const selector = `[data-test-${dasherize(key)}]`; 51 | const input = find(`${selector} input`); 52 | 53 | assert.ok(input, `${selector} found`); 54 | await fillIn(input, validInputValues[key]); 55 | assert.dom(selector).hasClass('has-success'); 56 | } 57 | 58 | await click('[data-test-sign-up]'); 59 | // assert.dom('[data-test-tomster]').exists(); 60 | assert.dom('.form .registered .icon-success').exists(); 61 | assert.dom('.form .registered h2.success').hasText('Success'); 62 | }); 63 | 64 | test('Invalid to valid email', async function (assert) { 65 | assert.expect(4); 66 | await visit('/'); 67 | 68 | const input = find('[data-test-email] input'); 69 | 70 | assert.ok(input); 71 | await fillIn(input, 'invalid-email'); 72 | 73 | assert.dom('[data-test-email]').hasClass('has-error'); 74 | assert 75 | .dom('[data-test-email] .input-error') 76 | .hasText('This field must be a valid email address'); 77 | 78 | await fillIn(input, validInputValues.email); 79 | assert.dom('[data-test-email]').hasClass('has-success'); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /tests/dummy/app/adapters/application.js: -------------------------------------------------------------------------------- 1 | import RESTAPIAdapter from '@ember-data/adapter/rest'; 2 | 3 | export default RESTAPIAdapter.extend({}); 4 | -------------------------------------------------------------------------------- /tests/dummy/app/app.js: -------------------------------------------------------------------------------- 1 | import Application from '@ember/application'; 2 | import Resolver from 'ember-resolver'; 3 | import loadInitializers from 'ember-load-initializers'; 4 | import config from 'dummy/config/environment'; 5 | 6 | export default class App extends Application { 7 | modulePrefix = config.modulePrefix; 8 | podModulePrefix = config.podModulePrefix; 9 | Resolver = Resolver; 10 | } 11 | 12 | loadInitializers(App, config.modulePrefix); 13 | -------------------------------------------------------------------------------- /tests/dummy/app/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adopted-ember-addons/ember-cp-validations/13d8f8457ac9a50370050205bff189f20f526e01/tests/dummy/app/components/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/components/code-snippet.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/components/code-snippet.js: -------------------------------------------------------------------------------- 1 | import Component from '@glimmer/component'; 2 | import snippets from '../snippets'; 3 | 4 | export default class extends Component { 5 | get language() { 6 | return this.args.name?.split('.')[1]; 7 | } 8 | 9 | get snippet() { 10 | return snippets[this.args.name]; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/dummy/app/components/named-v-get.hbs: -------------------------------------------------------------------------------- 1 | {{!-- This component is used to test v-get helper usage with named args --}} 2 | {{#if @attr}} 3 | {{v-get @model @attr @field}} 4 | {{else}} 5 | {{v-get @model @field}} 6 | {{/if}} 7 | -------------------------------------------------------------------------------- /tests/dummy/app/components/validated-input.hbs: -------------------------------------------------------------------------------- 1 | {{! template-lint-disable }} 2 | 3 | {{! BEGIN-SNIPPET validated-input.hbs }} 4 |
5 |
6 | 15 | 16 | {{#if this.isValid}} 17 | 18 | {{/if}} 19 | 20 |
21 | {{#if this.showErrorMessage}} 22 |
23 | {{v-get @model @valuePath "message"}} 24 |
25 | {{/if}} 26 | 27 | {{#if this.showWarningMessage}} 28 |
29 | {{v-get @model @valuePath "warningMessage"}} 30 |
31 | {{/if}} 32 |
33 |
34 |
35 | {{! END-SNIPPET }} -------------------------------------------------------------------------------- /tests/dummy/app/components/validated-input.js: -------------------------------------------------------------------------------- 1 | // BEGIN-SNIPPET validated-input.js 2 | import Component from '@glimmer/component'; 3 | import { tracked } from '@glimmer/tracking'; 4 | import { action } from '@ember/object'; 5 | 6 | export default class ValidatedInput extends Component { 7 | @tracked showValidations = false; 8 | 9 | get notValidating() { 10 | return !this.validation.isValidating; 11 | } 12 | get hasContent() { 13 | return !!this.value; 14 | } 15 | get hasWarnings() { 16 | return !!this.validation.warnings; 17 | } 18 | get isValid() { 19 | return this.hasContent && this.validation.isTruelyValid; 20 | } 21 | get shouldDisplayValidations() { 22 | return this.showValidations || this.args.didValidate || this.hasContent; 23 | } 24 | get showErrorClass() { 25 | return ( 26 | this.notValidating && 27 | this.showErrorMessage && 28 | this.hasContent && 29 | this.validation 30 | ); 31 | } 32 | get showErrorMessage() { 33 | return this.shouldDisplayValidations && this.validation.isInvalid; 34 | } 35 | get showWarningMessage() { 36 | return this.shouldDisplayValidations && this.hasWarnings && this.isValid; 37 | } 38 | get validation() { 39 | return this.args.model.get(`validations.attrs.${this.args.valuePath}`); 40 | } 41 | get value() { 42 | return this.args.model.get(this.args.valuePath); 43 | } 44 | 45 | get classes() { 46 | let classes = []; 47 | 48 | if (this.showErrorClass) { 49 | classes.push('has-error'); 50 | } 51 | 52 | if (this.isValid) { 53 | classes.push('has-success'); 54 | } 55 | 56 | return classes.join(' '); 57 | } 58 | 59 | @action 60 | focusOut() { 61 | this.showValidations = true; 62 | } 63 | 64 | @action 65 | updateValue(event) { 66 | this.args.model.set(this.args.valuePath, event.target.value); 67 | } 68 | } 69 | // END-SNIPPET 70 | -------------------------------------------------------------------------------- /tests/dummy/app/controllers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adopted-ember-addons/ember-cp-validations/13d8f8457ac9a50370050205bff189f20f526e01/tests/dummy/app/controllers/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/controllers/index.js: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | import { action } from '@ember/object'; 3 | import { tracked } from '@glimmer/tracking'; 4 | 5 | export default class extends Controller { 6 | queryParams = ['file']; 7 | @tracked file = 'user-model.js'; 8 | 9 | @tracked showAlert = false; 10 | @tracked isRegistered = false; 11 | @tracked showCode = false; 12 | @tracked didValidate = false; 13 | 14 | @action 15 | validate() { 16 | this.model.validate().then(({ validations }) => { 17 | this.didValidate = true; 18 | 19 | if (validations.get('isValid')) { 20 | this.showAlert = false; 21 | this.isRegistered = true; 22 | this.showCode = false; 23 | } else { 24 | this.showAlert = true; 25 | } 26 | }); 27 | } 28 | 29 | @action 30 | toggleProperty(p) { 31 | this[p] = !this[p]; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/dummy/app/helpers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adopted-ember-addons/ember-cp-validations/13d8f8457ac9a50370050205bff189f20f526e01/tests/dummy/app/helpers/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Ember CP Validations 6 | 7 | 8 | 9 | {{content-for 'head'}} 10 | 11 | 12 | 13 | 14 | 15 | {{content-for 'head-footer'}} 16 | 17 | 18 | {{content-for 'body'}} 19 | 20 | 21 | 22 | 23 | {{content-for 'body-footer'}} 24 | 25 | 26 | -------------------------------------------------------------------------------- /tests/dummy/app/models/company.js: -------------------------------------------------------------------------------- 1 | import Model, { attr } from '@ember-data/model'; 2 | import { validator, buildValidations } from 'ember-cp-validations'; 3 | 4 | export const Validations = buildValidations({ 5 | name: validator('presence', { presence: true, description: 'Name' }), 6 | }); 7 | 8 | export default Model.extend(Validations, { 9 | name: attr('string'), 10 | }); 11 | -------------------------------------------------------------------------------- /tests/dummy/app/models/order-line.js: -------------------------------------------------------------------------------- 1 | import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; 2 | import { validator, buildValidations } from 'ember-cp-validations'; 3 | 4 | const Validations = buildValidations({ 5 | type: { 6 | description: 'Order Line Type', 7 | validators: [validator('ds-error'), validator('presence', true)], 8 | }, 9 | order: { 10 | description: 'Order', 11 | validators: [validator('ds-error'), validator('presence', true)], 12 | }, 13 | selections: { 14 | description: 'Order Selections', 15 | validators: [ 16 | validator('ds-error'), 17 | validator('has-many'), 18 | validator('presence', true), 19 | ], 20 | }, 21 | }); 22 | 23 | export default Model.extend(Validations, { 24 | order: belongsTo('order', { async: true, inverse: 'lines' }), 25 | selections: hasMany('order-selection', { async: true, inverse: 'line' }), 26 | type: attr('string'), 27 | }); 28 | -------------------------------------------------------------------------------- /tests/dummy/app/models/order-selection-question.js: -------------------------------------------------------------------------------- 1 | import Model, { attr, belongsTo } from '@ember-data/model'; 2 | import { validator, buildValidations } from 'ember-cp-validations'; 3 | 4 | const Validations = buildValidations( 5 | { 6 | order: { 7 | description: 'Order', 8 | validators: [validator('ds-error'), validator('presence', true)], 9 | }, 10 | selection: { 11 | description: 'Order Selection', 12 | validators: [validator('ds-error'), validator('presence', true)], 13 | }, 14 | text: { 15 | description: 'Question Text', 16 | validators: [validator('ds-error'), validator('presence', true)], 17 | }, 18 | }, 19 | { 20 | debounce: 10, 21 | }, 22 | ); 23 | 24 | export default Model.extend(Validations, { 25 | order: belongsTo('order', { async: true, inverse: 'questions' }), 26 | selection: belongsTo('order-selection', { 27 | async: true, 28 | inverse: 'questions', 29 | }), 30 | text: attr('string'), 31 | }); 32 | -------------------------------------------------------------------------------- /tests/dummy/app/models/order-selection.js: -------------------------------------------------------------------------------- 1 | import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; 2 | import { validator, buildValidations } from 'ember-cp-validations'; 3 | 4 | const Validations = buildValidations({ 5 | quantity: { 6 | description: 'Quantity', 7 | validators: [ 8 | validator('ds-error'), 9 | validator('number', { 10 | gte: 1, 11 | }), 12 | ], 13 | }, 14 | order: { 15 | description: 'Order', 16 | validators: [ 17 | validator('ds-error'), 18 | validator('belongs-to'), 19 | validator('presence', true), 20 | ], 21 | }, 22 | line: { 23 | description: 'Order Line', 24 | validators: [validator('ds-error'), validator('presence', true)], 25 | }, 26 | questions: { 27 | description: 'Order Selection Questions', 28 | validators: [ 29 | validator('ds-error'), 30 | validator('has-many'), 31 | validator('length', { 32 | min: 1, 33 | }), 34 | ], 35 | }, 36 | }); 37 | 38 | export default Model.extend(Validations, { 39 | order: belongsTo('order', { async: true, inverse: 'selections' }), 40 | line: belongsTo('order-line', { async: true, inverse: 'selections' }), 41 | questions: hasMany('order-selection-question', { 42 | async: true, 43 | inverse: 'selection', 44 | }), 45 | quantity: attr('number'), 46 | }); 47 | -------------------------------------------------------------------------------- /tests/dummy/app/models/order.js: -------------------------------------------------------------------------------- 1 | import Model, { attr, hasMany } from '@ember-data/model'; 2 | import { validator, buildValidations } from 'ember-cp-validations'; 3 | 4 | const Validations = buildValidations({ 5 | source: { 6 | description: 'Order Source', 7 | validators: [validator('ds-error'), validator('presence', true)], 8 | }, 9 | lines: { 10 | description: 'Order Lines', 11 | validators: [ 12 | validator('ds-error'), 13 | validator('has-many'), 14 | validator('presence', true), 15 | ], 16 | }, 17 | }); 18 | 19 | export default Model.extend(Validations, { 20 | source: attr('string'), 21 | lines: hasMany('order-line', { async: true, inverse: 'order' }), 22 | questions: hasMany('order-selection-question', { 23 | async: true, 24 | inverse: 'order', 25 | }), 26 | selections: hasMany('order-selection', { 27 | async: true, 28 | inverse: 'order', 29 | }), 30 | }); 31 | -------------------------------------------------------------------------------- /tests/dummy/app/models/signup.js: -------------------------------------------------------------------------------- 1 | import Model, { attr } from '@ember-data/model'; 2 | import { validator, buildValidations } from 'ember-cp-validations'; 3 | 4 | let Validations = buildValidations({ 5 | name: validator('presence', true), 6 | acceptTerms: validator('inline', { 7 | validate(value) { 8 | return value || 'You must accept the terms.'; 9 | }, 10 | }), 11 | }); 12 | 13 | export default Model.extend(Validations, { 14 | name: attr('string', { defaultValue: '' }), 15 | acceptTerms: attr('boolean', { defaultValue: false }), 16 | }); 17 | -------------------------------------------------------------------------------- /tests/dummy/app/models/user-detail.js: -------------------------------------------------------------------------------- 1 | // BEGIN-SNIPPET user-detail-model.js 2 | import Model, { attr } from '@ember-data/model'; 3 | import moment from 'moment'; 4 | import { validator, buildValidations } from 'ember-cp-validations'; 5 | 6 | const Validations = buildValidations( 7 | { 8 | firstName: validator('presence', true), 9 | lastName: validator('presence', true), 10 | dob: { 11 | description: 'Date of birth', 12 | validators: [ 13 | validator('presence', true), 14 | validator('date', { 15 | before: new Date(), 16 | get after() { 17 | return moment().subtract(120, 'years').format('M/D/YYYY'); 18 | }, 19 | format: 'M/D/YYYY', 20 | message(type, value /*, context */) { 21 | if (type === 'before') { 22 | return 'Are you from the future?'; 23 | } 24 | if (type === 'after') { 25 | return `There is no way you are ${moment().diff( 26 | value, 27 | 'years', 28 | )} years old`; 29 | } 30 | }, 31 | }), 32 | ], 33 | }, 34 | phone: [ 35 | validator('format', { 36 | allowBlank: true, 37 | type: 'phone', 38 | }), 39 | ], 40 | url: [ 41 | validator('format', { 42 | allowBlank: true, 43 | type: 'url', 44 | }), 45 | ], 46 | }, 47 | { 48 | debounce: 500, 49 | }, 50 | ); 51 | 52 | export default Model.extend(Validations, { 53 | firstName: attr('string'), 54 | lastName: attr('string'), 55 | dob: attr('date'), 56 | phone: attr('string'), 57 | url: attr('string'), 58 | }); 59 | // END-SNIPPET 60 | -------------------------------------------------------------------------------- /tests/dummy/app/models/user.js: -------------------------------------------------------------------------------- 1 | // BEGIN-SNIPPET user-model.js 2 | import Model, { attr, belongsTo } from '@ember-data/model'; 3 | import { validator, buildValidations } from 'ember-cp-validations'; 4 | 5 | const Validations = buildValidations( 6 | { 7 | username: { 8 | description: 'Username', 9 | validators: [ 10 | validator('presence', true), 11 | validator('length', { 12 | min: 5, 13 | max: 15, 14 | }), 15 | ], 16 | }, 17 | password: { 18 | description: 'Password', 19 | validators: [ 20 | validator('presence', true), 21 | validator('length', { 22 | min: 4, 23 | max: 10, 24 | }), 25 | validator('format', { 26 | regex: /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{4,10}$/, 27 | message: 28 | '{description} must include at least one upper case letter, one lower case letter, and a number', 29 | }), 30 | validator('length', { 31 | isWarning: true, 32 | min: 6, 33 | message: 'What kind of weak password is that?', 34 | }), 35 | ], 36 | }, 37 | email: { 38 | validators: [ 39 | validator('presence', true), 40 | validator('format', { 41 | type: 'email', 42 | }), 43 | ], 44 | }, 45 | emailConfirmation: validator('confirmation', { 46 | on: 'email', 47 | message: 'Email addresses do not match', 48 | }), 49 | details: validator('belongs-to'), 50 | }, 51 | { 52 | debounce: 500, 53 | }, 54 | ); 55 | 56 | export default Model.extend(Validations, { 57 | username: attr('string'), 58 | password: attr('string'), 59 | email: attr('string'), 60 | details: belongsTo('user-detail', { async: true, inverse: null }), 61 | }); 62 | // END-SNIPPET 63 | -------------------------------------------------------------------------------- /tests/dummy/app/router.js: -------------------------------------------------------------------------------- 1 | import EmberRouter from '@ember/routing/router'; 2 | import config from 'dummy/config/environment'; 3 | 4 | export default class Router extends EmberRouter { 5 | location = config.locationType; 6 | rootURL = config.rootURL; 7 | } 8 | 9 | Router.map(function () {}); 10 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adopted-ember-addons/ember-cp-validations/13d8f8457ac9a50370050205bff189f20f526e01/tests/dummy/app/routes/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/routes/index.js: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | import { inject as service } from '@ember/service'; 3 | 4 | export default class extends Route { 5 | @service store; 6 | 7 | model() { 8 | return this.store.createRecord('user', { 9 | details: this.store.createRecord('user-detail'), 10 | }); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/dummy/app/styles/app.scss: -------------------------------------------------------------------------------- 1 | $accent-color: #f23818; 2 | 3 | @import './navbar'; 4 | @import './form'; 5 | @import './code-snippet'; 6 | 7 | *, ::after, ::before { 8 | box-sizing: border-box; 9 | } 10 | 11 | body, 12 | html { 13 | min-height: 100%; 14 | min-width: 100%; 15 | background-color: #F3F3F3; 16 | height: 100%; 17 | font-family: 'Open Sans', sans-serif; 18 | -webkit-font-smoothing: antialiased; 19 | -moz-osx-font-smoothing: grayscale; 20 | margin: 0; 21 | } 22 | 23 | body { 24 | font-size: 14px; 25 | line-height: 1.428571429; 26 | color: #333; 27 | } 28 | 29 | dl, ol, ul { 30 | margin-top: 0; 31 | } 32 | 33 | .content { 34 | display: flex; 35 | justify-content: center; 36 | padding: 25px; 37 | flex: 1 0 auto; 38 | } 39 | -------------------------------------------------------------------------------- /tests/dummy/app/styles/code-snippet.scss: -------------------------------------------------------------------------------- 1 | $background-color: #fdfdfd; 2 | 3 | .snippet-container { 4 | background: $background-color; 5 | align-self: flex-start; 6 | overflow: auto; 7 | width: 0; 8 | height: 670px; 9 | margin: auto 0; 10 | -webkit-transition: width 0.35s ease; 11 | -moz-transition: width 0.35s ease; 12 | -o-transition: width 0.35s ease; 13 | transition: width 0.35s ease; 14 | border-radius: 3px; 15 | padding: 15px 10px; 16 | margin-left: -20px; 17 | z-index: 0; 18 | 19 | &.show { 20 | display: block; 21 | width: 600px; 22 | margin-left: -2px; 23 | box-shadow: 3px 3px 10px 1px rgba(0, 0, 0, 0.15); 24 | } 25 | 26 | .nav { 27 | padding-left: 0; 28 | margin-bottom: 0; 29 | list-style: none; 30 | } 31 | 32 | .nav::after, 33 | .nav::before { 34 | display: table; 35 | content: " "; 36 | } 37 | 38 | .nav > li, 39 | .nav > li > a { 40 | display: block; 41 | } 42 | 43 | .nav > li > a { 44 | padding: 10px 15px; 45 | text-decoration: none; 46 | } 47 | 48 | .nav-tabs { 49 | border-bottom: 1px solid #ddd; 50 | 51 | li { 52 | display: inline-block; 53 | margin-bottom: -1px; 54 | } 55 | 56 | li > a { 57 | color: #8A8A8A; 58 | border-radius: 2px 2px 0 0; 59 | font-size: 12px; 60 | line-height: 1.428571429; 61 | margin-right: 2px; 62 | border: 1px solid transparent; 63 | 64 | &.active { 65 | background-color: $background-color !important; 66 | color: #696969; 67 | cursor: default; 68 | border: 1px solid #ddd; 69 | border-bottom-color: transparent; 70 | } 71 | } 72 | } 73 | 74 | .tab-content { 75 | background-color: $background-color; 76 | max-height: 600px; 77 | overflow: auto; 78 | height: 100%; 79 | font-size: 12px; 80 | line-height: 1.428571429; 81 | } 82 | 83 | pre { 84 | background-color: $background-color; 85 | height: 100%; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /tests/dummy/app/styles/form.scss: -------------------------------------------------------------------------------- 1 | .form { 2 | text-align: center; 3 | position: relative; 4 | background: #ffffff; 5 | width: 485px; 6 | padding: 40px; 7 | border-top: 5px solid $accent-color; 8 | border-radius: 3px; 9 | box-shadow: 5px 5px 10px 1px rgba(0, 0, 0, 0.15); 10 | z-index: 5; 11 | align-self: center; 12 | } 13 | 14 | .form input { 15 | outline: none; 16 | display: block; 17 | width: 100%; 18 | padding: 0 15px; 19 | border: 1px solid #d9d9d9; 20 | border-radius: 3px; 21 | color: #6E6E6E; 22 | font-family: 'Roboto', sans-serif; 23 | -webkit-box-sizing: border-box; 24 | box-sizing: border-box; 25 | font-size: 14px; 26 | font-weight: 400; 27 | -webkit-font-smoothing: antialiased; 28 | -moz-osx-font-smoothing: grayscale; 29 | transition: all 0.3s linear 0s; 30 | box-shadow: none; 31 | } 32 | 33 | .form .form-group { 34 | margin: 0 0 20px; 35 | } 36 | 37 | .form-control { 38 | height: 34px; 39 | } 40 | 41 | .has-error .form-control:focus, 42 | .has-success .form-control:focus { 43 | box-shadow: none; 44 | } 45 | 46 | .has-error .form-control:focus { 47 | border-color: tomato; 48 | } 49 | 50 | .has-success .form-control:focus { 51 | border-color: rgb(31, 187, 20); 52 | } 53 | 54 | .has-success .form-control, 55 | .has-error .form-control { 56 | border-color: #d9d9d9; 57 | } 58 | 59 | input.form-control:focus { 60 | color: #333333; 61 | border-color: #b9b9b9; 62 | outline: none; 63 | box-shadow: none; 64 | -webkit-box-shadow: none; 65 | } 66 | 67 | .form h2 { 68 | margin: -15px 0 25px 0; 69 | line-height: 1; 70 | color: $accent-color; 71 | font-size: 18px; 72 | font-weight: 400; 73 | } 74 | 75 | .form form h4 { 76 | margin-bottom: 20px; 77 | color: rgb(107, 106, 106); 78 | font-weight: 400; 79 | font-size: 16px; 80 | } 81 | 82 | .form .alert { 83 | position: relative; 84 | background: #f3f3f3; 85 | color: #666666; 86 | font-size: 12px; 87 | margin-bottom: 20px; 88 | padding: 15px; 89 | text-align: left; 90 | } 91 | 92 | .form .alert .icon-remove { 93 | cursor: pointer; 94 | position: absolute; 95 | top: 50%; 96 | right: 10px; 97 | display: block; 98 | width: 16px; 99 | height: 16px; 100 | line-height: 16px; 101 | margin-top: -8px; 102 | float: right; 103 | } 104 | 105 | .form button { 106 | cursor: pointer; 107 | outline: none; 108 | background: $accent-color; 109 | width: 100%; 110 | padding: 10px 15px; 111 | margin-bottom: 25px; 112 | border: 0; 113 | border-radius: 3px; 114 | color: #ffffff; 115 | font-family: 'Roboto'; 116 | font-size: 14px; 117 | font-weight: 400; 118 | -webkit-font-smoothing: antialiased; 119 | -moz-osx-font-smoothing: grayscale; 120 | transition: all 0.3s linear 0s; 121 | } 122 | 123 | .form .section { 124 | position: relative; 125 | } 126 | 127 | .section-info { 128 | text-align: left; 129 | position: absolute; 130 | padding: 10px; 131 | width: 250px; 132 | top: 0; 133 | font-size: 12px; 134 | line-height: 1.75; 135 | display: block; 136 | background: rgba(0, 0, 0, 0.6); 137 | border-radius: 3px; 138 | color: rgba(255, 255, 255, 0.8); 139 | } 140 | 141 | .section-info.left:before, 142 | .section-info.right:before, 143 | .section-info.bottom:before { 144 | content: ''; 145 | position: absolute; 146 | top: 40%; 147 | display: block; 148 | border-top: 10px solid transparent; 149 | border-bottom: 10px solid transparent; 150 | } 151 | 152 | .form .section-info.left { 153 | left: -325px; 154 | } 155 | 156 | .section-info.left:before { 157 | right: -10px; 158 | border-left: 10px solid rgba(0, 0, 0, 0.6); 159 | } 160 | 161 | .section-info.right { 162 | right: -325px; 163 | } 164 | 165 | .section-info.right:before { 166 | left: -10px; 167 | border-right: 10px solid rgba(0, 0, 0, 0.6); 168 | } 169 | 170 | .section-info.bottom:before { 171 | top: -10px; 172 | border-bottom: 10px solid rgba(0, 0, 0, 0.8); 173 | border-left: 10px solid transparent; 174 | border-right: 10px solid transparent; 175 | border-top: initial; 176 | } 177 | 178 | 179 | /* Submit button info message */ 180 | 181 | .section-info.last { 182 | top: -50%; 183 | } 184 | 185 | .form footer { 186 | background: #eae9e9; 187 | width: 485px; 188 | padding: 15px 40px; 189 | margin: 0 0 -40px -40px; 190 | border-radius: 0 0 3px 3px; 191 | color: #666666; 192 | font-size: 12px; 193 | text-align: center; 194 | } 195 | 196 | .form footer a { 197 | color: #333333; 198 | text-decoration: none; 199 | } 200 | 201 | .form h2.success { 202 | font-size: 40px; 203 | margin-bottom: 40px; 204 | padding-top: 20px; 205 | } 206 | 207 | .form .tomster { 208 | position: absolute; 209 | top: 0; 210 | z-index: -1; 211 | width: 140px; 212 | right: -30px; 213 | -webkit-animation-name: peek; 214 | animation-name: peek; 215 | -webkit-animation-duration: 3s; 216 | animation-duration: 3s; 217 | -webkit-animation-fill-mode: forwards; 218 | animation-fill-mode: forwards; 219 | } 220 | 221 | .form .registered .icon-success { 222 | font-size: 160px; 223 | color: #A5A5A5; 224 | margin-bottom: 15px; 225 | } 226 | 227 | .validated-input { 228 | position: relative; 229 | } 230 | 231 | .validated-input .valid-input { 232 | position: absolute; 233 | right: 15px; 234 | top: 11px; 235 | font-size: 12px; 236 | color: #646262; 237 | } 238 | 239 | .validated-input .input-error { 240 | font-size: 12px; 241 | } 242 | 243 | .validated-input .input-error .error, 244 | .validated-input .input-error .warning { 245 | padding: 8px 5px 0 0; 246 | text-align: left; 247 | } 248 | 249 | .validated-input .input-error .error { 250 | color: rgb(255, 65, 31); 251 | } 252 | 253 | .validated-input .input-error .warning { 254 | color: rgb(255, 165, 31); 255 | } 256 | 257 | a.show-code { 258 | cursor: pointer; 259 | } 260 | 261 | @-webkit-keyframes peek { 262 | from { 263 | top: 0; 264 | } 265 | to { 266 | top: -130px; 267 | } 268 | } 269 | 270 | @keyframes peek { 271 | from { 272 | top: 0; 273 | } 274 | to { 275 | top: -130px; 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /tests/dummy/app/styles/navbar.scss: -------------------------------------------------------------------------------- 1 | .navbar { 2 | background-color: white; 3 | margin: 0; 4 | border-radius: 0; 5 | border-width: 0; 6 | border-bottom-width: 1px; 7 | display: flex; 8 | flex-direction: row; 9 | align-items: center; 10 | justify-content: space-between; 11 | height: 50px; 12 | font-size: 16px; 13 | padding: 0 20px; 14 | 15 | a { 16 | text-decoration: none; 17 | } 18 | 19 | .navbar-brand { 20 | color: #797979; 21 | font-weight: 400; 22 | display: block; 23 | height: 50px; 24 | display: flex; 25 | align-items: end; 26 | padding-bottom: 10px; 27 | 28 | img { 29 | height: 32px; 30 | display: inline-block; 31 | } 32 | 33 | span { 34 | font-size: 12px; 35 | vertical-align: super; 36 | text-transform: uppercase; 37 | color: #444444; 38 | } 39 | } 40 | 41 | .github { 42 | font-size: 24px; 43 | color: #333; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/application.hbs: -------------------------------------------------------------------------------- 1 | {{! template-lint-disable }} 2 | 3 | 13 | 14 |
15 | {{outlet}} 16 |
17 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/index.hbs: -------------------------------------------------------------------------------- 1 | {{! template-lint-disable }} 2 | 3 |
4 | {{#unless this.isRegistered}} 5 |
6 |

Create an Account

7 | 8 | {{#if this.showAlert}} 9 |
10 | 15 | Please fix all the errors below before continuing. 16 |
17 | {{/if}} 18 |
19 |
20 | {{#unless this.showCode}} 21 | 28 | {{/unless}} 29 | 30 | 37 | 45 | 52 | 59 |
60 | 61 |

About Me

62 | 63 |
64 | {{#unless this.showCode}} 65 | 72 | {{/unless}} 73 | 74 | 81 | 88 | 95 | 102 | 109 |
110 |
111 | {{#unless this.showCode}} 112 | 120 | {{/unless}} 121 | 127 |
128 |
129 |
130 | {{else}} 131 |
132 | {{! }} 133 | 134 |

Success

135 |
136 | {{/unless}} 137 | 154 |
155 | 156 | {{#unless this.isRegistered}} 157 |
158 | 176 |
177 | 178 |
179 |
180 | {{/unless}} -------------------------------------------------------------------------------- /tests/dummy/app/validators/messages.js: -------------------------------------------------------------------------------- 1 | import Messages from 'ember-cp-validations/validators/messages'; 2 | 3 | export default Messages.extend({ 4 | test: 'Test error message', 5 | }); 6 | -------------------------------------------------------------------------------- /tests/dummy/config/ember-cli-update.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.0.0", 3 | "packages": [ 4 | { 5 | "name": "ember-cli", 6 | "version": "5.12.0", 7 | "blueprints": [ 8 | { 9 | "name": "addon", 10 | "outputRepo": "https://github.com/ember-cli/ember-addon-output", 11 | "codemodsSource": "ember-addon-codemods-manifest@1", 12 | "isBaseBlueprint": true, 13 | "options": [ 14 | "--pnpm", 15 | "--no-welcome" 16 | ] 17 | } 18 | ] 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /tests/dummy/config/ember-try.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const getChannelURL = require('ember-source-channel-url'); 4 | const { embroiderSafe, embroiderOptimized } = require('@embroider/test-setup'); 5 | 6 | module.exports = async function () { 7 | return { 8 | usePnpm: true, 9 | buildManagerOptions() { 10 | return ['--ignore-scripts', '--no-frozen-lockfile']; 11 | }, 12 | scenarios: [ 13 | { 14 | name: 'ember-lts-4.4', 15 | npm: { 16 | devDependencies: { 17 | 'ember-resolver': '^11.0.1', 18 | 'ember-source': '~4.4.0', 19 | 'ember-data': '~4.4.0', 20 | }, 21 | }, 22 | }, 23 | { 24 | name: 'ember-lts-4.8', 25 | npm: { 26 | devDependencies: { 27 | 'ember-resolver': '^11.0.1', 28 | 'ember-source': '~4.8.0', 29 | 'ember-data': '~4.8.0', 30 | }, 31 | }, 32 | }, 33 | { 34 | name: 'ember-lts-4.12', 35 | npm: { 36 | devDependencies: { 37 | 'ember-source': '~4.12.0', 38 | }, 39 | }, 40 | }, 41 | { 42 | name: 'ember-lts-5.4', 43 | npm: { 44 | devDependencies: { 45 | 'ember-source': '~5.4.0', 46 | }, 47 | }, 48 | }, 49 | { 50 | name: 'ember-lts-5.8', 51 | npm: { 52 | devDependencies: { 53 | 'ember-source': '~5.8.0', 54 | }, 55 | }, 56 | }, 57 | { 58 | name: 'ember-lts-5.12', 59 | npm: { 60 | devDependencies: { 61 | 'ember-source': '~5.12.0', 62 | }, 63 | }, 64 | }, 65 | { 66 | name: 'ember-release', 67 | npm: { 68 | devDependencies: { 69 | 'ember-source': await getChannelURL('release'), 70 | }, 71 | }, 72 | }, 73 | { 74 | name: 'ember-beta', 75 | npm: { 76 | devDependencies: { 77 | 'ember-source': await getChannelURL('beta'), 78 | }, 79 | }, 80 | }, 81 | { 82 | name: 'ember-canary', 83 | npm: { 84 | devDependencies: { 85 | 'ember-source': await getChannelURL('canary'), 86 | }, 87 | }, 88 | }, 89 | embroiderSafe(), 90 | embroiderOptimized(), 91 | ], 92 | }; 93 | }; 94 | -------------------------------------------------------------------------------- /tests/dummy/config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (environment) { 4 | const ENV = { 5 | modulePrefix: 'dummy', 6 | environment, 7 | rootURL: '/', 8 | locationType: 'history', 9 | EmberENV: { 10 | EXTEND_PROTOTYPES: false, 11 | FEATURES: { 12 | // Here you can enable experimental features on an ember canary build 13 | // e.g. 'with-controller': true 14 | }, 15 | }, 16 | 17 | APP: { 18 | // Here you can pass flags/options to your application instance 19 | // when it is created 20 | }, 21 | }; 22 | 23 | if (environment === 'development') { 24 | // ENV.APP.LOG_RESOLVER = true; 25 | // ENV.APP.LOG_ACTIVE_GENERATION = true; 26 | // ENV.APP.LOG_TRANSITIONS = true; 27 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; 28 | // ENV.APP.LOG_VIEW_LOOKUPS = true; 29 | } 30 | 31 | if (environment === 'test') { 32 | // Testem prefers this... 33 | ENV.locationType = 'none'; 34 | 35 | // keep test console output quieter 36 | ENV.APP.LOG_ACTIVE_GENERATION = false; 37 | ENV.APP.LOG_VIEW_LOOKUPS = false; 38 | 39 | ENV.APP.rootElement = '#ember-testing'; 40 | ENV.APP.autoboot = false; 41 | } 42 | 43 | if (environment === 'production') { 44 | ENV.rootURL = '/ember-cp-validations/'; 45 | } 46 | 47 | return ENV; 48 | }; 49 | -------------------------------------------------------------------------------- /tests/dummy/config/icons.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | return { 3 | 'free-brands-svg-icons': ['github'], 4 | 'free-solid-svg-icons': ['check', 'check-circle', 'code', 'times'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /tests/dummy/config/optional-features.json: -------------------------------------------------------------------------------- 1 | { 2 | "application-template-wrapper": false, 3 | "default-async-observers": true, 4 | "jquery-integration": false, 5 | "template-only-glimmer-components": true 6 | } 7 | -------------------------------------------------------------------------------- /tests/dummy/config/targets.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const browsers = [ 4 | 'last 1 Chrome versions', 5 | 'last 1 Firefox versions', 6 | 'last 1 Safari versions', 7 | ]; 8 | 9 | module.exports = { 10 | browsers, 11 | }; 12 | -------------------------------------------------------------------------------- /tests/dummy/public/images/ember-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adopted-ember-addons/ember-cp-validations/13d8f8457ac9a50370050205bff189f20f526e01/tests/dummy/public/images/ember-logo.png -------------------------------------------------------------------------------- /tests/dummy/public/images/tomsterzilla.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adopted-ember-addons/ember-cp-validations/13d8f8457ac9a50370050205bff189f20f526e01/tests/dummy/public/images/tomsterzilla.jpeg -------------------------------------------------------------------------------- /tests/dummy/public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /tests/helpers/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | setupApplicationTest as upstreamSetupApplicationTest, 3 | setupRenderingTest as upstreamSetupRenderingTest, 4 | setupTest as upstreamSetupTest, 5 | } from 'ember-qunit'; 6 | 7 | // This file exists to provide wrappers around ember-qunit's 8 | // test setup functions. This way, you can easily extend the setup that is 9 | // needed per test type. 10 | 11 | function setupApplicationTest(hooks, options) { 12 | upstreamSetupApplicationTest(hooks, options); 13 | 14 | // Additional setup for application tests can be done here. 15 | // 16 | // For example, if you need an authenticated session for each 17 | // application test, you could do: 18 | // 19 | // hooks.beforeEach(async function () { 20 | // await authenticateSession(); // ember-simple-auth 21 | // }); 22 | // 23 | // This is also a good place to call test setup functions coming 24 | // from other addons: 25 | // 26 | // setupIntl(hooks, 'en-us'); // ember-intl 27 | // setupMirage(hooks); // ember-cli-mirage 28 | } 29 | 30 | function setupRenderingTest(hooks, options) { 31 | upstreamSetupRenderingTest(hooks, options); 32 | 33 | // Additional setup for rendering tests can be done here. 34 | } 35 | 36 | function setupTest(hooks, options) { 37 | upstreamSetupTest(hooks, options); 38 | 39 | // Additional setup for unit tests can be done here. 40 | } 41 | 42 | export { setupApplicationTest, setupRenderingTest, setupTest }; 43 | -------------------------------------------------------------------------------- /tests/helpers/setup-object.js: -------------------------------------------------------------------------------- 1 | export default function (context, obj, options = {}) { 2 | return obj.create(context.owner.ownerInjection(), options); 3 | } 4 | -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Dummy Tests 6 | 7 | 8 | 9 | {{content-for "head"}} 10 | {{content-for "test-head"}} 11 | 12 | 13 | 14 | 15 | 16 | {{content-for "head-footer"}} 17 | {{content-for "test-head-footer"}} 18 | 19 | 20 | {{content-for "body"}} 21 | {{content-for "test-body"}} 22 | 23 |
24 |
25 |
26 |
27 |
28 |
29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | {{content-for "body-footer"}} 37 | {{content-for "test-body-footer"}} 38 | 39 | 40 | -------------------------------------------------------------------------------- /tests/integration/helpers/v-get-test.js: -------------------------------------------------------------------------------- 1 | import EmberObject from '@ember/object'; 2 | import { module, test } from 'qunit'; 3 | import { setupRenderingTest } from 'ember-qunit'; 4 | import { render } from '@ember/test-helpers'; 5 | import hbs from 'htmlbars-inline-precompile'; 6 | 7 | module('Integration | Helper | v-get', function (hooks) { 8 | setupRenderingTest(hooks); 9 | 10 | hooks.beforeEach(function () { 11 | let model = EmberObject.create({ 12 | validations: { 13 | isValid: false, 14 | isInvalid: true, 15 | attrs: { 16 | username: { 17 | isValid: false, 18 | message: 'This field is invalid', 19 | }, 20 | email: { 21 | isValid: true, 22 | }, 23 | }, 24 | }, 25 | }); 26 | 27 | this.set('model', model); 28 | }); 29 | 30 | test('it renders', async function (assert) { 31 | assert.expect(1); 32 | 33 | await render(hbs`{{v-get this.model 'isValid'}}`); 34 | assert.dom(this.element).hasText('false'); 35 | }); 36 | 37 | test('access attribute validations', async function (assert) { 38 | assert.expect(3); 39 | 40 | await render(hbs`{{v-get this.model 'username' 'isValid'}}`); 41 | assert.dom(this.element).hasText('false'); 42 | 43 | await render(hbs`{{v-get this.model 'username' 'message'}}`); 44 | assert.dom(this.element).hasText('This field is invalid'); 45 | 46 | await render(hbs`{{v-get this.model 'email' 'isValid'}}`); 47 | assert.dom(this.element).hasText('true'); 48 | }); 49 | 50 | test('updating validation should rerender', async function (assert) { 51 | assert.expect(2); 52 | 53 | await render(hbs`{{v-get this.model 'username' 'isValid'}}`); 54 | assert.dom(this.element).hasText('false'); 55 | 56 | this.set('model.validations.attrs.username.isValid', true); 57 | 58 | assert.dom(this.element).hasText('true'); 59 | }); 60 | 61 | test('block statement param', async function (assert) { 62 | assert.expect(2); 63 | 64 | await render(hbs` 65 | {{#if (v-get this.model 'email' 'isValid')}} 66 | Email address is valid 67 | {{/if}} 68 | `); 69 | 70 | assert.dom(this.element).hasText('Email address is valid'); 71 | 72 | await render(hbs` 73 | {{#unless (v-get this.model 'username' 'isValid')}} 74 | {{v-get this.model 'username' 'message'}} 75 | {{/unless}} 76 | `); 77 | 78 | assert.dom(this.element).hasText('This field is invalid'); 79 | }); 80 | 81 | test('element node attribute', async function (assert) { 82 | assert.expect(2); 83 | 84 | await render( 85 | hbs``, 86 | ); 87 | 88 | assert.dom(this.element).hasText('Button'); 89 | assert.true(this.element.querySelector('button').disabled); 90 | }); 91 | 92 | test('element node attribute in class string', async function (assert) { 93 | assert.expect(3); 94 | 95 | await render( 96 | hbs`Text`, 97 | ); 98 | 99 | assert.dom(this.element).hasText('Text'); 100 | assert 101 | .dom(this.element.querySelector('span')) 102 | .hasClass('base', 'base class present'); 103 | assert 104 | .dom(this.element.querySelector('span')) 105 | .hasClass('has-error', 'error class present'); 106 | }); 107 | 108 | test('access validations with named args', async function (assert) { 109 | assert.expect(2); 110 | 111 | await render(hbs``); 112 | assert.dom(this.element).hasText('false'); 113 | 114 | await render( 115 | hbs``, 116 | ); 117 | assert.dom(this.element).hasText('false'); 118 | }); 119 | }); 120 | -------------------------------------------------------------------------------- /tests/integration/validators/composable-test.js: -------------------------------------------------------------------------------- 1 | import DefaultMessages from 'dummy/validators/messages'; 2 | import LengthValidator from 'ember-cp-validations/validators/length'; 3 | import PresenceValidator from 'ember-cp-validations/validators/presence'; 4 | import BaseValidator from 'ember-cp-validations/validators/base'; 5 | import EmberObject from '@ember/object'; 6 | import setupObject from '../../helpers/setup-object'; 7 | import { validator, buildValidations } from 'ember-cp-validations'; 8 | import { module, test } from 'qunit'; 9 | import { setupTest } from 'ember-qunit'; 10 | import { Promise } from 'rsvp'; 11 | 12 | const ComposedValidations = buildValidations({ 13 | value: validator('composed'), 14 | }); 15 | 16 | module('Integration | Validators | Composable', function (hooks) { 17 | setupTest(hooks); 18 | 19 | hooks.beforeEach(function () { 20 | this.owner.register('validator:messages', DefaultMessages); 21 | }); 22 | 23 | test('Composability - simple', function (assert) { 24 | assert.expect(5); 25 | 26 | this.owner.register('validator:presence', PresenceValidator); 27 | this.owner.register( 28 | 'validator:composed', 29 | BaseValidator.extend({ 30 | validate(value) { 31 | return this.test('presence', value, { presence: true }); 32 | }, 33 | }), 34 | ); 35 | 36 | const obj = setupObject(this, EmberObject.extend(ComposedValidations), { 37 | value: '', 38 | }); 39 | 40 | assert.false(obj.get('validations.isValid')); 41 | assert.false(obj.get('validations.isValidating')); 42 | assert.strictEqual( 43 | obj.get('validations.message'), 44 | "This field can't be blank", 45 | ); 46 | 47 | obj.set('value', 'foo'); 48 | 49 | assert.true(obj.get('validations.isValid')); 50 | assert.false(obj.get('validations.isValidating')); 51 | }); 52 | 53 | test('Composability - multiple', function (assert) { 54 | assert.expect(8); 55 | 56 | this.owner.register('validator:presence', PresenceValidator); 57 | this.owner.register('validator:length', LengthValidator); 58 | this.owner.register( 59 | 'validator:composed', 60 | BaseValidator.extend({ 61 | validate(value) { 62 | let result = this.test('presence', value, { presence: true }); 63 | 64 | if (!result.isValid) { 65 | return result.message; 66 | } 67 | 68 | result = this.test('length', value, { max: 5 }); 69 | 70 | if (!result.isValid) { 71 | return result.message; 72 | } 73 | 74 | return true; 75 | }, 76 | }), 77 | ); 78 | 79 | const obj = setupObject(this, EmberObject.extend(ComposedValidations), { 80 | value: '', 81 | }); 82 | 83 | assert.false(obj.get('validations.isValid')); 84 | assert.false(obj.get('validations.isValidating')); 85 | assert.strictEqual( 86 | obj.get('validations.message'), 87 | "This field can't be blank", 88 | ); 89 | 90 | obj.set('value', 'foobar'); 91 | 92 | assert.false(obj.get('validations.isValid')); 93 | assert.false(obj.get('validations.isValidating')); 94 | assert.strictEqual( 95 | obj.get('validations.message'), 96 | 'This field is too long (maximum is 5 characters)', 97 | ); 98 | 99 | obj.set('value', 'foo'); 100 | 101 | assert.true(obj.get('validations.isValid')); 102 | assert.false(obj.get('validations.isValidating')); 103 | }); 104 | 105 | test('Composability - async', async function (assert) { 106 | assert.expect(8); 107 | 108 | this.owner.register( 109 | 'validator:async-resolve', 110 | BaseValidator.extend({ 111 | validate(value) { 112 | return Promise.resolve( 113 | value.includes('foo') ? true : 'Must include foo!', 114 | ); 115 | }, 116 | }), 117 | ); 118 | 119 | this.owner.register( 120 | 'validator:async-reject', 121 | BaseValidator.extend({ 122 | validate(value) { 123 | return Promise.reject( 124 | value.includes('bar') ? true : 'Must include bar!', 125 | ); 126 | }, 127 | }), 128 | ); 129 | 130 | this.owner.register( 131 | 'validator:composed', 132 | BaseValidator.extend({ 133 | async validate(value) { 134 | let result = await this.test('async-resolve', value); 135 | 136 | if (!result.isValid) { 137 | return result.message; 138 | } 139 | 140 | result = await this.test('async-reject', value); 141 | 142 | if (!result.isValid) { 143 | return result.message; 144 | } 145 | 146 | return true; 147 | }, 148 | }), 149 | ); 150 | 151 | const obj = setupObject(this, EmberObject.extend(ComposedValidations), { 152 | value: '', 153 | }); 154 | 155 | await obj.validate(); 156 | 157 | assert.false(obj.get('validations.isValid')); 158 | assert.false(obj.get('validations.isValidating')); 159 | assert.strictEqual(obj.get('validations.message'), 'Must include foo!'); 160 | 161 | obj.set('value', 'foo'); 162 | await obj.validate(); 163 | 164 | assert.false(obj.get('validations.isValid')); 165 | assert.false(obj.get('validations.isValidating')); 166 | assert.strictEqual(obj.get('validations.message'), 'Must include bar!'); 167 | 168 | obj.set('value', 'foobar'); 169 | await obj.validate(); 170 | 171 | assert.true(obj.get('validations.isValid')); 172 | assert.false(obj.get('validations.isValidating')); 173 | }); 174 | 175 | test('Composability - unsupported types', function (assert) { 176 | const unsupportedTypes = ['alias', 'belongs-to', 'dependent', 'has-many']; 177 | 178 | assert.expect(unsupportedTypes.length); 179 | 180 | unsupportedTypes.forEach((type) => 181 | this.owner.register(`validator:${type}`, BaseValidator), 182 | ); 183 | 184 | this.owner.register( 185 | 'validator:composed', 186 | BaseValidator.extend({ 187 | validate(type) { 188 | this.test(type); 189 | }, 190 | }), 191 | ); 192 | 193 | const obj = setupObject(this, EmberObject.extend(ComposedValidations), { 194 | value: '', 195 | }); 196 | 197 | unsupportedTypes.forEach((type) => { 198 | obj.set('value', type); 199 | assert.throws(() => obj.validateSync(), new RegExp(type)); 200 | }); 201 | }); 202 | 203 | test('Validators get cached', function (assert) { 204 | assert.expect(3); 205 | 206 | this.owner.register('validator:presence', PresenceValidator); 207 | this.owner.register( 208 | 'validator:composed', 209 | BaseValidator.extend({ 210 | validate(value) { 211 | const cache = this._testValidatorCache; 212 | 213 | assert.notOk(cache.presence); 214 | 215 | this.test('presence', value, { presence: true }); 216 | 217 | const presenceValidator = cache.presence; 218 | assert.ok(cache.presence); 219 | 220 | this.test('presence', value, { presence: false }); 221 | 222 | assert.strictEqual(presenceValidator, cache.presence); 223 | }, 224 | }), 225 | ); 226 | 227 | const obj = setupObject(this, EmberObject.extend(ComposedValidations), { 228 | value: '', 229 | }); 230 | 231 | obj.validateSync(); 232 | }); 233 | }); 234 | -------------------------------------------------------------------------------- /tests/test-helper.js: -------------------------------------------------------------------------------- 1 | import Application from 'dummy/app'; 2 | import config from 'dummy/config/environment'; 3 | import * as QUnit from 'qunit'; 4 | import { setApplication } from '@ember/test-helpers'; 5 | import { setup } from 'qunit-dom'; 6 | import { start } from 'ember-qunit'; 7 | 8 | setApplication(Application.create(config.APP)); 9 | 10 | setup(QUnit.assert); 11 | 12 | start(); 13 | -------------------------------------------------------------------------------- /tests/unit/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adopted-ember-addons/ember-cp-validations/13d8f8457ac9a50370050205bff189f20f526e01/tests/unit/.gitkeep -------------------------------------------------------------------------------- /tests/unit/utils/assign-test.js: -------------------------------------------------------------------------------- 1 | import EmberObject, { computed } from '@ember/object'; 2 | import { module, test } from 'qunit'; 3 | import deepSet from 'ember-cp-validations/utils/deep-set'; 4 | 5 | module('Unit | Utils | deepSet', function () { 6 | test('single level', function (assert) { 7 | let obj = {}; 8 | deepSet(obj, 'foo.bar', 1); 9 | assert.deepEqual(obj, { foo: { bar: 1 } }); 10 | }); 11 | 12 | test('single level - ember object', function (assert) { 13 | let obj = EmberObject.create(); 14 | deepSet(obj, 'foo.bar', 1, true); 15 | assert.ok(obj.foo instanceof EmberObject); 16 | assert.strictEqual(obj.get('foo.bar'), 1); 17 | }); 18 | 19 | test('single level - ember object w/ CP', function (assert) { 20 | let obj = EmberObject.create(); 21 | deepSet( 22 | obj, 23 | 'foo.bar', 24 | computed(function () { 25 | return 1; 26 | }), 27 | true, 28 | ); 29 | assert.ok(obj.foo instanceof EmberObject); 30 | assert.strictEqual(obj.get('foo.bar'), 1); 31 | }); 32 | 33 | test('multi level', function (assert) { 34 | let obj = {}; 35 | deepSet(obj, 'foo.bar.baz.boo', 1); 36 | assert.deepEqual(obj, { foo: { bar: { baz: { boo: 1 } } } }); 37 | }); 38 | 39 | test('multi level - ember object', function (assert) { 40 | let obj = EmberObject.create(); 41 | deepSet(obj, 'foo.bar.baz.boo', 1, true); 42 | assert.ok(obj.foo.bar.baz instanceof EmberObject); 43 | assert.strictEqual(obj.get('foo.bar.baz.boo'), 1); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /tests/unit/utils/flatten-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { flatten } from 'ember-cp-validations/utils/array'; 3 | 4 | module('Unit | Utils | array:flatten', function () { 5 | test('flattens an array of arrays', function (assert) { 6 | let result = flatten([[1], [2, 3], [4, 5]]); 7 | 8 | assert.deepEqual(result, [1, 2, 3, 4, 5]); 9 | }); 10 | 11 | test('flattens an array of arrays of arrays ;P', function (assert) { 12 | let result = flatten([ 13 | [[1], [2, 3]], 14 | [4, 5], 15 | ]); 16 | 17 | assert.deepEqual(result, [1, 2, 3, 4, 5]); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /tests/unit/utils/get-with-default-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import getWithDefault from 'ember-cp-validations/utils/get-with-default'; 3 | 4 | module('Unit | Utils | get with default', function () { 5 | test('return the default value when value is undefined', function (assert) { 6 | const undefinedObj = { 7 | foo: undefined, 8 | }; 9 | const defaultValue = 'something'; 10 | let result = getWithDefault(undefinedObj, 'foo', defaultValue); 11 | 12 | assert.strictEqual(result, defaultValue); 13 | }); 14 | 15 | test('return the correct value if defined', function (assert) { 16 | const obj = { 17 | yehuda: 'katz', 18 | }; 19 | 20 | const defaultValue = 'something'; 21 | let result = getWithDefault(obj, 'yehuda', defaultValue); 22 | 23 | assert.strictEqual(result, 'katz'); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /tests/unit/utils/should-call-super-test.js: -------------------------------------------------------------------------------- 1 | import EmberObject, { computed } from '@ember/object'; 2 | import { module, test } from 'qunit'; 3 | import shouldCallSuper from 'ember-cp-validations/utils/should-call-super'; 4 | 5 | module('Unit | Utils | shouldCallSuper', function () { 6 | test('shouldCallSuper - true', function (assert) { 7 | let Parent = EmberObject.extend({ 8 | foo: computed(function () { 9 | return null; 10 | }), 11 | }); 12 | 13 | let Child = Parent.extend({ 14 | foo: computed(function () { 15 | assert.ok(shouldCallSuper(this, 'foo')); 16 | return null; 17 | }), 18 | }); 19 | 20 | let child = Child.create(); 21 | child.get('foo'); 22 | }); 23 | 24 | test('shouldCallSuper - false', function (assert) { 25 | let Parent = EmberObject.extend(); 26 | 27 | let Child = Parent.extend({ 28 | foo: computed(function () { 29 | assert.notOk(shouldCallSuper(this, 'foo')); 30 | return null; 31 | }), 32 | }); 33 | 34 | let child = Child.create(); 35 | child.get('foo'); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /tests/unit/validations/ds-model-test.js: -------------------------------------------------------------------------------- 1 | import { run } from '@ember/runloop'; 2 | import { module, test } from 'qunit'; 3 | import { setupTest } from 'ember-qunit'; 4 | 5 | module('Unit | Validations | Model', function (hooks) { 6 | setupTest(hooks); 7 | 8 | test('create model with defaults', function (assert) { 9 | let object = run(() => 10 | this.owner.lookup('service:store').createRecord('signup'), 11 | ); 12 | 13 | assert.false( 14 | object.get('validations.attrs.acceptTerms.isValid'), 15 | 'isValid was expected to be FALSE', 16 | ); 17 | 18 | run(() => { 19 | object.set('acceptTerms', true); 20 | }); 21 | 22 | assert.true( 23 | object.get('validations.attrs.acceptTerms.isValid'), 24 | 'isValid was expected to be TRUE', 25 | ); 26 | }); 27 | 28 | test('create model overriding defaults', function (assert) { 29 | let object = run(() => 30 | this.owner 31 | .lookup('service:store') 32 | .createRecord('signup', { acceptTerms: true }), 33 | ); 34 | 35 | assert.true( 36 | object.get('validations.attrs.acceptTerms.isValid'), 37 | 'isValid was expected to be TRUE', 38 | ); 39 | 40 | run(() => { 41 | object.set('acceptTerms', false); 42 | }); 43 | 44 | assert.false( 45 | object.get('validations.attrs.acceptTerms.isValid'), 46 | 'isValid was expected to be FALSE', 47 | ); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /tests/unit/validations/ember-proxy-test.js: -------------------------------------------------------------------------------- 1 | import ObjectProxy from '@ember/object/proxy'; 2 | import { run } from '@ember/runloop'; 3 | import { module, test } from 'qunit'; 4 | import { setupTest } from 'ember-qunit'; 5 | import { Validations } from 'dummy/models/company'; 6 | 7 | module('Unit | Validations | Ember ObjectProxy', function (hooks) { 8 | setupTest(hooks); 9 | 10 | test('extend ObjectProxy with validations', function (assert) { 11 | run(() => { 12 | let company = this.owner.lookup('service:store').createRecord('company'); 13 | 14 | const container = this.owner.ownerInjection(); 15 | const proxy = ObjectProxy.extend(Validations).create(container, { 16 | content: company, 17 | }); 18 | 19 | assert.notOk(proxy.get('validations.isValid')); 20 | assert.notOk(company.get('validations.isValid')); 21 | 22 | proxy.set('name', 'whatever'); 23 | 24 | assert.ok(proxy.get('validations.isValid')); 25 | assert.ok(company.get('validations.isValid')); 26 | }); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /tests/unit/validators/base-test.js: -------------------------------------------------------------------------------- 1 | import { htmlSafe } from '@ember/template'; 2 | import EmberObject from '@ember/object'; 3 | import { alias } from '@ember/object/computed'; 4 | import BaseValidator from 'ember-cp-validations/validators/base'; 5 | import { module, test } from 'qunit'; 6 | import { setupTest } from 'ember-qunit'; 7 | import setupObject from '../../helpers/setup-object'; 8 | 9 | let defaultOptions, options, validator, message; 10 | 11 | module('Unit | Validator | base', function (hooks) { 12 | setupTest(hooks); 13 | 14 | hooks.beforeEach(function () { 15 | validator = setupObject(this, BaseValidator); 16 | }); 17 | 18 | test('buildOptions - merge all options', function (assert) { 19 | assert.expect(1); 20 | 21 | options = { 22 | foo: 'a', 23 | }; 24 | 25 | defaultOptions = { 26 | bar: 'b', 27 | }; 28 | 29 | options = validator.buildOptions(options, defaultOptions); 30 | assert.deepEqual(options.getProperties(['foo', 'bar']), { 31 | foo: 'a', 32 | bar: 'b', 33 | }); 34 | }); 35 | 36 | test('buildOptions - does not overwrite options', function (assert) { 37 | assert.expect(1); 38 | 39 | options = { 40 | foo: 'a', 41 | bar: 'b', 42 | }; 43 | 44 | defaultOptions = { 45 | bar: 'c', 46 | }; 47 | 48 | options = validator.buildOptions(options, defaultOptions); 49 | assert.deepEqual(options.getProperties(['foo', 'bar']), { 50 | foo: 'a', 51 | bar: 'b', 52 | }); 53 | }); 54 | 55 | test('buildOptions - toObject', function (assert) { 56 | assert.expect(4); 57 | 58 | options = validator.buildOptions({ 59 | foo: alias('bar'), 60 | bar: 'bar', 61 | }); 62 | 63 | assert.ok(options instanceof EmberObject); 64 | 65 | let optionsObj = options.toObject(); 66 | 67 | assert.strictEqual(typeof optionsObj, 'object'); 68 | assert.notOk(optionsObj instanceof EmberObject); 69 | assert.strictEqual(optionsObj.foo, 'bar'); 70 | }); 71 | 72 | test('createErrorMessage - message function', function (assert) { 73 | assert.expect(1); 74 | 75 | options = { 76 | message() { 77 | return '{description} has some sort of error'; 78 | }, 79 | }; 80 | 81 | message = validator.createErrorMessage(undefined, undefined, options); 82 | assert.strictEqual(message, 'This field has some sort of error'); 83 | }); 84 | 85 | test('value - default gets model value', function (assert) { 86 | assert.expect(2); 87 | 88 | validator.setProperties({ 89 | model: EmberObject.create({ foo: 'bar' }), 90 | attribute: 'foo', 91 | options: {}, 92 | }); 93 | 94 | validator.init(); 95 | 96 | assert.strictEqual(validator.get('attribute'), 'foo'); 97 | assert.strictEqual(validator.getValue(), 'bar'); 98 | }); 99 | 100 | test('value - overwrite value method via options', function (assert) { 101 | assert.expect(3); 102 | 103 | validator.setProperties({ 104 | model: EmberObject.create({ foo: 'bar', bar: 'baz' }), 105 | attribute: 'foo', 106 | options: { 107 | value() { 108 | return this.model.bar; 109 | }, 110 | }, 111 | }); 112 | 113 | validator.init(); 114 | 115 | assert.strictEqual(validator.get('attribute'), 'foo'); 116 | assert.strictEqual(validator.getValue(), 'baz'); 117 | assert.notOk(validator.get('options.value')); 118 | }); 119 | 120 | test('message - handles SafeString', function (assert) { 121 | assert.expect(1); 122 | 123 | options = { 124 | message: htmlSafe('should be more than €15'), 125 | }; 126 | 127 | message = validator.createErrorMessage(undefined, undefined, options); 128 | assert.strictEqual(message, 'should be more than €15'); 129 | }); 130 | }); 131 | -------------------------------------------------------------------------------- /tests/unit/validators/collection-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { setupTest } from 'ember-qunit'; 3 | 4 | let options, builtOptions, validator, message; 5 | 6 | module('Unit | Validator | collection', function (hooks) { 7 | setupTest(hooks); 8 | 9 | hooks.beforeEach(function () { 10 | validator = this.owner.lookup('validator:collection'); 11 | }); 12 | 13 | test('buildOptions', function (assert) { 14 | assert.expect(2); 15 | 16 | options = true; 17 | builtOptions = validator.buildOptions(options, {}); 18 | 19 | assert.true(builtOptions.get('collection')); 20 | 21 | options = { collection: true }; 22 | builtOptions = validator.buildOptions(options, {}); 23 | assert.true(builtOptions.get('collection')); 24 | }); 25 | 26 | test('value is collection', function (assert) { 27 | assert.expect(1); 28 | 29 | options = { collection: true }; 30 | builtOptions = validator.buildOptions(options); 31 | 32 | message = validator.validate(['foo', 'bar'], builtOptions.toObject()); 33 | assert.true(message); 34 | }); 35 | 36 | test('value not collection', function (assert) { 37 | assert.expect(1); 38 | 39 | options = { collection: true }; 40 | builtOptions = validator.buildOptions(options); 41 | 42 | message = validator.validate('foo', builtOptions.toObject()); 43 | assert.strictEqual(message, 'This field must be a collection'); 44 | }); 45 | 46 | test('singular - value is singular', function (assert) { 47 | assert.expect(1); 48 | 49 | options = { collection: false }; 50 | builtOptions = validator.buildOptions(options); 51 | 52 | message = validator.validate('value', builtOptions.toObject()); 53 | assert.true(message); 54 | }); 55 | 56 | test('singular - value not singular', function (assert) { 57 | assert.expect(1); 58 | 59 | options = { collection: false }; 60 | builtOptions = validator.buildOptions(options); 61 | 62 | message = validator.validate(['foo', 'bar'], builtOptions.toObject()); 63 | assert.strictEqual(message, "This field can't be a collection"); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /tests/unit/validators/confirmation-test.js: -------------------------------------------------------------------------------- 1 | import EmberObject from '@ember/object'; 2 | import { module, test } from 'qunit'; 3 | import { setupTest } from 'ember-qunit'; 4 | 5 | let model, options, builtOptions, validator, message; 6 | 7 | module('Unit | Validator | confirmation', function (hooks) { 8 | setupTest(hooks); 9 | 10 | hooks.beforeEach(function () { 11 | validator = this.owner.lookup('validator:confirmation'); 12 | }); 13 | 14 | test('attribute', function (assert) { 15 | assert.expect(2); 16 | 17 | options = { on: 'email' }; 18 | builtOptions = validator.buildOptions(options); 19 | 20 | model = EmberObject.create({ 21 | email: 'foo@gmail.com', 22 | }); 23 | 24 | message = validator.validate( 25 | 'bar@gmail.com', 26 | builtOptions.toObject(), 27 | model, 28 | ); 29 | assert.strictEqual(message, "This field doesn't match email"); 30 | 31 | model.set('emailConfirmation', 'foo@gmail.com'); 32 | 33 | message = validator.validate( 34 | 'foo@gmail.com', 35 | builtOptions.toObject(), 36 | model, 37 | ); 38 | assert.true(message); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /tests/unit/validators/dependent-test.js: -------------------------------------------------------------------------------- 1 | import EmberObject from '@ember/object'; 2 | import { validator, buildValidations } from 'ember-cp-validations'; 3 | import { module, test } from 'qunit'; 4 | import { setupTest } from 'ember-qunit'; 5 | import setupObject from '../../helpers/setup-object'; 6 | 7 | let Validator, message, model, options, builtOptions; 8 | 9 | let Validations = buildValidations({ 10 | firstName: validator('presence', true), 11 | lastName: validator('presence', true), 12 | }); 13 | 14 | let defaultOptions = { 15 | on: ['firstName', 'lastName'], 16 | }; 17 | 18 | module('Unit | Validator | dependent', function (hooks) { 19 | setupTest(hooks); 20 | 21 | hooks.beforeEach(function () { 22 | Validator = this.owner.lookup('validator:dependent'); 23 | }); 24 | 25 | test('no options', function (assert) { 26 | assert.expect(1); 27 | 28 | builtOptions = Validator.buildOptions({}).toObject(); 29 | 30 | try { 31 | message = Validator.validate(undefined, builtOptions); 32 | } catch (e) { 33 | assert.ok(true); 34 | } 35 | }); 36 | 37 | test('all empty attributes', function (assert) { 38 | assert.expect(4); 39 | 40 | options = defaultOptions; 41 | builtOptions = Validator.buildOptions(options); 42 | 43 | model = setupObject(this, EmberObject.extend(Validations)); 44 | 45 | assert.false(model.validations.isValid); 46 | 47 | message = Validator.validate(undefined, builtOptions.toObject(), model); 48 | 49 | assert.strictEqual(message, 'This field is invalid'); 50 | assert.strictEqual(model.validations.messages.length, 1); 51 | assert.false(model.validations.isValid); 52 | }); 53 | 54 | test('one dependent error', function (assert) { 55 | assert.expect(4); 56 | 57 | options = defaultOptions; 58 | builtOptions = Validator.buildOptions(options); 59 | 60 | model = setupObject(this, EmberObject.extend(Validations), { 61 | firstName: 'Offir', 62 | }); 63 | 64 | assert.false(model.validations.isValid); 65 | 66 | message = Validator.validate(undefined, builtOptions.toObject(), model); 67 | 68 | assert.strictEqual(message, 'This field is invalid'); 69 | assert.strictEqual(model.validations.messages.length, 1); 70 | assert.false(model.validations.isValid); 71 | }); 72 | 73 | test('no dependent errors', function (assert) { 74 | assert.expect(4); 75 | options = defaultOptions; 76 | builtOptions = Validator.buildOptions(options); 77 | 78 | model = setupObject(this, EmberObject.extend(Validations), { 79 | firstName: 'Offir', 80 | lastName: 'Golan', 81 | }); 82 | 83 | assert.true(model.validations.isValid); 84 | 85 | message = Validator.validate(undefined, builtOptions.toObject(), model); 86 | 87 | assert.true(message); 88 | assert.strictEqual(model.validations.messages.length, 0); 89 | assert.true(model.validations.isValid); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /tests/unit/validators/ds-error-test.js: -------------------------------------------------------------------------------- 1 | import EmberObject from '@ember/object'; 2 | import { module, test } from 'qunit'; 3 | import { setupTest } from 'ember-qunit'; 4 | 5 | let model, validator, message; 6 | 7 | module('Unit | Validator | ds-error', function (hooks) { 8 | setupTest(hooks); 9 | 10 | hooks.beforeEach(function () { 11 | validator = this.owner.lookup('validator:ds-error'); 12 | }); 13 | 14 | test('works with empty object', function (assert) { 15 | assert.expect(1); 16 | 17 | model = EmberObject.create(); 18 | 19 | message = validator.validate(undefined, undefined, model, 'username'); 20 | assert.true(message); 21 | }); 22 | 23 | test('it works', function (assert) { 24 | assert.expect(2); 25 | 26 | let model = this.owner.lookup('service:store').createRecord('user'); 27 | 28 | message = validator.validate(undefined, undefined, model, 'username'); 29 | assert.true(message); 30 | 31 | model.get('errors').add('username', 'Username is not unique'); 32 | 33 | message = validator.validate(undefined, undefined, model, 'username'); 34 | assert.strictEqual(message, 'Username is not unique'); 35 | }); 36 | 37 | test('gets last message', function (assert) { 38 | assert.expect(2); 39 | 40 | let model = this.owner.lookup('service:store').createRecord('user'); 41 | 42 | message = validator.validate(undefined, undefined, model, 'username'); 43 | assert.true(message); 44 | 45 | model.get('errors').add('username', 'Username is not unique'); 46 | model.get('errors').add('username', 'Username is too long'); 47 | 48 | message = validator.validate(undefined, undefined, model, 'username'); 49 | assert.strictEqual(message, 'Username is too long'); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /tests/unit/validators/exclusion-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { setupTest } from 'ember-qunit'; 3 | 4 | let options, builtOptions, validator, message; 5 | 6 | module('Unit | Validator | exclusion', function (hooks) { 7 | setupTest(hooks); 8 | 9 | hooks.beforeEach(function () { 10 | validator = this.owner.lookup('validator:exclusion'); 11 | }); 12 | 13 | test('no options', function (assert) { 14 | assert.expect(1); 15 | 16 | builtOptions = validator.buildOptions({}).toObject(); 17 | 18 | try { 19 | message = validator.validate(undefined, builtOptions); 20 | } catch (e) { 21 | assert.ok(true); 22 | } 23 | }); 24 | 25 | test('allow blank', function (assert) { 26 | assert.expect(2); 27 | 28 | options = { 29 | allowBlank: true, 30 | in: ['foo', 'bar', 'baz'], 31 | }; 32 | 33 | builtOptions = validator.buildOptions(options); 34 | 35 | message = validator.validate('', builtOptions.toObject()); 36 | assert.true(message); 37 | 38 | message = validator.validate('foo', builtOptions.toObject()); 39 | assert.strictEqual(message, 'This field is reserved'); 40 | }); 41 | 42 | test('not in array', function (assert) { 43 | assert.expect(4); 44 | 45 | options = { 46 | in: ['foo', 'bar', 'baz'], 47 | }; 48 | 49 | builtOptions = validator.buildOptions(options); 50 | 51 | message = validator.validate('foo', builtOptions.toObject()); 52 | assert.strictEqual(message, 'This field is reserved'); 53 | 54 | message = validator.validate('bar', builtOptions.toObject()); 55 | assert.strictEqual(message, 'This field is reserved'); 56 | 57 | message = validator.validate('baz', builtOptions.toObject()); 58 | assert.strictEqual(message, 'This field is reserved'); 59 | 60 | message = validator.validate('test', builtOptions.toObject()); 61 | assert.true(message); 62 | }); 63 | 64 | test('not in range', function (assert) { 65 | assert.expect(5); 66 | 67 | options = { 68 | range: [1, 10], 69 | }; 70 | 71 | builtOptions = validator.buildOptions(options); 72 | 73 | message = validator.validate(1, builtOptions.toObject()); 74 | assert.strictEqual(message, 'This field is reserved'); 75 | 76 | message = validator.validate(5, builtOptions.toObject()); 77 | assert.strictEqual(message, 'This field is reserved'); 78 | 79 | message = validator.validate(10, builtOptions.toObject()); 80 | assert.strictEqual(message, 'This field is reserved'); 81 | 82 | message = validator.validate(0, builtOptions.toObject()); 83 | assert.true(message); 84 | 85 | message = validator.validate(100, builtOptions.toObject()); 86 | assert.true(message); 87 | }); 88 | 89 | test('range type check - number', function (assert) { 90 | assert.expect(4); 91 | 92 | options = { 93 | range: [1, 10], 94 | }; 95 | 96 | builtOptions = validator.buildOptions(options); 97 | 98 | message = validator.validate(1, builtOptions.toObject()); 99 | assert.strictEqual(message, 'This field is reserved'); 100 | 101 | message = validator.validate(5, builtOptions.toObject()); 102 | assert.strictEqual(message, 'This field is reserved'); 103 | 104 | message = validator.validate('1', builtOptions.toObject()); 105 | assert.true(message); 106 | 107 | message = validator.validate('5', builtOptions.toObject()); 108 | assert.true(message); 109 | }); 110 | 111 | test('range type check - string', function (assert) { 112 | assert.expect(4); 113 | 114 | options = { 115 | range: ['a', 'z'], 116 | }; 117 | 118 | builtOptions = validator.buildOptions(options); 119 | 120 | message = validator.validate('a', builtOptions.toObject()); 121 | assert.strictEqual(message, 'This field is reserved'); 122 | 123 | message = validator.validate('z', builtOptions.toObject()); 124 | assert.strictEqual(message, 'This field is reserved'); 125 | 126 | message = validator.validate(97, builtOptions.toObject()); 127 | assert.true(message); 128 | 129 | message = validator.validate('zzz', builtOptions.toObject()); 130 | assert.true(message); 131 | }); 132 | }); 133 | -------------------------------------------------------------------------------- /tests/unit/validators/format-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { setupTest } from 'ember-qunit'; 3 | 4 | let options, builtOptions, validator, message; 5 | 6 | module('Unit | Validator | format', function (hooks) { 7 | setupTest(hooks); 8 | 9 | hooks.beforeEach(function () { 10 | validator = this.owner.lookup('validator:format'); 11 | }); 12 | 13 | test('no options', function (assert) { 14 | assert.expect(1); 15 | 16 | builtOptions = validator.buildOptions({}).toObject(); 17 | 18 | try { 19 | message = validator.validate(undefined, builtOptions); 20 | } catch (e) { 21 | assert.ok(true); 22 | } 23 | }); 24 | 25 | test('allow blank', function (assert) { 26 | assert.expect(2); 27 | 28 | options = { 29 | allowBlank: true, 30 | type: 'email', 31 | }; 32 | options = validator.buildOptions(options, {}).toObject(); 33 | 34 | message = validator.validate(undefined, options); 35 | assert.true(message); 36 | 37 | message = validator.validate('email', options); 38 | assert.strictEqual(message, 'This field must be a valid email address'); 39 | }); 40 | 41 | test('email no option', function (assert) { 42 | let validAddresses = [ 43 | 'email@domain.com', 44 | 'firstname.lastname@domain.com', 45 | 'email@subdomain.domain.com', 46 | 'firstname+lastname@domain.com', 47 | '1234567890@domain.com', 48 | 'email@domain-one.com', 49 | '_______@domain.com', 50 | 'email@domain.name', 51 | 'email@domain.co.jp', 52 | 'firstname-lastname@domain.com', 53 | 'EMAIL@DOMAIN.COM', 54 | ]; 55 | let invalidAddresses = [ 56 | 'plainaddress', 57 | '#@%^%#$@#$@#.com', 58 | '@domain.com', 59 | 'Joe Smith ', 60 | 'email.domain.com', 61 | 'email@domain@domain.com', 62 | '.email@domain.com', 63 | 'email.@domain.com', 64 | 'email..email@domain.com', 65 | 'あいうえお@domain.com', 66 | 'email@domain.com (Joe Smith)', 67 | 'email@domain', 68 | 'email@domain.', 69 | 'email@domain.-', 70 | 'email@domain-', 71 | 'email@domain-.', 72 | 'email@domain.com.', 73 | 'email@domain.com.-', 74 | 'email@domain.com-', 75 | 'email@domain.com-.', 76 | 'email@-domain.com', 77 | 'email@domain..com', 78 | ]; 79 | 80 | assert.expect(validAddresses.length + invalidAddresses.length); 81 | 82 | options = { 83 | type: 'email', 84 | }; 85 | 86 | options = validator.buildOptions(options, {}).toObject(); 87 | 88 | validAddresses.forEach((email) => 89 | assert.true( 90 | validator.validate(email, options), 91 | `validation of ${email} must succeed`, 92 | ), 93 | ); 94 | invalidAddresses.forEach((email) => 95 | assert.strictEqual( 96 | validator.validate(email, options), 97 | 'This field must be a valid email address', 98 | `validation of ${email} must fail`, 99 | ), 100 | ); 101 | }); 102 | 103 | test('email option allowNonTld', function (assert) { 104 | let validAddresses = [ 105 | 'email@domain.com', 106 | 'firstname.lastname@domain.com', 107 | 'email@subdomain.domain.com', 108 | 'firstname+lastname@domain.com', 109 | '1234567890@domain.com', 110 | 'email@domain-one.com', 111 | '_______@domain.com', 112 | 'email@domain.name', 113 | 'email@domain.co.jp', 114 | 'firstname-lastname@domain.com', 115 | 'EMAIL@DOMAIN.COM', 116 | 'email@domain', 117 | ]; 118 | let invalidAddresses = [ 119 | 'plainaddress', 120 | '#@%^%#$@#$@#.com', 121 | '@domain.com', 122 | 'Joe Smith ', 123 | 'email.domain.com', 124 | 'email@domain@domain.com', 125 | '.email@domain.com', 126 | 'email.@domain.com', 127 | 'email..email@domain.com', 128 | 'あいうえお@domain.com', 129 | 'email@domain.com (Joe Smith)', 130 | 'email@domain.', 131 | 'email@domain.-', 132 | 'email@domain-', 133 | 'email@domain-.', 134 | 'email@domain.com.', 135 | 'email@domain.com.-', 136 | 'email@domain.com-', 137 | 'email@domain.com-.', 138 | 'email@-domain.com', 139 | 'email@domain..com', 140 | ]; 141 | 142 | assert.expect(validAddresses.length + invalidAddresses.length); 143 | 144 | options = { 145 | type: 'email', 146 | allowNonTld: true, 147 | }; 148 | 149 | options = validator.buildOptions(options, {}).toObject(); 150 | 151 | validAddresses.forEach((email) => 152 | assert.true( 153 | validator.validate(email, options), 154 | `validation of ${email} must succeed`, 155 | ), 156 | ); 157 | invalidAddresses.forEach((email) => 158 | assert.strictEqual( 159 | validator.validate(email, options), 160 | 'This field must be a valid email address', 161 | `validation of ${email} must fail`, 162 | ), 163 | ); 164 | }); 165 | 166 | test('phone', function (assert) { 167 | assert.expect(2); 168 | 169 | options = { 170 | type: 'phone', 171 | }; 172 | 173 | options = validator.buildOptions(options, {}).toObject(); 174 | 175 | message = validator.validate('123', options); 176 | assert.strictEqual(message, 'This field must be a valid phone number'); 177 | 178 | message = validator.validate('(408) 555-1234', options); 179 | assert.true(message); 180 | }); 181 | 182 | test('url', function (assert) { 183 | assert.expect(2); 184 | 185 | options = { 186 | type: 'url', 187 | }; 188 | 189 | options = validator.buildOptions(options, {}).toObject(); 190 | 191 | message = validator.validate('offirgolan', options); 192 | assert.strictEqual(message, 'This field must be a valid url'); 193 | 194 | message = validator.validate('http://www.offirgolan.com', options); 195 | assert.true(message); 196 | }); 197 | 198 | test('custom', function (assert) { 199 | assert.expect(2); 200 | 201 | options = { 202 | regex: /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{4,8}$/, 203 | }; 204 | 205 | options = validator.buildOptions(options, {}).toObject(); 206 | 207 | message = validator.validate('password', options); 208 | assert.strictEqual(message, 'This field is invalid'); 209 | 210 | message = validator.validate('Pass123', options); 211 | assert.true(message); 212 | }); 213 | }); 214 | -------------------------------------------------------------------------------- /tests/unit/validators/inclusion-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { setupTest } from 'ember-qunit'; 3 | 4 | let options, builtOptions, validator, message; 5 | 6 | module('Unit | Validator | inclusion', function (hooks) { 7 | setupTest(hooks); 8 | 9 | hooks.beforeEach(function () { 10 | validator = this.owner.lookup('validator:inclusion'); 11 | }); 12 | 13 | test('no options', function (assert) { 14 | assert.expect(1); 15 | 16 | builtOptions = validator.buildOptions({}).toObject(); 17 | 18 | try { 19 | message = validator.validate(undefined, builtOptions); 20 | } catch (e) { 21 | assert.ok(true); 22 | } 23 | }); 24 | 25 | test('allow blank', function (assert) { 26 | assert.expect(2); 27 | 28 | options = { 29 | allowBlank: true, 30 | in: ['foo', 'bar', 'baz'], 31 | }; 32 | builtOptions = validator.buildOptions(options); 33 | 34 | message = validator.validate('', builtOptions.toObject()); 35 | assert.true(message); 36 | 37 | message = validator.validate('test', builtOptions.toObject()); 38 | assert.strictEqual(message, 'This field is not included in the list'); 39 | }); 40 | 41 | test('in array', function (assert) { 42 | assert.expect(4); 43 | 44 | options = { 45 | in: ['foo', 'bar', 'baz'], 46 | }; 47 | builtOptions = validator.buildOptions(options); 48 | 49 | message = validator.validate('test', builtOptions.toObject()); 50 | assert.strictEqual(message, 'This field is not included in the list'); 51 | 52 | message = validator.validate('foo', builtOptions.toObject()); 53 | assert.true(message); 54 | 55 | message = validator.validate('bar', builtOptions.toObject()); 56 | assert.true(message); 57 | 58 | message = validator.validate('baz', builtOptions.toObject()); 59 | assert.true(message); 60 | }); 61 | 62 | test('in range', function (assert) { 63 | assert.expect(5); 64 | 65 | options = { 66 | range: [1, 10], 67 | }; 68 | builtOptions = validator.buildOptions(options); 69 | 70 | message = validator.validate(0, builtOptions.toObject()); 71 | assert.strictEqual(message, 'This field is not included in the list'); 72 | 73 | message = validator.validate(100, builtOptions.toObject()); 74 | assert.strictEqual(message, 'This field is not included in the list'); 75 | 76 | message = validator.validate(1, builtOptions.toObject()); 77 | assert.true(message); 78 | 79 | message = validator.validate(5, builtOptions.toObject()); 80 | assert.true(message); 81 | 82 | message = validator.validate(10, builtOptions.toObject()); 83 | assert.true(message); 84 | }); 85 | 86 | test('range type check - number', function (assert) { 87 | assert.expect(7); 88 | 89 | options = { 90 | range: [1, 10], 91 | }; 92 | builtOptions = validator.buildOptions(options); 93 | 94 | message = validator.validate('0', builtOptions.toObject()); 95 | assert.strictEqual(message, 'This field is not included in the list'); 96 | 97 | message = validator.validate(0, builtOptions.toObject()); 98 | assert.strictEqual(message, 'This field is not included in the list'); 99 | 100 | message = validator.validate('1', builtOptions.toObject()); 101 | assert.strictEqual(message, 'This field is not included in the list'); 102 | 103 | message = validator.validate('5', builtOptions.toObject()); 104 | assert.strictEqual(message, 'This field is not included in the list'); 105 | 106 | message = validator.validate(1, builtOptions.toObject()); 107 | assert.true(message); 108 | 109 | message = validator.validate(5, builtOptions.toObject()); 110 | assert.true(message); 111 | 112 | message = validator.validate(10, builtOptions.toObject()); 113 | assert.true(message); 114 | }); 115 | 116 | test('range type check - string', function (assert) { 117 | assert.expect(5); 118 | 119 | options = { 120 | range: ['a', 'z'], 121 | }; 122 | builtOptions = validator.buildOptions(options); 123 | 124 | message = validator.validate(97, builtOptions.toObject()); 125 | assert.strictEqual(message, 'This field is not included in the list'); 126 | 127 | message = validator.validate('zzz', builtOptions.toObject()); 128 | assert.strictEqual(message, 'This field is not included in the list'); 129 | 130 | message = validator.validate('a', builtOptions.toObject()); 131 | assert.true(message); 132 | 133 | message = validator.validate('o', builtOptions.toObject()); 134 | assert.true(message); 135 | 136 | message = validator.validate('z', builtOptions.toObject()); 137 | assert.true(message); 138 | }); 139 | }); 140 | -------------------------------------------------------------------------------- /tests/unit/validators/inline-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { setupTest } from 'ember-qunit'; 3 | 4 | module('Unit | Validator | inline', function (hooks) { 5 | setupTest(hooks); 6 | 7 | test('no options', function (assert) { 8 | assert.expect(1); 9 | 10 | try { 11 | this.owner.lookup('validator:inline'); 12 | } catch (e) { 13 | assert.ok(true); 14 | } 15 | }); 16 | 17 | test('it works', function (assert) { 18 | assert.expect(3); 19 | 20 | const validator = this.owner.factoryFor('validator:inline').create({ 21 | options: { 22 | foo: 'bar', 23 | validate(value, options) { 24 | assert.strictEqual(this, validator, 'Context is preserved'); 25 | assert.strictEqual(options.foo, 'bar', 'It receives options'); 26 | assert.notOk( 27 | options.validate, 28 | 'Validate fn removed from the options', 29 | ); 30 | }, 31 | }, 32 | }); 33 | 34 | validator.validate('foo', validator.get('options').toObject()); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /tests/unit/validators/length-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { setupTest } from 'ember-qunit'; 3 | 4 | let options, builtOptions, validator, message; 5 | 6 | module('Unit | Validator | length', function (hooks) { 7 | setupTest(hooks); 8 | 9 | hooks.beforeEach(function () { 10 | validator = this.owner.lookup('validator:length'); 11 | }); 12 | 13 | test('no options', function (assert) { 14 | assert.expect(1); 15 | 16 | builtOptions = validator.buildOptions({}); 17 | 18 | message = validator.validate(undefined, builtOptions.toObject()); 19 | assert.true(message); 20 | }); 21 | 22 | test('allow blank', function (assert) { 23 | assert.expect(2); 24 | 25 | options = { 26 | allowBlank: true, 27 | min: 5, 28 | }; 29 | 30 | builtOptions = validator.buildOptions(options); 31 | 32 | message = validator.validate('', builtOptions.toObject()); 33 | assert.true(message); 34 | 35 | message = validator.validate('test', builtOptions.toObject()); 36 | assert.strictEqual( 37 | message, 38 | 'This field is too short (minimum is 5 characters)', 39 | ); 40 | }); 41 | 42 | test('allow none', function (assert) { 43 | assert.expect(2); 44 | 45 | options = { 46 | // default allowNone should be true 47 | }; 48 | 49 | builtOptions = validator.buildOptions(options); 50 | 51 | message = validator.validate(undefined, builtOptions.toObject()); 52 | assert.true(message); 53 | 54 | options.allowNone = false; 55 | builtOptions = validator.buildOptions(options); 56 | 57 | message = validator.validate(null, builtOptions.toObject()); 58 | assert.strictEqual(message, 'This field is invalid'); 59 | }); 60 | 61 | test('is', function (assert) { 62 | assert.expect(2); 63 | 64 | options = { 65 | is: 4, 66 | }; 67 | 68 | builtOptions = validator.buildOptions(options); 69 | 70 | message = validator.validate('testing', builtOptions.toObject()); 71 | assert.strictEqual( 72 | message, 73 | 'This field is the wrong length (should be 4 characters)', 74 | ); 75 | 76 | message = validator.validate('test', builtOptions.toObject()); 77 | assert.true(message); 78 | }); 79 | 80 | test('min', function (assert) { 81 | assert.expect(2); 82 | 83 | options = { 84 | min: 5, 85 | }; 86 | 87 | builtOptions = validator.buildOptions(options); 88 | 89 | message = validator.validate('test', builtOptions.toObject()); 90 | assert.strictEqual( 91 | message, 92 | 'This field is too short (minimum is 5 characters)', 93 | ); 94 | 95 | message = validator.validate('testing', builtOptions.toObject()); 96 | assert.true(message); 97 | }); 98 | 99 | test('max', function (assert) { 100 | assert.expect(2); 101 | 102 | options = { 103 | max: 5, 104 | }; 105 | 106 | builtOptions = validator.buildOptions(options); 107 | 108 | message = validator.validate('testing', builtOptions.toObject()); 109 | assert.strictEqual( 110 | message, 111 | 'This field is too long (maximum is 5 characters)', 112 | ); 113 | 114 | message = validator.validate('test', builtOptions.toObject()); 115 | assert.true(message); 116 | }); 117 | 118 | test('array', function (assert) { 119 | assert.expect(2); 120 | 121 | options = { 122 | min: 1, 123 | }; 124 | 125 | builtOptions = validator.buildOptions(options); 126 | 127 | message = validator.validate([], builtOptions.toObject()); 128 | assert.strictEqual( 129 | message, 130 | 'This field is too short (minimum is 1 characters)', 131 | ); 132 | 133 | message = validator.validate([1], builtOptions.toObject()); 134 | assert.true(message); 135 | }); 136 | }); 137 | -------------------------------------------------------------------------------- /tests/unit/validators/messages-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { setupTest } from 'ember-qunit'; 3 | 4 | let messages; 5 | 6 | module('Unit | Validator | messages', function (hooks) { 7 | setupTest(hooks); 8 | 9 | hooks.beforeEach(function () { 10 | messages = this.owner.lookup('validator:messages'); 11 | }); 12 | 13 | test('message strings present', function (assert) { 14 | assert.expect(2); 15 | assert.strictEqual(messages.get('invalid'), '{description} is invalid'); 16 | assert.strictEqual( 17 | messages.get('tooShort'), 18 | '{description} is too short (minimum is {min} characters)', 19 | ); 20 | }); 21 | 22 | test('formatMessage', function (assert) { 23 | assert.expect(3); 24 | let context = { 25 | description: 'This field', 26 | }; 27 | assert.strictEqual( 28 | messages.formatMessage(undefined, context), 29 | 'This field is invalid', 30 | ); 31 | assert.strictEqual( 32 | messages.formatMessage('{foo} is undefined'), 33 | 'undefined is undefined', 34 | ); 35 | assert.strictEqual( 36 | messages.formatMessage('{foo} {foo} {bar} {baz}', { 37 | foo: 'a', 38 | bar: 1, 39 | baz: 'abc', 40 | }), 41 | 'a a 1 abc', 42 | ); 43 | }); 44 | 45 | test('getMessageFor', function (assert) { 46 | assert.expect(2); 47 | let context = { 48 | description: 'This field', 49 | min: 4, 50 | }; 51 | assert.strictEqual( 52 | messages.getMessageFor('foo', context), 53 | 'This field is invalid', 54 | ); 55 | assert.strictEqual( 56 | messages.getMessageFor('tooShort', context), 57 | 'This field is too short (minimum is 4 characters)', 58 | ); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /tests/unit/validators/number-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { setupTest } from 'ember-qunit'; 3 | 4 | let options, builtOptions, validator, message; 5 | 6 | module('Unit | Validator | number', function (hooks) { 7 | setupTest(hooks); 8 | 9 | hooks.beforeEach(function () { 10 | validator = this.owner.lookup('validator:number'); 11 | }); 12 | 13 | test('no options', function (assert) { 14 | assert.expect(2); 15 | 16 | builtOptions = validator.buildOptions({}); 17 | 18 | message = validator.validate(undefined, builtOptions.toObject()); 19 | assert.true(message); 20 | 21 | message = validator.validate(22, builtOptions.toObject()); 22 | assert.true(message); 23 | }); 24 | 25 | test('allow string', function (assert) { 26 | assert.expect(6); 27 | 28 | options = { 29 | allowString: true, 30 | }; 31 | builtOptions = validator.buildOptions(options); 32 | 33 | message = validator.validate('22', builtOptions.toObject()); 34 | assert.true(message); 35 | 36 | message = validator.validate('22.22', builtOptions.toObject()); 37 | assert.true(message); 38 | 39 | message = validator.validate('test', builtOptions.toObject()); 40 | assert.strictEqual(message, 'This field must be a number'); 41 | 42 | message = validator.validate('', builtOptions.toObject()); 43 | assert.strictEqual(message, 'This field must be a number'); 44 | 45 | options.allowString = false; 46 | builtOptions = validator.buildOptions(options); 47 | 48 | message = validator.validate('22', builtOptions.toObject()); 49 | assert.strictEqual(message, 'This field must be a number'); 50 | 51 | message = validator.validate('22.22', builtOptions.toObject()); 52 | assert.strictEqual(message, 'This field must be a number'); 53 | }); 54 | 55 | test('integer', function (assert) { 56 | assert.expect(3); 57 | 58 | options = { 59 | integer: true, 60 | }; 61 | builtOptions = validator.buildOptions(options); 62 | 63 | message = validator.validate(22, builtOptions.toObject()); 64 | assert.true(message); 65 | 66 | message = validator.validate(22.22, builtOptions.toObject()); 67 | assert.strictEqual(message, 'This field must be an integer'); 68 | 69 | message = validator.validate(-2.2, builtOptions.toObject()); 70 | assert.strictEqual(message, 'This field must be an integer'); 71 | }); 72 | 73 | test('is', function (assert) { 74 | assert.expect(2); 75 | 76 | options = { 77 | is: 22, 78 | }; 79 | builtOptions = validator.buildOptions(options); 80 | 81 | message = validator.validate(1, builtOptions.toObject()); 82 | assert.strictEqual(message, 'This field must be equal to 22'); 83 | 84 | message = validator.validate(22, builtOptions.toObject()); 85 | assert.true(message); 86 | }); 87 | 88 | test('lt', function (assert) { 89 | assert.expect(3); 90 | 91 | options = { 92 | lt: 22, 93 | }; 94 | builtOptions = validator.buildOptions(options); 95 | 96 | message = validator.validate(21, builtOptions.toObject()); 97 | assert.true(message); 98 | 99 | message = validator.validate(22, builtOptions.toObject()); 100 | assert.strictEqual(message, 'This field must be less than 22'); 101 | 102 | message = validator.validate(23, builtOptions.toObject()); 103 | assert.strictEqual(message, 'This field must be less than 22'); 104 | }); 105 | 106 | test('lte', function (assert) { 107 | assert.expect(3); 108 | 109 | options = { 110 | lte: 22, 111 | }; 112 | builtOptions = validator.buildOptions(options); 113 | 114 | message = validator.validate(21, builtOptions.toObject()); 115 | assert.true(message); 116 | 117 | message = validator.validate(22, builtOptions.toObject()); 118 | assert.true(message); 119 | 120 | message = validator.validate(23, builtOptions.toObject()); 121 | assert.strictEqual(message, 'This field must be less than or equal to 22'); 122 | }); 123 | 124 | test('gt', function (assert) { 125 | assert.expect(3); 126 | 127 | options = { 128 | gt: 22, 129 | }; 130 | builtOptions = validator.buildOptions(options); 131 | 132 | message = validator.validate(21, builtOptions.toObject()); 133 | assert.strictEqual(message, 'This field must be greater than 22'); 134 | 135 | message = validator.validate(22, builtOptions.toObject()); 136 | assert.strictEqual(message, 'This field must be greater than 22'); 137 | 138 | message = validator.validate(23, builtOptions.toObject()); 139 | assert.true(message); 140 | }); 141 | 142 | test('gte', function (assert) { 143 | assert.expect(3); 144 | 145 | options = { 146 | gte: 22, 147 | }; 148 | builtOptions = validator.buildOptions(options); 149 | 150 | message = validator.validate(21, builtOptions.toObject()); 151 | assert.strictEqual( 152 | message, 153 | 'This field must be greater than or equal to 22', 154 | ); 155 | 156 | message = validator.validate(22, builtOptions.toObject()); 157 | assert.true(message); 158 | 159 | message = validator.validate(23, builtOptions.toObject()); 160 | assert.true(message); 161 | }); 162 | 163 | test('positive', function (assert) { 164 | assert.expect(4); 165 | 166 | options = { 167 | positive: true, 168 | }; 169 | builtOptions = validator.buildOptions(options); 170 | 171 | message = validator.validate(-1, builtOptions.toObject()); 172 | assert.strictEqual(message, 'This field must be positive'); 173 | 174 | message = validator.validate(-144, builtOptions.toObject()); 175 | assert.strictEqual(message, 'This field must be positive'); 176 | 177 | message = validator.validate(0, builtOptions.toObject()); 178 | assert.true(message); 179 | 180 | message = validator.validate(22, builtOptions.toObject()); 181 | assert.true(message); 182 | }); 183 | 184 | test('odd', function (assert) { 185 | assert.expect(4); 186 | 187 | options = { 188 | odd: true, 189 | }; 190 | builtOptions = validator.buildOptions(options); 191 | 192 | message = validator.validate(22, builtOptions.toObject()); 193 | assert.strictEqual(message, 'This field must be odd'); 194 | 195 | message = validator.validate(-144, builtOptions.toObject()); 196 | assert.strictEqual(message, 'This field must be odd'); 197 | 198 | message = validator.validate(21, builtOptions.toObject()); 199 | assert.true(message); 200 | 201 | message = validator.validate(-21, builtOptions.toObject()); 202 | assert.true(message); 203 | }); 204 | 205 | test('even', function (assert) { 206 | assert.expect(5); 207 | 208 | options = { 209 | even: true, 210 | }; 211 | builtOptions = validator.buildOptions(options); 212 | 213 | message = validator.validate(22, builtOptions.toObject()); 214 | assert.true(message); 215 | 216 | message = validator.validate(-22, builtOptions.toObject()); 217 | assert.true(message); 218 | 219 | message = validator.validate(22.22, builtOptions.toObject()); 220 | assert.strictEqual(message, 'This field must be even'); 221 | 222 | message = validator.validate(21, builtOptions.toObject()); 223 | assert.strictEqual(message, 'This field must be even'); 224 | 225 | message = validator.validate(-33, builtOptions.toObject()); 226 | assert.strictEqual(message, 'This field must be even'); 227 | }); 228 | 229 | test('allowBlank', function (assert) { 230 | assert.expect(3); 231 | 232 | options = { 233 | allowBlank: true, 234 | }; 235 | builtOptions = validator.buildOptions(options); 236 | 237 | message = validator.validate(null, builtOptions.toObject()); 238 | assert.true(message); 239 | 240 | message = validator.validate(undefined, builtOptions.toObject()); 241 | assert.true(message); 242 | 243 | message = validator.validate('', builtOptions.toObject()); 244 | assert.true(message); 245 | }); 246 | }); 247 | -------------------------------------------------------------------------------- /tests/unit/validators/presence-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { setupTest } from 'ember-qunit'; 3 | 4 | let options, builtOptions, validator, message; 5 | 6 | module('Unit | Validator | presence', function (hooks) { 7 | setupTest(hooks); 8 | 9 | hooks.beforeEach(function () { 10 | validator = this.owner.lookup('validator:presence'); 11 | }); 12 | 13 | test('buildOptions', function (assert) { 14 | assert.expect(2); 15 | 16 | options = true; 17 | builtOptions = validator.buildOptions(options, {}); 18 | assert.true(builtOptions.get('presence')); 19 | 20 | options = { presence: true }; 21 | builtOptions = validator.buildOptions(options, {}); 22 | assert.true(builtOptions.get('presence')); 23 | }); 24 | 25 | test('presence - value present', function (assert) { 26 | assert.expect(1); 27 | 28 | options = { presence: true }; 29 | 30 | builtOptions = validator.buildOptions(options); 31 | 32 | message = validator.validate('value', builtOptions.toObject()); 33 | assert.true(message); 34 | }); 35 | 36 | test('presence - value blank', function (assert) { 37 | assert.expect(1); 38 | 39 | options = { presence: true }; 40 | 41 | builtOptions = validator.buildOptions(options); 42 | 43 | message = validator.validate(' ', builtOptions.toObject()); 44 | assert.true(message); 45 | }); 46 | 47 | test('presence with ignoreBlank - value blank', function (assert) { 48 | assert.expect(1); 49 | 50 | options = { presence: true, ignoreBlank: true }; 51 | 52 | builtOptions = validator.buildOptions(options); 53 | 54 | message = validator.validate(' ', builtOptions.toObject()); 55 | assert.strictEqual(message, "This field can't be blank"); 56 | }); 57 | 58 | test('presence - value not present', function (assert) { 59 | assert.expect(1); 60 | 61 | options = { presence: true }; 62 | builtOptions = validator.buildOptions(options); 63 | 64 | message = validator.validate(undefined, builtOptions.toObject()); 65 | assert.strictEqual(message, "This field can't be blank"); 66 | }); 67 | 68 | test('absence - value present', function (assert) { 69 | assert.expect(1); 70 | 71 | options = { presence: false }; 72 | builtOptions = validator.buildOptions(options); 73 | 74 | message = validator.validate('value', builtOptions.toObject()); 75 | assert.strictEqual(message, 'This field must be blank'); 76 | }); 77 | 78 | test('absence - value not present', function (assert) { 79 | assert.expect(1); 80 | 81 | options = { presence: false }; 82 | builtOptions = validator.buildOptions(options); 83 | 84 | message = validator.validate(undefined, builtOptions.toObject()); 85 | assert.true(message); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /yuidoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Ember CP Validations", 3 | "description": "Ember computed property based validations", 4 | "url": "https://github.com/adopted-ember-addons/ember-cp-validations", 5 | "indexModule": "Usage", 6 | "externalDocs": [{ 7 | "name": "ember-validators", 8 | "path": "node_modules/ember-validators", 9 | "url": "https://github.com/adopted-ember-addons/ember-validators", 10 | "version": "master" 11 | }], 12 | "options": { 13 | "enabledEnvironments": ["production"], 14 | "paths": [ 15 | "app", 16 | "addon", 17 | "htmlbars-plugins", 18 | "node_modules/ember-validators/addon" 19 | ], 20 | "external": { 21 | "data": [{ 22 | "base": "http://emberjs.com/api/", 23 | "json": "http://builds.emberjs.com/tags/v2.8.0/ember-docs.json" 24 | }] 25 | }, 26 | "themedir": "node_modules/yuidoc-ember-theme", 27 | "helpers": ["node_modules/yuidoc-ember-theme/helpers/helpers.js"], 28 | "exclude": "vendor", 29 | "outdir": "docs", 30 | "linkNatives": true, 31 | "quiet": true, 32 | "parseOnly": false, 33 | "lint": false, 34 | "nocode": false 35 | } 36 | } 37 | --------------------------------------------------------------------------------