├── tests ├── unit │ └── .gitkeep ├── dummy │ ├── app │ │ ├── helpers │ │ │ └── .gitkeep │ │ ├── components │ │ │ ├── x-custom-date.hbs │ │ │ ├── x-custom-hint.hbs │ │ │ ├── x-custom-button.hbs │ │ │ ├── x-custom-error.hbs │ │ │ ├── x-custom-input.hbs │ │ │ ├── x-custom-label.hbs │ │ │ ├── x-custom-select.hbs │ │ │ ├── x-custom-checkbox.hbs │ │ │ ├── x-custom-textarea.hbs │ │ │ ├── x-custom-radio-group.hbs │ │ │ ├── x-custom-checkbox-group.hbs │ │ │ ├── custom-hint.hbs │ │ │ ├── x-custom-render.js │ │ │ ├── custom-button.hbs │ │ │ ├── color-component.hbs │ │ │ ├── validated-input │ │ │ │ ├── hint.js │ │ │ │ └── hint.hbs │ │ │ ├── custom-label.hbs │ │ │ ├── custom-error.hbs │ │ │ ├── color-component.js │ │ │ ├── flatpickr-wrapper.hbs │ │ │ ├── favorite-colors.js │ │ │ ├── favorite-colors.hbs │ │ │ └── x-custom-render.hbs │ │ ├── services │ │ │ └── store.js │ │ ├── resolver.js │ │ ├── snippets │ │ │ ├── config-theme.js │ │ │ └── config-features.js │ │ ├── templates │ │ │ ├── not-found.hbs │ │ │ ├── docs │ │ │ │ ├── quickstart.md │ │ │ │ ├── troubleshooting.md │ │ │ │ ├── usage.md │ │ │ │ ├── configuration.md │ │ │ │ ├── migration-v8.md │ │ │ │ ├── global-defaults.md │ │ │ │ ├── index.md │ │ │ │ ├── migration-v6.md │ │ │ │ ├── components │ │ │ │ │ ├── validated-button.md │ │ │ │ │ ├── validated-form.md │ │ │ │ │ └── validated-input.md │ │ │ │ └── customization.md │ │ │ ├── docs.hbs │ │ │ └── index.hbs │ │ ├── models │ │ │ └── user.js │ │ ├── styles │ │ │ └── app.css │ │ ├── font-awesome.js │ │ ├── locales │ │ │ └── fr │ │ │ │ └── translations.js │ │ ├── routes │ │ │ ├── index.js │ │ │ └── docs │ │ │ │ └── components │ │ │ │ └── validated-form.js │ │ ├── controllers │ │ │ ├── docs │ │ │ │ └── components │ │ │ │ │ └── validated-form.js │ │ │ └── index.js │ │ ├── app.js │ │ ├── validations │ │ │ └── user.js │ │ ├── deprecation-workflow.js │ │ ├── index.html │ │ └── router.js │ ├── public │ │ └── robots.txt │ └── config │ │ ├── optional-features.json │ │ ├── targets.js │ │ ├── ember-cli-update.json │ │ ├── ember-try.js │ │ └── environment.js ├── test-helper.js ├── integration │ ├── helpers │ │ ├── evf-class-list-test.js │ │ └── evf-theme-test.js │ └── components │ │ ├── validated-form-defaults-test.js │ │ ├── validated-button-test.js │ │ ├── validated-input │ │ ├── label-test.js │ │ ├── render │ │ │ └── wrapper-test.js │ │ ├── types │ │ │ ├── input-test.js │ │ │ ├── textarea-test.js │ │ │ ├── checkbox-test.js │ │ │ ├── radio-group-test.js │ │ │ ├── checkbox-group-test.js │ │ │ └── select-test.js │ │ ├── error-test.js │ │ ├── hint-test.js │ │ └── render-test.js │ │ ├── validated-button │ │ └── button-test.js │ │ ├── validated-label-test.js │ │ ├── validated-input-test.js │ │ └── validated-form-test.js ├── acceptance │ └── documentation-test.js ├── index.html └── helpers │ └── index.js ├── .watchmanconfig ├── .husky ├── pre-commit └── commit-msg ├── demo.gif ├── app ├── helpers │ ├── evf-theme.js │ └── evf-class-list.js └── components │ ├── validated-form.js │ ├── validated-input.js │ ├── validated-button.js │ ├── validated-input │ ├── hint.js │ ├── error.js │ ├── label.js │ ├── render.js │ ├── types │ │ ├── date.js │ │ ├── input.js │ │ ├── select.js │ │ ├── checkbox.js │ │ ├── textarea.js │ │ ├── radio-group.js │ │ └── checkbox-group.js │ └── render │ │ └── wrapper.js │ └── validated-button │ └── button.js ├── .stylelintignore ├── .stylelintrc.js ├── addon ├── components │ ├── validated-input │ │ ├── render │ │ │ └── wrapper.hbs │ │ ├── label.hbs │ │ ├── error.js │ │ ├── hint.hbs │ │ ├── types │ │ │ ├── input.js │ │ │ ├── textarea.js │ │ │ ├── radio-group.js │ │ │ ├── checkbox.js │ │ │ ├── date.hbs │ │ │ ├── checkbox-group.js │ │ │ ├── input.hbs │ │ │ ├── textarea.hbs │ │ │ ├── checkbox.hbs │ │ │ ├── select.hbs │ │ │ ├── checkbox-group.hbs │ │ │ ├── radio-group.hbs │ │ │ └── select.js │ │ ├── error.hbs │ │ └── render.hbs │ ├── validated-button.hbs │ ├── validated-button │ │ └── button.hbs │ ├── validated-form.hbs │ ├── validated-button.js │ ├── validated-form.js │ ├── validated-input.js │ └── validated-input.hbs ├── -private │ └── features.js └── helpers │ ├── evf-class-list.js │ └── evf-theme.js ├── .prettierignore ├── .template-lintrc.js ├── .gitignore ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md ├── dependabot.yml └── workflows │ ├── deploy.yml │ ├── release.yml │ └── ci.yml ├── .prettierrc.js ├── config ├── addon-docs.js └── deploy.js ├── index.js ├── .editorconfig ├── .npmignore ├── blueprints └── ember-validated-form │ └── index.js ├── eslint.config.mjs ├── testem.js ├── CONTRIBUTING.md ├── .ember-cli ├── ember-cli-build.js ├── LICENSE.md ├── README.md ├── package.json └── CHANGELOG.md /tests/unit/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/helpers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": ["dist"] 3 | } 4 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | # lint only staged files 2 | pnpm lint-staged 3 | -------------------------------------------------------------------------------- /tests/dummy/app/components/x-custom-date.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/components/x-custom-hint.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | # lint commit message 2 | pnpm commitlint --edit "$1" 3 | -------------------------------------------------------------------------------- /tests/dummy/app/components/x-custom-button.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/components/x-custom-error.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/components/x-custom-input.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/components/x-custom-label.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/components/x-custom-select.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adfinis/ember-validated-form/HEAD/demo.gif -------------------------------------------------------------------------------- /tests/dummy/app/components/x-custom-checkbox.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/components/x-custom-textarea.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/services/store.js: -------------------------------------------------------------------------------- 1 | export { default } from "ember-data/store"; 2 | -------------------------------------------------------------------------------- /tests/dummy/app/components/x-custom-radio-group.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /app/helpers/evf-theme.js: -------------------------------------------------------------------------------- 1 | export { default } from "ember-validated-form/helpers/evf-theme"; 2 | -------------------------------------------------------------------------------- /tests/dummy/app/components/x-custom-checkbox-group.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | # unconventional files 2 | /blueprints/*/files/ 3 | 4 | # compiled output 5 | /dist/ 6 | -------------------------------------------------------------------------------- /app/helpers/evf-class-list.js: -------------------------------------------------------------------------------- 1 | export { default } from "ember-validated-form/helpers/evf-class-list"; 2 | -------------------------------------------------------------------------------- /tests/dummy/app/resolver.js: -------------------------------------------------------------------------------- 1 | import Resolver from "ember-resolver"; 2 | 3 | export default Resolver; 4 | -------------------------------------------------------------------------------- /app/components/validated-form.js: -------------------------------------------------------------------------------- 1 | export { default } from "ember-validated-form/components/validated-form"; 2 | -------------------------------------------------------------------------------- /app/components/validated-input.js: -------------------------------------------------------------------------------- 1 | export { default } from "ember-validated-form/components/validated-input"; 2 | -------------------------------------------------------------------------------- /app/components/validated-button.js: -------------------------------------------------------------------------------- 1 | export { default } from "ember-validated-form/components/validated-button"; 2 | -------------------------------------------------------------------------------- /app/components/validated-input/hint.js: -------------------------------------------------------------------------------- 1 | export { default } from "ember-validated-form/components/validated-input/hint"; 2 | -------------------------------------------------------------------------------- /app/components/validated-button/button.js: -------------------------------------------------------------------------------- 1 | export { default } from "ember-validated-form/components/validated-button/button"; 2 | -------------------------------------------------------------------------------- /app/components/validated-input/error.js: -------------------------------------------------------------------------------- 1 | export { default } from "ember-validated-form/components/validated-input/error"; 2 | -------------------------------------------------------------------------------- /app/components/validated-input/label.js: -------------------------------------------------------------------------------- 1 | export { default } from "ember-validated-form/components/validated-input/label"; 2 | -------------------------------------------------------------------------------- /app/components/validated-input/render.js: -------------------------------------------------------------------------------- 1 | export { default } from "ember-validated-form/components/validated-input/render"; 2 | -------------------------------------------------------------------------------- /app/components/validated-input/types/date.js: -------------------------------------------------------------------------------- 1 | export { default } from "ember-validated-form/components/validated-input/types/date"; 2 | -------------------------------------------------------------------------------- /app/components/validated-input/types/input.js: -------------------------------------------------------------------------------- 1 | export { default } from "ember-validated-form/components/validated-input/types/input"; 2 | -------------------------------------------------------------------------------- /app/components/validated-input/types/select.js: -------------------------------------------------------------------------------- 1 | export { default } from "ember-validated-form/components/validated-input/types/select"; 2 | -------------------------------------------------------------------------------- /app/components/validated-input/render/wrapper.js: -------------------------------------------------------------------------------- 1 | export { default } from "ember-validated-form/components/validated-input/render/wrapper"; 2 | -------------------------------------------------------------------------------- /app/components/validated-input/types/checkbox.js: -------------------------------------------------------------------------------- 1 | export { default } from "ember-validated-form/components/validated-input/types/checkbox"; 2 | -------------------------------------------------------------------------------- /app/components/validated-input/types/textarea.js: -------------------------------------------------------------------------------- 1 | export { default } from "ember-validated-form/components/validated-input/types/textarea"; 2 | -------------------------------------------------------------------------------- /.stylelintrc.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | extends: ["stylelint-config-standard", "stylelint-prettier/recommended"], 5 | }; 6 | -------------------------------------------------------------------------------- /app/components/validated-input/types/radio-group.js: -------------------------------------------------------------------------------- 1 | export { default } from "ember-validated-form/components/validated-input/types/radio-group"; 2 | -------------------------------------------------------------------------------- /app/components/validated-input/types/checkbox-group.js: -------------------------------------------------------------------------------- 1 | export { default } from "ember-validated-form/components/validated-input/types/checkbox-group"; 2 | -------------------------------------------------------------------------------- /addon/components/validated-input/render/wrapper.hbs: -------------------------------------------------------------------------------- 1 | {{#if (evf-theme "uikit")}} 2 |
{{yield}}
3 | {{else}} 4 | {{yield}} 5 | {{/if}} -------------------------------------------------------------------------------- /tests/dummy/app/components/custom-hint.hbs: -------------------------------------------------------------------------------- 1 | {{! BEGIN-SNIPPET custom-hint-component-template.hbs }} 2 | Hint: {{@hint}} 3 | {{! END-SNIPPET }} -------------------------------------------------------------------------------- /tests/dummy/app/snippets/config-theme.js: -------------------------------------------------------------------------------- 1 | const app = new EmberAddon(defaults, { 2 | // ... 3 | "ember-validated-form": { 4 | theme: "bootstrap", 5 | }, 6 | // ... 7 | }); 8 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/not-found.hbs: -------------------------------------------------------------------------------- 1 |
2 |

Not found

3 |

This page doesn't exist. Head home?

4 |
-------------------------------------------------------------------------------- /tests/dummy/app/components/x-custom-render.js: -------------------------------------------------------------------------------- 1 | import RenderComponent from "ember-validated-form/components/validated-input/render"; 2 | 3 | export default class extends RenderComponent {} 4 | -------------------------------------------------------------------------------- /tests/dummy/app/snippets/config-features.js: -------------------------------------------------------------------------------- 1 | const app = new EmberAddon(defaults, { 2 | // ... 3 | "ember-validated-form": { 4 | scrollErrorIntoView: true, 5 | }, 6 | // ... 7 | }); 8 | -------------------------------------------------------------------------------- /tests/dummy/app/components/custom-button.hbs: -------------------------------------------------------------------------------- 1 | {{! BEGIN-SNIPPET custom-button-component-template.hbs }} 2 | 4 | {{! END-SNIPPET }} -------------------------------------------------------------------------------- /tests/dummy/app/templates/docs/quickstart.md: -------------------------------------------------------------------------------- 1 | # Quickstart 2 | 3 | You'll find a basic example in [this 4 | twiddle](https://ember-twiddle.com/95b040c96b7dc60dc4d0bb2dc5f5de26?openFiles=templates.application.hbs%2C) 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /addon/components/validated-input/label.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/components/color-component.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # unconventional js 2 | /blueprints/*/files/ 3 | 4 | # compiled output 5 | /dist/ 6 | 7 | # misc 8 | /coverage/ 9 | !.* 10 | .*/ 11 | /pnpm-lock.yaml 12 | ember-cli-update.json 13 | *.html 14 | CHANGELOG.md 15 | -------------------------------------------------------------------------------- /tests/dummy/app/components/validated-input/hint.js: -------------------------------------------------------------------------------- 1 | // BEGIN-SNIPPET permanent-custom-hint-component-component.js 2 | import templateOnly from "@ember/component/template-only"; 3 | 4 | export default templateOnly(); 5 | // END-SNIPPET 6 | -------------------------------------------------------------------------------- /addon/components/validated-input/error.js: -------------------------------------------------------------------------------- 1 | import Component from "@glimmer/component"; 2 | 3 | export default class ErrorComponent extends Component { 4 | get errorString() { 5 | return this.args.errors?.join(", "); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /addon/components/validated-input/hint.hbs: -------------------------------------------------------------------------------- 1 | {{yield}}{{@hint}} -------------------------------------------------------------------------------- /tests/dummy/app/components/validated-input/hint.hbs: -------------------------------------------------------------------------------- 1 | {{! BEGIN-SNIPPET permanent-custom-hint-component-template.hbs }} 2 | 3 | 4 | {{@hint}} 5 | 6 | {{! END-SNIPPET }} -------------------------------------------------------------------------------- /.template-lintrc.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | extends: ["recommended"], 5 | overrides: [ 6 | { 7 | files: ["tests/**/*"], 8 | rules: { "require-input-label": false, "no-inline-styles": false }, 9 | }, 10 | ], 11 | }; 12 | -------------------------------------------------------------------------------- /addon/-private/features.js: -------------------------------------------------------------------------------- 1 | import { getOwner } from "@ember/application"; 2 | 3 | export function scrollErrorIntoViewEnabled(target) { 4 | return getOwner(target).resolveRegistration("config:environment")[ 5 | "ember-validated-form" 6 | ].scrollErrorIntoView; 7 | } 8 | -------------------------------------------------------------------------------- /tests/dummy/app/models/user.js: -------------------------------------------------------------------------------- 1 | import Model, { attr } from "@ember-data/model"; 2 | 3 | export default class UserModel extends Model { 4 | @attr firstName; 5 | @attr lastName; 6 | @attr aboutMe; 7 | @attr country; 8 | @attr gender; 9 | @attr terms; 10 | @attr color; 11 | } 12 | -------------------------------------------------------------------------------- /tests/dummy/app/styles/app.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css?family=Source+Sans+Pro:200,400"); 2 | @import url("https://fonts.googleapis.com/css?family=Source+Code+Pro"); 3 | 4 | body { 5 | font-family: "Source Sans Pro", sans-serif; 6 | background-color: rgb(255 255 255); 7 | } 8 | -------------------------------------------------------------------------------- /tests/dummy/app/font-awesome.js: -------------------------------------------------------------------------------- 1 | import { library, config } from "@fortawesome/fontawesome-svg-core"; 2 | import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons"; 3 | 4 | import "@fortawesome/fontawesome-svg-core/styles.css"; 5 | 6 | config.autoAddCss = false; 7 | 8 | library.add(faQuestionCircle); 9 | -------------------------------------------------------------------------------- /tests/dummy/app/locales/fr/translations.js: -------------------------------------------------------------------------------- 1 | // BEGIN-SNIPPET translations.js 2 | export default { 3 | some: { 4 | scope: { 5 | shapes: "les formes", 6 | triangle: "un triangle", 7 | square: "un carré", 8 | circle: "un cercle", 9 | }, 10 | }, 11 | }; 12 | // END-SNIPPET 13 | -------------------------------------------------------------------------------- /addon/helpers/evf-class-list.js: -------------------------------------------------------------------------------- 1 | import { helper } from "@ember/component/helper"; 2 | import { isEmpty } from "@ember/utils"; 3 | 4 | export default helper(function classList(classes) { 5 | return classes 6 | .filter((cls) => !isEmpty(cls)) 7 | .map((cls) => cls.trim()) 8 | .join(" "); 9 | }); 10 | -------------------------------------------------------------------------------- /tests/dummy/app/components/custom-label.hbs: -------------------------------------------------------------------------------- 1 | {{! template-lint-disable no-inline-styles }} 2 | {{! BEGIN-SNIPPET custom-label-component-template.hbs }} 3 | 8 | {{! END-SNIPPET }} -------------------------------------------------------------------------------- /.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 | .idea 17 | *.swp 18 | .DS_Store 19 | .orig 20 | 21 | # broccoli-debug 22 | /DEBUG/ 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Template for bug reports 4 | --- 5 | 6 | -- 7 | If possible, please consider creating a basic reproduction of your issue with Ember Twiddle. You can use this twiddle as a starting point: 8 | 9 | https://ember-twiddle.com/3691a8576c35ff149bfc26a564ec5437 10 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | plugins: ["prettier-plugin-ember-template-tag"], 5 | overrides: [ 6 | { 7 | files: "*.{js,gjs,ts,gts,mjs,mts,cjs,cts}", 8 | options: { 9 | singleQuote: false, 10 | templateSingleQuote: false, 11 | }, 12 | }, 13 | ], 14 | }; 15 | -------------------------------------------------------------------------------- /addon/components/validated-input/types/input.js: -------------------------------------------------------------------------------- 1 | import { action } from "@ember/object"; 2 | import Component from "@glimmer/component"; 3 | export default class InputComponent extends Component { 4 | @action 5 | onUpdate(event) { 6 | event.preventDefault(); 7 | 8 | this.args.update(event.target.value); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /addon/components/validated-input/types/textarea.js: -------------------------------------------------------------------------------- 1 | import { action } from "@ember/object"; 2 | import Component from "@glimmer/component"; 3 | 4 | export default class TextareaComponent extends Component { 5 | @action 6 | onUpdate(event) { 7 | event.preventDefault(); 8 | 9 | this.args.update(event.target.value); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /config/addon-docs.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | "use strict"; 3 | 4 | const AddonDocsConfig = require("ember-cli-addon-docs/lib/config"); 5 | 6 | module.exports = class extends AddonDocsConfig { 7 | // See https://ember-learn.github.io/ember-cli-addon-docs/latest/docs/deploying 8 | // for details on configuration you can override here. 9 | }; 10 | -------------------------------------------------------------------------------- /tests/dummy/app/components/custom-error.hbs: -------------------------------------------------------------------------------- 1 | {{! template-lint-disable no-inline-styles }} 2 | {{! BEGIN-SNIPPET custom-error-component-template.hbs }} 3 |
4 |
5 | {{#each @errors as |error i|}} 6 | > Error #{{i}}: 7 | {{error}}
8 | {{/each}} 9 |
10 | {{! END-SNIPPET }} -------------------------------------------------------------------------------- /tests/dummy/app/components/color-component.js: -------------------------------------------------------------------------------- 1 | import { htmlSafe } from "@ember/template"; 2 | import Component from "@glimmer/component"; 3 | 4 | export default class ColorComponent extends Component { 5 | get style() { 6 | return htmlSafe( 7 | `background-color: ${this.args.color.color}; font-size: 1rem; cursor: pointer;`, 8 | ); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /addon/components/validated-input/types/radio-group.js: -------------------------------------------------------------------------------- 1 | import { action } from "@ember/object"; 2 | import Component from "@glimmer/component"; 3 | 4 | export default class RadioGroupComponent extends Component { 5 | @action 6 | onUpdate(value, event) { 7 | event.preventDefault(); 8 | 9 | this.args.update(value); 10 | this.args.setDirty(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /addon/components/validated-input/types/checkbox.js: -------------------------------------------------------------------------------- 1 | import { action } from "@ember/object"; 2 | import Component from "@glimmer/component"; 3 | 4 | export default class CheckboxComponent extends Component { 5 | @action 6 | onUpdate(event) { 7 | event.preventDefault(); 8 | 9 | this.args.update(event.target.checked); 10 | this.args.setDirty(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /addon/components/validated-input/error.hbs: -------------------------------------------------------------------------------- 1 | {{#if (evf-theme "uikit")}} 2 | 3 | {{yield}}{{this.errorString}} 4 | 5 | {{else}} 6 | 11 | {{yield}}{{this.errorString}} 12 | 13 | {{/if}} -------------------------------------------------------------------------------- /tests/dummy/app/routes/index.js: -------------------------------------------------------------------------------- 1 | // BEGIN-SNIPPET quickstart-route.js 2 | import Route from "@ember/routing/route"; 3 | import { tracked } from "@glimmer/tracking"; 4 | 5 | class Model { 6 | @tracked saved = false; 7 | 8 | save() { 9 | this.saved = true; 10 | } 11 | } 12 | 13 | export default class IndexRoute extends Route { 14 | model() { 15 | return new Model(); 16 | } 17 | } 18 | // END-SNIPPET 19 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | name: require("./package").name, 5 | 6 | config() { 7 | const app = this._findHost(this); 8 | 9 | const appConfig = app.options["ember-validated-form"] ?? {}; 10 | 11 | return { 12 | "ember-validated-form": { 13 | theme: "default", 14 | scrollErrorIntoView: false, 15 | ...appConfig, 16 | }, 17 | }; 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /addon/helpers/evf-theme.js: -------------------------------------------------------------------------------- 1 | import { getOwner } from "@ember/application"; 2 | import Helper from "@ember/component/helper"; 3 | 4 | export default class EVFThemeHelper extends Helper { 5 | get currentTheme() { 6 | return getOwner(this).resolveRegistration("config:environment")[ 7 | "ember-validated-form" 8 | ].theme; 9 | } 10 | 11 | compute([expectedTheme]) { 12 | return this.currentTheme === expectedTheme; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /tests/dummy/app/components/flatpickr-wrapper.hbs: -------------------------------------------------------------------------------- 1 | {{! BEGIN-SNIPPET flatpickr-wrapper.hbs }} 2 | <@labelComponent /> 3 | 4 |
5 | 12 | 13 |
14 | 15 | <@hintComponent /> 16 | <@errorComponent /> 17 | {{! END-SNIPPET }} -------------------------------------------------------------------------------- /tests/dummy/app/routes/docs/components/validated-form.js: -------------------------------------------------------------------------------- 1 | import Route from "@ember/routing/route"; 2 | import { later } from "@ember/runloop"; 3 | import { Promise } from "rsvp"; 4 | 5 | export default class extends Route { 6 | // BEGIN-SNIPPET validated-form-route.js 7 | model() { 8 | return new (class { 9 | save() { 10 | return new Promise((resolve) => later(resolve, 1000)); 11 | } 12 | })(); 13 | } 14 | // END-SNIPPET 15 | } 16 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | day: "friday" 8 | time: "12:00" 9 | timezone: "Europe/Zurich" 10 | - package-ecosystem: npm 11 | directory: "/" 12 | schedule: 13 | interval: "weekly" 14 | day: "friday" 15 | time: "12:00" 16 | timezone: "Europe/Zurich" 17 | open-pull-requests-limit: 10 18 | -------------------------------------------------------------------------------- /addon/components/validated-input/types/date.hbs: -------------------------------------------------------------------------------- 1 | {{! template-lint-disable no-autofocus-attribute }} 2 | -------------------------------------------------------------------------------- /addon/components/validated-input/types/checkbox-group.js: -------------------------------------------------------------------------------- 1 | import { action } from "@ember/object"; 2 | import Component from "@glimmer/component"; 3 | 4 | export default class CheckboxGroupComponent extends Component { 5 | @action 6 | onUpdate(key, event) { 7 | event.preventDefault(); 8 | 9 | const value = new Set(this.args.value || []); 10 | 11 | value.delete(key) || value.add(key); 12 | 13 | this.args.update([...value]); 14 | this.args.setDirty(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist/ 3 | /tmp/ 4 | 5 | # misc 6 | /.editorconfig 7 | /.ember-cli 8 | /.env* 9 | /.eslintcache 10 | /.git/ 11 | /.github/ 12 | /.gitignore 13 | /.prettierignore 14 | /.prettierrc.js 15 | /.stylelintignore 16 | /.stylelintrc.js 17 | /.template-lintrc.js 18 | /.watchmanconfig 19 | /CONTRIBUTING.md 20 | /ember-cli-build.js 21 | /eslint.config.mjs 22 | /testem.js 23 | /tests/ 24 | /tsconfig.declarations.json 25 | /tsconfig.json 26 | /yarn-error.log 27 | /yarn.lock 28 | .gitkeep 29 | -------------------------------------------------------------------------------- /blueprints/ember-validated-form/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | normalizeEntityName() { 3 | // this prevents an error when the entityName is 4 | // not specified (since that doesn't actually matter 5 | // to us 6 | }, 7 | 8 | afterInstall() { 9 | return this.addAddonsToProject({ 10 | packages: [ 11 | { name: "ember-changeset" }, 12 | { name: "ember-changeset-validations" }, 13 | { name: "ember-truth-helpers" }, 14 | ], 15 | }); 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/docs/troubleshooting.md: -------------------------------------------------------------------------------- 1 | # Troubleshooting 2 | 3 | ## 'Proxy' is undefined (IE11) 4 | 5 | `ember-validated-form` installs `ember-changeset` and 6 | `ember-changeset-validations` **v3+** per default which relies heavily on 7 | `Proxy` which is not supported by IE11. If you need to support IE11 you'll 8 | need to [polyfill it](https://github.com/GoogleChrome/proxy-polyfill) or if 9 | you don't want to polyfill it, you can use `ember-changeset` and 10 | `ember-changeset-validations` **v2.x**. 11 | -------------------------------------------------------------------------------- /tests/test-helper.js: -------------------------------------------------------------------------------- 1 | import { setApplication } from "@ember/test-helpers"; 2 | import { start, setupEmberOnerrorValidation } from "ember-qunit"; 3 | import { loadTests } from "ember-qunit/test-loader"; 4 | import * as QUnit from "qunit"; 5 | import { setup } from "qunit-dom"; 6 | 7 | import Application from "dummy/app"; 8 | import config from "dummy/config/environment"; 9 | 10 | setApplication(Application.create(config.APP)); 11 | 12 | setup(QUnit.assert); 13 | setupEmberOnerrorValidation(); 14 | loadTests(); 15 | start(); 16 | -------------------------------------------------------------------------------- /addon/components/validated-button.hbs: -------------------------------------------------------------------------------- 1 | {{#let 2 | (component 3 | (ensure-safe-component 4 | (or @buttonComponent (component "validated-button/button")) 5 | ) 6 | onClick=this.click 7 | loading=this.loading 8 | disabled=(or @disabled this.loading) 9 | label=@label 10 | type=@type 11 | ) 12 | as |ButtonComponent| 13 | }} 14 | {{#if (has-block)}} 15 | {{yield}} 16 | {{else}} 17 | 18 | {{/if}} 19 | {{/let}} -------------------------------------------------------------------------------- /tests/dummy/app/templates/docs/usage.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | First, install the addon: 4 | 5 | ```bash 6 | $ ember install ember-validated-form 7 | ``` 8 | 9 | This will also install `ember-changeset` and `ember-changeset-validations`. 10 | After, you'll need to set up 11 | 12 | - a template containing your form elements 13 | - a validations file (see [ember-changeset-validations](https://github.com/poteto/ember-changeset-validations)) 14 | - a controller, route and/or component that provides your template with the validations and your model 15 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import adfinisEmberAddonConfig from "@adfinis/eslint-config/ember-addon"; 2 | import ember from "eslint-plugin-ember"; 3 | 4 | export default [ 5 | ...adfinisEmberAddonConfig, 6 | { 7 | plugins: { ember }, 8 | settings: { 9 | "import/internal-regex": "^(ember-validated-form|dummy)/", 10 | }, 11 | rules: { 12 | "ember/no-runloop": "warn", 13 | }, 14 | }, 15 | { 16 | files: ["tests/dummy/app/snippets/*.js"], 17 | rules: { "no-undef": "off", "no-unused-vars": "off" }, 18 | }, 19 | ]; 20 | -------------------------------------------------------------------------------- /addon/components/validated-input/types/input.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/helpers/evf-class-list-test.js: -------------------------------------------------------------------------------- 1 | import { render } from "@ember/test-helpers"; 2 | import { hbs } from "ember-cli-htmlbars"; 3 | import { module, test } from "qunit"; 4 | 5 | import { setupRenderingTest } from "dummy/tests/helpers"; 6 | 7 | module("Integration | Helper | evf-class-list", function (hooks) { 8 | setupRenderingTest(hooks); 9 | 10 | test("it renders", async function (assert) { 11 | await render(hbs`{{evf-class-list "foo" null undefined "bar" ""}}`); 12 | 13 | assert.dom(this.element).hasText("foo bar"); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /tests/dummy/config/ember-cli-update.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.0.0", 3 | "packages": [ 4 | { 5 | "name": "ember-cli", 6 | "version": "6.8.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 | -------------------------------------------------------------------------------- /addon/components/validated-input/types/textarea.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /addon/components/validated-button/button.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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: [], 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/dummy/app/controllers/docs/components/validated-form.js: -------------------------------------------------------------------------------- 1 | import Controller from "@ember/controller"; 2 | import { action } from "@ember/object"; 3 | import { task } from "ember-concurrency"; 4 | 5 | export default class extends Controller { 6 | // BEGIN-SNIPPET validated-form-task-controller.js 7 | submitTask = task({ drop: true }, async (model) => { 8 | await model.save(); 9 | // ... more code to show success messages etc. 10 | }); 11 | // END-SNIPPET 12 | 13 | // BEGIN-SNIPPET validated-form-action-controller.js 14 | @action 15 | async submitAction(model) { 16 | await model.save(); 17 | // ... more code to show success messages etc. 18 | } 19 | // END-SNIPPET 20 | } 21 | -------------------------------------------------------------------------------- /tests/dummy/app/app.js: -------------------------------------------------------------------------------- 1 | import Application from "@ember/application"; 2 | import { importSync, isDevelopingApp, macroCondition } from "@embroider/macros"; 3 | import loadInitializers from "ember-load-initializers"; 4 | import Resolver from "ember-resolver"; 5 | 6 | import config from "dummy/config/environment"; 7 | import "./font-awesome"; 8 | 9 | import "flatpickr/dist/flatpickr.css"; 10 | 11 | if (macroCondition(isDevelopingApp())) { 12 | importSync("./deprecation-workflow"); 13 | } 14 | 15 | export default class App extends Application { 16 | modulePrefix = config.modulePrefix; 17 | podModulePrefix = config.podModulePrefix; 18 | Resolver = Resolver; 19 | } 20 | 21 | loadInitializers(App, config.modulePrefix); 22 | -------------------------------------------------------------------------------- /tests/dummy/app/components/favorite-colors.js: -------------------------------------------------------------------------------- 1 | // BEGIN-SNIPPET favorite-colors.js 2 | import { action } from "@ember/object"; 3 | import Component from "@glimmer/component"; 4 | import { tracked } from "@glimmer/tracking"; 5 | 6 | export default class FavoriteColorsComponent extends Component { 7 | @tracked isShowingColors = false; 8 | 9 | @action 10 | onColorSelected(color) { 11 | this.isShowingColors = !this.isShowingColors; 12 | 13 | this.args.setDirty(); 14 | this.args.update(color); 15 | } 16 | 17 | @action 18 | toggle() { 19 | this.isShowingColors = !this.isShowingColors; 20 | } 21 | 22 | @action 23 | clearSelection() { 24 | this.args.update(null); 25 | } 26 | } 27 | // END-SNIPPET 28 | -------------------------------------------------------------------------------- /tests/dummy/app/validations/user.js: -------------------------------------------------------------------------------- 1 | // BEGIN-SNIPPET quickstart-validations.js 2 | import { 3 | validatePresence, 4 | validateLength, 5 | validateInclusion, 6 | } from "ember-changeset-validations/validators"; 7 | 8 | export default { 9 | firstName: [validatePresence(true), validateLength({ min: 3, max: 40 })], 10 | lastName: [validatePresence(true), validateLength({ min: 3, max: 40 })], 11 | aboutMe: [validateLength({ allowBlank: true, max: 200 })], 12 | country: [validatePresence(true)], 13 | title: [validatePresence(true)], 14 | terms: [ 15 | validateInclusion({ 16 | list: [true], 17 | message: "Please accept the terms and conditions!", 18 | }), 19 | ], 20 | color: [validatePresence(true)], 21 | }; 22 | // END-SNIPPET 23 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How To Contribute 2 | 3 | ## Installation 4 | 5 | - `git clone ` 6 | - `cd ember-validated-form` 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 | -------------------------------------------------------------------------------- /tests/dummy/app/components/favorite-colors.hbs: -------------------------------------------------------------------------------- 1 | {{! BEGIN-SNIPPET favorite-colors.hbs }} 2 |
3 | <@labelComponent /> 4 | 5 |
6 | {{#if @value}} 7 | {{@value.name}} 8 | 13 |
14 |
15 | {{/if}} 16 | 17 | {{#each @colors as |color|}} 18 | 22 | {{/each}} 23 |
24 | 25 | <@hintComponent /> 26 | <@errorComponent /> 27 |
28 | {{! END-SNIPPET }} -------------------------------------------------------------------------------- /tests/dummy/app/templates/docs/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | ## Theme 4 | 5 | By setting the `theme` option, you can choose from a set of provided themes 6 | for `ember-uikit`. Currently we only support `bootstrap` and `uikit`. 7 | 8 | 9 | 10 | The idea for the future is to support various commonly used CSS frameworks 11 | like Material Design, Semantic UI or Bulma. **Pull Requests implementing such a theme 12 | are more than welcome!** 13 | 14 | ## Other features 15 | 16 | If you want to scroll the first invalid field into view, you can set the 17 | `scrollErrorIntoView` property to `true` (default: `false`). 18 | 19 | 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 | /** 9 | Setting `componentAuthoringFormat` to "strict" will force the blueprint generators to generate GJS 10 | or GTS files for the component and the component rendering test. "strict" is the default. 11 | */ 12 | "componentAuthoringFormat": "strict", 13 | 14 | /** 15 | Setting `routeAuthoringFormat` to "strict" will force the blueprint generators to generate GJS 16 | or GTS templates for routes. "strict" is the default 17 | */ 18 | "routeAuthoringFormat": "strict" 19 | } 20 | -------------------------------------------------------------------------------- /tests/dummy/app/components/x-custom-render.hbs: -------------------------------------------------------------------------------- 1 | 2 | <@labelComponent /> 3 | <@hintComponent /> 4 | <@errorComponent /> 5 | 6 | {{#if (eq @type "select")}} 7 | 8 | {{else if (or (eq @type "radioGroup") (eq @type "radio-group"))}} 9 | 10 | {{else if (or (eq @type "checkboxGroup") (eq @type "checkbox-group"))}} 11 | 12 | {{else if (eq @type "checkbox")}} 13 | 14 | {{else if (eq @type "textarea")}} 15 | 16 | {{else if 17 | (and (eq @type "date") (not-eq this.dateComponent this.inputComponent)) 18 | }} 19 | 20 | {{else}} 21 | 22 | {{/if}} 23 | -------------------------------------------------------------------------------- /tests/integration/components/validated-form-defaults-test.js: -------------------------------------------------------------------------------- 1 | import { render } from "@ember/test-helpers"; 2 | import hbs from "htmlbars-inline-precompile"; 3 | import { module, test } from "qunit"; 4 | 5 | import { setupRenderingTest } from "dummy/tests/helpers"; 6 | 7 | module("Integration | Component | validated form defaults", function (hooks) { 8 | setupRenderingTest(hooks); 9 | 10 | test("renders custom hint component", async function (assert) { 11 | assert.expect(2); 12 | 13 | await render(hbs` 14 | 20 | `); 21 | 22 | assert.dom("small > svg.fa-circle-question").exists(); 23 | assert.dom("small").hasText("Hint!"); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | release: 8 | types: [published] 9 | 10 | concurrency: 11 | group: deploy 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | deploy: 16 | name: Deploy 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v5 20 | - uses: pnpm/action-setup@v4 21 | - uses: actions/setup-node@v6 22 | with: 23 | node-version: 20 24 | cache: pnpm 25 | 26 | - name: Install Dependencies 27 | run: pnpm install --frozen-lockfile 28 | 29 | - name: Deploy to Github Pages 30 | run: pnpm ember deploy production 31 | env: 32 | CI: true 33 | DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }} 34 | ADDON_DOCS_UPDATE_LATEST: true 35 | -------------------------------------------------------------------------------- /addon/components/validated-form.hbs: -------------------------------------------------------------------------------- 1 |
6 | {{yield 7 | (hash 8 | model=@model 9 | loading=this.loading 10 | input=(component 11 | "validated-input" 12 | model=@model 13 | submitted=this.submitted 14 | validateBeforeSubmit=@validateBeforeSubmit 15 | ) 16 | submit=(component 17 | "validated-button" 18 | type="submit" 19 | loading=this.loading 20 | label="Save" 21 | action=this.submit 22 | ) 23 | button=(component 24 | "validated-button" 25 | type="button" 26 | loading=this.loading 27 | label="Action" 28 | model=@model 29 | markAsDirty=this.markAsDirty 30 | ) 31 | ) 32 | }} 33 |
-------------------------------------------------------------------------------- /tests/dummy/app/deprecation-workflow.js: -------------------------------------------------------------------------------- 1 | import setupDeprecationWorkflow from "ember-cli-deprecation-workflow"; 2 | 3 | /** 4 | * Docs: https://github.com/ember-cli/ember-cli-deprecation-workflow 5 | */ 6 | setupDeprecationWorkflow({ 7 | /** 8 | false by default, but if a developer / team wants to be more aggressive about being proactive with 9 | handling their deprecations, this should be set to "true" 10 | */ 11 | throwOnUnhandled: false, 12 | workflow: [ 13 | /* ... handlers ... */ 14 | /* to generate this list, run your app for a while (or run the test suite), 15 | * and then run in the browser console: 16 | * 17 | * deprecationWorkflow.flushDeprecations() 18 | * 19 | * And copy the handlers here 20 | */ 21 | /* example: */ 22 | /* { handler: 'silence', matchId: 'template-action' }, */ 23 | ], 24 | }); 25 | -------------------------------------------------------------------------------- /tests/integration/components/validated-button-test.js: -------------------------------------------------------------------------------- 1 | import { render } from "@ember/test-helpers"; 2 | import hbs from "htmlbars-inline-precompile"; 3 | import { module, test } from "qunit"; 4 | 5 | import { setupRenderingTest } from "dummy/tests/helpers"; 6 | 7 | module("Integration | Component | validated button", function (hooks) { 8 | setupRenderingTest(hooks); 9 | 10 | hooks.beforeEach(function () { 11 | this.noop = () => {}; 12 | }); 13 | 14 | test("it renders a button with a label", async function (assert) { 15 | await render(hbs``); 16 | assert.dom("button").hasText("Test"); 17 | }); 18 | 19 | test("it renders a button with block style", async function (assert) { 20 | await render( 21 | hbs`Test`, 22 | ); 23 | assert.dom("button").hasText("Test"); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /tests/dummy/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Dummy 6 | 7 | 8 | 9 | {{content-for "head"}} 10 | 11 | 12 | 13 | 14 | {{content-for "head-footer"}} 15 | 16 | 17 | {{content-for "body"}} 18 | 19 | 20 | 21 | 22 | {{content-for "body-footer"}} 23 | 24 | 25 | -------------------------------------------------------------------------------- /tests/dummy/app/router.js: -------------------------------------------------------------------------------- 1 | import AddonDocsRouter, { docsRoute } from "ember-cli-addon-docs/router"; 2 | 3 | import config from "./config/environment"; 4 | 5 | export default class Router extends AddonDocsRouter { 6 | location = config.locationType; 7 | rootURL = config.rootURL; 8 | } 9 | 10 | /* eslint-disable-next-line array-callback-return */ 11 | Router.map(function () { 12 | docsRoute(this, function () { 13 | this.route("usage"); 14 | this.route("quickstart"); 15 | this.route("configuration"); 16 | this.route("global-defaults"); 17 | this.route("customization"); 18 | this.route("troubleshooting"); 19 | this.route("migration-v6"); 20 | this.route("migration-v8"); 21 | 22 | this.route("components", function () { 23 | this.route("validated-form"); 24 | this.route("validated-input"); 25 | this.route("validated-button"); 26 | }); 27 | }); 28 | this.route("not-found", { path: "/*path" }); 29 | }); 30 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: workflow_dispatch 4 | 5 | jobs: 6 | release: 7 | name: Release 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v5 11 | with: 12 | ref: ${{ github.event.ref }} 13 | fetch-depth: 0 14 | token: ${{ secrets.GH_TOKEN }} 15 | - uses: pnpm/action-setup@v4 16 | - uses: actions/setup-node@v6 17 | with: 18 | node-version: 20 19 | cache: pnpm 20 | 21 | - name: Install Dependencies 22 | run: pnpm install --frozen-lockfile 23 | 24 | - name: Configure git 25 | run: | 26 | git config user.name "${GITHUB_ACTOR}" 27 | git config user.email "${GITHUB_ACTOR}@users.noreply.github.com" 28 | 29 | - name: Release on NPM 30 | env: 31 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 32 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 33 | run: pnpm semantic-release 34 | -------------------------------------------------------------------------------- /config/deploy.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | "use strict"; 3 | 4 | module.exports = function (deployTarget) { 5 | const ENV = { 6 | build: {}, 7 | // include other plugin configuration that applies to all deploy targets here 8 | }; 9 | 10 | if (deployTarget === "development") { 11 | ENV.build.environment = "development"; 12 | // configure other plugins for development deploy target here 13 | } 14 | 15 | if (deployTarget === "staging") { 16 | ENV.build.environment = "production"; 17 | // configure other plugins for staging deploy target here 18 | } 19 | 20 | if (deployTarget === "production") { 21 | ENV.build.environment = "production"; 22 | // configure other plugins for production deploy target here 23 | } 24 | 25 | // Note: if you need to build some configuration asynchronously, you can return 26 | // a promise that resolves with the ENV object instead of returning the 27 | // ENV object synchronously. 28 | return ENV; 29 | }; 30 | -------------------------------------------------------------------------------- /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 | snippetPaths: ["tests/dummy/app/snippets"], 8 | "ember-validated-form": { 9 | theme: "bootstrap", 10 | scrollErrorIntoView: false, 11 | }, 12 | babel: { 13 | plugins: [ 14 | require.resolve("ember-concurrency/async-arrow-task-transform"), 15 | ], 16 | }, 17 | }); 18 | 19 | /* 20 | This build file specifies the options for the dummy test app of this 21 | addon, located in `/tests/dummy` 22 | This build file does *not* influence how the addon or the app using it 23 | behave. You most likely want to be modifying `./index.js` or app's build file 24 | */ 25 | 26 | const { maybeEmbroider } = require("@embroider/test-setup"); 27 | return maybeEmbroider(app, { 28 | skipBabel: [ 29 | { 30 | package: "qunit", 31 | }, 32 | ], 33 | }); 34 | }; 35 | -------------------------------------------------------------------------------- /tests/integration/helpers/evf-theme-test.js: -------------------------------------------------------------------------------- 1 | import { render } from "@ember/test-helpers"; 2 | import { hbs } from "ember-cli-htmlbars"; 3 | import { module, test } from "qunit"; 4 | 5 | import { setupRenderingTest } from "dummy/tests/helpers"; 6 | 7 | module("Integration | Helper | evf-theme", function (hooks) { 8 | setupRenderingTest(hooks); 9 | 10 | test("it renders", async function (assert) { 11 | await render( 12 | hbs`{{evf-theme "default"}};{{evf-theme "bootstrap"}};{{evf-theme "uikit"}}`, 13 | ); 14 | assert.dom(this.element).hasText("true;false;false"); 15 | 16 | this.setTheme("bootstrap"); 17 | await render( 18 | hbs`{{evf-theme "default"}};{{evf-theme "bootstrap"}};{{evf-theme "uikit"}}`, 19 | ); 20 | assert.dom(this.element).hasText("false;true;false"); 21 | 22 | this.setTheme("uikit"); 23 | await render( 24 | hbs`{{evf-theme "default"}};{{evf-theme "bootstrap"}};{{evf-theme "uikit"}}`, 25 | ); 26 | assert.dom(this.element).hasText("false;false;true"); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ember-validated-form 2 | 3 | [![npm version](https://badge.fury.io/js/ember-validated-form.svg)](https://badge.fury.io/js/ember-validated-form) 4 | [![Ember Observer Score](https://emberobserver.com/badges/ember-validated-form.svg)](https://emberobserver.com/addons/ember-validated-form) 5 | [![CI](https://github.com/adfinis/ember-validated-form/actions/workflows/ci.yml/badge.svg)](https://github.com/adfinis/ember-validated-form/actions/workflows/ci.yml) 6 | [![Code Style: Prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier) 7 | 8 | Easily create forms with client side validations. 9 | 10 | ![gif](https://raw.githubusercontent.com/adfinis/ember-validated-form/main/demo.gif) 11 | 12 | Want to try it yourself? [View the docs here.](https://adfinis.github.io/ember-validated-form) 13 | 14 | # Contributing 15 | 16 | Bug reports, suggestions and pull requests are always welcome! 17 | 18 | See the [Contributing](CONTRIBUTING.md) guide for details. 19 | 20 | ### License 21 | 22 | This project is licensed under the [MIT License](LICENSE.md). 23 | -------------------------------------------------------------------------------- /tests/integration/components/validated-input/label-test.js: -------------------------------------------------------------------------------- 1 | import { render } from "@ember/test-helpers"; 2 | import hbs from "htmlbars-inline-precompile"; 3 | import { module, test } from "qunit"; 4 | 5 | import { 6 | setupRenderingTest, 7 | setupUikit, 8 | setupBootstrap, 9 | } from "dummy/tests/helpers"; 10 | 11 | module("Integration | Component | validated-input/label", function (hooks) { 12 | setupRenderingTest(hooks); 13 | 14 | test("it renders", async function (assert) { 15 | await render(hbs``); 16 | 17 | assert.dom("label").hasText("Test"); 18 | }); 19 | 20 | module("uikit", function (hooks) { 21 | setupUikit(hooks); 22 | 23 | test("it renders", async function (assert) { 24 | await render(hbs``); 25 | 26 | assert.dom("label").hasClass("uk-form-label"); 27 | assert.dom("label").hasText("Test"); 28 | }); 29 | }); 30 | 31 | module("bootstrap", function (hooks) { 32 | setupBootstrap(hooks); 33 | 34 | test("it renders", async function (assert) { 35 | await render(hbs``); 36 | 37 | assert.dom("label").hasText("Test"); 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /tests/acceptance/documentation-test.js: -------------------------------------------------------------------------------- 1 | import { visit } from "@ember/test-helpers"; 2 | import { module, test } from "qunit"; 3 | 4 | import { setupApplicationTest } from "dummy/tests/helpers"; 5 | 6 | module("Acceptance | documentation", function (hooks) { 7 | setupApplicationTest(hooks); 8 | 9 | test("demo is rendered", async function (assert) { 10 | await visit("/"); 11 | 12 | assert.dom("h1").hasText("Ember ValidatedForm"); 13 | 14 | assert.dom("form").exists(); 15 | assert.dom("form input[name=title]").exists({ count: 5 }); 16 | assert.dom("form input[name=firstName]").exists(); 17 | assert.dom("form input[name=lastName]").exists(); 18 | assert.dom("form textarea[name=aboutMe]").exists(); 19 | assert.dom("form select[name=country]").exists(); 20 | assert.dom("form input[name=birthday]").exists(); 21 | assert.dom("form input[name=notifications]").exists({ count: 3 }); 22 | assert.dom("form button[type=submit]").exists(); 23 | }); 24 | 25 | test("api docs are rendered", async function (assert) { 26 | await visit("/docs"); 27 | 28 | assert.dom("nav.docs-absolute").exists(); 29 | assert.dom("div.docs-container").exists(); 30 | assert.dom("h1").hasText("Introduction"); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /addon/components/validated-input/types/checkbox.hbs: -------------------------------------------------------------------------------- 1 | {{#if (evf-theme "uikit")}} 2 | <@labelComponent 3 | class="{{if @isValid 'uk-text-success'}} {{if @isInvalid 'uk-text-danger'}}" 4 | > 5 | 15 | 16 | {{else if (evf-theme "bootstrap")}} 17 |
18 | 30 | <@labelComponent class="custom-control-label" /> 31 |
32 | {{else}} 33 | 42 | <@labelComponent /> 43 | {{/if}} -------------------------------------------------------------------------------- /tests/dummy/app/controllers/index.js: -------------------------------------------------------------------------------- 1 | // BEGIN-SNIPPET quickstart-controller.js 2 | import Controller from "@ember/controller"; 3 | import { task, timeout } from "ember-concurrency"; 4 | 5 | import UserValidations from "dummy/validations/user"; 6 | 7 | export default class IndexController extends Controller { 8 | UserValidations = UserValidations; 9 | 10 | get colors() { 11 | return [ 12 | { name: "Red", color: "red" }, 13 | { name: "Green", color: "green" }, 14 | { name: "Blue", color: "blue" }, 15 | ]; 16 | } 17 | 18 | get countries() { 19 | return ["United States", "United Kingdom", "Switzerland", "Other"]; 20 | } 21 | 22 | get titles() { 23 | return [ 24 | { key: "mr", label: "Mr." }, 25 | { key: "mrs", label: "Mrs." }, 26 | { key: "ms", label: "Ms." }, 27 | { key: "prof", label: "Prof." }, 28 | { key: "dr", label: "Dr." }, 29 | ]; 30 | } 31 | 32 | get notifications() { 33 | return [ 34 | { key: "offers", label: "Offers" }, 35 | { key: "news", label: "News" }, 36 | { key: "features", label: "Features" }, 37 | ]; 38 | } 39 | 40 | submit = task(async (model) => { 41 | await timeout(1000); 42 | await model.save(); 43 | }); 44 | } 45 | // END-SNIPPET 46 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/docs/migration-v8.md: -------------------------------------------------------------------------------- 1 | # Migration to v8 2 | 3 | ## Global defaults 4 | 5 | Before v8, `ember-validated-form` used `@embroider/macros`s `importSync` feature 6 | to inject custom global components. This method proved to be very unreliable and 7 | broke embroider support. In v8, we decided to drop this configuration in favor 8 | of using the built-in override functionality provided by Ember.js. 9 | 10 | Now, in order to globally override a component by `ember-validated-form`, simply 11 | create a component with the same name (see global defaults). 13 | 14 | **Before:** 15 | 16 | ```js 17 | // ember-cli-build.js 18 | const app = new EmberAddon(defaults, { 19 | // ... 20 | "ember-validated-form": { 21 | defaults: { 22 | error: "myapp/components/some-component", 23 | // ... 24 | }, 25 | }, 26 | // ... 27 | }); 28 | ``` 29 | 30 | **After:** 31 | 32 | ```hbs 33 | {{! components/validated-input/error.hbs }} 34 | 35 | Error: 36 | {{this.errorString}} 37 | 38 | ``` 39 | 40 | ```js 41 | // components/validated-input/error.js 42 | import ErrorComponent from "ember-validated-form/validated-input/error"; 43 | 44 | export default class extends ErrorComponent {} 45 | ``` 46 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/docs/global-defaults.md: -------------------------------------------------------------------------------- 1 | # Global defaults 2 | 3 | If you want to specify a global custom component for yourself you can simply create your own component and use the same path that `ember-validated-form` uses. For instance, to create a custom hint component that is used globally in your application, run the following command: 4 | 5 | ```bash 6 | $ ember g component --component-class @ember/component/template-only validated-input/hint 7 | ``` 8 | 9 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /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/dummy/app/templates/docs/index.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | This [ember-cli](http://www.ember-cli.com) addon is based on the following 4 | excellent addons 5 | 6 | - [ember-changeset](https://github.com/DockYard/ember-changeset) 7 | - [ember-changeset-validations](https://github.com/DockYard/ember-changeset-validations/) 8 | 9 | and provides a handy out-of-the-box setup for user-friendly client-side 10 | validations, featuring 11 | 12 | - Hiding of validation errors until field has been interacted with (or submit button was pressed) 13 | - Preventing submit action until form is valid 14 | - Live-updating validation errors 15 | - Bootstrap integration 16 | - Loading class on submit button while async task is executed 17 | - Loading contextual template parameter set while async submit task is executed 18 | 19 | ## Why \*YAEFA? 20 | 21 | \*_Yet another ember form addon_ 22 | 23 | There are many [existing ember 24 | addons](https://emberobserver.com/categories/forms) with this style of API, 25 | the most prominent probably being 26 | [ember-form-for](https://github.com/martndemus/ember-form-for). With this 27 | addon, we want to: 28 | 29 | - focus on forms that require client-side validations 30 | - provide good user experience out of the box 31 | 32 | For more information, see this [blog 33 | post](https://adfinis.com/en/blog/form-validation-with-ember-js/). 34 | -------------------------------------------------------------------------------- /tests/dummy/config/ember-try.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { embroiderSafe, embroiderOptimized } = require("@embroider/test-setup"); 4 | const getChannelURL = require("ember-source-channel-url"); 5 | 6 | module.exports = async function () { 7 | return { 8 | packageManager: "pnpm", 9 | scenarios: [ 10 | { 11 | name: "ember-lts-5.12", 12 | npm: { 13 | devDependencies: { 14 | "ember-source": "~5.12.0", 15 | }, 16 | }, 17 | }, 18 | { 19 | name: "ember-lts-6.4", 20 | npm: { 21 | devDependencies: { 22 | "ember-source": "~6.4.0", 23 | }, 24 | }, 25 | }, 26 | { 27 | name: "ember-release", 28 | npm: { 29 | devDependencies: { 30 | "ember-source": await getChannelURL("release"), 31 | }, 32 | }, 33 | }, 34 | { 35 | name: "ember-beta", 36 | npm: { 37 | devDependencies: { 38 | "ember-source": await getChannelURL("beta"), 39 | }, 40 | }, 41 | }, 42 | { 43 | name: "ember-canary", 44 | npm: { 45 | devDependencies: { 46 | "ember-source": await getChannelURL("canary"), 47 | }, 48 | }, 49 | }, 50 | embroiderSafe(), 51 | embroiderOptimized(), 52 | ], 53 | }; 54 | }; 55 | -------------------------------------------------------------------------------- /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 | historySupportMiddleware: true, 10 | EmberENV: { 11 | EXTEND_PROTOTYPES: false, 12 | FEATURES: { 13 | // Here you can enable experimental features on an ember canary build 14 | // e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true 15 | }, 16 | }, 17 | 18 | APP: { 19 | // Here you can pass flags/options to your application instance 20 | // when it is created 21 | }, 22 | }; 23 | 24 | if (environment === "development") { 25 | // ENV.APP.LOG_RESOLVER = true; 26 | // ENV.APP.LOG_ACTIVE_GENERATION = true; 27 | // ENV.APP.LOG_TRANSITIONS = true; 28 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; 29 | // ENV.APP.LOG_VIEW_LOOKUPS = true; 30 | } 31 | 32 | if (environment === "test") { 33 | // Testem prefers this... 34 | ENV.locationType = "none"; 35 | 36 | // keep test console output quieter 37 | ENV.APP.LOG_ACTIVE_GENERATION = false; 38 | ENV.APP.LOG_VIEW_LOOKUPS = false; 39 | 40 | ENV.APP.rootElement = "#ember-testing"; 41 | ENV.APP.autoboot = false; 42 | } 43 | 44 | if (environment === "production") { 45 | ENV.rootURL = "/ADDON_DOCS_ROOT_URL/"; 46 | } 47 | 48 | return ENV; 49 | }; 50 | -------------------------------------------------------------------------------- /addon/components/validated-input/types/select.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/components/validated-input/render/wrapper-test.js: -------------------------------------------------------------------------------- 1 | import { render } from "@ember/test-helpers"; 2 | import { hbs } from "ember-cli-htmlbars"; 3 | import { module, test } from "qunit"; 4 | 5 | import { 6 | setupRenderingTest, 7 | setupUikit, 8 | setupBootstrap, 9 | } from "dummy/tests/helpers"; 10 | 11 | module( 12 | "Integration | Component | validated-input/render/wrapper", 13 | function (hooks) { 14 | setupRenderingTest(hooks); 15 | 16 | test("it renders", async function (assert) { 17 | await render( 18 | hbs`Test`, 19 | ); 20 | 21 | assert.dom(this.element).hasText("Test"); 22 | }); 23 | 24 | module("bootstrap", function (hooks) { 25 | setupBootstrap(hooks); 26 | 27 | test("it renders", async function (assert) { 28 | await render( 29 | hbs`Test`, 30 | ); 31 | 32 | assert.dom(this.element).hasText("Test"); 33 | }); 34 | }); 35 | 36 | module("uikit", function (hooks) { 37 | setupUikit(hooks); 38 | 39 | test("it renders", async function (assert) { 40 | await render( 41 | hbs`Test`, 42 | ); 43 | 44 | assert.dom("div.uk-form-controls").hasText("Test"); 45 | }); 46 | }); 47 | }, 48 | ); 49 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/docs.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 24 | 28 | 32 | 33 | 34 | 35 |
36 |
37 | {{outlet}} 38 |
39 |
40 |
41 |
-------------------------------------------------------------------------------- /tests/integration/components/validated-input/types/input-test.js: -------------------------------------------------------------------------------- 1 | import { render } from "@ember/test-helpers"; 2 | import hbs from "htmlbars-inline-precompile"; 3 | import { module, test } from "qunit"; 4 | 5 | import { 6 | setupRenderingTest, 7 | setupUikit, 8 | setupBootstrap, 9 | } from "dummy/tests/helpers"; 10 | 11 | module( 12 | "Integration | Component | validated-input/types/input", 13 | function (hooks) { 14 | setupRenderingTest(hooks); 15 | 16 | test("it renders", async function (assert) { 17 | await render(hbs``); 21 | 22 | assert.dom("input").exists(); 23 | }); 24 | 25 | module("uikit", function (hooks) { 26 | setupUikit(hooks); 27 | 28 | test("it renders", async function (assert) { 29 | await render(hbs``); 33 | 34 | assert.dom("input").hasClass("uk-input"); 35 | }); 36 | }); 37 | 38 | module("bootstrap", function (hooks) { 39 | setupBootstrap(hooks); 40 | 41 | test("it renders", async function (assert) { 42 | await render(hbs``); 46 | 47 | assert.dom("input").hasClass("form-control"); 48 | }); 49 | }); 50 | }, 51 | ); 52 | -------------------------------------------------------------------------------- /tests/integration/components/validated-input/types/textarea-test.js: -------------------------------------------------------------------------------- 1 | import { render } from "@ember/test-helpers"; 2 | import hbs from "htmlbars-inline-precompile"; 3 | import { module, test } from "qunit"; 4 | 5 | import { 6 | setupRenderingTest, 7 | setupUikit, 8 | setupBootstrap, 9 | } from "dummy/tests/helpers"; 10 | 11 | module( 12 | "Integration | Component | validated-input/types/textarea", 13 | function (hooks) { 14 | setupRenderingTest(hooks); 15 | 16 | test("it renders", async function (assert) { 17 | await render(hbs``); 21 | 22 | assert.dom("textarea").exists(); 23 | }); 24 | 25 | module("uikit", function (hooks) { 26 | setupUikit(hooks); 27 | 28 | test("it renders", async function (assert) { 29 | await render(hbs``); 33 | 34 | assert.dom("textarea").hasClass("uk-textarea"); 35 | }); 36 | }); 37 | 38 | module("bootstrap", function (hooks) { 39 | setupBootstrap(hooks); 40 | 41 | test("it renders", async function (assert) { 42 | await render(hbs``); 46 | 47 | assert.dom("textarea").hasClass("form-control"); 48 | }); 49 | }); 50 | }, 51 | ); 52 | -------------------------------------------------------------------------------- /tests/integration/components/validated-input/error-test.js: -------------------------------------------------------------------------------- 1 | import { render } from "@ember/test-helpers"; 2 | import hbs from "htmlbars-inline-precompile"; 3 | import { module, test } from "qunit"; 4 | 5 | import { 6 | setupRenderingTest, 7 | setupUikit, 8 | setupBootstrap, 9 | } from "dummy/tests/helpers"; 10 | 11 | module("Integration | Component | validated-input/error", function (hooks) { 12 | setupRenderingTest(hooks); 13 | 14 | test("it renders", async function (assert) { 15 | this.set("errors", ["foo", "bar", "baz"]); 16 | 17 | await render(hbs``); 18 | 19 | assert.dom("span").hasText("foo, bar, baz"); 20 | }); 21 | 22 | module("uikit", function (hooks) { 23 | setupUikit(hooks); 24 | 25 | test("it renders", async function (assert) { 26 | this.set("errors", ["foo", "bar", "baz"]); 27 | 28 | await render(hbs``); 29 | 30 | assert.dom("small").hasClass("uk-text-danger"); 31 | assert.dom("small").hasText("foo, bar, baz"); 32 | }); 33 | }); 34 | 35 | module("bootstrap", function (hooks) { 36 | setupBootstrap(hooks); 37 | 38 | test("it renders", async function (assert) { 39 | this.set("errors", ["foo", "bar", "baz"]); 40 | 41 | await render(hbs``); 42 | 43 | assert.dom("span").hasClass("invalid-feedback"); 44 | assert.dom("span").hasClass("d-block"); 45 | assert.dom("span").hasText("foo, bar, baz"); 46 | }); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/docs/migration-v6.md: -------------------------------------------------------------------------------- 1 | # Migration to v6 2 | 3 | ## Config 4 | 5 | `ember-validated-form` is heavily based on dynamic component invokation which 6 | needed alot of changes in order to make it work with embroider. For the 7 | consumers of the addon, the only thing that changes is the static configuration. 8 | 9 | Since we switched from runtime configuration to build time configuration, the 10 | current configuration of `ember-validated-form` needs to be moved from 11 | `config/environment.js` to the `ember-cli-build.js` file: 12 | 13 | **Before:** 14 | 15 | ```js 16 | // config/environment.js 17 | var ENV = { 18 | // ... 19 | "ember-validated-form": { 20 | theme: "bootstrap", 21 | features: { 22 | scrollErrorIntoView: true, 23 | }, 24 | defaults: { 25 | error: "some-component", 26 | // ... 27 | }, 28 | }, 29 | // ... 30 | }; 31 | ``` 32 | 33 | **After:** 34 | 35 | ```js 36 | // ember-cli-build.js 37 | const app = new EmberAddon(defaults, { 38 | // ... 39 | "ember-validated-form": { 40 | theme: "bootstrap", 41 | scrollErrorIntoView: true, 42 | defaults: { 43 | error: "myapp/components/some-component", 44 | // ... 45 | }, 46 | }, 47 | // ... 48 | }); 49 | ``` 50 | 51 | As you can see above, the values in the section `defaults` changed as well. 52 | Previously the value was just the name of the component used as default, since 53 | v6 this needs to be an importable path (which allows static analysis). 54 | 55 | ## Removed deprecations 56 | 57 | The `includeBlank` argument for validated inputs has been removed in favor of 58 | `prompt`. 59 | -------------------------------------------------------------------------------- /addon/components/validated-input/types/checkbox-group.hbs: -------------------------------------------------------------------------------- 1 | {{#each @options as |option i|}} 2 | {{#if (evf-theme "uikit")}} 3 | {{#if (not-eq i 0)}}
{{/if}} 4 | 21 | {{else if (evf-theme "bootstrap")}} 22 |
23 | 35 | 39 |
40 | {{else}} 41 | 54 | {{/if}} 55 | {{/each}} -------------------------------------------------------------------------------- /addon/components/validated-input/types/radio-group.hbs: -------------------------------------------------------------------------------- 1 | {{#each @options as |option i|}} 2 | {{#if (evf-theme "uikit")}} 3 | {{#if (not-eq i 0)}}
{{/if}} 4 | 22 | {{else if (evf-theme "bootstrap")}} 23 |
24 | 37 | 41 |
42 | {{else}} 43 | 56 | {{/if}} 57 | {{/each}} -------------------------------------------------------------------------------- /tests/integration/components/validated-input/types/checkbox-test.js: -------------------------------------------------------------------------------- 1 | import { render } from "@ember/test-helpers"; 2 | import hbs from "htmlbars-inline-precompile"; 3 | import { module, test } from "qunit"; 4 | 5 | import { 6 | setupRenderingTest, 7 | setupUikit, 8 | setupBootstrap, 9 | } from "dummy/tests/helpers"; 10 | 11 | module( 12 | "Integration | Component | validated-input/types/checkbox", 13 | function (hooks) { 14 | setupRenderingTest(hooks); 15 | 16 | test("it renders", async function (assert) { 17 | await render(hbs``); 21 | 22 | assert.dom("input[type=checkbox]").exists(); 23 | }); 24 | 25 | module("uikit", function (hooks) { 26 | setupUikit(hooks); 27 | 28 | test("it renders", async function (assert) { 29 | await render(hbs``); 33 | 34 | assert.dom("label > input").exists(); 35 | assert.dom("input").hasClass("uk-checkbox"); 36 | assert.dom("label").hasClass("uk-form-label"); 37 | }); 38 | }); 39 | 40 | module("bootstrap", function (hooks) { 41 | setupBootstrap(hooks); 42 | 43 | test("it renders", async function (assert) { 44 | await render(hbs``); 48 | 49 | assert.dom("div.custom-control.custom-checkbox").exists(); 50 | assert.dom("input").hasClass("custom-control-input"); 51 | assert.dom("label").hasClass("custom-control-label"); 52 | }); 53 | }); 54 | }, 55 | ); 56 | -------------------------------------------------------------------------------- /tests/integration/components/validated-input/hint-test.js: -------------------------------------------------------------------------------- 1 | import { render } from "@ember/test-helpers"; 2 | import hbs from "htmlbars-inline-precompile"; 3 | import { module, test } from "qunit"; 4 | 5 | import { 6 | setupRenderingTest, 7 | setupUikit, 8 | setupBootstrap, 9 | } from "dummy/tests/helpers"; 10 | import HintComponent from "ember-validated-form/components/validated-input/hint"; 11 | 12 | module("Integration | Component | validated-input/hint", function (hooks) { 13 | setupRenderingTest(hooks); 14 | 15 | hooks.beforeEach(function () { 16 | // We need to use the directly imported original component from 17 | // ember-validate-form for tests as the dummy app overrides this component 18 | // to demonstrate a global override of a component. In order to have 19 | // meaningful tests, we simply import the original component and use that 20 | // instead of `` 21 | this.HintComponent = HintComponent; 22 | }); 23 | 24 | test("it renders", async function (assert) { 25 | await render(hbs``); 26 | 27 | assert.dom("small").hasText("Test"); 28 | }); 29 | 30 | module("uikit", function (hooks) { 31 | setupUikit(hooks); 32 | 33 | test("it renders", async function (assert) { 34 | await render(hbs``); 35 | 36 | assert.dom("small").hasClass("uk-text-muted"); 37 | assert.dom("small").hasText("Test"); 38 | }); 39 | }); 40 | 41 | module("bootstrap", function (hooks) { 42 | setupBootstrap(hooks); 43 | 44 | test("it renders", async function (assert) { 45 | await render(hbs``); 46 | 47 | assert.dom("small").hasClass("form-text"); 48 | assert.dom("small").hasClass("text-muted"); 49 | assert.dom("small").hasText("Test"); 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /addon/components/validated-button.js: -------------------------------------------------------------------------------- 1 | import { action } from "@ember/object"; 2 | import Component from "@glimmer/component"; 3 | import { tracked } from "@glimmer/tracking"; 4 | import { resolve } from "rsvp"; 5 | 6 | import { scrollErrorIntoViewEnabled } from "ember-validated-form/-private/features"; 7 | 8 | const ON_CLICK = "on-click"; 9 | const ON_INVALID_CLICK = "on-invalid-click"; 10 | export default class ValidatedButtonComponent extends Component { 11 | @tracked _loading; 12 | 13 | get loading() { 14 | return this.args.loading || this._loading; 15 | } 16 | 17 | @action 18 | async click(event) { 19 | // handle only clicks for custom buttons 20 | // everything else is handled by the validated form itself 21 | if (this.args.type !== "button") { 22 | return this.args.action(event); 23 | } 24 | 25 | event.preventDefault(); 26 | 27 | if (this.args.triggerValidations) { 28 | this.args.markAsDirty(); 29 | } 30 | 31 | const model = this.args.model; 32 | 33 | if (!model || !model.validate) { 34 | this.runCallback(ON_CLICK); 35 | return; 36 | } 37 | 38 | await model.validate(); 39 | 40 | if (scrollErrorIntoViewEnabled(this) && model.errors[0]?.key) { 41 | document 42 | .querySelector(`[name=${model.errors[0].key.replaceAll(".", "\\.")}]`) 43 | ?.scrollIntoView({ behavior: "smooth" }); 44 | } 45 | 46 | if (model.get("isInvalid")) { 47 | this.runCallback(ON_INVALID_CLICK); 48 | } else { 49 | this.runCallback(ON_CLICK); 50 | } 51 | } 52 | 53 | runCallback(callbackProp) { 54 | const callback = this.args[callbackProp]; 55 | if (typeof callback !== "function") { 56 | return; 57 | } 58 | 59 | this._loading = true; 60 | resolve(callback(this.args.model)).finally(() => { 61 | this._loading = false; 62 | }); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /addon/components/validated-form.js: -------------------------------------------------------------------------------- 1 | import { action } from "@ember/object"; 2 | import { scheduleOnce } from "@ember/runloop"; 3 | import Component from "@glimmer/component"; 4 | import { tracked } from "@glimmer/tracking"; 5 | import { resolve } from "rsvp"; 6 | 7 | import { scrollErrorIntoViewEnabled } from "ember-validated-form/-private/features"; 8 | 9 | const PROP_ON_SUBMIT = "on-submit"; 10 | const PROP_ON_INVALID_SUBMIT = "on-invalid-submit"; 11 | 12 | export default class ValidatedFormComponent extends Component { 13 | @tracked loading = false; 14 | @tracked submitted = false; 15 | @tracked validateBeforeSubmit = true; 16 | 17 | constructor(...args) { 18 | super(...args); 19 | 20 | if (this.args.model && this.args.model.validate) { 21 | scheduleOnce("actions", this, "validateModel", this.args.model); 22 | } 23 | } 24 | 25 | validateModel(model) { 26 | model.validate(); 27 | } 28 | 29 | @action 30 | markAsDirty() { 31 | this.submitted = true; 32 | } 33 | 34 | @action 35 | async submit(event) { 36 | event.preventDefault(); 37 | 38 | this.submitted = true; 39 | const model = this.args.model; 40 | 41 | if (!model || !model.validate) { 42 | this.runCallback(PROP_ON_SUBMIT); 43 | return false; 44 | } 45 | 46 | await model.validate(); 47 | 48 | if (model.get("isInvalid")) { 49 | if (scrollErrorIntoViewEnabled(this) && model.errors[0]?.key) { 50 | document 51 | .querySelector(`[name=${model.errors[0].key.replaceAll(".", "\\.")}]`) 52 | ?.scrollIntoView({ behavior: "smooth" }); 53 | } 54 | this.runCallback(PROP_ON_INVALID_SUBMIT); 55 | } else { 56 | this.runCallback(PROP_ON_SUBMIT); 57 | } 58 | 59 | return false; 60 | } 61 | 62 | runCallback(callbackProp) { 63 | const callback = this.args[callbackProp]; 64 | if (typeof callback !== "function") { 65 | return; 66 | } 67 | 68 | this.loading = true; 69 | resolve(callback(this.args.model)).finally(() => { 70 | this.loading = false; 71 | }); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /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 | hooks.beforeEach(function () { 34 | this.setTheme = (theme) => { 35 | this.owner.resolveRegistration("config:environment")[ 36 | "ember-validated-form" 37 | ].theme = theme; 38 | }; 39 | 40 | this.setTheme("default"); 41 | }); 42 | 43 | hooks.afterEach(function () { 44 | this.setTheme("default"); 45 | }); 46 | 47 | // Additional setup for rendering tests can be done here. 48 | } 49 | 50 | function setupTest(hooks, options) { 51 | upstreamSetupTest(hooks, options); 52 | 53 | // Additional setup for unit tests can be done here. 54 | } 55 | 56 | function setupUikit(hooks) { 57 | hooks.beforeEach(function () { 58 | this.setTheme("uikit"); 59 | }); 60 | } 61 | 62 | function setupBootstrap(hooks) { 63 | hooks.beforeEach(function () { 64 | this.setTheme("bootstrap"); 65 | }); 66 | } 67 | 68 | export { 69 | setupApplicationTest, 70 | setupRenderingTest, 71 | setupTest, 72 | setupUikit, 73 | setupBootstrap, 74 | }; 75 | -------------------------------------------------------------------------------- /.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@v5 22 | - uses: pnpm/action-setup@v4 23 | - name: Install Node 24 | uses: actions/setup-node@v6 25 | with: 26 | node-version: 20 27 | cache: pnpm 28 | - name: Install Dependencies 29 | run: pnpm install --frozen-lockfile 30 | - name: Lint 31 | run: pnpm lint 32 | - name: Run Tests 33 | run: pnpm test:ember 34 | 35 | floating: 36 | name: "Floating Dependencies" 37 | runs-on: ubuntu-latest 38 | timeout-minutes: 10 39 | 40 | steps: 41 | - uses: actions/checkout@v5 42 | - uses: pnpm/action-setup@v4 43 | - uses: actions/setup-node@v6 44 | with: 45 | node-version: 20 46 | cache: pnpm 47 | - name: Install Dependencies 48 | run: pnpm install --no-lockfile 49 | - name: Run Tests 50 | run: pnpm test:ember 51 | 52 | try-scenarios: 53 | name: ${{ matrix.try-scenario }} 54 | runs-on: ubuntu-latest 55 | needs: "test" 56 | timeout-minutes: 10 57 | 58 | strategy: 59 | fail-fast: false 60 | matrix: 61 | try-scenario: 62 | - ember-lts-5.12 63 | - ember-lts-6.4 64 | - ember-release 65 | - ember-beta 66 | - ember-canary 67 | - embroider-safe 68 | - embroider-optimized 69 | 70 | steps: 71 | - uses: actions/checkout@v5 72 | - uses: pnpm/action-setup@v4 73 | - name: Install Node 74 | uses: actions/setup-node@v6 75 | with: 76 | node-version: 20 77 | cache: pnpm 78 | - name: Install Dependencies 79 | run: pnpm install --frozen-lockfile 80 | - name: Run Tests 81 | run: ./node_modules/.bin/ember try:one ${{ matrix.try-scenario }} 82 | -------------------------------------------------------------------------------- /tests/integration/components/validated-input/render-test.js: -------------------------------------------------------------------------------- 1 | import { render } from "@ember/test-helpers"; 2 | import hbs from "htmlbars-inline-precompile"; 3 | import { module, test } from "qunit"; 4 | 5 | import { 6 | setupRenderingTest, 7 | setupUikit, 8 | setupBootstrap, 9 | } from "dummy/tests/helpers"; 10 | 11 | module("Integration | Component | validated-input/render", function (hooks) { 12 | setupRenderingTest(hooks); 13 | 14 | test("it renders", async function (assert) { 15 | await render(hbs``); 22 | 23 | assert.dom("input[type=text]").exists(); 24 | assert.dom("input[type=text]").hasAttribute("name", "test"); 25 | assert.dom("label").hasText("Test"); 26 | }); 27 | 28 | module("uikit", function (hooks) { 29 | setupUikit(hooks); 30 | 31 | test("it renders", async function (assert) { 32 | await render(hbs``); 39 | 40 | assert.dom(".uk-margin").exists(); 41 | assert.dom(".uk-margin > .uk-form-label").exists(); 42 | assert.dom(".uk-margin > .uk-form-controls").exists(); 43 | 44 | assert.dom("input[type=text].uk-input").exists(); 45 | assert.dom("input[type=text].uk-input").hasAttribute("name", "test"); 46 | assert.dom("label").hasText("Test"); 47 | }); 48 | }); 49 | 50 | module("bootstrap", function (hooks) { 51 | setupBootstrap(hooks); 52 | 53 | test("it renders", async function (assert) { 54 | await render(hbs``); 61 | 62 | assert.dom(".form-group").exists(); 63 | 64 | assert.dom("input[type=text].form-control").exists(); 65 | assert.dom("input[type=text].form-control").hasAttribute("name", "test"); 66 | assert.dom("label").hasText("Test"); 67 | }); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /tests/integration/components/validated-input/types/radio-group-test.js: -------------------------------------------------------------------------------- 1 | import { render } from "@ember/test-helpers"; 2 | import hbs from "htmlbars-inline-precompile"; 3 | import { module, test } from "qunit"; 4 | 5 | import { 6 | setupRenderingTest, 7 | setupUikit, 8 | setupBootstrap, 9 | } from "dummy/tests/helpers"; 10 | 11 | module( 12 | "Integration | Component | validated-input/types/radio-group", 13 | function (hooks) { 14 | setupRenderingTest(hooks); 15 | 16 | test("it renders", async function (assert) { 17 | this.set("options", [ 18 | { key: 1, label: 1 }, 19 | { key: 2, label: 2 }, 20 | ]); 21 | 22 | await render(hbs``); 26 | 27 | assert.dom("input[type=radio]").exists({ count: 2 }); 28 | }); 29 | 30 | module("uikit", function (hooks) { 31 | setupUikit(hooks); 32 | 33 | test("it renders", async function (assert) { 34 | this.set("options", [ 35 | { 36 | key: "opt1", 37 | label: "Option 1", 38 | }, 39 | { 40 | key: "opt2", 41 | label: "Option 2", 42 | }, 43 | ]); 44 | 45 | await render(hbs``); 49 | 50 | assert.dom("label > input").exists({ count: 2 }); 51 | assert.dom("input").hasClass("uk-radio"); 52 | assert.dom("label").hasClass("uk-form-label"); 53 | }); 54 | }); 55 | 56 | module("bootstrap", function (hooks) { 57 | setupBootstrap(hooks); 58 | 59 | test("it renders", async function (assert) { 60 | this.set("options", [ 61 | { 62 | key: "opt1", 63 | label: "Option 1", 64 | }, 65 | { 66 | key: "opt2", 67 | label: "Option 2", 68 | }, 69 | ]); 70 | 71 | await render(hbs``); 75 | 76 | assert.dom("div.custom-control.custom-radio").exists({ count: 2 }); 77 | assert.dom("input").hasClass("custom-control-input"); 78 | assert.dom("label").hasClass("custom-control-label"); 79 | }); 80 | }); 81 | }, 82 | ); 83 | -------------------------------------------------------------------------------- /addon/components/validated-input.js: -------------------------------------------------------------------------------- 1 | import { action, set, get } from "@ember/object"; 2 | import { guidFor } from "@ember/object/internals"; 3 | import { isEmpty } from "@ember/utils"; 4 | import Component from "@glimmer/component"; 5 | import { tracked } from "@glimmer/tracking"; 6 | 7 | /** 8 | * This component wraps form inputs. 9 | * 10 | * It can be used in a two-way-binding style like 11 | * (model will be updated) 12 | * 13 | * or in a one-way-binding style 14 | * 30 | 31 | {{yield 32 | (hash 33 | value=this._val 34 | update=this.update 35 | setDirty=this.setDirty 36 | model=@model 37 | name=@name 38 | inputId=this.inputId 39 | isValid=this.isValid 40 | isInvalid=this.isInvalid 41 | ) 42 | }} 43 | 44 | {{#if @hint}} 45 | 46 | {{/if}} 47 | 48 | {{#if (and this.showValidity this.errors)}} 49 | 50 | {{/if}} 51 | {{else}} 52 | {{#let 53 | (component 54 | (ensure-safe-component 55 | (or @renderComponent (component "validated-input/render")) 56 | ) 57 | type=this.type 58 | value=this._val 59 | inputId=this.inputId 60 | options=@options 61 | name=@name 62 | inputName=@inputName 63 | disabled=@disabled 64 | autofocus=@autofocus 65 | autocomplete=@autocomplete 66 | rows=@rows 67 | cols=@cols 68 | model=@model 69 | isValid=this.isValid 70 | isInvalid=this.isInvalid 71 | placeholder=@placeholder 72 | class=@class 73 | prompt=@prompt 74 | promptIsSelectable=@promptIsSelectable 75 | optionLabelPath=@optionLabelPath 76 | optionValuePath=@optionValuePath 77 | optionTargetPath=@optionTargetPath 78 | multiple=@multiple 79 | update=this.update 80 | setDirty=this.setDirty 81 | submitted=@submitted 82 | labelComponent=LabelComponent 83 | hintComponent=(if @hint HintComponent) 84 | errorComponent=(if (and this.showValidity this.errors) ErrorComponent) 85 | ) 86 | as |RenderComponent| 87 | }} 88 | 93 | {{/let}} 94 | {{/if}} 95 | {{/let}} -------------------------------------------------------------------------------- /tests/dummy/app/templates/docs/components/validated-button.md: -------------------------------------------------------------------------------- 1 | # Validated button 2 | 3 | `` yields two kinds of button components: 4 | 5 | - ``: a submit button for the form 6 | - ``: a customizable button without HTML-form specific functionality. 7 | 8 | You can use them as a block style component `Test` if you don't want to pass the label as a 9 | property. 10 | 11 | Both take the following properties: 12 | 13 | **label ``** 14 | The label of the form button. 15 | 16 | **type ``** 17 | Type of the button. Default for submit: `submit` and for standard button: `button`. 18 | _Watch out:_ If you define `type=submit` then the `on-submit` handler of the form will be triggered. 19 | 20 | **disabled ``** 21 | Specifies if the button is disabled. 22 | 23 | **loading ``** 24 | Specifies if the button is loading. Default: Automatic integration of `ember-concurrency`. 25 | 26 | 27 | 28 | 29 | 30 | {{#let f.submit as |Submit|}} 31 | 32 | Save button in block style... 33 | {{/let}} 34 | {{if this.saved 'Saved!'}} 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | Further you can leverage the `{{f.button}}` component for custom actions. The model of the wrapping form component will get passed to the on-click handler as first argument. 43 | 44 | Custom buttons support the following additional options: 45 | 46 | **on-click ``** 47 | Passes an on-click function to the button component. 48 | 49 | **on-invalid-click ``** 50 | Passes a function which is triggered after clicking on the button and when the validation proved the contents to be invalid. 51 | 52 | **triggerValidations ``** 53 | Trigger the form validations when the button is clicked (or, more precisely: show all error messages). 54 | 55 | 56 | 57 | 58 | 59 | {{#let f.button as |CustomButton|}} 60 | 61 | Custom action button in block style... 62 | {{/let}} 63 | {{if this.triggered 'Action triggered!'}} 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/docs/components/validated-form.md: -------------------------------------------------------------------------------- 1 | # Validated form 2 | 3 | The `` component accepts the following arguments: 4 | 5 | **model ``** 6 | Changeset object containing the model that backs the form. 7 | 8 | **validateBeforeSubmit ``** 9 | Specifies whether to run validations on inputs before the form has been 10 | submitted. Defaults to true. 11 | 12 | **on-submit ``** 13 | Action, that is triggered on form submit if the changeset is valid. The 14 | changeset is passed as a parameter. If the action returns a promise, then any 15 | rendered submit buttons will have a customizable CSS class added and the yielded 16 | `loading` template parameter will be set. 17 | 18 | **on-invalid-submit ``** 19 | Action, that is triggered on form submit if the changset is invalid. The 20 | changeset is passed as a parameter. (Optional) 21 | 22 | **autocomplete ``** 23 | Binding to the [`
` `autocomplete` 24 | attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#attr-autocomplete). 25 | 26 | When the submission of your form takes a little longer and your users are of 27 | the impatient kind, it is often necessary to disable the submit button to 28 | prevent the form from being submitted multiple times. This can be done using 29 | the `loading` template parameter: 30 | 31 | 32 | 33 | 34 | 35 | {{#let f.submit as |Submit|}} 36 | 37 | {{/let}} 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | It also works very well with [ember-concurrency](http://ember-concurrency.com/) tasks: 48 | 49 | 50 | 51 | 52 | 53 | {{#let f.submit as |Submit|}} 54 | 55 | {{/let}} 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /tests/integration/components/validated-input/types/checkbox-group-test.js: -------------------------------------------------------------------------------- 1 | import { click, render } from "@ember/test-helpers"; 2 | import hbs from "htmlbars-inline-precompile"; 3 | import { module, test } from "qunit"; 4 | 5 | import { 6 | setupRenderingTest, 7 | setupUikit, 8 | setupBootstrap, 9 | } from "dummy/tests/helpers"; 10 | 11 | module( 12 | "Integration | Component | validated-input/types/checkbox-group", 13 | function (hooks) { 14 | setupRenderingTest(hooks); 15 | 16 | test("it renders", async function (assert) { 17 | this.set("options", [ 18 | { key: 1, label: 1 }, 19 | { key: 2, label: 2 }, 20 | ]); 21 | 22 | await render(hbs``); 26 | 27 | assert.dom("input[type=checkbox]").exists({ count: 2 }); 28 | }); 29 | 30 | test("it can select multiple values", async function (assert) { 31 | this.options = [ 32 | { key: 1, label: 1 }, 33 | { key: 2, label: 2 }, 34 | ]; 35 | this.value = []; 36 | 37 | await render(hbs``); 43 | 44 | await click('input[value="1"]'); 45 | await click('input[value="2"]'); 46 | 47 | assert.deepEqual(this.value, [1, 2]); 48 | assert.true(this.dirty); 49 | }); 50 | 51 | module("uikit", function (hooks) { 52 | setupUikit(hooks); 53 | 54 | test("it renders", async function (assert) { 55 | this.set("options", [ 56 | { 57 | key: "opt1", 58 | label: "Option 1", 59 | }, 60 | { 61 | key: "opt2", 62 | label: "Option 2", 63 | }, 64 | ]); 65 | 66 | await render(hbs``); 70 | 71 | assert.dom("label > input").exists(); 72 | assert.dom("input").hasClass("uk-checkbox"); 73 | assert.dom("label").hasClass("uk-form-label"); 74 | }); 75 | }); 76 | 77 | module("bootstrap", function (hooks) { 78 | setupBootstrap(hooks); 79 | 80 | test("it renders", async function (assert) { 81 | await render(hbs``); 88 | 89 | assert.dom("div.custom-control.custom-checkbox").exists(); 90 | assert.dom("input").hasClass("custom-control-input"); 91 | assert.dom("label").hasClass("custom-control-label"); 92 | }); 93 | }); 94 | }, 95 | ); 96 | -------------------------------------------------------------------------------- /tests/integration/components/validated-button/button-test.js: -------------------------------------------------------------------------------- 1 | import { render } from "@ember/test-helpers"; 2 | import hbs from "htmlbars-inline-precompile"; 3 | import { module, test } from "qunit"; 4 | 5 | import { 6 | setupRenderingTest, 7 | setupUikit, 8 | setupBootstrap, 9 | } from "dummy/tests/helpers"; 10 | 11 | module("Integration | Component | validated-button/button", function (hooks) { 12 | setupRenderingTest(hooks); 13 | 14 | hooks.beforeEach(function () { 15 | this.noop = () => {}; 16 | }); 17 | 18 | test("it renders", async function (assert) { 19 | await render(hbs``); 20 | 21 | assert.dom("button").exists(); 22 | 23 | await render(hbs` 24 | Test 25 | `); 26 | 27 | assert.dom("button").hasText("Test"); 28 | }); 29 | 30 | module("uikit", function (hooks) { 31 | setupUikit(hooks); 32 | 33 | test("it renders", async function (assert) { 34 | await render( 35 | hbs``, 36 | ); 37 | 38 | assert.dom("button").hasText("Test"); 39 | assert.dom("button").hasClass("uk-button"); 40 | assert.dom("button").hasClass("uk-button-default"); 41 | }); 42 | 43 | test("it renders in block style", async function (assert) { 44 | await render(hbs` 45 | Test 46 | `); 47 | 48 | assert.dom("button").hasText("Test"); 49 | }); 50 | 51 | test("it renders a primary button for submit buttons", async function (assert) { 52 | await render( 53 | hbs``, 54 | ); 55 | 56 | assert.dom("button").hasClass("uk-button-primary"); 57 | }); 58 | }); 59 | 60 | module("bootstrap", function (hooks) { 61 | setupBootstrap(hooks); 62 | 63 | test("it renders", async function (assert) { 64 | await render( 65 | hbs``, 66 | ); 67 | 68 | assert.dom("button").hasText("Test"); 69 | assert.dom("button").hasClass("btn"); 70 | assert.dom("button").hasClass("btn-default"); 71 | }); 72 | 73 | test("it renders in block style", async function (assert) { 74 | await render(hbs` 75 | Test 76 | `); 77 | 78 | assert.dom("button").hasText("Test"); 79 | }); 80 | 81 | test("it renders a primary button for submit buttons", async function (assert) { 82 | await render( 83 | hbs``, 84 | ); 85 | 86 | assert.dom("button").hasClass("btn-primary"); 87 | }); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /tests/integration/components/validated-label-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable ember/no-empty-glimmer-component-classes */ 2 | 3 | import { setComponentTemplate } from "@ember/component"; 4 | import { render } from "@ember/test-helpers"; 5 | import Component from "@glimmer/component"; 6 | import hbs from "htmlbars-inline-precompile"; 7 | import { module, test } from "qunit"; 8 | 9 | import { setupRenderingTest } from "dummy/tests/helpers"; 10 | 11 | module("Integration | Component | validated label", function (hooks) { 12 | setupRenderingTest(hooks); 13 | 14 | test("it renders labels", async function (assert) { 15 | await render( 16 | hbs``, 17 | ); 18 | 19 | assert.dom("label").hasText("Default name"); 20 | const input = this.element.querySelector("input"); 21 | assert.dom("label").hasAttribute("for", input.getAttribute("id")); 22 | }); 23 | 24 | test("it renders custom label component", async function (assert) { 25 | class CustomLabel extends Component {} 26 | setComponentTemplate( 27 | hbs``, 28 | CustomLabel, 29 | ); 30 | this.CustomLabel = CustomLabel; 31 | 32 | await render(hbs``); 33 | 34 | assert.dom("label").hasAttribute("style", "color: green;"); 35 | }); 36 | 37 | test("it passes original variables to custom component", async function (assert) { 38 | class CustomLabel extends Component {} 39 | setComponentTemplate( 40 | hbs``, 45 | CustomLabel, 46 | ); 47 | this.CustomLabel = CustomLabel; 48 | 49 | await render(hbs``); 55 | 56 | assert.dom("label").hasAttribute("style", "color: green;"); 57 | assert.dom("#orig-label").hasText("Name custom"); 58 | assert.dom("#orig-input-required").hasText("true"); 59 | 60 | const input = this.element.querySelector("input"); 61 | assert.dom("#orig-input-id").hasText(input.getAttribute("id")); 62 | }); 63 | 64 | test("it passes custom variables to custom component", async function (assert) { 65 | class CustomLabel extends Component {} 66 | setComponentTemplate( 67 | hbs``, 68 | CustomLabel, 69 | ); 70 | this.CustomLabel = CustomLabel; 71 | 72 | await render(hbs``); 78 | 79 | assert.dom("label").hasAttribute("style", "color: green;"); 80 | assert.dom("label").hasText("Awesome!"); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /addon/components/validated-input/types/select.js: -------------------------------------------------------------------------------- 1 | import EmberObject, { action, get } from "@ember/object"; 2 | import Component from "@glimmer/component"; 3 | 4 | /** 5 | * This component is heavily inspired by the ember-one-way-select addon. 6 | * https://github.com/DockYard/ember-one-way-select 7 | * Our implementation is slightly simpler, since it does not support 8 | * the block syntax for options. 9 | */ 10 | export default class SelectComponent extends Component { 11 | get hasPreGroupedOptions() { 12 | return ( 13 | this.args.options[0]?.groupName && 14 | Array.isArray(this.args.options[0]?.options) 15 | ); 16 | } 17 | 18 | get hasGrouping() { 19 | return this.hasPreGroupedOptions || this.args.groupLabelPath; 20 | } 21 | 22 | get normalizedOptions() { 23 | // normalize options to common data structure, only for rendering 24 | return this.args.options.map((opt) => this.normalize(opt)); 25 | } 26 | 27 | normalize(option) { 28 | if (typeof option !== "object") { 29 | return { id: option, label: option }; 30 | } 31 | const valuePath = this.args.optionValuePath ?? this.args.optionTargetPath; 32 | const labelPath = this.args.optionLabelPath; 33 | return { 34 | id: valuePath ? option[valuePath] : option.id, 35 | label: labelPath ? option[labelPath] : option.label, 36 | }; 37 | } 38 | 39 | get optionGroups() { 40 | const groupLabelPath = this.args.groupLabelPath; 41 | if (!groupLabelPath) { 42 | return this.args.options; 43 | } 44 | const groups = []; 45 | 46 | this.args.options.forEach((item) => { 47 | const label = get(item, groupLabelPath); 48 | 49 | if (label) { 50 | let group = groups.find((group) => group.groupName === label); 51 | 52 | if (!group) { 53 | group = EmberObject.create({ 54 | groupName: label, 55 | options: [], 56 | }); 57 | 58 | groups.push(group); 59 | } 60 | 61 | group.options.push(this.normalize(item)); 62 | } else { 63 | groups.push(item); 64 | } 65 | }); 66 | 67 | return groups; 68 | } 69 | 70 | findOption(target) { 71 | const targetPath = this.args.optionTargetPath; 72 | const valuePath = this.args.optionValuePath || targetPath; 73 | 74 | const getValue = (item) => { 75 | if (valuePath) { 76 | return String(item[valuePath]); 77 | } 78 | if (typeof item === "object") { 79 | return String(item.id); 80 | } 81 | return String(item); 82 | }; 83 | 84 | let options = this.args.options; 85 | 86 | //flatten pre grouped options 87 | if (this.hasPreGroupedOptions) { 88 | options = options.flatMap((group) => group.options); 89 | } 90 | 91 | // multi select 92 | if (this.args.multiple) { 93 | const selectedValues = Array.prototype.filter 94 | .call(target.options, (option) => option.selected) 95 | .map((option) => option.value); 96 | 97 | const foundOptions = options.filter((item) => { 98 | return selectedValues.includes(getValue(item)); 99 | }); 100 | if (targetPath) { 101 | return foundOptions.map((item) => item[targetPath]); 102 | } 103 | return foundOptions; 104 | } 105 | 106 | //single select 107 | const foundOption = options.find((item) => getValue(item) === target.value); 108 | 109 | // If @promptIsSelectable is set to true, foundOption in this case will be undefined. 110 | if (targetPath && foundOption) { 111 | return foundOption[targetPath]; 112 | } 113 | return foundOption; 114 | } 115 | 116 | @action 117 | onUpdate(event) { 118 | if (this.args.update) { 119 | this.args.update(this.findOption(event.target)); 120 | } 121 | } 122 | 123 | @action 124 | onBlur(event) { 125 | if (this.args.setDirty) { 126 | this.args.setDirty(event.target.value); 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/index.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 |

Built with ♥ by 11 | Adfinis

12 | 13 |
14 | Ember Observer Score 22 | CI 30 | Code Style: Prettier 34 |
35 | 36 |
37 | 38 | 39 | {{! BEGIN-SNIPPET quickstart-template.hbs }} 40 | {{#if @model.saved}} 41 |
42 | Model was successfully saved! 43 |
44 | {{/if}} 45 | 46 | 51 | 58 | 59 | 60 | 61 | 62 | 63 | 70 | 71 | 80 | 81 | 82 | 83 | 89 | 90 | 96 | 97 | 103 | 104 | 108 | 109 | {{! END-SNIPPET }} 110 |
111 | 112 | 113 | 114 | 115 | 119 | 123 | 127 |
128 |
-------------------------------------------------------------------------------- /addon/components/validated-input/render.hbs: -------------------------------------------------------------------------------- 1 | {{#let 2 | (ensure-safe-component 3 | (or @selectComponent (component "validated-input/types/select")) 4 | ) 5 | (ensure-safe-component 6 | (or @radioGroupComponent (component "validated-input/types/radio-group")) 7 | ) 8 | (ensure-safe-component 9 | (or 10 | @checkboxGroupComponent (component "validated-input/types/checkbox-group") 11 | ) 12 | ) 13 | (ensure-safe-component 14 | (or @checkboxComponent (component "validated-input/types/checkbox")) 15 | ) 16 | (ensure-safe-component 17 | (or @textareaComponent (component "validated-input/types/textarea")) 18 | ) 19 | (ensure-safe-component 20 | (or @dateComponent (component "validated-input/types/date")) 21 | ) 22 | (ensure-safe-component 23 | (or @inputComponent (component "validated-input/types/input")) 24 | ) 25 | as |SelectComponent RadioGroupComponent CheckboxGroupComponent CheckboxComponent TextareaComponent DateComponent InputComponent| 26 | }} 27 | {{! template-lint-disable no-autofocus-attribute }} 28 |
34 | {{#if (not-eq @type "checkbox")}} 35 | <@labelComponent /> 36 | {{/if}} 37 | 38 | 39 | {{#if (eq @type "select")}} 40 | 58 | {{else if (or (eq @type "radioGroup") (eq @type "radio-group"))}} 59 | 71 | {{else if (or (eq @type "checkboxGroup") (eq @type "checkbox-group"))}} 72 | 84 | {{else if (eq @type "checkbox")}} 85 | 98 | {{else if (eq @type "textarea")}} 99 | 115 | {{else if (eq @type "date")}} 116 | 130 | {{else}} 131 | 146 | {{/if}} 147 | 148 | 149 | <@hintComponent /> 150 | <@errorComponent /> 151 |
152 | {{/let}} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-validated-form", 3 | "version": "7.0.1", 4 | "description": "Easily create forms with client-side validations", 5 | "keywords": [ 6 | "ember-addon", 7 | "forms", 8 | "form validation", 9 | "validation messages" 10 | ], 11 | "repository": "https://github.com/adfinis/ember-validated-form", 12 | "license": "MIT", 13 | "author": "Adfinis AG ", 14 | "directories": { 15 | "test": "tests" 16 | }, 17 | "homepage": "https://adfinis.github.io/ember-validated-form", 18 | "bugs": { 19 | "url": "https://github.com/adfinis/ember-validated-form/issues" 20 | }, 21 | "scripts": { 22 | "build": "ember build --environment=production", 23 | "format": "prettier . --cache --write", 24 | "lint": "concurrently \"pnpm:lint:*(!fix)\" --names \"lint:\" --prefixColors auto", 25 | "lint:css": "stylelint \"**/*.css\"", 26 | "lint:css:fix": "concurrently \"pnpm:lint:css -- --fix\"", 27 | "lint:fix": "concurrently \"pnpm:lint:*:fix\" --names \"fix:\" --prefixColors auto && pnpm format", 28 | "lint:format": "prettier . --cache --check", 29 | "lint:hbs": "ember-template-lint .", 30 | "lint:hbs:fix": "ember-template-lint . --fix", 31 | "lint:js": "eslint . --cache", 32 | "lint:js:fix": "eslint . --fix", 33 | "start": "ember serve", 34 | "test": "concurrently \"pnpm:lint\" \"pnpm:test:*\" --names \"lint,test:\" --prefixColors auto", 35 | "test:ember": "ember test", 36 | "test:ember-compatibility": "ember try:each", 37 | "prepare": "husky" 38 | }, 39 | "dependencies": { 40 | "@babel/core": "^7.24.6", 41 | "ember-auto-import": "^2.11.0", 42 | "ember-cli-babel": "^8.2.0", 43 | "ember-cli-htmlbars": "^6.3.0", 44 | "ember-template-imports": "^4.3.0" 45 | }, 46 | "devDependencies": { 47 | "@adfinis/eslint-config": "3.0.2", 48 | "@adfinis/semantic-release-config": "5.0.0", 49 | "@babel/eslint-parser": "7.28.5", 50 | "@babel/plugin-proposal-decorators": "7.28.0", 51 | "@commitlint/cli": "20.1.0", 52 | "@commitlint/config-conventional": "20.0.0", 53 | "@ember/optional-features": "2.2.0", 54 | "@ember/string": "3.1.1", 55 | "@ember/test-helpers": "4.0.4", 56 | "@embroider/macros": "1.19.2", 57 | "@embroider/test-setup": "4.0.0", 58 | "@eslint/js": "9.39.1", 59 | "@fortawesome/ember-fontawesome": "3.1.0", 60 | "@fortawesome/fontawesome-svg-core": "7.1.0", 61 | "@fortawesome/free-solid-svg-icons": "7.1.0", 62 | "@glimmer/component": "1.1.2", 63 | "@glimmer/tracking": "1.1.2", 64 | "broccoli-asset-rev": "3.0.0", 65 | "concurrently": "9.2.1", 66 | "ember-changeset": "5.0.0", 67 | "ember-changeset-validations": "5.0.0", 68 | "ember-cli": "6.8.0", 69 | "ember-cli-addon-docs": "10.0.1", 70 | "ember-cli-clean-css": "3.0.0", 71 | "ember-cli-dependency-checker": "3.3.3", 72 | "ember-cli-deploy": "2.0.0", 73 | "ember-cli-deploy-build": "3.0.0", 74 | "ember-cli-deploy-git": "1.3.4", 75 | "ember-cli-deploy-git-ci": "1.0.1", 76 | "ember-cli-deprecation-workflow": "3.4.0", 77 | "ember-cli-inject-live-reload": "2.1.0", 78 | "ember-cli-sri": "2.1.1", 79 | "ember-cli-terser": "4.0.2", 80 | "ember-cli-test-loader": "3.1.0", 81 | "ember-concurrency": "5.1.0", 82 | "ember-data": "5.7.0", 83 | "ember-flatpickr": "9.0.1", 84 | "ember-load-initializers": "3.0.1", 85 | "ember-qunit": "9.0.4", 86 | "ember-resolver": "13.1.1", 87 | "ember-source": "6.8.2", 88 | "ember-source-channel-url": "3.0.0", 89 | "ember-template-lint": "7.9.3", 90 | "ember-truth-helpers": "5.0.0", 91 | "ember-try": "4.0.0", 92 | "eslint": "9.39.1", 93 | "eslint-config-prettier": "10.1.8", 94 | "eslint-plugin-ember": "12.7.4", 95 | "eslint-plugin-import": "2.32.0", 96 | "eslint-plugin-n": "17.23.1", 97 | "eslint-plugin-prettier": "5.5.4", 98 | "eslint-plugin-qunit": "8.2.5", 99 | "flatpickr": "4.6.13", 100 | "globals": "16.5.0", 101 | "husky": "9.1.7", 102 | "lint-staged": "16.2.6", 103 | "loader.js": "4.7.0", 104 | "prettier": "3.6.2", 105 | "prettier-plugin-ember-template-tag": "2.1.0", 106 | "qunit": "2.24.2", 107 | "qunit-dom": "3.5.0", 108 | "semantic-release": "25.0.1", 109 | "stylelint": "16.25.0", 110 | "stylelint-config-standard": "39.0.1", 111 | "stylelint-prettier": "5.0.3", 112 | "webpack": "5.102.1" 113 | }, 114 | "peerDependencies": { 115 | "@embroider/util": "^1.13.0", 116 | "ember-changeset": "^4.1.2 || ^5.0.0", 117 | "ember-changeset-validations": "^4.1.1 || ^5.0.0", 118 | "ember-source": ">= 4.0.0", 119 | "ember-truth-helpers": "^3.1.0 || ^4.0.0 || ^5.0.0" 120 | }, 121 | "packageManager": "pnpm@9.15.3", 122 | "pnpm": { 123 | "peerDependencyRules": { 124 | "allowedVersions": { 125 | "ember-source": "^5.0.0" 126 | } 127 | } 128 | }, 129 | "engines": { 130 | "node": ">= 20.11" 131 | }, 132 | "ember": { 133 | "edition": "octane" 134 | }, 135 | "ember-addon": { 136 | "configPath": "tests/dummy/config", 137 | "demoURL": "https://adfinis.github.io/ember-validated-form" 138 | }, 139 | "release": { 140 | "extends": "@adfinis/semantic-release-config" 141 | }, 142 | "lint-staged": { 143 | "*.js": "eslint --cache --fix", 144 | "*.hbs": "ember-template-lint --fix", 145 | "*.css": "stylelint --fix", 146 | "*.{json,md,yml}": "prettier --write" 147 | }, 148 | "commitlint": { 149 | "extends": [ 150 | "@commitlint/config-conventional" 151 | ] 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/docs/components/validated-input.md: -------------------------------------------------------------------------------- 1 | ## Validated input 2 | 3 | `` yields an object, that contains the [contextual 4 | component](https://emberjs.com/blog/2016/01/15/ember-2-3-released.html#toc_contextual-components) 5 | `input`. All input fields share some common properties: 6 | 7 | **label ``** 8 | The label of the form field. 9 | 10 | **name ``** 11 | This is is the name of the model property this input is bound to. 12 | 13 | **inputName ``** 14 | The name attribute of the input element. If not passed it will default to the 15 | passed `name`. 16 | 17 | **hint ``** 18 | Additional explanatory text displayed below the input field. 19 | 20 | **type ``** 21 | Type of the form field (see supported field types below). Default: `text`. 22 | 23 | **disabled ``** 24 | Specifies if the input field is disabled. 25 | 26 | **required ``** 27 | If true, a "\*" is appended to the field"s label indicating that it is 28 | required. 29 | 30 | **value ``** 31 | Initial value of the form field. Default: model property defined by name. 32 | 33 | **validateBeforeSubmit ``** 34 | Specifies whether to run validations on this input before the form has been 35 | submitted. Defaults to the value set on the form. 36 | 37 | **on-update ``** 38 | Per default, the input elements are two-way-bound. If you want to implement 39 | custom update behavior, pass an action as `on-update`. The function receives 40 | two arguments: `update(value, changeset)`. 41 | 42 | **autocomplete ``** 43 | Binding to the [`` `autocomplete` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-autocomplete). 44 | 45 | The supported field types are `checkbox`, `checkboxGroup`, `radioGroup`, 46 | `select`, `textarea` and any type that can be specified on an `` element. 47 | This addon does not much more than translating `` to 48 | `` with the 49 | various other properties (`name`, `disabled`, etc.) and event handlers. 50 | 51 | However, some field types require extra parameters. The supported field types 52 | are listed below. 53 | 54 | ### Text input 55 | 56 | If no field type is specified, a simple `` is rendered. 57 | Other HTML5 text-like inputs like `email`, `number`, `search` require 58 | specifying their type. The element also supports the following options: 59 | 60 | - `placeholder` 61 | - `autofocus` 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | ### Textarea 77 | 78 | The textarea element also supports the following options: 79 | 80 | - `rows` and `cols` 81 | - `autofocus` 82 | - `placeholder` 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | ### Select 97 | 98 | The select element also supports the following options: 99 | 100 | - `value` 101 | - `options` 102 | - `optionLabelPath` 103 | - `optionValuePath` 104 | - `optionTargetPath` 105 | - `prompt` 106 | - `promptIsSelectable` 107 | - `groupLabelPath` 108 | 109 | 110 | 111 | 112 | 113 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | **Grouping** is supported in two ways: First by using the `groupLabelPath` property (e.g. `type` in th example below) or second by pre-grouped options in the form of: 130 | 131 | ```js 132 | [ 133 | { 134 | groupName: "one", 135 | options: [ 136 | { id: 1, label: "First", type: "group1" }, 137 | { id: 2, label: "Second", type: "group1" }, 138 | ], 139 | }, 140 | { 141 | groupName: "two", 142 | options: [{ id: 3, label: "Third", type: "group2" }], 143 | }, 144 | ]; 145 | ``` 146 | 147 | ### Checkbox 148 | 149 | This component renders an `` elements. 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | ### Radio button group 164 | 165 | This component renders a list of `` elements. 166 | 167 | 168 | 169 | 170 | 171 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 208 | 209 | ### Checkbox group 210 | 211 | This component renders a list of `` elements. 212 | 213 | 214 | 215 | 216 | 217 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/docs/customization.md: -------------------------------------------------------------------------------- 1 | # Customization 2 | 3 | ## validated-input 4 | 5 | The whole `` appearance and behaviour can be customized to 6 | your specific needs. 7 | 8 | ### Input 9 | 10 | If the input element you need is not explicitly supported, you can easily 11 | integrate it with this addon by using the `component` template helper to pass 12 | a `renderComponent`: 13 | 14 | 15 | 16 | 17 | 18 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | **Arguments** 38 | 39 | - `<*>` **value** The current value of the field 40 | - `` **type** The type of the field 41 | - `` **inputId** The ID of the field (generated with `guidFor` from `@ember/object/internals`) 42 | - `` **options** The options for selects or radio groups 43 | - `` **name** The name of the field 44 | - `` **disabled** Whether the field is disabled 45 | - `` **autofocus** Whether to autofocus the field 46 | - `` **autocomplete** Whether to enable autocompletion on the field 47 | - `` **rows** The rows of the textarea 48 | - `` **cols** The cols of the textarea 49 | - `` **model** The model of the form 50 | - `` **isValid** Whether the form data is valid 51 | - `` **isInvalid** Whether the form data is invalid 52 | - `` **placeholder** The placeholder of the field 53 | - `` **class** The class for the render wrapper 54 | 55 | - `` **optionLabelPath** The property name of the label if objects are given as options 56 | - `` **optionValuePath** The property name of the value if objects are given as options 57 | - `` **optionTargetPath** Identical to `optionValuePath` but the return value will then be that property instead of the whole object 58 | - `` **prompt** Label of the prompt option (most likely an empty default option), will be omitted if left empty 59 | - `` **promptIsSelectable** Whether the prompt option is selectable 60 | - `` **multiple** Whether multiple options can be selected 61 | 62 | - `` **update** Action to update the value 63 | - `` **setDirty** Action to mark the field as dirty 64 | 65 | - `` **labelComponent** The label component 66 | - `` **hintComponent** The hint component 67 | - `` **errorComponent** The error component 68 | 69 | ### Label 70 | 71 | If you want to have a label on your input which renders something 72 | non-standard (for instance tooltips), then you can pass your custom component 73 | to the input in the following manner: 74 | 75 | _Note:_ When adding a custom component for input of type checkbox, one has to 76 | add `{{yield}}` inside the label. This is because, this kind of input renders 77 | inside a label tag. 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | **Arguments** 93 | 94 | - `` **label** The label of the field 95 | - `` **inputId** The id of the field, this is mostly used for the `for` attribute 96 | - `` **required** Whether the field is required 97 | - `` **isValid** Whether the field is valid 98 | - `` **isInvalid** Whether the field is invalid 99 | 100 | ### Hint 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | **Arguments** 116 | 117 | - `` **hint** The hint for the field 118 | 119 | ### Error 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | **Arguments** 135 | 136 | - `` **errors** The error messages of the field 137 | 138 | ### Date 139 | 140 | `ember-validated-form` has no default date picker implemented. If you specify an input 141 | with type `date`, a plain input with the HTML5 (depending on your browser) will 142 | be rendered. 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | This is on purpose due to there being many date picker components/addons 155 | available. And not every date picker fits every theme. 156 | 157 | If you would like to configure a custom date picker, configure a custom date 158 | component as specified in global defaults. 159 | 160 | 161 | 162 | 163 | 164 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | **Arguments** 180 | 181 | - `<*>` **value** The current value of the field 182 | - `` **update** Action to update the value 183 | - `` **inputId** The ID of the field (generated with `guidFor` from `@ember/object/internals`) 184 | - `` **placeholder** The placeholder of the field 185 | - `` **isValid** Whether the form data is valid 186 | - `` **isInvalid** Whether the form data is invalid 187 | - `` **setDirty** Action to mark the field as dirty 188 | - `` **name** The name of the field 189 | - `` **disabled** Whether the field is disabled 190 | - `` **autocomplete** Whether to enable autocompletion on the field 191 | 192 | ## validated-button 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | **Arguments** 208 | 209 | - `` **onclick** The onclick action passed to the button. Mostly, this will be the `submit` action of the form 210 | - `` **disabled** Whether the button is disabled 211 | - `` **label** The text of the button 212 | - `` **type** The type of the button 213 | - `` **class** The class of the button 214 | -------------------------------------------------------------------------------- /tests/integration/components/validated-input-test.js: -------------------------------------------------------------------------------- 1 | import { render, click, fillIn, settled } from "@ember/test-helpers"; 2 | import Changeset from "ember-changeset"; 3 | import { hbs } from "ember-cli-htmlbars"; 4 | import { module, test } from "qunit"; 5 | 6 | import { setupRenderingTest } from "dummy/tests/helpers"; 7 | 8 | module("Integration | Component | validated input", function (hooks) { 9 | setupRenderingTest(hooks); 10 | 11 | test("it renders simple text inputs with correct name", async function (assert) { 12 | await render(hbs``); 13 | 14 | assert.dom("input").hasAttribute("type", "text"); 15 | assert.dom("input").hasAttribute("name", "bar"); 16 | }); 17 | 18 | test("it renders email input", async function (assert) { 19 | await render(hbs``); 20 | 21 | assert.dom("input").hasAttribute("type", "email"); 22 | }); 23 | 24 | test("it renders tel input", async function (assert) { 25 | await render(hbs``); 26 | 27 | assert.dom("input").hasAttribute("type", "tel"); 28 | }); 29 | 30 | test("it renders disabled inputs", async function (assert) { 31 | await render(hbs``); 32 | 33 | assert.dom("input").isDisabled(); 34 | }); 35 | 36 | test("it renders inputs with placeholder", async function (assert) { 37 | await render(hbs``); 38 | 39 | assert.dom("input").hasAttribute("placeholder", "foo"); 40 | }); 41 | 42 | test("it renders inputs with value", async function (assert) { 43 | await render(hbs``); 44 | 45 | assert.dom("input").hasValue("foo"); 46 | }); 47 | 48 | test("it renders inputs with model", async function (assert) { 49 | this.set("model", new Changeset({ firstName: "Max" })); 50 | 51 | await render( 52 | hbs``, 53 | ); 54 | 55 | assert.dom("input").hasValue("Max"); 56 | }); 57 | 58 | test("it calls on-update if given", async function (assert) { 59 | this.set("model", new Changeset({ firstName: "Max" })); 60 | this.set("update", (value, changeset) => { 61 | changeset.set("firstName", value.toUpperCase()); 62 | }); 63 | await render( 64 | hbs``, 69 | ); 70 | 71 | await fillIn("input", "foo"); 72 | 73 | assert.dom("input").hasValue("FOO"); 74 | }); 75 | 76 | test("it renders inputs with value even if model is defined", async function (assert) { 77 | this.set("model", new Changeset({ firstName: "Max" })); 78 | 79 | await render( 80 | hbs``, 81 | ); 82 | 83 | assert.dom("input").hasValue("foobar"); 84 | }); 85 | 86 | test("it renders textarea inputs with correct name", async function (assert) { 87 | await render(hbs``); 88 | 89 | assert.dom("textarea").hasAttribute("name", "bar"); 90 | }); 91 | 92 | test("it renders disabled textareas", async function (assert) { 93 | await render(hbs``); 94 | 95 | assert.dom("textarea").isDisabled(); 96 | }); 97 | 98 | test("it renders textareas with placeholder", async function (assert) { 99 | await render(hbs``); 100 | 101 | assert.dom("textarea").hasAttribute("placeholder", "foo"); 102 | }); 103 | 104 | test("it renders textareas with value", async function (assert) { 105 | await render(hbs``); 106 | 107 | assert.dom("textarea").hasValue("foo"); 108 | }); 109 | 110 | test("it renders textareas with model", async function (assert) { 111 | this.set("model", new Changeset({ firstName: "Max" })); 112 | 113 | await render( 114 | hbs``, 115 | ); 116 | 117 | assert.dom("textarea").hasValue("Max"); 118 | }); 119 | 120 | test("it renders textareas autocomplete attribute", async function (assert) { 121 | await render( 122 | hbs``, 123 | ); 124 | 125 | assert.dom("textarea").hasAttribute("autocomplete", "given-name"); 126 | }); 127 | 128 | test("it renders input autocomplete attribute", async function (assert) { 129 | await render( 130 | hbs``, 135 | ); 136 | 137 | assert.dom("input").hasAttribute("autocomplete", "new-password"); 138 | }); 139 | 140 | test("it renders the block if provided", async function (assert) { 141 | await render( 142 | hbs` 143 |
144 |
`, 145 | ); 146 | 147 | assert.dom("#custom-input").exists(); 148 | }); 149 | 150 | test("it yields the value provided to the block", async function (assert) { 151 | await render( 152 | hbs` 153 | 154 | `, 155 | ); 156 | 157 | assert.dom("input").hasValue("my-value"); 158 | }); 159 | 160 | test("it yields the name from the model as value", async function (assert) { 161 | this.set("model", new Changeset({ firstName: "Max" })); 162 | 163 | await render( 164 | hbs` 165 | 166 | `, 167 | ); 168 | 169 | assert.dom("input").hasValue("Max"); 170 | }); 171 | 172 | test("it yields the value as value if both model and value is provided", async function (assert) { 173 | this.set("model", new Changeset({ firstName: "Max" })); 174 | 175 | await render( 176 | hbs` 182 | 183 | `, 184 | ); 185 | 186 | assert.dom("input").hasValue("Other Value"); 187 | }); 188 | 189 | test("it yields the provided name", async function (assert) { 190 | await render( 191 | hbs` 192 | 193 | `, 194 | ); 195 | 196 | assert.dom("input").hasAttribute("name", "foobar"); 197 | }); 198 | 199 | test("it yields the model", async function (assert) { 200 | this.set("model", new Changeset({ firstName: "Max" })); 201 | 202 | await render( 203 | hbs` 204 | 205 | `, 206 | ); 207 | 208 | assert.dom("input").hasValue("Max"); 209 | }); 210 | 211 | test("it yields an action for updating the model", async function (assert) { 212 | const model = new Changeset({ firstName: "Max" }); 213 | this.set("model", model); 214 | 215 | await render( 216 | hbs` 217 | 218 | `, 219 | ); 220 | 221 | await click("button"); 222 | 223 | assert.strictEqual(model.get("firstName"), "Merlin"); 224 | }); 225 | 226 | test("it yields an action marking the input as dirty", async function (assert) { 227 | this.setTheme("bootstrap"); 228 | 229 | this.set("model", { error: { test: { validation: ["Error"] } } }); 230 | 231 | await render( 232 | hbs` 233 | 234 | `, 235 | ); 236 | 237 | assert.dom("span.invalid-feedback").doesNotExist(); 238 | 239 | await click("button"); 240 | 241 | assert.dom("span.invalid-feedback").exists(); 242 | }); 243 | 244 | test("it yields the input id to the block", async function (assert) { 245 | await render( 246 | hbs` 247 | 248 | `, 249 | ); 250 | 251 | const label = this.element.querySelector("label"); 252 | const input = this.element.querySelector("input"); 253 | assert.strictEqual(label.getAttribute("for"), input.getAttribute("id")); 254 | }); 255 | 256 | test("it can change the value from outside the input", async function (assert) { 257 | this.set("model", new Changeset({ firstName: "Max" })); 258 | 259 | await render( 260 | hbs``, 261 | ); 262 | 263 | assert.dom("input").hasValue("Max"); 264 | 265 | this.set("model.firstName", "Hans"); 266 | 267 | assert.dom("input").hasValue("Hans"); 268 | }); 269 | 270 | test("it can overwrite the input name", async function (assert) { 271 | this.set("model", new Changeset({ firstName: "Max" })); 272 | 273 | await render( 274 | hbs``, 279 | ); 280 | 281 | assert.dom("input").hasValue("Max"); 282 | assert.dom("input").hasAttribute("name", "testFirstName"); 283 | }); 284 | 285 | test("it updates _val on nested fields", async function (assert) { 286 | this.set("model", new Changeset({ nested: { name: "Max" } })); 287 | 288 | await render( 289 | hbs`{{this.model.nested.name}} 290 | 291 | {{Input.value}} 292 | `, 293 | ); 294 | 295 | assert.dom("#raw").hasText("Max"); 296 | assert.dom("#_val").hasText("Max"); 297 | 298 | this.model.set("nested.name", "Tom"); 299 | await settled(); 300 | 301 | assert.dom("#raw").hasText("Tom"); 302 | assert.dom("#_val").hasText("Tom"); 303 | }); 304 | }); 305 | -------------------------------------------------------------------------------- /tests/integration/components/validated-input/types/select-test.js: -------------------------------------------------------------------------------- 1 | import { render, select } from "@ember/test-helpers"; 2 | import hbs from "htmlbars-inline-precompile"; 3 | import { module, test } from "qunit"; 4 | 5 | import { 6 | setupRenderingTest, 7 | setupUikit, 8 | setupBootstrap, 9 | } from "dummy/tests/helpers"; 10 | 11 | module( 12 | "Integration | Component | validated-input/types/select", 13 | function (hooks) { 14 | setupRenderingTest(hooks); 15 | 16 | test("it renders", async function (assert) { 17 | this.set("options", [ 18 | { id: 1, label: 1 }, 19 | { id: 2, label: 2 }, 20 | ]); 21 | 22 | await render( 23 | hbs``, 24 | ); 25 | 26 | assert.dom("select").exists(); 27 | assert.dom("option").exists({ count: 2 }); 28 | assert.dom("option:first-child").hasProperty("selected", true); 29 | }); 30 | 31 | test("it supports plain (non-object) options", async function (assert) { 32 | assert.expect(6); 33 | this.set("options", ["foo", "bar"]); 34 | 35 | this.set("update", (value) => { 36 | assert.strictEqual(value, "bar"); 37 | }); 38 | await render( 39 | hbs``, 43 | ); 44 | 45 | assert.dom("select").exists(); 46 | assert.dom("option").exists({ count: 2 }); 47 | assert.dom("option:first-child").hasProperty("selected", true); 48 | await select("select", "bar"); 49 | assert.dom("option:first-child").hasProperty("selected", false); 50 | assert.dom("option:last-child").hasProperty("selected", true); 51 | }); 52 | 53 | test("it works with solitary optionTargetPath property", async function (assert) { 54 | assert.expect(2); 55 | this.set("options", [ 56 | { key: 111, label: "firstOption" }, 57 | { key: 222, label: "secondOption" }, 58 | ]); 59 | 60 | this.set("update", (value) => { 61 | assert.strictEqual(value, 222); 62 | }); 63 | 64 | await render( 65 | hbs``, 70 | ); 71 | 72 | assert.dom("option:first-child").hasText("firstOption"); 73 | await select("select", "222"); 74 | }); 75 | 76 | test("it renders option groups via groupLabelPath", async function (assert) { 77 | this.set("options", [ 78 | { key: 1, label: 1, group: "one" }, 79 | { key: 2, label: 2, group: "two" }, 80 | ]); 81 | 82 | await render( 83 | hbs``, 89 | ); 90 | 91 | assert.dom("select").exists(); 92 | assert.dom("optgroup[label='one']").exists({ count: 1 }); 93 | assert.dom("optgroup[label='two']").exists({ count: 1 }); 94 | assert.dom("optgroup:first-child option:first-child").hasText("1"); 95 | assert.dom("optgroup:last-child option:first-child").hasValue("2"); 96 | }); 97 | 98 | test("it renders option groups pre grouped options", async function (assert) { 99 | this.set("options", [ 100 | { 101 | groupName: "one", 102 | options: [ 103 | { id: 1, label: "First", type: "group1" }, 104 | { id: 2, label: "Second", type: "group1" }, 105 | ], 106 | }, 107 | { 108 | groupName: "two", 109 | options: [{ id: 3, label: "Third", type: "group2" }], 110 | }, 111 | ]); 112 | 113 | await render( 114 | hbs``, 119 | ); 120 | 121 | assert.dom("select").exists(); 122 | assert.dom("optgroup[label='one']").exists({ count: 1 }); 123 | assert.dom("optgroup:first-child option:first-child").hasText("First"); 124 | assert.dom("optgroup[label='two']").exists({ count: 1 }); 125 | assert.dom("optgroup:last-child option:first-child").hasText("Third"); 126 | assert.dom("optgroup:last-child option:first-child").hasValue("3"); 127 | }); 128 | 129 | test("it selects the pre-defined value", async function (assert) { 130 | this.set("value", "2"); 131 | this.set("options", [ 132 | { key: "1", label: 1 }, 133 | { key: "2", label: 2 }, 134 | ]); 135 | 136 | await render( 137 | hbs``, 143 | ); 144 | 145 | assert.dom("select").hasValue(this.options[1].key); 146 | assert.dom("option:first-child").hasProperty("selected", false); 147 | assert.dom("option:last-child").hasProperty("selected", true); 148 | }); 149 | 150 | test("prompt is present and disabled", async function (assert) { 151 | this.set("options", [ 152 | { key: 1, label: 1 }, 153 | { key: 2, label: 2 }, 154 | ]); 155 | 156 | await render( 157 | hbs``, 161 | ); 162 | 163 | assert.dom("option:first-child").hasText("Choose this"); 164 | assert.dom("option:first-child").hasProperty("disabled", true); 165 | }); 166 | 167 | test("prompt is selectable", async function (assert) { 168 | this.set("options", [ 169 | { key: 1, label: 1 }, 170 | { key: 2, label: 2 }, 171 | ]); 172 | 173 | await render( 174 | hbs``, 179 | ); 180 | 181 | assert.dom("option:first-child").hasProperty("disabled", false); 182 | }); 183 | 184 | test("multiselect is working", async function (assert) { 185 | assert.expect(4); 186 | this.set("options", [ 187 | { id: 1, label: "label 1" }, 188 | { id: 2, label: "label 2" }, 189 | { id: 3, label: "label 3" }, 190 | ]); 191 | this.set("update", (values) => { 192 | assert.deepEqual(values, [ 193 | { id: 1, label: "label 1" }, 194 | { id: 3, label: "label 3" }, 195 | ]); 196 | }); 197 | 198 | await render( 199 | hbs``, 204 | ); 205 | 206 | await select("select", ["1", "3"]); 207 | assert.dom("option:first-child").hasProperty("selected", true); 208 | assert.dom("option:nth-child(2)").hasProperty("selected", false); 209 | assert.dom("option:last-child").hasProperty("selected", true); 210 | }); 211 | 212 | test("multiselect is working with plain options", async function (assert) { 213 | assert.expect(4); 214 | this.set("options", ["1", "2", "3"]); 215 | this.set("update", (values) => { 216 | assert.deepEqual(values, ["1", "3"]); 217 | }); 218 | 219 | await render( 220 | hbs``, 225 | ); 226 | 227 | await select("select", ["1", "3"]); 228 | assert.dom("option:first-child").hasProperty("selected", true); 229 | assert.dom("option:nth-child(2)").hasProperty("selected", false); 230 | assert.dom("option:last-child").hasProperty("selected", true); 231 | }); 232 | 233 | test("multiselect works with pre grouped options", async function (assert) { 234 | assert.expect(1); 235 | this.set("update", (values) => { 236 | assert.deepEqual(values, this.options[0].options); 237 | }); 238 | this.set("options", [ 239 | { 240 | groupName: "one", 241 | options: [ 242 | { id: 1, label: "First", type: "group1" }, 243 | { id: 2, label: "Second", type: "group1" }, 244 | ], 245 | }, 246 | { 247 | groupName: "two", 248 | options: [{ id: 3, label: "Third", type: "group2" }], 249 | }, 250 | ]); 251 | 252 | await render( 253 | hbs``, 260 | ); 261 | 262 | await select("select", ["1", "2"]); 263 | }); 264 | 265 | test("multiselect works with pre grouped options and optionsTargetPath", async function (assert) { 266 | assert.expect(1); 267 | this.set("update", (values) => { 268 | assert.deepEqual( 269 | values, 270 | this.options[0].options.map((val) => val.id), 271 | ); 272 | }); 273 | this.set("options", [ 274 | { 275 | groupName: "one", 276 | options: [ 277 | { id: 1, label: "First", type: "group1" }, 278 | { id: 2, label: "Second", type: "group1" }, 279 | ], 280 | }, 281 | { 282 | groupName: "two", 283 | options: [{ id: 3, label: "Third", type: "group2" }], 284 | }, 285 | ]); 286 | 287 | await render( 288 | hbs``, 296 | ); 297 | 298 | await select("select", ["1", "2"]); 299 | }); 300 | 301 | module("uikit", function (hooks) { 302 | setupUikit(hooks); 303 | 304 | test("it renders", async function (assert) { 305 | this.set("options", [ 306 | { 307 | key: "opt1", 308 | label: "Option 1", 309 | }, 310 | { 311 | key: "opt2", 312 | label: "Option 2", 313 | }, 314 | ]); 315 | 316 | await render( 317 | hbs``, 318 | ); 319 | 320 | assert.dom("select").hasClass("uk-select"); 321 | assert.dom("option").exists({ count: 2 }); 322 | }); 323 | }); 324 | 325 | module("bootstrap", function (hooks) { 326 | setupBootstrap(hooks); 327 | 328 | test("it renders", async function (assert) { 329 | this.set("options", [ 330 | { 331 | key: "opt1", 332 | label: "Option 1", 333 | }, 334 | { 335 | key: "opt2", 336 | label: "Option 2", 337 | }, 338 | ]); 339 | 340 | await render( 341 | hbs``, 342 | ); 343 | 344 | assert.dom("select").hasClass("form-control"); 345 | assert.dom("option").exists({ count: 2 }); 346 | }); 347 | }); 348 | 349 | test("prompt is selectable in compination with optionTargetPath, optionValuePath and optionLabelPath", async function (assert) { 350 | assert.expect(3); 351 | const values = [2, undefined]; 352 | this.set("options", [ 353 | { value: 1, text: "one" }, 354 | { value: 2, text: "two" }, 355 | ]); 356 | this.set("update", (value) => { 357 | assert.strictEqual(value, values.shift()); 358 | }); 359 | 360 | await render( 361 | hbs``, 370 | ); 371 | 372 | await select("select", "2"); 373 | await select("select", "option:first-child"); 374 | assert.dom("option:first-child").hasProperty("disabled", false); 375 | }); 376 | }, 377 | ); 378 | -------------------------------------------------------------------------------- /tests/integration/components/validated-form-test.js: -------------------------------------------------------------------------------- 1 | import EmberObject from "@ember/object"; 2 | import { run } from "@ember/runloop"; 3 | import { render, click, blur, fillIn, focus } from "@ember/test-helpers"; 4 | import { validateLength } from "ember-changeset-validations/validators"; 5 | import hbs from "htmlbars-inline-precompile"; 6 | import { module, test } from "qunit"; 7 | import { defer } from "rsvp"; 8 | 9 | import { setupRenderingTest } from "dummy/tests/helpers"; 10 | import UserValidations from "dummy/validations/user"; 11 | 12 | module("Integration | Component | validated form", function (hooks) { 13 | setupRenderingTest(hooks); 14 | 15 | test("it renders simple inputs", async function (assert) { 16 | await render(hbs` 17 | 18 | `); 19 | 20 | assert.dom("form label").hasText("First name"); 21 | assert.dom("form input").hasAttribute("type", "text"); 22 | }); 23 | 24 | test("it renders textareas", async function (assert) { 25 | await render(hbs` 26 | 27 | `); 28 | 29 | assert.dom("form label").hasText("my label"); 30 | assert.dom("form textarea").exists({ count: 1 }); 31 | }); 32 | 33 | test("it renders a radio group", async function (assert) { 34 | this.setTheme("bootstrap"); 35 | 36 | this.set("buttonGroupData", { 37 | options: [ 38 | { key: "1", label: "Option 1" }, 39 | { key: "2", label: "Option 2" }, 40 | { key: "3", label: "Option 3" }, 41 | ], 42 | }); 43 | 44 | await render(hbs` 45 | 51 | `); 52 | 53 | assert.dom('input[type="radio"]').exists({ count: 3 }); 54 | assert.dom("label:nth-of-type(1)").hasText("Options"); 55 | assert.dom("div:nth-of-type(1) > input + label").hasText("Option 1"); 56 | assert.dom("div:nth-of-type(2) > input + label").hasText("Option 2"); 57 | assert.dom("div:nth-of-type(3) > input + label").hasText("Option 3"); 58 | assert.dom("div > input + label").exists({ count: 3 }); 59 | assert.dom('input[type="radio"][value="1"]').exists(); 60 | assert.dom('input[type="radio"][value="2"]').exists(); 61 | assert.dom('input[type="radio"][value="3"]').exists(); 62 | }); 63 | 64 | test("it renders submit buttons", async function (assert) { 65 | this.set("stub", function () {}); 66 | 67 | await render(hbs` 68 | 69 | 70 | `); 71 | 72 | assert.dom("form button").hasAttribute("type", "submit"); 73 | assert.dom("form button").hasText("Save!"); 74 | }); 75 | 76 | test("it renders an always-showing hint", async function (assert) { 77 | await render(hbs` 78 | 79 | `); 80 | 81 | assert.dom("input + div").doesNotExist(); 82 | assert.dom("input + small").exists({ count: 1 }); 83 | assert.dom("input + small").hasText("Not your middle name!"); 84 | 85 | const hintId = this.element.querySelector("input + small").id; 86 | 87 | assert.dom("input").hasAria("describedby", hintId); 88 | }); 89 | 90 | test("does not render a

tag for buttons if no callbacks were passed", async function (assert) { 91 | await render(hbs` 92 | 93 | `); 94 | 95 | assert.dom("form > p").doesNotExist(); 96 | }); 97 | 98 | test("it supports default button labels", async function (assert) { 99 | this.set("stub", function () {}); 100 | 101 | await render(hbs` 102 | 103 | `); 104 | 105 | assert.dom("form button[type=submit]").hasText("Save"); 106 | }); 107 | 108 | test("it performs basic validations on submit", async function (assert) { 109 | this.setTheme("bootstrap"); 110 | 111 | this.set("submit", function () {}); 112 | this.set("UserValidations", UserValidations); 113 | 114 | run(() => { 115 | this.set( 116 | "model", 117 | EmberObject.create({ 118 | firstName: "x", 119 | }), 120 | ); 121 | }); 122 | 123 | await render(hbs` 128 | 129 | 130 | `); 131 | 132 | assert.dom("span.invalid-feedback").doesNotExist(); 133 | assert.dom("input").doesNotHaveAria("invalid"); 134 | 135 | await click("button"); 136 | 137 | assert.dom("input").hasValue("x"); 138 | assert.dom("span.invalid-feedback").exists({ count: 1 }); 139 | assert 140 | .dom("span.invalid-feedback") 141 | .hasText("First name must be between 3 and 40 characters"); 142 | 143 | const errorId = this.element.querySelector("span.invalid-feedback").id; 144 | 145 | assert.dom("input").hasAria("invalid", "true"); 146 | assert.dom("input").hasAria("describedby", errorId); 147 | }); 148 | 149 | test("it shows error message for custom buttons if (and only if) triggerValidations is passed", async function (assert) { 150 | this.setTheme("bootstrap"); 151 | 152 | this.set("UserValidations", UserValidations); 153 | 154 | run(() => { 155 | this.set( 156 | "model", 157 | EmberObject.create({ 158 | firstName: "x", 159 | }), 160 | ); 161 | }); 162 | 163 | this.set("triggerValidations", false); 164 | 165 | await render(hbs` 166 | 167 | 168 | `); 169 | 170 | assert.dom("span.invalid-feedback").doesNotExist(); 171 | await click("button"); 172 | assert.dom("span.invalid-feedback").doesNotExist(); 173 | 174 | this.set("triggerValidations", true); 175 | await click("button"); 176 | 177 | assert.dom("input").hasValue("x"); 178 | assert.dom("span.invalid-feedback").exists({ count: 1 }); 179 | assert 180 | .dom("span.invalid-feedback") 181 | .hasText("First name must be between 3 and 40 characters"); 182 | }); 183 | 184 | test("it calls on-invalid-submit after submit if changeset is invalid", async function (assert) { 185 | let invalidSubmitCalled; 186 | this.set("invalidSubmit", function () { 187 | invalidSubmitCalled = true; 188 | }); 189 | this.set("UserValidations", UserValidations); 190 | 191 | run(() => { 192 | this.set( 193 | "model", 194 | EmberObject.create({ 195 | firstName: "x", 196 | }), 197 | ); 198 | }); 199 | 200 | await render(hbs` 205 | 206 | 207 | `); 208 | 209 | await click("button"); 210 | 211 | assert.true(invalidSubmitCalled); 212 | }); 213 | 214 | test("it does not call on-invalid-submit after submit if changeset is valid", async function (assert) { 215 | let invalidSubmitCalled; 216 | let submitCalled; 217 | this.set("submit", function () { 218 | submitCalled = true; 219 | }); 220 | this.set("invalidSubmit", function () { 221 | invalidSubmitCalled = true; 222 | }); 223 | 224 | run(() => { 225 | this.set("model", EmberObject.create({})); 226 | }); 227 | 228 | await render(hbs` 234 | 235 | 236 | `); 237 | 238 | await click("button"); 239 | 240 | assert.notOk(invalidSubmitCalled); 241 | assert.true(submitCalled); 242 | }); 243 | 244 | test("it performs validation and calls onInvalidClick function on custom buttons", async function (assert) { 245 | assert.expect(5); 246 | 247 | this.set("onClick", function () { 248 | assert.step("onClick"); 249 | }); 250 | this.set("onInvalidClick", function (model) { 251 | assert.step("onInvalidClick"); 252 | assert.strictEqual(model.firstName, "x"); 253 | }); 254 | this.set("SimpleValidations", { 255 | firstName: [validateLength({ min: 3, max: 40 })], 256 | }); 257 | 258 | run(() => { 259 | this.set( 260 | "model", 261 | EmberObject.create({ 262 | firstName: "x", 263 | }), 264 | ); 265 | }); 266 | 267 | await render(hbs` 268 | 269 | 273 | `); 274 | 275 | await click("button"); 276 | assert.verifySteps(["onInvalidClick"]); 277 | await fillIn("input[name=firstName]", "Some name"); 278 | await click("button"); 279 | assert.verifySteps(["onClick"]); 280 | }); 281 | 282 | test("it performs basic validations on focus out", async function (assert) { 283 | this.setTheme("bootstrap"); 284 | 285 | this.set("submit", function () {}); 286 | this.set("UserValidations", UserValidations); 287 | 288 | run(() => { 289 | this.set("model", EmberObject.create({})); 290 | }); 291 | 292 | await render(hbs` 297 | 298 | 299 | `); 300 | 301 | assert.dom("input + div").doesNotExist(); 302 | 303 | await focus("input"); 304 | await blur("input"); 305 | 306 | assert.dom("span.invalid-feedback").exists({ count: 1 }); 307 | assert.dom("span.invalid-feedback").hasText("First name can't be blank"); 308 | }); 309 | 310 | test("it skips basic validations on focus out with validateBeforeSubmit=false set on the form", async function (assert) { 311 | this.setTheme("bootstrap"); 312 | 313 | this.set("submit", function () {}); 314 | this.set("UserValidations", UserValidations); 315 | 316 | run(() => { 317 | this.set("model", EmberObject.create({})); 318 | }); 319 | 320 | await render(hbs` 326 | 327 | 328 | `); 329 | 330 | assert.dom("span.invalid-feedback").doesNotExist(); 331 | 332 | await focus("input"); 333 | await blur("input"); 334 | 335 | assert.dom("span.invalid-feedback").doesNotExist(); 336 | 337 | await click("button"); 338 | 339 | assert.dom("span.invalid-feedback").exists({ count: 1 }); 340 | }); 341 | 342 | test("it skips basic validations on focus out with validateBeforeSubmit=false set on the input", async function (assert) { 343 | this.set("submit", function () {}); 344 | this.set("UserValidations", UserValidations); 345 | 346 | run(() => { 347 | this.set("model", EmberObject.create({})); 348 | }); 349 | 350 | await render(hbs` 355 | 360 | `); 361 | 362 | assert.dom("input + div").doesNotExist(); 363 | 364 | await focus("input"); 365 | await blur("input"); 366 | 367 | assert.dom("input + div").doesNotExist(); 368 | }); 369 | 370 | test("on-submit can be an action returning a promise", async function (assert) { 371 | const deferred = defer(); 372 | 373 | this.set("submit", () => deferred.promise); 374 | 375 | run(() => { 376 | this.set("model", EmberObject.create({})); 377 | }); 378 | 379 | await render(hbs` 384 | 385 | `); 386 | 387 | assert.dom("button").doesNotHaveClass("loading"); 388 | 389 | await click("button"); 390 | 391 | assert.dom("button").hasClass("loading"); 392 | 393 | run(() => deferred.resolve()); 394 | 395 | assert.dom("button").doesNotHaveClass("loading"); 396 | }); 397 | 398 | test("on-submit can be an action returning a non-promise", async function (assert) { 399 | this.set("submit", () => undefined); 400 | 401 | run(() => { 402 | this.set("model", EmberObject.create({})); 403 | }); 404 | 405 | await render(hbs` 410 | 411 | `); 412 | 413 | assert.dom("button").doesNotHaveClass("loading"); 414 | 415 | await click("button"); 416 | 417 | assert.dom("button").doesNotHaveClass("loading"); 418 | }); 419 | 420 | test("it yields the loading state", async function (assert) { 421 | const deferred = defer(); 422 | 423 | this.set("submit", () => deferred.promise); 424 | 425 | run(() => { 426 | this.set("model", EmberObject.create({})); 427 | }); 428 | 429 | await render(hbs` 434 | {{#if f.loading}} 435 | loading... 436 | {{/if}} 437 | 438 | `); 439 | assert.dom("span.loading").doesNotExist(); 440 | 441 | await click("button"); 442 | 443 | assert.dom("span.loading").exists(); 444 | 445 | run(() => deferred.resolve()); 446 | 447 | assert.dom("span.loading").doesNotExist(); 448 | }); 449 | 450 | test("it handles being removed from the DOM during sync submit", async function (assert) { 451 | this.set("show", true); 452 | 453 | this.set("submit", () => { 454 | this.set("show", false); 455 | }); 456 | 457 | run(() => { 458 | this.set("model", EmberObject.create({})); 459 | }); 460 | 461 | await render(hbs`{{#if this.show}} 462 | 467 | {{#if f.loading}} 468 | loading... 469 | {{/if}} 470 | 471 | 472 | {{/if}}`); 473 | 474 | await click("button"); 475 | assert.ok(true); 476 | }); 477 | 478 | test("it handles being removed from the DOM during async submit", async function (assert) { 479 | this.set("show", true); 480 | const deferred = defer(); 481 | 482 | this.set("submit", () => { 483 | return deferred.promise.then(() => { 484 | this.set("show", false); 485 | }); 486 | }); 487 | 488 | run(() => { 489 | this.set("model", EmberObject.create({})); 490 | }); 491 | 492 | await render(hbs`{{#if this.show}} 493 | 498 | {{#if f.loading}} 499 | loading... 500 | {{/if}} 501 | 502 | 503 | {{/if}}`); 504 | 505 | await click("button"); 506 | run(() => deferred.resolve()); 507 | assert.ok(true); 508 | }); 509 | 510 | test("it binds the autocomplete attribute", async function (assert) { 511 | await render(hbs``); 512 | 513 | assert.dom("form").hasAttribute("autocomplete", "off"); 514 | }); 515 | }); 516 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [7.0.1](https://github.com/adfinis/ember-validated-form/compare/v7.0.0...v7.0.1) (2024-03-18) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * **deps:** update dependencies ([c2d187b](https://github.com/adfinis/ember-validated-form/commit/c2d187bbbf8652f0bc30637a9b5b5f5c46e44296)) 7 | 8 | # [7.0.0](https://github.com/adfinis/ember-validated-form/compare/v6.2.0...v7.0.0) (2023-11-21) 9 | 10 | 11 | ### chore 12 | 13 | * **deps:** update ember and dependencies ([fc26b0b](https://github.com/adfinis/ember-validated-form/commit/fc26b0be302c24dafccad37a53dc7dd2bcc59bc9)) 14 | 15 | 16 | ### Features 17 | 18 | * allow html attributes to be passed to wrapping form element ([b29f4d8](https://github.com/adfinis/ember-validated-form/commit/b29f4d88133aa58a9613aa9bf9f1f6a2761a7974)) 19 | 20 | 21 | ### BREAKING CHANGES 22 | 23 | * **deps:** Drop support for Ember v3. 24 | 25 | # [6.2.0](https://github.com/adfinis/ember-validated-form/compare/v6.1.2...v6.2.0) (2023-02-23) 26 | 27 | 28 | ### Bug Fixes 29 | 30 | * scroll error into view with nested key names ([#904](https://github.com/adfinis/ember-validated-form/issues/904)) ([5c192a5](https://github.com/adfinis/ember-validated-form/commit/5c192a5b00abf05d80f36d1fad8b67a79a36e43c)) 31 | 32 | 33 | ### Features 34 | 35 | * **a11y:** add aria invalid and describedby attributes on validation ([6e16a51](https://github.com/adfinis/ember-validated-form/commit/6e16a5122f134ca991205c03c0dd8628288e4b08)), closes [#48](https://github.com/adfinis/ember-validated-form/issues/48) 36 | 37 | ## [6.1.2](https://github.com/adfinis/ember-validated-form/compare/v6.1.1...v6.1.2) (2022-09-20) 38 | 39 | 40 | ### Bug Fixes 41 | 42 | * **label:** only show requiredness indicator if a label is given ([72c212d](https://github.com/adfinis/ember-validated-form/commit/72c212dc036c643dca43e1c22a7ec43384f70ab5)) 43 | 44 | ## [6.1.1](https://github.com/adfinis/ember-validated-form/compare/v6.1.0...v6.1.1) (2022-09-20) 45 | 46 | 47 | ### Bug Fixes 48 | 49 | * **bootstrap:** make sure error feedback is always visible ([1722144](https://github.com/adfinis/ember-validated-form/commit/17221448567a4fb6dc53c39c3f42eb8eb66bdc3d)) 50 | * **checkbox-group:** fix multiple selection in checkbox group ([6bb902b](https://github.com/adfinis/ember-validated-form/commit/6bb902b0577c68671f09f9cda65b5f16ac1b8fcb)) 51 | 52 | # [6.1.0](https://github.com/adfinis/ember-validated-form/compare/v6.0.2...v6.1.0) (2022-09-16) 53 | 54 | 55 | ### Features 56 | 57 | * **input:** allow custom date component to be configured ([#861](https://github.com/adfinis/ember-validated-form/issues/861)) ([33bbafa](https://github.com/adfinis/ember-validated-form/commit/33bbafa4ac6a24d332ab4b1548abc4f2daef541f)) 58 | 59 | ## [6.0.2](https://github.com/adfinis/ember-validated-form/compare/v6.0.1...v6.0.2) (2022-09-09) 60 | 61 | 62 | ### Bug Fixes 63 | 64 | * **deps:** remove unnecessary dependency to tracked-toolbox ([605c001](https://github.com/adfinis/ember-validated-form/commit/605c0018c44dcfd99685c35b3ba656cfab420a35)) 65 | 66 | ## [6.0.1](https://github.com/adfinis/ember-validated-form/compare/v6.0.0...v6.0.1) (2022-09-08) 67 | 68 | 69 | ### Bug Fixes 70 | 71 | * **deps:** add missing dependency tracked-toolbox ([5b0f1a7](https://github.com/adfinis/ember-validated-form/commit/5b0f1a74dd33b20bcdb6bc09d55b7dc1426f5642)) 72 | 73 | # [6.0.0](https://github.com/adfinis/ember-validated-form/compare/v5.3.1...v6.0.0) (2022-09-08) 74 | 75 | 76 | ### chore 77 | 78 | * **deps:** update ember and drop node 12 support ([3f3fb67](https://github.com/adfinis/ember-validated-form/commit/3f3fb67e269b8e8e30f5939ccbbc58a01a2a6aa0)) 79 | * **select:** remove deprecated option includeBlank on selects ([3ef60b2](https://github.com/adfinis/ember-validated-form/commit/3ef60b21c1bc10259ab3865aacb5b3e2fdeda6bc)) 80 | 81 | 82 | ### Features 83 | 84 | * support embroider builds ([b0b0bde](https://github.com/adfinis/ember-validated-form/commit/b0b0bdea32a035fa499a9b0bae7ed245b4dd66f7)) 85 | 86 | 87 | ### BREAKING CHANGES 88 | 89 | * **select:** Remove deprecated option `includeBlank` on select 90 | inputs. 91 | * This changes the global configuration for features, 92 | themes and default components completely. For instructions on how to 93 | migrate, check the migration to v6 guide. 94 | * **deps:** Drop support for Node v12 and Ember LTS 3.24 95 | 96 | ## [5.3.1](https://github.com/adfinis/ember-validated-form/compare/v5.3.0...v5.3.1) (2022-09-02) 97 | 98 | ### Bug Fixes 99 | 100 | - **select:** fix promptIsSelectable in combination with targetPath ([#853](https://github.com/adfinis/ember-validated-form/issues/853)) ([526d81f](https://github.com/adfinis/ember-validated-form/commit/526d81f25eb47f938fa2fa031d5f5e710a0386bf)) 101 | 102 | # [5.3.0](https://github.com/adfinis/ember-validated-form/compare/v5.2.2...v5.3.0) (2022-02-22) 103 | 104 | ### Bug Fixes 105 | 106 | - addon docs root url ([7d8151f](https://github.com/adfinis/ember-validated-form/commit/7d8151fa9d854cdf9595f27347fa57652e58ef10)) 107 | - **blueprint:** add ember-truth-helpers to default blueprint ([a401589](https://github.com/adfinis/ember-validated-form/commit/a401589dab6d013f5d659865927919346655295d)) 108 | - **deps:** move required deps to dependencies instead of devDependencies ([46629ce](https://github.com/adfinis/ember-validated-form/commit/46629ceb48210bbc58898c299a9388ce7fa816ec)) 109 | - new link to docs ([c1073e5](https://github.com/adfinis/ember-validated-form/commit/c1073e55e3a916ed393e33bcf4a045124c320967)) 110 | - **select:** pass prompt argument down to input component ([64a1377](https://github.com/adfinis/ember-validated-form/commit/64a1377d64618fa880b332f8a693e1c7f1a321f7)) 111 | 112 | ### Features 113 | 114 | - pass attributes to input components ([dc0417d](https://github.com/adfinis/ember-validated-form/commit/dc0417d125fe4c20cb59b76531ae4f7391258259)) 115 | 116 | ## [5.2.2](https://github.com/adfinis/ember-validated-form/compare/v5.2.1...v5.2.2) (2022-02-10) 117 | 118 | ### Bug Fixes 119 | 120 | - select support for plain options ([#747](https://github.com/adfinis/ember-validated-form/issues/747)) ([a58e26d](https://github.com/adfinis/ember-validated-form/commit/a58e26ddcd46ec5328c3bb5351bafc7781eacdbd)) 121 | 122 | ## [5.2.1](https://github.com/adfinis/ember-validated-form/compare/v5.2.0...v5.2.1) (2022-02-09) 123 | 124 | ### Bug Fixes 125 | 126 | - respect scrollErrorIntoView for validated buttons as well ([#743](https://github.com/adfinis/ember-validated-form/issues/743)) ([fd6be2a](https://github.com/adfinis/ember-validated-form/commit/fd6be2a49c5f947bfcf5eb3f7ab61a23ac00064a)) 127 | 128 | # [5.2.0](https://github.com/adfinis/ember-validated-form/compare/v5.1.1...v5.2.0) (2022-02-03) 129 | 130 | ### Features 131 | 132 | - scroll first invalid element into view ([#733](https://github.com/adfinis/ember-validated-form/issues/733)) ([ae7c8b2](https://github.com/adfinis/ember-validated-form/commit/ae7c8b2b160307646adf90cd09a091effa549238)) 133 | - **validated-button:** add `triggerValidations` flag ([#721](https://github.com/adfinis/ember-validated-form/issues/721)) ([765f5f4](https://github.com/adfinis/ember-validated-form/commit/765f5f40c9d2e5ccfca7129fb701ff3bb0ed661e)) 134 | 135 | # [5.0.0](https://github.com/adfinis/ember-validated-form/compare/v4.1.0...v5.0.0) (2021-10-08) 136 | 137 | ### chore 138 | 139 | - **deps:** update ember and other dependencies ([41e099c](https://github.com/adfinis/ember-validated-form/commit/41e099c4da82135c562493e5b2a4f9420dca73c6)) 140 | - **ember:** remove support for ember 3.20 ([0cfebfc](https://github.com/adfinis/ember-validated-form/commit/0cfebfcc5792a1df52093a972878af1617ec8100)) 141 | 142 | ### Features 143 | 144 | - refactor all components to glimmer and use native classes ([cee7373](https://github.com/adfinis/ember-validated-form/commit/cee7373a3c0783a02fe00b5e510c41ba604403c2)) 145 | 146 | ### BREAKING CHANGES 147 | 148 | - **ember:** Remove support for ember LTS 3.20 since that version 149 | has a bug with autotracking. 150 | - **deps:** Require `ember-auto-import` v2+ 151 | - While the public API won't change, there is a huge 152 | chance that this will break implementations if someone's extending the 153 | components of this addon. Components that do need to be refactored to 154 | glimmer. 155 | 156 | # [4.1.0](https://github.com/adfinis/ember-validated-form/compare/v4.0.1...v4.1.0) (2021-09-30) 157 | 158 | ### Bug Fixes 159 | 160 | - **deps:** [security] bump handlebars from 4.7.6 to 4.7.7 ([#588](https://github.com/adfinis/ember-validated-form/issues/588)) ([d167207](https://github.com/adfinis/ember-validated-form/commit/d167207ee059bd9b968a08fb61f18f43dadab0ab)) 161 | - **deps:** [security] bump striptags from 3.1.1 to 3.2.0 ([#637](https://github.com/adfinis/ember-validated-form/issues/637)) ([3632f52](https://github.com/adfinis/ember-validated-form/commit/3632f52e7fa1fc6f0e17dd3365a74a80ceb92833)) 162 | - **deps:** [security] bump trim-newlines from 3.0.0 to 3.0.1 ([#634](https://github.com/adfinis/ember-validated-form/issues/634)) ([10e3974](https://github.com/adfinis/ember-validated-form/commit/10e397452b30a3af11a299c5bcdafd703d9b0c18)) 163 | - **deps:** [security] bump ws from 6.2.1 to 6.2.2 ([#624](https://github.com/adfinis/ember-validated-form/issues/624)) ([910ec64](https://github.com/adfinis/ember-validated-form/commit/910ec64b2f84562fd77a8be14094fd2f326d60b6)) 164 | - call on-update hook correctly ([#641](https://github.com/adfinis/ember-validated-form/issues/641)) ([b8688b6](https://github.com/adfinis/ember-validated-form/commit/b8688b6d9dedbd9096d34c3b236bf59efb045556)) 165 | 166 | ### Features 167 | 168 | - checkbox groups ([#640](https://github.com/adfinis/ember-validated-form/issues/640)) ([9099ce8](https://github.com/adfinis/ember-validated-form/commit/9099ce81bbedc53c961653dca59c555d96ee9128)) 169 | 170 | ## [4.0.1](https://github.com/adfinis/ember-validated-form/compare/v4.0.0...v4.0.1) (2021-05-21) 171 | 172 | ### Bug Fixes 173 | 174 | - **validated-input:** use changeset.set if available to preserve state tracking on nested objects ([#609](https://github.com/adfinis/ember-validated-form/issues/609)) ([d3b92ee](https://github.com/adfinis/ember-validated-form/commit/d3b92ee5dfb7e0a6f4fbdb1899d9be34b67d1722)) 175 | 176 | # [4.0.0](https://github.com/adfinis/ember-validated-form/compare/v3.0.3...v4.0.0) (2021-05-19) 177 | 178 | ### Bug Fixes 179 | 180 | - **validated-input:** rewrite to glimmer and support nested changesets ([#581](https://github.com/adfinis/ember-validated-form/issues/581)) ([2f3e7c5](https://github.com/adfinis/ember-validated-form/commit/2f3e7c5c9e13ad39ecba9358305cfcc4bac8f6b8)) 181 | 182 | ### BREAKING CHANGES 183 | 184 | - **validated-input:** This drops support for Ember LTS 3.16 and `ember-changeset` < 3.0.0 and `ember-changeset-validations` < 3.0.0 185 | 186 | - refactor(validated-input): refactor dynamic component call to angle-brackets 187 | 188 | - chore(\*): drop node v10 support 189 | - **validated-input:** drop node v10 support since v10 has reached EOL 190 | 191 | - fix(themed-component): convert array to string befor using in key path 192 | 193 | ## [3.0.3](https://github.com/adfinis/ember-validated-form/compare/v3.0.2...v3.0.3) (2021-04-15) 194 | 195 | ### Bug Fixes 196 | 197 | - **deps:** [security] bump elliptic from 6.5.3 to 6.5.4 ([#538](https://github.com/adfinis/ember-validated-form/issues/538)) ([b36030c](https://github.com/adfinis/ember-validated-form/commit/b36030cd289485154fbaaefe914535cad7639043)) 198 | - **deps:** [security] bump ini from 1.3.5 to 1.3.8 ([#450](https://github.com/adfinis/ember-validated-form/issues/450)) ([97870d1](https://github.com/adfinis/ember-validated-form/commit/97870d152b12a273328fae257fb7399a067d7f9c)) 199 | - **deps:** [security] bump socket.io from 2.3.0 to 2.4.1 ([#474](https://github.com/adfinis/ember-validated-form/issues/474)) ([fe46f19](https://github.com/adfinis/ember-validated-form/commit/fe46f197439ff85170c92b6236f73c34f459d01f)) 200 | - **deps:** [security] bump y18n from 3.2.1 to 3.2.2 ([#560](https://github.com/adfinis/ember-validated-form/issues/560)) ([727144e](https://github.com/adfinis/ember-validated-form/commit/727144edc1920d323aee882240de695ca5f0e228)) 201 | - remove unused modules which lead to invalid imports ([#464](https://github.com/adfinis/ember-validated-form/issues/464)) ([d0405bf](https://github.com/adfinis/ember-validated-form/commit/d0405bf8733119394c953f73155349f65e2e5a88)) 202 | - **deps:** bump ember-auto-import from 1.10.1 to 1.11.2 ([#557](https://github.com/adfinis/ember-validated-form/issues/557)) ([f01f882](https://github.com/adfinis/ember-validated-form/commit/f01f8826fe6cddfa1d5776960fb773973a25545d)) 203 | - **deps:** bump ember-auto-import from 1.7.0 to 1.10.1 ([#456](https://github.com/adfinis/ember-validated-form/issues/456)) ([d4940d7](https://github.com/adfinis/ember-validated-form/commit/d4940d7cf6a367184dccd0e6677f3bdae6afdee8)) 204 | - **deps:** bump ember-cli-babel from 7.23.0 to 7.23.1 ([#473](https://github.com/adfinis/ember-validated-form/issues/473)) ([df7cec0](https://github.com/adfinis/ember-validated-form/commit/df7cec06c35b0665df6e177f495360a14572cb73)) 205 | - **deps:** bump ember-cli-babel from 7.23.1 to 7.26.3 ([#558](https://github.com/adfinis/ember-validated-form/issues/558)) ([6b0ee99](https://github.com/adfinis/ember-validated-form/commit/6b0ee99faf936d329378959c63303a18fd855912)) 206 | - **deps:** bump ember-cli-htmlbars from 5.6.4 to 5.7.1 ([#549](https://github.com/adfinis/ember-validated-form/issues/549)) ([e00e417](https://github.com/adfinis/ember-validated-form/commit/e00e41791039ce1b410f442900b8f079a42cae4d)) 207 | - **deps:** bump ember-one-way-select from 4.0.0 to 4.0.1 ([#565](https://github.com/adfinis/ember-validated-form/issues/565)) ([0564346](https://github.com/adfinis/ember-validated-form/commit/05643467b6edffc6e08eb067f77acdd79161ae5b)) 208 | - **deps:** bump uuid from 8.3.1 to 8.3.2 ([#439](https://github.com/adfinis/ember-validated-form/issues/439)) ([d72f013](https://github.com/adfinis/ember-validated-form/commit/d72f0130f5b34cda2bda3b82da5204403d026541)) 209 | 210 | ## [3.0.2](https://github.com/adfinis/ember-validated-form/compare/v3.0.1...v3.0.2) (2020-11-20) 211 | 212 | ### Bug Fixes 213 | 214 | - **intl:** remove unused translation that caused warnings in apps ([f20f233](https://github.com/adfinis/ember-validated-form/commit/f20f2332cbf8cf70711f292864e7a9dd2df8b995)) 215 | 216 | ## [3.0.1](https://github.com/adfinis/ember-validated-form/compare/v3.0.0...v3.0.1) (2020-11-18) 217 | 218 | ### Bug Fixes 219 | 220 | - **deps:** update ember-changeset and validations to v3+ ([a9d83e2](https://github.com/adfinis/ember-validated-form/commit/a9d83e208ad2599b0bd3847bc170179294d479ad)) 221 | 222 | # [3.0.0](https://github.com/adfinis/ember-validated-form/compare/v2.0.0...v3.0.0) (2020-11-06) 223 | 224 | - chore!: update dependencies (#392) ([2664399](https://github.com/adfinis/ember-validated-form/commit/266439966050dee4ec6033f24828c84f5e0543b8)), closes [#392](https://github.com/adfinis/ember-validated-form/issues/392) 225 | 226 | ### Bug Fixes 227 | 228 | - **deps:** bump ember-auto-import from 1.6.0 to 1.7.0 ([#394](https://github.com/adfinis/ember-validated-form/issues/394)) ([9d464e4](https://github.com/adfinis/ember-validated-form/commit/9d464e4b5e83f19f3bd46897ef56a3caaaeb7153)) 229 | - **select:** pass promptIsSelectable to select ([e739b4f](https://github.com/adfinis/ember-validated-form/commit/e739b4fea27165e97ae0130e5d330990bdf501ee)) 230 | 231 | ### BREAKING CHANGES 232 | 233 | - This drops support for Ember LTS 3.8 and 3.12 234 | 235 | # Change Log 236 | 237 | All notable changes to this project will be documented in this file. 238 | 239 | The format is based on [Keep a Changelog](https://keepachangelog.com/) 240 | and this project adheres to [Semantic Versioning](https://semver.org/). 241 | 242 | ## [1.4.2] 243 | 244 | ### Changed 245 | 246 | - Bump version because of a wrong npm publish 247 | 248 | ## [1.4.1] 249 | 250 | ### Changed 251 | 252 | - Fix changing of a changeset property from outside of the form (#118) 253 | 254 | ## [1.4.0] 255 | 256 | ### Changed 257 | 258 | - Update ember to version 3.1 259 | - Fix mixed content in docs (#107 / #108, credits to @sliverc) 260 | - Fix wrong bootstrap class in documentation (#114, credits to @kimroen) 261 | 262 | ### Added 263 | 264 | - `on-invalid-submit` action gets called on submitting an invalid form (#111, credits to @omairvaiyani) 265 | - `inputId` is yielded from the `validated-input` component (#115, credits to @kimroen) 266 | 267 | ## [1.3.1] 268 | 269 | ### Changed 270 | 271 | - Addon docs with [`ember-cli-addon-docs`](https://github.com/ember-learn/ember-cli-addon-docs) 272 | 273 | ## [1.3.0] 274 | 275 | ### Changed 276 | 277 | - Bootstrap 4 support (#100, credits to @anehx) 278 | - Add valid and invalid class to input component (#100, credits to @anehx) 279 | - Only set valid class if value is dirty (#100, credits to @anehx) 280 | 281 | ## [1.2.0] 282 | 283 | ### Changed 284 | 285 | - Add support for custom label components (#95, credits to @przywartya) 286 | - Allow setting autocomplete attribute on validated-form and validated-input (#97, credits to @makepanic) 287 | 288 | ## [1.1.0] 289 | 290 | ### Changed 291 | 292 | - Update ember-cli to 3.0.0, update dependencies 293 | 294 | ## [1.0.1] 295 | 296 | ### Changed 297 | 298 | - fix: use radio value to expose option key (#91, credits to @makepanic) 299 | 300 | ## [1.0.0] 301 | 302 | ### Changed 303 | 304 | - Install `ember-changeset` and `ember-changeset-validations` from the blueprint (#86, credits to @bendemboski) 305 | - Update dependencies (#87, credits to @bendemboski) 306 | - Remove deprecated task support (#88, credits to @bendemboski) 307 | 308 | The 1.0.0 release removes ember-changeset and ember-changeset-validations as dependencies because this addon is built to make them very easy to use, but doesn't require them. So if you are using either or both of them in your application code but do not already have them in your package.json, you should run 309 | 310 | ```bash 311 | ember install ember-changeset 312 | ember install ember-changeset-validations 313 | ``` 314 | 315 | This release also removes the previously deprecated API for passing ember-concourrency tasks to `on-submit`. Please see section [0.6.2] for migration instructions. 316 | 317 | ## [0.7.1] 318 | 319 | ### Changed 320 | 321 | - Fix for removal from DOM during submit (#85, credits to @bendemboski) 322 | 323 | ## [0.7.0] 324 | 325 | ### Changed 326 | 327 | - Wrap radio, checkbox in span to allow custom styles (#82, credits to @makepanic) 328 | 329 | ## [0.6.4] 330 | 331 | ### Changed 332 | 333 | - Update ember truth helpers (#79, credits to @bendemboski) 334 | - Update ember-cli to 2.18.0 (#80) 335 | 336 | ## [0.6.3] 337 | 338 | ### Changed 339 | 340 | - Add disabled flag on checkbox instance (#77, credits to @toumbask) 341 | 342 | ## [0.6.2] 343 | 344 | ### Changed 345 | 346 | - Yield loading state, change `on-submit` to promise semantics (#75, credits to @bendemboski) 347 | 348 | This release deprecates passing an ember-concurrency task directly to `on-submit`: 349 | 350 | ```Handlebars 351 | // deprecated: 352 | {{#validated-form on-submit = myTask (...)}} 353 | ``` 354 | 355 | Instead, `on-submit` accepts a promise - which is returned by wrapping the task in `perform`: 356 | 357 | ```Handlebars 358 | {{#validated-form on-submit = (perform myTask) (...)}} 359 | ``` 360 | 361 | ## [0.6.1] 362 | 363 | ### Added 364 | 365 | - Add more input attributes (#71, credits to @bendemboski) 366 | - Add validateBeforeSubmit option (#70, credits to @bendemboski) 367 | 368 | ## [0.6.0] 369 | 370 | ### Changed 371 | 372 | - Use yarn instead of npm (#62) 373 | - Update dependencies (#62) 374 | - Remove automatic disabling of submit button while task is running (#63) 375 | 376 | To restore the old behaviour, all you have to do is pass the `isRunning` state as `disabled` property to the submit button: 377 | 378 | ```Handlebars 379 | {{#validated-form 380 | ... 381 | on-submit = myTask 382 | as |f|}} 383 | {{f.submit label="Test" disabled=myTask.isRunning}} 384 | {{/validated-form}} 385 | ``` 386 | 387 | ### Added 388 | 389 | - Add loading class to button if task is running (#63) 390 | - Support block style usage of submit button (#61) 391 | 392 | ### Removed 393 | 394 | - Useless class name binding on button (#65) 395 | - Dependency to ember-data (#62) 396 | - Various unused dependencies (#62) 397 | 398 | ## [0.5.4] 399 | 400 | ### Changed 401 | 402 | - Input, Textarea: Use native input tag instead of one-way-input (#60) 403 | - Dummy app: load bootstrap from CDN (#59) 404 | 405 | ## [0.5.3] 406 | 407 | ### Changed 408 | 409 | - Fix bug that caused ID collisions when multiple forms on the same page use inputs with 410 | the same name (#55, credits to @anehx) 411 | 412 | ## [0.5.2] 413 | 414 | ### Changed 415 | 416 | - Update ember-cli to v2.5.1 (#53) 417 | 418 | ## [0.5.1] 419 | 420 | ### Changed 421 | 422 | - Add proper `id` attributes to select and simple form components (#51) 423 | 424 | ## [0.5.0] 425 | 426 | ### Changed 427 | 428 | - Separate classes for field hints and validation messages (#42, credits to @jacob-financit). If you've 429 | been using the (previously undocumented) option `help` on input fields, you'll have to rename them to `hint`. 430 | 431 | ## [0.4.2] 432 | 433 | ### Added 434 | 435 | - Better documentation for custom component integration (#46) 436 | 437 | ### Changed 438 | 439 | - Fix bug that causes form to break for select fields without option `promptIsSelectable` (#45) 440 | 441 | ## [0.4.1] 442 | 443 | ### Added 444 | 445 | - Support selectable prompts from the select dropdown (#40, credits to @steverhoades) 446 | 447 | ### Changed 448 | 449 | - Update dependencies (#33, #34, credits to @okachynskyy) 450 | - Move addon from pods to default structure (#32, credits to @okachynskyy) 451 | 452 | ## [0.4.0] 453 | 454 | ### Added 455 | 456 | - Support block form usage of radioGroups (#28, credits to @jacob-financeit) 457 | 458 | ### Changed 459 | 460 | - remove hardcoded `radio` class for radio button groups. If you're using bootstrap, you 461 | might have to add `radio: 'radio'` to the CSS config. 462 | 463 | ## [0.3.1] 464 | 465 | ### Added 466 | 467 | - More CSS configuration options and docs on how to integrate semantic UI (#26) 468 | 469 | ## [0.3.0] 470 | 471 | ### Changed 472 | 473 | - Removed automatic inference if field is required (which appended "\*" to labels) because it 474 | was incorrect. (#27, credits to @andreabettich) Now, fields have to be marked as required explicitly: 475 | 476 | ```Handlebars 477 | {{f.input label="First name" name="firstName" required=true}} 478 | ``` 479 | 480 | ## [0.2.2] 481 | 482 | - Upgrade to ember 2.12.0 (#24, credits to @sproj) 483 | 484 | ## [0.2.1] 485 | 486 | ### Added 487 | 488 | - Override initial value of input field using `value` attribute (#22, credits to @kaldras) 489 | - Document `on-update` property for custom update functions of input elements, 490 | and add `changeset` argument to its signature 491 | 492 | ## [0.2.0] 493 | 494 | ### Changed 495 | 496 | - yield `submit` button instead of automatically rendering it, removed `cancel` button. Migration is simple: 497 | 498 | Before: 499 | 500 | ```Handlebars 501 | {{#validated-form 502 | model = (changeset model UserValidations) 503 | on-submit = (action "submit") 504 | on-cancel = (action "cancel") 505 | submit-label = 'Save' as |f|}} 506 | {{!-- form content --}} 507 | {{/validated-form}} 508 | ``` 509 | 510 | After: 511 | 512 | ```Handlebars 513 | {{#validated-form 514 | model = (changeset model UserValidations) 515 | on-submit = (action "submit") 516 | as |f|}} 517 | {{!-- form content --}} 518 | {{f.submit label='Save'}} 519 | 520 | {{/validated-form}} 521 | ``` 522 | 523 | ## [0.1.11] - 2017-03-23 524 | 525 | ### Added 526 | 527 | - Checkbox support (#5, credits to @psteininger) 528 | 529 | ## [0.1.10] - 2017-03-23 530 | 531 | ### Added 532 | 533 | - Easy integration of custom components (#17, credits to @feanor07) 534 | --------------------------------------------------------------------------------