diff --git a/.eslintignore b/.eslintignore index 9221655..d474a40 100644 --- a/.eslintignore +++ b/.eslintignore @@ -13,9 +13,13 @@ # misc /coverage/ !.* +.*/ .eslintcache # ember-try /.node_modules.ember-try/ /bower.json.ember-try +/npm-shrinkwrap.json.ember-try /package.json.ember-try +/package-lock.json.ember-try +/yarn.lock.ember-try diff --git a/.eslintrc.js b/.eslintrc.js index 2504063..e661283 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,54 +1,59 @@ 'use strict'; module.exports = { - root: true, - parser: 'babel-eslint', - parserOptions: { - ecmaVersion: 2018, - sourceType: 'module', - ecmaFeatures: { - legacyDecorators: true, + root: true, + parser: '@typescript-eslint/parser', + plugins: ['ember', '@typescript-eslint', 'prettier'], + extends: [ + 'eslint:recommended', + 'plugin:ember/recommended', + 'plugin:@typescript-eslint/recommended', + 'prettier', + 'plugin:prettier/recommended' + ], + env: { + browser: true }, - }, - plugins: ['ember'], - extends: [ - 'eslint:recommended', - 'plugin:ember/recommended', - 'plugin:prettier/recommended', - ], - env: { - browser: true, - }, - rules: {}, - overrides: [ - // node files - { - files: [ - '.eslintrc.js', - '.prettierrc.js', - '.template-lintrc.js', - 'ember-cli-build.js', - 'index.js', - 'testem.js', - 'blueprints/*/index.js', - 'config/**/*.js', - 'tests/dummy/config/**/*.js', - ], - excludedFiles: [ - 'addon/**', - 'addon-test-support/**', - 'app/**', - 'tests/dummy/app/**', - ], - parserOptions: { - sourceType: 'script', - }, - env: { - browser: false, - node: true, - }, - plugins: ['node'], - extends: ['plugin:node/recommended'], + rules: { + 'ember/no-jquery': 'error', + 'ember/no-classic-components': 'off', + 'ember/require-tagless-components': 'off', + '@typescript-eslint/no-inferrable-types': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-var-requires': 'off', + '@typescript-eslint/no-empty-function': 'off', + '@typescript-eslint/no-empty-interface': 'off', + '@typescript-eslint/ban-ts-comment': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off' }, - ], + overrides: [ + // node files + { + files: [ + './.eslintrc.js', + './.prettierrc.js', + './.template-lintrc.js', + './ember-cli-build.js', + './index.js', + './testem.js', + './blueprints/*/index.js', + './config/**/*.js', + './tests/dummy/config/**/*.js' + ], + parserOptions: { + sourceType: 'script' + }, + env: { + browser: false, + node: true + }, + plugins: ['node'], + extends: ['plugin:node/recommended'] + }, + { + // test files + files: ['tests/**/*-test.{js,ts}'], + extends: ['plugin:qunit/recommended'] + } + ] }; diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ed83160 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,76 @@ +name: CI + +on: + push: + branches: + - main + - master + pull_request: {} + +concurrency: + group: ci-${{ github.head_ref || github.ref }} + cancel-in-progress: true + +jobs: + test: + name: 'Tests' + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Install Node + uses: actions/setup-node@v2 + with: + node-version: 14.x + cache: yarn + - name: Install Dependencies + run: yarn install --frozen-lockfile + - name: Lint + run: yarn lint + - name: Run Tests + run: yarn test:ember + + floating: + name: 'Floating Dependencies' + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: 14.x + cache: yarn + - name: Install Dependencies + run: yarn install --no-lockfile + - name: Run Tests + run: yarn test:ember + + try-scenarios: + name: ${{ matrix.try-scenario }} + runs-on: ubuntu-latest + needs: 'test' + + strategy: + fail-fast: false + matrix: + try-scenario: + - ember-lts-3.24 + - ember-lts-3.28 + - ember-release + - ember-beta + - ember-canary + - ember-classic + - embroider-safe + - embroider-optimized + + steps: + - uses: actions/checkout@v2 + - name: Install Node + uses: actions/setup-node@v2 + with: + node-version: 14.x + cache: yarn + - name: Install Dependencies + run: yarn install --frozen-lockfile + - name: Run Tests + run: ./node_modules/.bin/ember try:one ${{ matrix.try-scenario }} diff --git a/.gitignore b/.gitignore index 7e0f7dd..9bab45a 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,7 @@ # ember-try /.node_modules.ember-try/ /bower.json.ember-try +/npm-shrinkwrap.json.ember-try /package.json.ember-try +/package-lock.json.ember-try +/yarn.lock.ember-try diff --git a/.npmignore b/.npmignore index 88818f2..3aac244 100644 --- a/.npmignore +++ b/.npmignore @@ -14,7 +14,10 @@ /.eslintignore /.eslintrc.js /.git/ +/.github/ /.gitignore +/.prettierignore +/.prettierrc.js /.template-lintrc.js /.travis.yml /.watchmanconfig @@ -24,10 +27,14 @@ /ember-cli-build.js /testem.js /tests/ +/yarn-error.log /yarn.lock .gitkeep # ember-try /.node_modules.ember-try/ /bower.json.ember-try +/npm-shrinkwrap.json.ember-try /package.json.ember-try +/package-lock.json.ember-try +/yarn.lock.ember-try diff --git a/.prettierignore b/.prettierignore index 9221655..4178fd5 100644 --- a/.prettierignore +++ b/.prettierignore @@ -14,8 +14,12 @@ /coverage/ !.* .eslintcache +.lint-todo/ # ember-try /.node_modules.ember-try/ /bower.json.ember-try +/npm-shrinkwrap.json.ember-try /package.json.ember-try +/package-lock.json.ember-try +/yarn.lock.ember-try diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..c587afb --- /dev/null +++ b/.prettierrc @@ -0,0 +1,19 @@ + { + "arrowParens": "always", + "bracketSpacing": true, + "printWidth": 120, + "proseWrap": "preserve", + "semi": true, + "singleQuote": true, + "tabWidth": 4, + "trailingComma": "none", + "useTabs": false, + "overrides": [ + { + "files": "*.hbs", + "options": { + "singleQuote": false + } + } + ] +} diff --git a/.prettierrc.js b/.prettierrc.js deleted file mode 100644 index 534e6d3..0000000 --- a/.prettierrc.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -module.exports = { - singleQuote: true, -}; diff --git a/.template-lintrc.js b/.template-lintrc.js index 3b0b9af..f655826 100644 --- a/.template-lintrc.js +++ b/.template-lintrc.js @@ -1,5 +1,5 @@ 'use strict'; module.exports = { - extends: 'octane', + extends: 'recommended' }; diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index adcd7d6..0000000 --- a/.travis.yml +++ /dev/null @@ -1,63 +0,0 @@ ---- -language: node_js -node_js: - # we recommend testing addons with the same minimum supported node version as Ember CLI - # so that your addon works for all apps - - '10' - -dist: xenial - -addons: - chrome: stable - -cache: - directories: - - $HOME/.npm - -env: - global: - # See https://git.io/vdao3 for details. - - JOBS=1 - -branches: - only: - - master - # npm version tags - - /^v\d+\.\d+\.\d+/ - -jobs: - fast_finish: true - allow_failures: - - env: EMBER_TRY_SCENARIO=ember-canary - - include: - # runs linting and tests with current locked deps - - stage: 'Tests' - name: 'Tests' - script: - - yarn lint - - yarn test:ember - - - stage: 'Additional Tests' - name: 'Floating Dependencies' - install: - - npm install --no-package-lock - script: - - yarn test:ember - - # we recommend new addons test the current and previous LTS - # as well as latest stable release (bonus points to beta/canary) - - env: EMBER_TRY_SCENARIO=ember-lts-3.16 - - env: EMBER_TRY_SCENARIO=ember-lts-3.20 - - env: EMBER_TRY_SCENARIO=ember-release - - env: EMBER_TRY_SCENARIO=ember-beta - - env: EMBER_TRY_SCENARIO=ember-canary - - env: EMBER_TRY_SCENARIO=ember-default-with-jquery - - env: EMBER_TRY_SCENARIO=ember-classic - -before_install: - - curl -o- -L https://yarnpkg.com/install.sh | bash - - export PATH=$HOME/.yarn/bin:$PATH - -script: - - node_modules/.bin/ember try:one $EMBER_TRY_SCENARIO diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 751ece5..6410357 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,25 +2,24 @@ ## Installation -* `git clone ` -* `cd my-addon` -* `npm install` +- `git clone ` +- `cd my-addon` +- `npm install` ## Linting -* `npm run lint:hbs` -* `npm run lint:js` -* `npm run lint:js -- --fix` +- `yarn lint` +- `yarn lint:fix` ## Running tests -* `ember test` – Runs the test suite on the current Ember version -* `ember test --server` – Runs the test suite in "watch mode" -* `ember try:each` – Runs the test suite against multiple Ember versions +- `ember test` – Runs the test suite on the current Ember version +- `ember test --server` – Runs the test suite in "watch mode" +- `ember try:each` – Runs the test suite against multiple Ember versions ## Running the dummy application -* `ember serve` -* Visit the dummy application at [http://localhost:4200](http://localhost:4200). +- `ember serve` +- Visit the dummy application at [http://localhost:4200](http://localhost:4200). -For more information on using ember-cli, visit [https://ember-cli.com/](https://ember-cli.com/). +For more information on using ember-cli, visit [https://cli.emberjs.com/release/](https://cli.emberjs.com/release/). diff --git a/README.md b/README.md index 2587a02..97c9d71 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,9 @@ DISCLAIMER: This addon is not actively maintained for public use. Pull requests Compatibility ------------------------------------------------------------------------------ -* Ember.js v3.16 or above -* Ember CLI v2.13 or above -* Node.js v10 or above +* Ember.js v3.24 or above +* Ember CLI v3.24 or above +* Node.js v12 or above Installation @@ -39,7 +39,6 @@ This addon is bundled with [ember-changset](https://github.com/poteto/ember-chan ```ts // app/pods/foo/route.ts import Route from '@ember/routing/route'; -import ChangesetRoute from '@gavant/ember-validations/mixins/changeset-route'; import Validations from 'my-app/validations/my-validations'; export default class FooRoute extends ChangesetRoute(Route) { diff --git a/addon/components/changeset-input.hbs b/addon/components/changeset-input/changeset-input.hbs similarity index 100% rename from addon/components/changeset-input.hbs rename to addon/components/changeset-input/changeset-input.hbs diff --git a/addon/components/changeset-input.ts b/addon/components/changeset-input/changeset-input.ts similarity index 59% rename from addon/components/changeset-input.ts rename to addon/components/changeset-input/changeset-input.ts index 183662e..807cec9 100644 --- a/addon/components/changeset-input.ts +++ b/addon/components/changeset-input/changeset-input.ts @@ -7,6 +7,5 @@ interface ChangesetInputArgs { path: keyof T; } -export default class ChangesetInput extends Component< - ChangesetInputArgs -> {} +// eslint-disable-next-line ember/no-empty-glimmer-component-classes +export default class ChangesetInput extends Component> {} diff --git a/addon/components/form-validator.ts b/addon/components/form-validator.ts deleted file mode 100644 index cac9fc5..0000000 --- a/addon/components/form-validator.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { A } from '@ember/array'; -import NativeArray from '@ember/array/-private/native-array'; -import Component from '@ember/component'; -import { action } from '@ember/object'; -import { tryInvoke } from '@ember/utils'; -import { BufferedChangeset } from 'ember-changeset/types'; -import { all, reject, resolve } from 'rsvp'; - -// @ts-ignore: Ignore import of compiled template -import layout from '../templates/components/form-validator'; -import FormValidatorChild from './form-validator/child'; - -export default class FormValidator extends Component { - tagName = 'form'; - layout = layout; - didInvokeValidate: boolean = false; - childValidators: NativeArray<{}> = A(); - changeset: BufferedChangeset | null = null; - showAllValidationFields: boolean = false; - - /** - * Registers a `FormValidatorChild` via adding to the `childValidators` array - * - * @param child The form validator child to register - */ - registerChild(child: FormValidatorChild) { - this.childValidators.pushObject(child); - } - - /** - * Deregisters a `FormValidatorChild` via removing it from the `childValidators` array - * - * @param child The form validator child to deregister - */ - deregisterChild(child: FormValidatorChild) { - this.childValidators.removeObject(child); - } - - /** - * Validate the changeset. Resolve promise if successful, reject if not - * - * @param changeset The changeset to validate - */ - validateChangeset(changeset: BufferedChangeset) { - return changeset.validate().then(() => { - if(changeset.isInvalid) { - return reject(); - } else { - return resolve(); - } - }); - } - - /** - * Submit the form. Check parent changeset and all child changesets to see if they validate - * If they do validate, try to invoke `submit`. Otherwise show all validation field errors - */ - @action - async submitForm(event: Event) { - event.preventDefault(); - const ownChangeset = this.changeset; - if (ownChangeset) { - const validations = A([this.validateChangeset(ownChangeset)]); - const childChangesets = this.childValidators.mapBy('changeset'); - childChangesets.forEach(changeset => validations.pushObject(this.validateChangeset(changeset))); - this.set('showAllValidationFields', false); - this.childValidators.setEach('showAllValidationFields', false); - - try { - await all(validations); - return tryInvoke(this, 'submit', [ownChangeset, childChangesets]); - } catch(error) { - this.set('showAllValidationFields', true); - this.childValidators.setEach('showAllValidationFields', true); - return reject(); - } - } else { - return reject(); - } - } -}; diff --git a/addon/components/form-validator/child.ts b/addon/components/form-validator/child.ts deleted file mode 100644 index 6401ca8..0000000 --- a/addon/components/form-validator/child.ts +++ /dev/null @@ -1,33 +0,0 @@ -import Component from '@ember/component'; -import { assert } from '@ember/debug'; - -// @ts-ignore: Ignore import of compiled template -import layout from '../../templates/components/form-validator/child'; -import FormValidator from '../form-validator'; - -export default class FormValidatorChild extends Component { - tagName = 'div'; - layout = layout; - parent!: FormValidator; - - /** - * Registers a `FormValidatorChild` with the parent `FormValidator` - */ - init() { - super.init(); - assert( - 'child form validators must be inside a form-validator block and pass it to this component in the "parent" attribute', - this.parent instanceof FormValidator - ); - - this.parent.registerChild(this); - } - - /** - * Deregisters a `FormValidatorChild` with the parent `FormValidator` - */ - willDestroyElement() { - super.willDestroyElement(); - this.parent.deregisterChild(this); - } -}; diff --git a/addon/templates/components/form-validator/child.hbs b/addon/components/form-validator/child/child.hbs similarity index 96% rename from addon/templates/components/form-validator/child.hbs rename to addon/components/form-validator/child/child.hbs index 5a58ff8..dd7222c 100644 --- a/addon/templates/components/form-validator/child.hbs +++ b/addon/components/form-validator/child/child.hbs @@ -1,4 +1,4 @@ {{yield @changeset (hash input=(component "input-validator" parent=this)) -}} +}} \ No newline at end of file diff --git a/addon/components/form-validator/child/child.ts b/addon/components/form-validator/child/child.ts new file mode 100644 index 0000000..33562ee --- /dev/null +++ b/addon/components/form-validator/child/child.ts @@ -0,0 +1,42 @@ +import { assert } from '@ember/debug'; +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; + +import { GenericChangeset } from '@gavant/ember-validations/utilities/create-changeset'; + +import FormValidator from '../form-validator'; + +interface FormValidatorChildArgs { + parent: FormValidator; + changeset: GenericChangeset; +} + +export default class FormValidatorChild extends Component> { + @tracked showAllValidationFields: boolean = false; + + /** + * Creates an instance of FormValidatorChild. + * @param {unknown} owner + * @param {FormValidatorChildArgs} args + * @memberof FormValidatorChild + */ + constructor(owner: unknown, args: FormValidatorChildArgs) { + super(owner, args); + assert( + 'child form validators must be inside a form-validator block and pass it to this component in the "parent" attribute', + this.args.parent.constructor.name === 'FormValidator' + ); + + this.args.parent.registerChild(this); + } + + /** + * Deregister the child from the parent + * + * @memberof FormValidatorChild + */ + willDestroy() { + super.willDestroy(); + this.args.parent.deregisterChild(this); + } +} diff --git a/addon/templates/components/form-validator.hbs b/addon/components/form-validator/form-validator.hbs similarity index 98% rename from addon/templates/components/form-validator.hbs rename to addon/components/form-validator/form-validator.hbs index 86d8b6f..0871b3a 100644 --- a/addon/templates/components/form-validator.hbs +++ b/addon/components/form-validator/form-validator.hbs @@ -5,4 +5,4 @@ input=(component "input-validator" parent=this) child=(component "form-validator/child" parent=this) ) -}} +}} \ No newline at end of file diff --git a/addon/components/form-validator/form-validator.ts b/addon/components/form-validator/form-validator.ts new file mode 100644 index 0000000..7575157 --- /dev/null +++ b/addon/components/form-validator/form-validator.ts @@ -0,0 +1,106 @@ +import { action } from '@ember/object'; +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; + +import { BufferedChangeset } from 'ember-changeset/types'; + +import FormValidatorChild from '@gavant/ember-validations/components/form-validator/child/child'; +import { GenericChangeset } from '@gavant/ember-validations/utilities/create-changeset'; + +import { all, reject, resolve } from 'rsvp'; + +interface FormValidatorArgs { + changeset: GenericChangeset; + submit: (changesets: [GenericChangeset, GenericChangeset[]]) => void; +} + +export default class FormValidator extends Component> { + @tracked childValidators: FormValidatorChild[] = []; + @tracked didInvokeValidate: boolean = false; + @tracked showAllValidationFields: boolean = false; + + /** + * Register a new child + * + * @param {FormValidatorChild} child + * @memberof FormValidator + */ + registerChild(child: FormValidatorChild) { + this.childValidators.push(child); + } + + /** + * Deregister child + * + * @param {FormValidatorChild} child + * @memberof FormValidator + */ + deregisterChild(child: FormValidatorChild) { + this.childValidators = this.childValidators.filter((item) => item !== child); + } + + /** + * Validate a changeset + * + * @param {BufferedChangeset} changeset + * @return {*} + * @memberof FormValidator + */ + validateChangeset(changeset: BufferedChangeset) { + return changeset.validate().then(() => { + if (changeset.isInvalid) { + return reject(); + } else { + return resolve(); + } + }); + } + + /** + * Submit the form. Check parent changeset and all child changesets to see if they validate + * If they do validate, try to invoke `submit`. Otherwise show all validation field errors + * + * @param {Event} event + * @return {*} + * @memberof FormValidator + */ + @action + async submitForm(event: Event) { + event.preventDefault(); + const ownChangeset = this.args.changeset; + if (ownChangeset) { + const validations = [this.validateChangeset(ownChangeset)]; + const children = this.childValidators.reduce<{ + changesets: GenericChangeset[]; + validations: Promise[]; + }>( + (prev, child) => { + prev.changesets.push(child.args.changeset); + prev.validations.push(this.validateChangeset(child.args.changeset)); + return prev; + }, + { changesets: [], validations: [] } + ); + + validations.push(...children.validations); + + this.showAllValidationFields = false; + this.childValidators.forEach((item) => { + item.showAllValidationFields = false; + }); + + try { + await all(validations); + return this.args?.submit([ownChangeset, children.changesets]); + } catch (error) { + this.showAllValidationFields = true; + this.childValidators.forEach((item) => { + item.showAllValidationFields = true; + }); + return reject(); + } + } else { + return reject(); + } + } +} diff --git a/addon/components/input-validator.ts b/addon/components/input-validator.ts deleted file mode 100644 index 8192f28..0000000 --- a/addon/components/input-validator.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { ValidationErr } from 'validated-changeset/dist/types'; - -import { observes } from '@ember-decorators/object'; -import Component from '@ember/component'; -import { assert } from '@ember/debug'; -import { computed } from '@ember/object'; -import { bool, reads } from '@ember/object/computed'; -import { scheduleOnce } from '@ember/runloop'; - -// @ts-ignore: Ignore import of compiled template -import layout from '../templates/components/input-validator'; -import FormValidator from './form-validator'; -import ChildFormValidator from './form-validator/child'; - -export default class InputValidator extends Component { - layout = layout; - classNames: string[] = ['form-group', 'input-validator']; - classNameBindings: string[] = ['showError:is-invalid', 'label:has-label']; - labelClass: string = 'control-label'; - errorClass: string = 'invalid-feedback'; - hideErrorText: boolean = false; - hasFocusedOut: boolean = false; - parent!: FormValidator; - label: boolean = false; - - /** - * A string or an array of strings which are the current error messages associated with the input field - * In almost all cases, this will be passed via `@errors={{changeset.error.FIELD_NAME.validation}}` - * Where "FIELD_NAME" is the associated changeset property, e.g. `changeset.error.firstName.validation` - * - * @type {string| string[] | ValidationErr[]} - */ - errors?: string | string[] | ValidationErr[]; - - /** - * On initialization, verify the component was invoked in the correct context - */ - init() { - super.init(); - assert( - 'input validators must be inside a form-validator block and invoked using the yielded contextual component', - this.parent! instanceof FormValidator || - this.parent! instanceof ChildFormValidator - ); - } - - /** - * On element insert, update the input's associated