diff --git a/.gitattributes b/.gitattributes index 176a458f..d0c0c4c1 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ -* text=auto +* text eol=lf +*.png binary diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..8a0872fa --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,27 @@ + + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..42cd724b --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,30 @@ + + +## What + + + +## Why + + + +## How + + + +For issue # + +Checklist: + +* [ ] Follows the commit message [conventions](https://github.com/stevemao/conventional-changelog-angular/blob/master/convention.md) +* [ ] Is [rebased with master](https://egghead.io/lessons/javascript-how-to-rebase-a-git-pull-request-branch?series=how-to-contribute-to-an-open-source-project-on-github) +* [ ] Is [only one (maybe two) commits](https://egghead.io/lessons/javascript-how-to-squash-multiple-git-commits) + diff --git a/.travis.yml b/.travis.yml index 94f38f39..94742a78 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,16 +9,16 @@ branches: notifications: email: false node_js: - - iojs + - 4.2 before_install: - - npm i -g npm@^2.0.0 + - npm i -g npm@^3.0.0 - "export DISPLAY=:99.0" - "sh -e /etc/init.d/xvfb start" before_script: - npm prune script: - - npm run test:ci + - npm run eslint + - npm run test - npm run check-coverage after_success: - npm run report-coverage - - npm run semantic-release diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c283fc96..61473334 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,7 +29,16 @@ with a link to a jsbin that demonstrates the issue with [issue.angular-formly.co ## Pull Requests -[Watch video](https://www.youtube.com/watch?v=QOchwBm9W-g&list=PLV5CVI1eNcJi7lVVIuNyRhEuck1Z007BH&index=1) +**Working on your first Pull Request?** You can learn how from this *free* series [How to Contribute to an Open Source Project on GitHub](https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github) + +‼️‼️‼️ 👉**Please follow our commit message conventions** even if you're making a small change! This repository follows the +[How to Write an Open Source JavaScript Library](https://egghead.io/series/how-to-write-an-open-source-javascript-library) +series on egghead.io (by yours truly). See +[this lesson](https://egghead.io/lessons/javascript-how-to-write-a-javascript-library-writing-conventional-commits-with-commitizen?series=how-to-write-an-open-source-javascript-library) +and [this repo](https://github.com/stevemao/conventional-changelog-angular/blob/master/convention.md) +to learn more about the commit message conventions. + +[Watch video](https://www.youtube.com/watch?v=QOchwBm9W-g&list=PLV5CVI1eNcJi7lVVIuNyRhEuck1Z007BH&index=1) (slightly out of date) If you would like to add functionality, please submit [an issue](https://github.com/formly-js/angular-formly/issues) first to make sure it's a direction we want to take. @@ -45,7 +54,7 @@ Please do the following: 2. run `npm start` (if you're on a windows machine, see [this issue](https://github.com/formly-js/angular-formly/issues/305)) 3. write tests & code in ES6 goodness :-) 4. run `git add src/` -5. run `npm run commit` and follow the prompt (this ensures that your commit message follows [our conventions](https://github.com/ajoslin/conventional-changelog/blob/master/conventions/angular.md)). +5. run `npm run commit` and follow the prompt (this ensures that your commit message follows [our conventions](https://github.com/stevemao/conventional-changelog-angular/blob/master/convention.md)). 6. notice that there's a pre-commit hook that runs to ensure tests pass and coverage doesn't drop to prevent the build from breaking :-) 7. push your changes 8. create a PR with a link to the original issue diff --git a/README.md b/README.md index 13ce77c9..4b752dba 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ ## [angular-formly](http://docs.angular-formly.com) +[THIS PROJECT NEEDS A MAINTAINER](https://github.com/formly-js/angular-formly/issues/638) + Status: [![npm version](https://img.shields.io/npm/v/angular-formly.svg?style=flat-square)](https://www.npmjs.org/package/angular-formly) [![npm downloads](https://img.shields.io/npm/dm/angular-formly.svg?style=flat-square)](http://npm-stat.com/charts.html?package=angular-formly&from=2015-01-01) @@ -14,6 +16,9 @@ Links: [![egghead.io lessons](https://img.shields.io/badge/egghead-lessons-blue.svg?style=flat-square)](https://egghead.io/playlists/advanced-angular-forms-with-angular-formly) [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/formly-js/angular-formly?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg?style=flat-square)](https://github.com/formly-js/angular-formly/releases) +[![PRs Welcome](https://img.shields.io/badge/prs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) + +Sponsor angular-formly is an AngularJS module which has a directive to help customize and render JavaScript/JSON configured forms. The `formly-form` directive and the `formlyConfig` service are very powerful and bring unmatched maintainability to your @@ -30,6 +35,9 @@ application's forms. From there, it's just JavaScript. Allowing for DRY, maintainable, reusable forms. +> **IMPORTANT:** This is the formly project for AngularJS (v1.x). If you're looking for an **Angular (v2+) alternative**, take a look at the [ngx-formly](https://github.com/formly-js/ngx-formly) project. + + ## [Learning](http://learn.angular-formly.com) ### Egghead.io Lessons diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 00000000..a88f54cb --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,12 @@ +const gulp = require('gulp') +const replace = require('gulp-replace') + +// bump npm package version into package.js +gulp.task('meteor', function() { + const pkg = require('./package.json') + const versionRegex = /(version\:\s*\')([^\']+)\'/gi + + return gulp.src('package.js') + .pipe(replace(versionRegex, '$1' + pkg.version + "'")) + .pipe(gulp.dest('./')) +}) diff --git a/other/karma.conf.es6.js b/other/karma.conf.es6.js index ee894ff5..a37cf374 100644 --- a/other/karma.conf.es6.js +++ b/other/karma.conf.es6.js @@ -1,4 +1,5 @@ /* eslint-env node */ +require('argv-set-env')(); const path = require('path'); process.env.NODE_ENV = process.env.NODE_ENV || 'test'; @@ -19,7 +20,7 @@ module.exports = function(config) { basePath: './', frameworks: ['sinon-chai', 'chai', 'mocha', 'sinon'], files: [ - 'node_modules/lodash/index.js', + 'node_modules/lodash/lodash.js', 'node_modules/api-check/dist/api-check.js', 'node_modules/angular/angular.js', 'node_modules/angular-mocks/angular-mocks.js', @@ -53,4 +54,3 @@ function getReporters() { } return reps; } - diff --git a/other/webpack.config.es6.js b/other/webpack.config.es6.js index 80f6814c..ceaeab2d 100644 --- a/other/webpack.config.es6.js +++ b/other/webpack.config.es6.js @@ -1,4 +1,5 @@ /* eslint-env node */ +require('argv-set-env')(); const packageJson = require('../package.json'); const here = require('path-here'); @@ -95,7 +96,8 @@ function getProdConfig() { plugins: _.union(getCommonPlugins(), [ new webpack.optimize.DedupePlugin(), new webpack.optimize.OccurenceOrderPlugin(), - new webpack.optimize.AggressiveMergingPlugin() + new webpack.optimize.AggressiveMergingPlugin(), + new webpack.optimize.UglifyJsPlugin() ]) }; } @@ -103,7 +105,6 @@ function getProdConfig() { function getTestConfig() { const coverage = process.env.COVERAGE === 'true'; const ci = process.env.CI === 'true'; - console.log(process.env.CI); return { entry: './index.test.js', module: { diff --git a/package.js b/package.js new file mode 100644 index 00000000..e8510962 --- /dev/null +++ b/package.js @@ -0,0 +1,23 @@ +/* global Package:false */ +// package metadata file for AtmosphereJS + +try { + Package.describe({ + name: 'formly:angular-formly', + summary: 'angular-formly (official): forms for AngularJS', + version: '0.0.0-semantically-released.0', + git: 'https://github.com/formly-js/angular-formly.git', + }) + + Package.onUse(function(api) { + api.versionsFrom(['METEOR@1.0']) + // api-check + api.use('wieldo:api-check@7.5.5') + api.imply('wieldo:api-check') + // angular + api.use('angular:angular@1.4.0') + api.addFiles('dist/formly.js', 'client') + }) +} catch (e) { + // +} diff --git a/package.json b/package.json index d104c404..890d705f 100644 --- a/package.json +++ b/package.json @@ -24,25 +24,29 @@ "main": "dist/formly.js", "license": "MIT", "scripts": { - "build:dist": "cross-env NODE_ENV=development webpack --progress --colors", - "build:prod": "cross-env NODE_ENV=production webpack --progress --colors", + "build:dist": "webpack --progress --colors --set-env-NODE_ENV=development", + "build:prod": "webpack --progress --colors --set-env-NODE_ENV=production", "build": "npm run build:dist & npm run build:prod", - "test": "cross-env COVERAGE=true NODE_ENV=test karma start --single-run", - "test:ci": "CI=true COVERAGE=true NODE_ENV=test karma start --single-run", - "test:watch": "cross-env COVERAGE=true NODE_ENV=test karma start", - "test:debug": "cross-env NODE_ENV=test karma start --browsers Chrome", - "start:mac": "npm run test:mac", + "eslint:test": "eslint -c other/test.eslintrc --ignore-pattern **/*.{test,mock}.js src/", + "eslint:src": "eslint -c other/src.eslintrc --ignore-pattern !**/*.{test,mock}.js src/", + "eslint": "npm run eslint:test -s && npm run eslint:src -s", + "test": "karma start --single-run --set-env-COVERAGE=true --set-env-NODE_ENV=test", + "test:watch": "karma start --set-env-COVERAGE=true --set-env-NODE_ENV=test", + "test:debug": "karma start --browsers Chrome --set-env-NODE_ENV=test", "start": "npm run test:watch", "check-coverage": "istanbul check-coverage --statements 93 --branches 89 --functions 92 --lines 92", "report-coverage": "cat ./coverage/lcov.info | node_modules/.bin/codecov", "commit": "git-cz", - "prepublish": "npm run build", - "postpublish": "publish-latest --user-email kent+formly-bot@doddsfamily.us --user-name formly-bot", - "semantic-release": "semantic-release pre && npm publish && semantic-release post" + "publish-latest": "publish-latest --user-email kent+formly-bot@doddsfamily.us --user-name formly-bot", + "meteor": "gulp meteor", + "semantic-release": "semantic-release pre && npm run build && npm run meteor && npm publish && npm run publish-latest && semantic-release post" }, "config": { "ghooks": { - "commit-msg": "./node_modules/.bin/validate-commit-msg && npm t && npm run check-coverage" + "commit-msg": "validate-commit-msg && npm run eslint && npm t && npm run check-coverage" + }, + "commitizen": { + "path": "node_modules/cz-conventional-changelog" } }, "description": "AngularJS directive which takes JSON representing a form and renders to HTML", @@ -51,61 +55,60 @@ "api-check": "^7.0.0" }, "devDependencies": { - "angular": "1.4.7", - "angular-mocks": "1.4.7", - "api-check": "7.5.3", + "angular": "1.5.0", + "angular-mocks": "1.5.0", + "api-check": "7.5.5", + "argv-set-env": "1.0.1", "babel": "5.8.23", "babel-eslint": "4.1.3", "babel-loader": "5.3.2", - "chai": "3.3.0", + "chai": "3.5.0", "codecov.io": "0.1.6", - "commitizen": "1.0.5", - "cracks": "3.1.1", - "cross-env": "1.0.1", - "cz-conventional-changelog": "1.1.4", + "commitizen": "2.7.2", + "cracks": "3.1.2", + "cz-conventional-changelog": "1.1.5", "deindent": "0.1.0", - "eslint": "1.6.0", - "eslint-config-kentcdodds": "4.0.1", - "eslint-loader": "1.0.0", + "eslint": "1.7.3", + "eslint-config-kentcdodds": "5.0.0", + "eslint-loader": "1.1.0", "eslint-plugin-mocha": "1.0.0", - "ghooks": "0.3.2", - "http-server": "0.8.5", + "ghooks": "1.0.3", + "gulp": "3.9.1", + "gulp-replace": "0.5.4", + "http-server": "0.9.0", "isparta": "3.1.0", "isparta-loader": "1.0.0", - "istanbul": "0.3.22", - "karma": "0.13.10", + "istanbul": "0.4.2", + "karma": "0.13.22", "karma-chai": "0.1.0", - "karma-chrome-launcher": "0.2.0", - "karma-coverage": "0.5.2", - "karma-firefox-launcher": "0.1.6", - "karma-mocha": "0.2.0", + "karma-chrome-launcher": "0.2.2", + "karma-coverage": "0.5.5", + "karma-firefox-launcher": "0.1.7", + "karma-mocha": "0.2.2", "karma-sinon": "1.0.4", - "karma-sinon-chai": "1.1.0", + "karma-sinon-chai": "1.2.0", "karma-webpack": "1.7.0", - "lodash": "3.10.1", - "mocha": "2.3.3", - "ng-annotate": "1.0.2", - "ng-annotate-loader": "0.0.10", - "node-libs-browser": "0.5.3", + "lodash": "4.6.1", + "lolex": "1.4.0", + "mocha": "2.4.5", + "ng-annotate": "1.2.1", + "ng-annotate-loader": "0.1.0", + "node-libs-browser": "1.0.0", "path-here": "1.1.0", "publish-latest": "1.1.2", "raw-loader": "0.5.1", "semantic-release": "4.3.5", - "sinon": "1.17.1", + "sinon": "1.17.3", "sinon-chai": "2.8.0", - "uglify-loader": "1.2.0", - "validate-commit-msg": "1.0.0", - "webpack": "1.12.2", - "webpack-notifier": "1.2.1" + "validate-commit-msg": "2.3.1", + "webpack": "1.12.14", + "webpack-notifier": "1.3.0" }, "jspm": { "peerDependencies": { "angular": "*" } }, - "czConfig": { - "path": "node_modules/cz-conventional-changelog" - }, "release": { "verfiyRelease": { "path": "cracks", diff --git a/src/angular-fix/index.js b/src/angular-fix/index.js index 295917ef..73bddb97 100644 --- a/src/angular-fix/index.js +++ b/src/angular-fix/index.js @@ -1,9 +1,9 @@ // some versions of angular don't export the angular module properly, // so we get it from window in this case. -let angular = require('angular'); +let angular = require('angular') /* istanbul ignore next */ if (!angular.version) { - angular = window.angular; + angular = window.angular } -export default angular; +export default angular diff --git a/src/directives/formly-custom-validation.js b/src/directives/formly-custom-validation.js index e991cb6f..a5a3271c 100644 --- a/src/directives/formly-custom-validation.js +++ b/src/directives/formly-custom-validation.js @@ -1,5 +1,5 @@ -import angular from 'angular-fix'; -export default formlyCustomValidation; +import angular from 'angular-fix' +export default formlyCustomValidation // @ngInject function formlyCustomValidation(formlyUtil) { @@ -7,76 +7,76 @@ function formlyCustomValidation(formlyUtil) { restrict: 'A', require: 'ngModel', link: function formlyCustomValidationLink(scope, el, attrs, ctrl) { - const opts = scope.options; - opts.validation.messages = opts.validation.messages || {}; + const opts = scope.options + opts.validation.messages = opts.validation.messages || {} angular.forEach(opts.validation.messages, (message, key) => { opts.validation.messages[key] = () => { - return formlyUtil.formlyEval(scope, message, ctrl.$modelValue, ctrl.$viewValue); - }; - }); + return formlyUtil.formlyEval(scope, message, ctrl.$modelValue, ctrl.$viewValue) + } + }) - const useNewValidatorsApi = ctrl.hasOwnProperty('$validators') && !attrs.hasOwnProperty('useParsers'); - angular.forEach(opts.validators, angular.bind(null, addValidatorToPipeline, false)); - angular.forEach(opts.asyncValidators, angular.bind(null, addValidatorToPipeline, true)); + const useNewValidatorsApi = ctrl.hasOwnProperty('$validators') && !attrs.hasOwnProperty('useParsers') + angular.forEach(opts.validators, angular.bind(null, addValidatorToPipeline, false)) + angular.forEach(opts.asyncValidators, angular.bind(null, addValidatorToPipeline, true)) function addValidatorToPipeline(isAsync, validator, name) { - setupMessage(validator, name); - validator = angular.isObject(validator) ? validator.expression : validator; + setupMessage(validator, name) + validator = angular.isObject(validator) ? validator.expression : validator if (useNewValidatorsApi) { - setupWithValidators(validator, name, isAsync); + setupWithValidators(validator, name, isAsync) } else { - setupWithParsers(validator, name, isAsync); + setupWithParsers(validator, name, isAsync) } } function setupMessage(validator, name) { - const message = validator.message; + const message = validator.message if (message) { opts.validation.messages[name] = () => { - return formlyUtil.formlyEval(scope, message, ctrl.$modelValue, ctrl.$viewValue); - }; + return formlyUtil.formlyEval(scope, message, ctrl.$modelValue, ctrl.$viewValue) + } } } function setupWithValidators(validator, name, isAsync) { - const validatorCollection = isAsync ? '$asyncValidators' : '$validators'; + const validatorCollection = isAsync ? '$asyncValidators' : '$validators' ctrl[validatorCollection][name] = function evalValidity(modelValue, viewValue) { - return formlyUtil.formlyEval(scope, validator, modelValue, viewValue); - }; + return formlyUtil.formlyEval(scope, validator, modelValue, viewValue) + } } function setupWithParsers(validator, name, isAsync) { - let inFlightValidator; + let inFlightValidator ctrl.$parsers.unshift(function evalValidityOfParser(viewValue) { - const isValid = formlyUtil.formlyEval(scope, validator, ctrl.$modelValue, viewValue); + const isValid = formlyUtil.formlyEval(scope, validator, ctrl.$modelValue, viewValue) if (isAsync) { - ctrl.$pending = ctrl.$pending || {}; - ctrl.$pending[name] = true; - inFlightValidator = isValid; + ctrl.$pending = ctrl.$pending || {} + ctrl.$pending[name] = true + inFlightValidator = isValid isValid.then(() => { if (inFlightValidator === isValid) { - ctrl.$setValidity(name, true); + ctrl.$setValidity(name, true) } }).catch(() => { if (inFlightValidator === isValid) { - ctrl.$setValidity(name, false); + ctrl.$setValidity(name, false) } }).finally(() => { - const $pending = ctrl.$pending || {}; + const $pending = ctrl.$pending || {} if (Object.keys($pending).length === 1) { - delete ctrl.$pending; + delete ctrl.$pending } else { - delete ctrl.$pending[name]; + delete ctrl.$pending[name] } - }); + }) } else { - ctrl.$setValidity(name, isValid); + ctrl.$setValidity(name, isValid) } - return viewValue; - }); + return viewValue + }) } - } - }; + }, + } } diff --git a/src/directives/formly-custom-validation.test.js b/src/directives/formly-custom-validation.test.js index cc9dd4e6..68fd72a1 100644 --- a/src/directives/formly-custom-validation.test.js +++ b/src/directives/formly-custom-validation.test.js @@ -1,128 +1,128 @@ /* eslint no-unused-vars:0, max-len:0 */ -import _ from 'lodash'; -import angular from 'angular-fix'; +import _ from 'lodash' +import angular from 'angular-fix' -import testUtils from '../test.utils.js'; +import testUtils from '../test.utils.js' -const {shouldWarnWithLog} = testUtils; +const {shouldWarnWithLog} = testUtils describe(`formly-custom-validation`, function() { - let $compile, $timeout, $q, scope, $log, formlyConfig; - const formTemplate = `
TEMPLATE
`; - beforeEach(window.module('formly')); + let $compile, $timeout, $q, scope, $log, formlyConfig + const formTemplate = `
TEMPLATE
` + beforeEach(window.module('formly')) beforeEach(inject((_$compile_, _$timeout_, _$q_, $rootScope, _$log_, _formlyConfig_) => { - $compile = _$compile_; - $timeout = _$timeout_; - $q = _$q_; - scope = $rootScope.$new(); - scope.options = {validation: {}, validators: {}, asyncValidators: {}}; - $log = _$log_; - formlyConfig = _formlyConfig_; - })); + $compile = _$compile_ + $timeout = _$timeout_ + $q = _$q_ + scope = $rootScope.$new() + scope.options = {validation: {}, validators: {}, asyncValidators: {}} + $log = _$log_ + formlyConfig = _formlyConfig_ + })) describe(`using parsers`, () => { checkApi(formTemplate.replace( `TEMPLATE`, `` - ), angular.version.minor >= 3); - }); + ), angular.version.minor >= 3) + }) describe(`using $validators`, () => { checkApi(formTemplate.replace( `TEMPLATE`, `` - )); - }); + )) + }) describe(`options.validation.messages`, () => { it(`should convert all strings to functions`, () => { scope.options.validation = { messages: { - isHello: `'"' + $viewValue + '" is not "hello"'` - } - }; + isHello: `'"' + $viewValue + '" is not "hello"'`, + }, + } $compile(formTemplate.replace( `TEMPLATE`, `` - ))(scope); + ))(scope) - expect(typeof scope.options.validation.messages.isHello).to.eq('function'); - const field = scope.myForm.field; - field.$setViewValue('sup'); - expect(scope.options.validation.messages.isHello()).to.eq('"sup" is not "hello"'); - }); - }); + expect(typeof scope.options.validation.messages.isHello).to.eq('function') + const field = scope.myForm.field + field.$setViewValue('sup') + expect(scope.options.validation.messages.isHello()).to.eq('"sup" is not "hello"') + }) + }) function checkApi(template, versionThreeOrBetterAndEmulating) { - const value = `hello`; + const value = `hello` describe(`validators`, () => { - const validate = doValidation.bind(null, template, 'hello', false); + const validate = doValidation.bind(null, template, 'hello', false) it(`should pass if returning a string that passes`, () => { - validate(`$viewValue === "${value}"`, true); - }); + validate(`$viewValue === "${value}"`, true) + }) it(`should fail if returning a string that fails`, () => { - validate(`$viewValue !== "${value}"`, false); - }); + validate(`$viewValue !== "${value}"`, false) + }) it(`should pass if it's a function that passes`, () => { - validate(viewValue => viewValue === value, true); - }); + validate(viewValue => viewValue === value, true) + }) it(`should fail if it's a function that fails`, () => { - validate(viewValue => viewValue !== value, false); - }); - }); + validate(viewValue => viewValue !== value, false) + }) + }) describe(`asyncValidators`, () => { - const validate = doValidation.bind(null, template, 'hello', true); + const validate = doValidation.bind(null, template, 'hello', true) it(`should pass if it's a function that returns a promise that resolves`, () => { - validate(() => $q.when(), true); - }); + validate(() => $q.when(), true) + }) it(`should fail if it's a function that returns a promise that rejects`, () => { - validate(() => $q.reject(), false); - }); + validate(() => $q.reject(), false) + }) it(`should be pending until the promise is resolved`, () => { - const deferred = $q.defer(); - const deferred2 = $q.defer(); - scope.options.asyncValidators.isHello = () => deferred.promise; - scope.options.asyncValidators.isHey = () => deferred2.promise; - $compile(template)(scope); - const field = scope.myForm.field; - scope.$digest(); - field.$setViewValue(value); - - expect(field.$pending).to.exist; - expect(field.$pending.isHello).to.be.true; - expect(field.$pending.isHey).to.be.true; + const deferred = $q.defer() + const deferred2 = $q.defer() + scope.options.asyncValidators.isHello = () => deferred.promise + scope.options.asyncValidators.isHey = () => deferred2.promise + $compile(template)(scope) + const field = scope.myForm.field + scope.$digest() + field.$setViewValue(value) + + expect(field.$pending).to.exist + expect(field.$pending.isHello).to.be.true + expect(field.$pending.isHey).to.be.true // because in angular 1.3 they do some interesting stuff with $pending, so can only test $pending in 1.2 if (!versionThreeOrBetterAndEmulating) { - deferred.resolve(); - scope.$digest(); + deferred.resolve() + scope.$digest() - expect(field.$pending).to.exist; - expect(field.$pending.isHey).to.be.true; - expect(field.$pending.isHello).to.not.exist; + expect(field.$pending).to.exist + expect(field.$pending.isHey).to.be.true + expect(field.$pending.isHello).to.not.exist - deferred2.reject(); - scope.$digest(); - expect(field.$pending).to.not.exist; + deferred2.reject() + scope.$digest() + expect(field.$pending).to.not.exist } - }); - }); + }) + }) } function doValidation(template, value, isAsync, validator, pass) { if (isAsync) { - scope.options.asyncValidators.isHello = validator; + scope.options.asyncValidators.isHello = validator } else { - scope.options.validators.isHello = validator; + scope.options.validators.isHello = validator } - $compile(template)(scope); - const field = scope.myForm.field; - scope.$digest(); - field.$setViewValue(value); - scope.$digest(); - expect(field.$valid).to.eq(pass); + $compile(template)(scope) + const field = scope.myForm.field + scope.$digest() + field.$setViewValue(value) + scope.$digest() + expect(field.$valid).to.eq(pass) } -}); +}) diff --git a/src/directives/formly-field.js b/src/directives/formly-field.js index d5949ac0..d98e3e9a 100644 --- a/src/directives/formly-field.js +++ b/src/directives/formly-field.js @@ -1,7 +1,7 @@ -import angular from 'angular-fix'; -import apiCheckFactory from 'api-check'; +import angular from 'angular-fix' +import apiCheckFactory from 'api-check' -export default formlyField; +export default formlyField /** * @ngdoc directive @@ -11,7 +11,7 @@ export default formlyField; // @ngInject function formlyField($http, $q, $compile, $templateCache, $interpolate, formlyConfig, formlyApiCheck, formlyUtil, formlyUsability, formlyWarn) { - const {arrayify} = formlyUtil; + const {arrayify} = formlyUtil return { restrict: 'AE', @@ -26,60 +26,139 @@ function formlyField($http, $q, $compile, $templateCache, $interpolate, formlyCo fields: '=?', formState: '=?', formOptions: '=?', - form: '=?' // TODO require form in a breaking release + form: '=?', // TODO require form in a breaking release }, controller: FormlyFieldController, - link: fieldLink - }; + link: fieldLink, + } // @ngInject function FormlyFieldController($scope, $timeout, $parse, $controller, formlyValidationMessages) { - /* eslint max-statements:[2, 31] */ + /* eslint max-statements:[2, 37] */ if ($scope.options.fieldGroup) { - setupFieldGroup(); - return; + setupFieldGroup() + return } - const fieldType = getFieldType($scope.options); - simplifyLife($scope.options); - mergeFieldOptionsWithTypeDefaults($scope.options, fieldType); - extendOptionsWithDefaults($scope.options, $scope.index); - checkApi($scope.options); + const fieldType = getFieldType($scope.options) + simplifyLife($scope.options) + mergeFieldOptionsWithTypeDefaults($scope.options, fieldType) + extendOptionsWithDefaults($scope.options, $scope.index) + checkApi($scope.options) // set field id to link labels and fields // initalization - setFieldIdAndName(); - setDefaultValue(); - setInitialValue(); - runExpressions(); - addValidationMessages($scope.options); - invokeControllers($scope, $scope.options, fieldType); + setFieldIdAndName() + setDefaultValue() + setInitialValue() + runExpressions() + watchExpressions() + addValidationMessages($scope.options) + invokeControllers($scope, $scope.options, fieldType) // function definitions function runExpressions() { + const deferred = $q.defer() // must run on next tick to make sure that the current value is correct. - return $timeout(function runExpressionsOnNextTick() { - const field = $scope.options; - const currentValue = valueGetterSetter(); + $timeout(function runExpressionsOnNextTick() { + const promises = [] + const field = $scope.options + const currentValue = valueGetterSetter() angular.forEach(field.expressionProperties, function runExpression(expression, prop) { - const setter = $parse(prop).assign; - const promise = $q.when(formlyUtil.formlyEval($scope, expression, currentValue, currentValue)); - promise.then(function setFieldValue(value) { - setter(field, value); - }); - }); - }, 0, false); + const setter = $parse(prop).assign + const promise = $q.when(formlyUtil.formlyEval($scope, expression, currentValue, currentValue)) + .then(function setFieldValue(value) { + setter(field, value) + }) + promises.push(promise) + }) + $q.all(promises).then(function() { + deferred.resolve() + }) + }, 0, false) + return deferred.promise + } + + function watchExpressions() { + if ($scope.formOptions.watchAllExpressions) { + const field = $scope.options + const currentValue = valueGetterSetter() + angular.forEach(field.expressionProperties, function watchExpression(expression, prop) { + const setter = $parse(prop).assign + $scope.$watch(function expressionPropertyWatcher() { + return formlyUtil.formlyEval($scope, expression, currentValue, currentValue) + }, function expressionPropertyListener(value) { + setter(field, value) + }, true) + }) + } } function valueGetterSetter(newVal) { if (!$scope.model || !$scope.options.key) { - return undefined; + return undefined } if (angular.isDefined(newVal)) { - $scope.model[$scope.options.key] = newVal; + parseSet($scope.options.key, $scope.model, newVal) + } + return parseGet($scope.options.key, $scope.model) + } + + function shouldNotUseParseKey(key) { + return angular.isNumber(key) || !formlyUtil.containsSelector(key) + } + + function keyContainsArrays(key) { + return /\[\d{1,}\]/.test(key) + } + + function deepAssign(obj, prop, value) { + if (angular.isString(prop)) { + prop = prop.replace(/\[(\w+)\]/g, '.$1').split('.') + } + + if (prop.length > 1) { + const e = prop.shift() + obj[e] = obj[e] || (isNaN(prop[0])) ? {} : [] + deepAssign(obj[e], prop, value) + } else { + obj[prop[0]] = value + } + } + + function parseSet(key, model, newVal) { + // If either of these are null/undefined then just return undefined + if ((!key && key !== 0) || !model) { + return + } + // If we are working with a number then $parse wont work, default back to the old way for now + if (shouldNotUseParseKey(key)) { + // TODO: Fix this so we can get several levels instead of just one with properties that are numeric + model[key] = newVal + } else if (formlyConfig.extras.parseKeyArrays && keyContainsArrays(key)) { + deepAssign($scope.model, key, newVal) + } else { + const setter = $parse($scope.options.key).assign + if (setter) { + setter($scope.model, newVal) + } + } + } + + function parseGet(key, model) { + // If either of these are null/undefined then just return undefined + if ((!key && key !== 0) || !model) { + return undefined + } + + // If we are working with a number then $parse wont work, default back to the old way for now + if (shouldNotUseParseKey(key)) { + // TODO: Fix this so we can get several levels instead of just one with properties that are numeric + return model[key] + } else { + return $parse(key)(model) } - return $scope.model[$scope.options.key]; } function simplifyLife(options) { @@ -89,121 +168,126 @@ function formlyField($http, $q, $compile, $templateCache, $interpolate, formlyCo extras: {}, data: {}, templateOptions: {}, - validation: {} - }); + validation: {}, + }) // create $scope.to so template authors can reference to instead of $scope.options.templateOptions - $scope.to = $scope.options.templateOptions; - $scope.formOptions = $scope.formOptions || {}; + $scope.to = $scope.options.templateOptions + $scope.formOptions = $scope.formOptions || {} } function setFieldIdAndName() { if (angular.isFunction(formlyConfig.extras.getFieldId)) { - $scope.id = formlyConfig.extras.getFieldId($scope.options, $scope.model, $scope); + $scope.id = formlyConfig.extras.getFieldId($scope.options, $scope.model, $scope) } else { - const formName = ($scope.form && $scope.form.$name) || $scope.formId; - $scope.id = formlyUtil.getFieldId(formName, $scope.options, $scope.index); + const formName = ($scope.form && $scope.form.$name) || $scope.formId + $scope.id = formlyUtil.getFieldId(formName, $scope.options, $scope.index) } - $scope.options.id = $scope.id; - $scope.name = $scope.options.name || $scope.options.id; - $scope.options.name = $scope.name; + $scope.options.id = $scope.id + $scope.name = $scope.options.name || $scope.options.id + $scope.options.name = $scope.name } function setDefaultValue() { - if (angular.isDefined($scope.options.defaultValue) && !angular.isDefined($scope.model[$scope.options.key])) { - const setter = $parse($scope.options.key).assign; - setter($scope.model, $scope.options.defaultValue); + if (angular.isDefined($scope.options.defaultValue) && + !angular.isDefined(parseGet($scope.options.key, $scope.model))) { + parseSet($scope.options.key, $scope.model, $scope.options.defaultValue) } } function setInitialValue() { - $scope.options.initialValue = $scope.model && $scope.model[$scope.options.key]; + $scope.options.initialValue = $scope.model && parseGet($scope.options.key, $scope.model) } function mergeFieldOptionsWithTypeDefaults(options, type) { if (type) { - mergeOptions(options, type.defaultOptions); + mergeOptions(options, type.defaultOptions) } - const properOrder = arrayify(options.optionsTypes).reverse(); // so the right things are overridden + const properOrder = arrayify(options.optionsTypes).reverse() // so the right things are overridden angular.forEach(properOrder, typeName => { - mergeOptions(options, formlyConfig.getType(typeName, true, options).defaultOptions); - }); + mergeOptions(options, formlyConfig.getType(typeName, true, options).defaultOptions) + }) } function mergeOptions(options, extraOptions) { if (extraOptions) { if (angular.isFunction(extraOptions)) { - extraOptions = extraOptions(options, $scope); + extraOptions = extraOptions(options, $scope) } - formlyUtil.reverseDeepMerge(options, extraOptions); + formlyUtil.reverseDeepMerge(options, extraOptions) } } function extendOptionsWithDefaults(options, index) { - const key = options.key || index || 0; + const key = options.key || index || 0 angular.extend(options, { // attach the key in case the formly-field directive is used directly key, value: options.value || valueGetterSetter, runExpressions, resetModel, - updateInitialValue - }); + updateInitialValue, + }) } function resetModel() { - $scope.model[$scope.options.key] = $scope.options.initialValue; + parseSet($scope.options.key, $scope.model, $scope.options.initialValue) if ($scope.options.formControl) { if (angular.isArray($scope.options.formControl)) { angular.forEach($scope.options.formControl, function(formControl) { - resetFormControl(formControl, true); - }); + resetFormControl(formControl, true) + }) } else { - resetFormControl($scope.options.formControl); + resetFormControl($scope.options.formControl) } } + if ($scope.form) { + $scope.form.$setUntouched && $scope.form.$setUntouched() + $scope.form.$setPristine() + } } function resetFormControl(formControl, isMultiNgModel) { if (!isMultiNgModel) { - formControl.$setViewValue($scope.model[$scope.options.key]); + formControl.$setViewValue(parseGet($scope.options.key, $scope.model)) } - formControl.$render(); - formControl.$setUntouched && formControl.$setUntouched(); - formControl.$setPristine(); + formControl.$render() + formControl.$setUntouched && formControl.$setUntouched() + formControl.$setPristine() // To prevent breaking change requiring a digest to reset $viewModel if (!$scope.$root.$$phase) { - $scope.$digest(); + $scope.$digest() } } function updateInitialValue() { - $scope.options.initialValue = $scope.model[$scope.options.key]; + $scope.options.initialValue = parseGet($scope.options.key, $scope.model) } function addValidationMessages(options) { - options.validation.messages = options.validation.messages || {}; + options.validation.messages = options.validation.messages || {} angular.forEach(formlyValidationMessages.messages, function createFunctionForMessage(expression, name) { if (!options.validation.messages[name]) { options.validation.messages[name] = function evaluateMessage(viewValue, modelValue, scope) { - return formlyUtil.formlyEval(scope, expression, modelValue, viewValue); - }; + return formlyUtil.formlyEval(scope, expression, modelValue, viewValue) + } } - }); + }) } function invokeControllers(scope, options = {}, type = {}) { angular.forEach([type.controller, options.controller], controller => { if (controller) { - $controller(controller, {$scope: scope}); + $controller(controller, {$scope: scope}) } - }); + }) } function setupFieldGroup() { - $scope.options.options = $scope.options.options || {}; - $scope.options.options.formState = $scope.formState; + $scope.options.options = $scope.options.options || {} + $scope.options.options.formState = $scope.formState + $scope.to = $scope.options.templateOptions } } @@ -211,23 +295,23 @@ function formlyField($http, $q, $compile, $templateCache, $interpolate, formlyCo // link function function fieldLink(scope, el, attrs, formlyFormCtrl) { if (scope.options.fieldGroup) { - setFieldGroupTemplate(); - return; + setFieldGroupTemplate() + return } // watch the field model (if exists) if there is no parent formly-form directive (that would watch it instead) if (!formlyFormCtrl && scope.options.model) { - scope.$watch('options.model', () => scope.options.runExpressions(), true); + scope.$watch('options.model', () => scope.options.runExpressions(), true) } - addAttributes(); - addClasses(); + addAttributes() + addClasses() - const type = getFieldType(scope.options); - const args = arguments; - const thusly = this; - let fieldCount = 0; - const fieldManipulators = getManipulators(scope.options, scope.formOptions); + const type = getFieldType(scope.options) + const args = arguments + const thusly = this + let fieldCount = 0 + const fieldManipulators = getManipulators(scope.options, scope.formOptions) getFieldTemplate(scope.options) .then(runManipulators(fieldManipulators.preWrapper)) .then(transcludeInWrappers(scope.options, scope.formOptions)) @@ -241,24 +325,24 @@ function formlyField($http, $q, $compile, $templateCache, $interpolate, formlyCo 'There was a problem setting the template for this field ', scope.options, error - ); - }); + ) + }) function setFieldGroupTemplate() { - checkFieldGroupApi(scope.options); - el.addClass('formly-field-group'); - let extraAttributes = ''; + checkFieldGroupApi(scope.options) + el.addClass('formly-field-group') + let extraAttributes = '' if (scope.options.elementAttributes) { extraAttributes = Object.keys(scope.options.elementAttributes).map(key => { - return `${key}="${scope.options.elementAttributes[key]}"`; - }).join(' '); + return `${key}="${scope.options.elementAttributes[key]}"` + }).join(' ') } - let modelValue = 'model'; - scope.options.form = scope.form; + let modelValue = 'model' + scope.options.form = scope.form if (scope.options.key) { - modelValue = `model['${scope.options.key}']`; + modelValue = `model['${scope.options.key}']` } - setElementTemplate(` + getTemplate(` - `); + `) + .then(transcludeInWrappers(scope.options, scope.formOptions)) + .then(setElementTemplate) } function addAttributes() { if (scope.options.elementAttributes) { - el.attr(scope.options.elementAttributes); + el.attr(scope.options.elementAttributes) } } function addClasses() { if (scope.options.className) { - el.addClass(scope.options.className); + el.addClass(scope.options.className) } if (scope.options.type) { - el.addClass(`formly-field-${scope.options.type}`); + el.addClass(`formly-field-${scope.options.type}`) } } function setElementTemplate(templateString) { - el.html(asHtml(templateString)); - $compile(el.contents())(scope); - return templateString; + el.html(asHtml(templateString)) + $compile(el.contents())(scope) + return templateString } function watchFormControl(templateString) { - let stopWatchingShowError = angular.noop; + let stopWatchingShowError = angular.noop if (scope.options.noFormControl) { - return; + return } - const templateEl = angular.element(`
${templateString}
`); - const ngModelNodes = templateEl[0].querySelectorAll('[ng-model],[data-ng-model]'); + const templateEl = angular.element(`
${templateString}
`) + const ngModelNodes = templateEl[0].querySelectorAll('[ng-model],[data-ng-model]') if (ngModelNodes.length) { angular.forEach(ngModelNodes, function(ngModelNode) { - fieldCount++; - watchFieldNameOrExistence(ngModelNode.getAttribute('name')); - }); + fieldCount++ + watchFieldNameOrExistence(ngModelNode.getAttribute('name')) + }) } function watchFieldNameOrExistence(name) { - const nameExpressionRegex = /\{\{(.*?)}}/; - const nameExpression = nameExpressionRegex.exec(name); + const nameExpressionRegex = /\{\{(.*?)}}/ + const nameExpression = nameExpressionRegex.exec(name) if (nameExpression) { - name = $interpolate(name)(scope); + name = $interpolate(name)(scope) } - watchFieldExistence(name); + watchFieldExistence(name) } function watchFieldExistence(name) { @@ -321,134 +407,138 @@ function formlyField($http, $q, $compile, $templateCache, $interpolate, formlyCo if (formControl) { if (fieldCount > 1) { if (!scope.options.formControl) { - scope.options.formControl = []; + scope.options.formControl = [] } - scope.options.formControl.push(formControl); + scope.options.formControl.push(formControl) } else { - scope.options.formControl = formControl; + scope.options.formControl = formControl } - scope.fc = scope.options.formControl; // shortcut for template authors - stopWatchingShowError(); - addShowMessagesWatcher(); - addParsers(); - addFormatters(); + scope.fc = scope.options.formControl // shortcut for template authors + stopWatchingShowError() + addShowMessagesWatcher() + addParsers() + addFormatters() } - }); + }) } function addShowMessagesWatcher() { stopWatchingShowError = scope.$watch(function watchShowValidationChange() { - const customExpression = formlyConfig.extras.errorExistsAndShouldBeVisibleExpression; - const {options, fc} = scope; - if (!fc.$invalid) { - return false; + const customExpression = formlyConfig.extras.errorExistsAndShouldBeVisibleExpression + const options = scope.options + const formControls = arrayify(scope.fc) + if (!formControls.some(fc => fc.$invalid)) { + return false } else if (typeof options.validation.show === 'boolean') { - return options.validation.show; + return options.validation.show } else if (customExpression) { - return formlyUtil.formlyEval(scope, customExpression, fc.$modelValue, fc.$viewValue); + return formControls.some(fc => + formlyUtil.formlyEval(scope, customExpression, fc.$modelValue, fc.$viewValue)) } else { - const noTouchedButDirty = (angular.isUndefined(fc.$touched) && fc.$dirty); - return (scope.fc.$touched || noTouchedButDirty); + return formControls.some(fc => { + const noTouchedButDirty = (angular.isUndefined(fc.$touched) && fc.$dirty) + return (fc.$touched || noTouchedButDirty) + }) } }, function onShowValidationChange(show) { - scope.options.validation.errorExistsAndShouldBeVisible = show; - scope.showError = show; // shortcut for template authors - }); + scope.options.validation.errorExistsAndShouldBeVisible = show + scope.showError = show // shortcut for template authors + }) } function addParsers() { - setParsersOrFormatters('parsers'); + setParsersOrFormatters('parsers') } function addFormatters() { - setParsersOrFormatters('formatters'); - const ctrl = scope.fc; - const formWasPristine = scope.form.$pristine; + setParsersOrFormatters('formatters') + const ctrl = scope.fc + const formWasPristine = scope.form.$pristine if (scope.options.formatters) { - let value = ctrl.$modelValue; + let value = ctrl.$modelValue ctrl.$formatters.forEach((formatter) => { - value = formatter(value); - }); + value = formatter(value) + }) - ctrl.$setViewValue(value); - ctrl.$render(); - ctrl.$setPristine(); + ctrl.$setViewValue(value) + ctrl.$render() + ctrl.$setPristine() if (formWasPristine) { - scope.form.$setPristine(); + scope.form.$setPristine() } } } function setParsersOrFormatters(which) { - let originalThingProp = 'originalParser'; + let originalThingProp = 'originalParser' if (which === 'formatters') { - originalThingProp = 'originalFormatter'; + originalThingProp = 'originalFormatter' } // init with type's parsers - let things = getThingsFromType(type); + let things = getThingsFromType(type) // get optionsTypes things - things = formlyUtil.extendArray(things, getThingsFromOptionsTypes(scope.options.optionsTypes)); + things = formlyUtil.extendArray(things, getThingsFromOptionsTypes(scope.options.optionsTypes)) // get field's things - things = formlyUtil.extendArray(things, scope.options[which]); + things = formlyUtil.extendArray(things, scope.options[which]) // convert things into formlyExpression things angular.forEach(things, (thing, index) => { - things[index] = getFormlyExpressionThing(thing); - }); + things[index] = getFormlyExpressionThing(thing) + }) - let ngModelCtrls = scope.fc; + let ngModelCtrls = scope.fc if (!angular.isArray(ngModelCtrls)) { - ngModelCtrls = [ngModelCtrls]; + ngModelCtrls = [ngModelCtrls] } angular.forEach(ngModelCtrls, ngModelCtrl => { - ngModelCtrl['$' + which] = ngModelCtrl['$' + which].concat(...things); - }); + ngModelCtrl['$' + which] = ngModelCtrl['$' + which].concat(...things) + }) function getThingsFromType(theType) { if (!theType) { - return []; + return [] } if (angular.isString(theType)) { - theType = formlyConfig.getType(theType, true, scope.options); + theType = formlyConfig.getType(theType, true, scope.options) } - let typeThings = []; + let typeThings = [] // get things from parent if (theType.extends) { - typeThings = formlyUtil.extendArray(typeThings, getThingsFromType(theType.extends)); + typeThings = formlyUtil.extendArray(typeThings, getThingsFromType(theType.extends)) } // get own type's things - typeThings = formlyUtil.extendArray(typeThings, getDefaultOptionsProperty(theType, which, [])); + typeThings = formlyUtil.extendArray(typeThings, getDefaultOptionsProperty(theType, which, [])) // get things from optionsTypes typeThings = formlyUtil.extendArray( typeThings, getThingsFromOptionsTypes(getDefaultOptionsOptionsTypes(theType)) - ); + ) - return typeThings; + return typeThings } function getThingsFromOptionsTypes(optionsTypes = []) { - let optionsTypesThings = []; + let optionsTypesThings = [] angular.forEach(angular.copy(arrayify(optionsTypes)).reverse(), optionsTypeName => { - optionsTypesThings = formlyUtil.extendArray(optionsTypesThings, getThingsFromType(optionsTypeName)); - }); - return optionsTypesThings; + optionsTypesThings = formlyUtil.extendArray(optionsTypesThings, getThingsFromType(optionsTypeName)) + }) + return optionsTypesThings } function getFormlyExpressionThing(thing) { - formlyExpressionParserOrFormatterFunction[originalThingProp] = thing; - return formlyExpressionParserOrFormatterFunction; + formlyExpressionParserOrFormatterFunction[originalThingProp] = thing + return formlyExpressionParserOrFormatterFunction function formlyExpressionParserOrFormatterFunction($viewValue) { - const $modelValue = scope.options.value(); - return formlyUtil.formlyEval(scope, thing, $modelValue, $viewValue); + const $modelValue = scope.options.value() + return formlyUtil.formlyEval(scope, thing, $modelValue, $viewValue) } } @@ -457,46 +547,46 @@ function formlyField($http, $q, $compile, $templateCache, $interpolate, formlyCo function callLinkFunctions() { if (type && type.link) { - type.link.apply(thusly, args); + type.link.apply(thusly, args) } if (scope.options.link) { - scope.options.link.apply(thusly, args); + scope.options.link.apply(thusly, args) } } function runManipulators(manipulators) { return function runManipulatorsOnTemplate(templateToManipulate) { - let chain = $q.when(templateToManipulate); + let chain = $q.when(templateToManipulate) angular.forEach(manipulators, manipulator => { chain = chain.then(template => { return $q.when(manipulator(template, scope.options, scope)).then(newTemplate => { - return angular.isString(newTemplate) ? newTemplate : asHtml(newTemplate); - }); - }); - }); - return chain; - }; + return angular.isString(newTemplate) ? newTemplate : asHtml(newTemplate) + }) + }) + }) + return chain + } } } // sort-of stateless util functions function asHtml(el) { - const wrapper = angular.element(''); - return wrapper.append(el).html(); + const wrapper = angular.element('') + return wrapper.append(el).html() } function getFieldType(options) { - return options.type && formlyConfig.getType(options.type); + return options.type && formlyConfig.getType(options.type) } function getManipulators(options, formOptions) { - let preWrapper = []; - let postWrapper = []; - addManipulators(options.templateManipulators); - addManipulators(formOptions.templateManipulators); - addManipulators(formlyConfig.templateManipulators); - return {preWrapper, postWrapper}; + let preWrapper = [] + let postWrapper = [] + addManipulators(options.templateManipulators) + addManipulators(formOptions.templateManipulators) + addManipulators(formlyConfig.templateManipulators) + return {preWrapper, postWrapper} function addManipulators(manipulators) { /* eslint-disable */ // it doesn't understand this :-( @@ -510,38 +600,38 @@ function formlyField($http, $q, $compile, $templateCache, $interpolate, formlyCo function getFieldTemplate(options) { function fromOptionsOrType(key, fieldType) { if (angular.isDefined(options[key])) { - return options[key]; + return options[key] } else if (fieldType && angular.isDefined(fieldType[key])) { - return fieldType[key]; + return fieldType[key] } } - const type = formlyConfig.getType(options.type, true, options); - const template = fromOptionsOrType('template', type); - const templateUrl = fromOptionsOrType('templateUrl', type); + const type = formlyConfig.getType(options.type, true, options) + const template = fromOptionsOrType('template', type) + const templateUrl = fromOptionsOrType('templateUrl', type) if (angular.isUndefined(template) && !templateUrl) { throw formlyUsability.getFieldError( 'type-type-has-no-template', `Type '${options.type}' has no template. On element:`, options - ); + ) } - return getTemplate(templateUrl || template, angular.isUndefined(template), options); + return getTemplate(templateUrl || template, angular.isUndefined(template), options) } function getTemplate(template, isUrl, options) { - let templatePromise; + let templatePromise if (angular.isFunction(template)) { - templatePromise = $q.when(template(options)); + templatePromise = $q.when(template(options)) } else { - templatePromise = $q.when(template); + templatePromise = $q.when(template) } if (!isUrl) { - return templatePromise; + return templatePromise } else { - const httpOptions = {cache: $templateCache}; + const httpOptions = {cache: $templateCache} return templatePromise .then((url) => $http.get(url, httpOptions)) .then((response) => response.data) @@ -550,142 +640,142 @@ function formlyField($http, $q, $compile, $templateCache, $interpolate, formlyCo 'problem-loading-template-for-templateurl', 'Problem loading template for ' + template, error - ); - }); + ) + }) } } function transcludeInWrappers(options, formOptions) { - const wrapper = getWrapperOption(options, formOptions); + const wrapper = getWrapperOption(options, formOptions) return function transcludeTemplate(template) { if (!wrapper.length) { - return $q.when(template); + return $q.when(template) } wrapper.forEach((aWrapper) => { - formlyUsability.checkWrapper(aWrapper, options); - runApiCheck(aWrapper, options); - }); - const promises = wrapper.map(w => getTemplate(w.template || w.templateUrl, !w.template)); + formlyUsability.checkWrapper(aWrapper, options) + runApiCheck(aWrapper, options) + }) + const promises = wrapper.map(w => getTemplate(w.template || w.templateUrl, !w.template)) return $q.all(promises).then(wrappersTemplates => { wrappersTemplates.forEach((wrapperTemplate, index) => { - formlyUsability.checkWrapperTemplate(wrapperTemplate, wrapper[index]); - }); - wrappersTemplates.reverse(); // wrapper 0 is wrapped in wrapper 1 and so on... - let totalWrapper = wrappersTemplates.shift(); + formlyUsability.checkWrapperTemplate(wrapperTemplate, wrapper[index]) + }) + wrappersTemplates.reverse() // wrapper 0 is wrapped in wrapper 1 and so on... + let totalWrapper = wrappersTemplates.shift() wrappersTemplates.forEach(wrapperTemplate => { - totalWrapper = doTransclusion(totalWrapper, wrapperTemplate); - }); - return doTransclusion(totalWrapper, template); - }); - }; + totalWrapper = doTransclusion(totalWrapper, wrapperTemplate) + }) + return doTransclusion(totalWrapper, template) + }) + } } function doTransclusion(wrapper, template) { - const superWrapper = angular.element(''); // this allows people not have to have a single root in wrappers - superWrapper.append(wrapper); - let transcludeEl = superWrapper.find('formly-transclude'); + const superWrapper = angular.element('') // this allows people not have to have a single root in wrappers + superWrapper.append(wrapper) + let transcludeEl = superWrapper.find('formly-transclude') if (!transcludeEl.length) { // try it using our custom find function - transcludeEl = formlyUtil.findByNodeName(superWrapper, 'formly-transclude'); + transcludeEl = formlyUtil.findByNodeName(superWrapper, 'formly-transclude') } - transcludeEl.replaceWith(template); - return superWrapper.html(); + transcludeEl.replaceWith(template) + return superWrapper.html() } function getWrapperOption(options, formOptions) { /* eslint complexity:[2, 6] */ - let wrapper = options.wrapper; + let wrapper = options.wrapper // explicit null means no wrapper if (wrapper === null) { - return []; + return [] } // nothing specified means use the default wrapper for the type if (!wrapper) { // get all wrappers that specify they apply to this type - wrapper = arrayify(formlyConfig.getWrapperByType(options.type)); + wrapper = arrayify(formlyConfig.getWrapperByType(options.type)) } else { - wrapper = arrayify(wrapper).map(formlyConfig.getWrapper); + wrapper = arrayify(wrapper).map(formlyConfig.getWrapper) } // get all wrappers for that the type specified that it uses. - const type = formlyConfig.getType(options.type, true, options); + const type = formlyConfig.getType(options.type, true, options) if (type && type.wrapper) { - const typeWrappers = arrayify(type.wrapper).map(formlyConfig.getWrapper); - wrapper = wrapper.concat(typeWrappers); + const typeWrappers = arrayify(type.wrapper).map(formlyConfig.getWrapper) + wrapper = wrapper.concat(typeWrappers) } // add form wrappers if (formOptions.wrapper) { - const formWrappers = arrayify(formOptions.wrapper).map(formlyConfig.getWrapper); - wrapper = wrapper.concat(formWrappers); + const formWrappers = arrayify(formOptions.wrapper).map(formlyConfig.getWrapper) + wrapper = wrapper.concat(formWrappers) } // add the default wrapper last - const defaultWrapper = formlyConfig.getWrapper(); + const defaultWrapper = formlyConfig.getWrapper() if (defaultWrapper) { - wrapper.push(defaultWrapper); + wrapper.push(defaultWrapper) } - return wrapper; + return wrapper } function checkApi(options) { formlyApiCheck.throw(formlyApiCheck.formlyFieldOptions, options, { prefix: 'formly-field directive', - url: 'formly-field-directive-validation-failed' - }); + url: 'formly-field-directive-validation-failed', + }) // validate with the type - const type = options.type && formlyConfig.getType(options.type); + const type = options.type && formlyConfig.getType(options.type) if (type) { - runApiCheck(type, options, true); + runApiCheck(type, options, true) } if (options.expressionProperties && options.expressionProperties.hide) { formlyWarn( 'dont-use-expressionproperties.hide-use-hideexpression-instead', 'You have specified `hide` in `expressionProperties`. Use `hideExpression` instead', options - ); + ) } } function checkFieldGroupApi(options) { formlyApiCheck.throw(formlyApiCheck.fieldGroup, options, { prefix: 'formly-field directive', - url: 'formly-field-directive-validation-failed' - }); + url: 'formly-field-directive-validation-failed', + }) } function runApiCheck({apiCheck, apiCheckInstance, apiCheckFunction, apiCheckOptions}, options, forType) { - runApiCheckForType(apiCheck, apiCheckInstance, apiCheckFunction, apiCheckOptions, options); + runApiCheckForType(apiCheck, apiCheckInstance, apiCheckFunction, apiCheckOptions, options) if (forType && options.type) { angular.forEach(formlyConfig.getTypeHeritage(options.type), function(type) { - runApiCheckForType(type.apiCheck, type.apiCheckInstance, type.apiCheckFunction, type.apiCheckOptions, options); - }); + runApiCheckForType(type.apiCheck, type.apiCheckInstance, type.apiCheckFunction, type.apiCheckOptions, options) + }) } } function runApiCheckForType(apiCheck, apiCheckInstance, apiCheckFunction, apiCheckOptions, options) { /* eslint complexity:[2, 9] */ if (!apiCheck) { - return; + return } - const instance = apiCheckInstance || formlyConfig.extras.apiCheckInstance || formlyApiCheck; + const instance = apiCheckInstance || formlyConfig.extras.apiCheckInstance || formlyApiCheck if (instance.config.disabled || apiCheckFactory.globalConfig.disabled) { - return; + return } - const fn = apiCheckFunction || 'warn'; + const fn = apiCheckFunction || 'warn' // this is the new API - const checkerObjects = apiCheck(instance); + const checkerObjects = apiCheck(instance) angular.forEach(checkerObjects, (shape, name) => { - const checker = instance.shape(shape); + const checker = instance.shape(shape) const checkOptions = angular.extend({ prefix: `formly-field type ${options.type} for property ${name}`, - url: formlyApiCheck.config.output.docsBaseUrl + 'formly-field-type-apicheck-failed' - }, apiCheckOptions); - instance[fn](checker, options[name], checkOptions); - }); + url: formlyApiCheck.config.output.docsBaseUrl + 'formly-field-type-apicheck-failed', + }, apiCheckOptions) + instance[fn](checker, options[name], checkOptions) + }) } @@ -694,9 +784,9 @@ function formlyField($http, $q, $compile, $templateCache, $interpolate, formlyCo // Stateless util functions function getDefaultOptionsOptionsTypes(type) { - return getDefaultOptionsProperty(type, 'optionsTypes', []); + return getDefaultOptionsProperty(type, 'optionsTypes', []) } function getDefaultOptionsProperty(type, prop, defaultValue) { - return type.defaultOptions && type.defaultOptions[prop] || defaultValue; + return type.defaultOptions && type.defaultOptions[prop] || defaultValue } diff --git a/src/directives/formly-field.test.js b/src/directives/formly-field.test.js index a4afd297..8daad3d1 100644 --- a/src/directives/formly-field.test.js +++ b/src/directives/formly-field.test.js @@ -1,26 +1,26 @@ /* eslint no-shadow:0 */ /* eslint max-statements:[2, 50] */ /* eslint max-len:0 */ -import angular from 'angular-fix'; -import apiCheck from 'api-check'; -import testUtils from '../test.utils.js'; -import _ from 'lodash'; +import angular from 'angular-fix' +import apiCheck from 'api-check' +import testUtils from '../test.utils.js' +import _ from 'lodash' -const {getNewField, input, basicForm, multiNgModelField, shouldWarn, shouldNotWarn} = testUtils; +const {getNewField, input, basicForm, multiNgModelField, shouldWarn, shouldNotWarn} = testUtils describe('formly-field', function() { /* jshint maxstatements:100 */ /* jshint maxlen:300 */ - let $compile, scope, el, node, formlyConfig, $q, isolateScope, field, $timeout; + let $compile, scope, el, node, formlyConfig, $q, isolateScope, field, $timeout - beforeEach(window.module('formly')); + beforeEach(window.module('formly')) beforeEach(inject((_$compile_, $rootScope, _formlyConfig_, _$q_, _$timeout_) => { - $compile = _$compile_; - scope = $rootScope.$new(); - formlyConfig = _formlyConfig_; - $q = _$q_; - $timeout = _$timeout_; - })); + $compile = _$compile_ + scope = $rootScope.$new() + formlyConfig = _formlyConfig_ + $q = _$q_ + $timeout = _$timeout_ + })) describe('with template wrapper', function() { beforeEach(() => { @@ -32,7 +32,7 @@ describe('formly-field', function() { - ` + `, }, { types: 'other', @@ -43,14 +43,14 @@ describe('formly-field', function() { This is great for ng-messages - ` - } - ]); + `, + }, + ]) formlyConfig.setType({ - name: 'text', template: `` - }); - scope.model = {}; - }); + name: 'text', template: ``, + }) + scope.model = {} + }) it('should take the entire wrapper, not just the contents of the wrapper', function() { scope.fields = [ @@ -58,13 +58,13 @@ describe('formly-field', function() { type: 'text', key: 'text', templateOptions: { - label: 'Text input' - } - } - ]; - const el = compileAndDigest(); - expect(el[0].querySelector('.my-template-wrapper')).to.exist; - }); + label: 'Text input', + }, + }, + ] + const el = compileAndDigest() + expect(el[0].querySelector('.my-template-wrapper')).to.exist + }) it('should wrap arrays of wrappers', () => { scope.fields = [ @@ -73,15 +73,15 @@ describe('formly-field', function() { key: 'text', wrapper: ['text', 'other'], templateOptions: { - label: 'Text input' - } - } - ]; - const el = compileAndDigest(); - const outerEl = el[0].querySelector('.my-other-template-wrapper'); - expect(outerEl).to.exist; - expect(outerEl.querySelector('.my-template-wrapper')).to.exist; - }); + label: 'Text input', + }, + }, + ] + const el = compileAndDigest() + const outerEl = el[0].querySelector('.my-other-template-wrapper') + expect(outerEl).to.exist + expect(outerEl.querySelector('.my-template-wrapper')).to.exist + }) it(`should allow for specifying null for the wrappers of a field`, () => { scope.fields = [ @@ -90,524 +90,648 @@ describe('formly-field', function() { key: 'text', wrapper: null, templateOptions: { - label: 'Text input' - } - } - ]; - const el = compileAndDigest(); - expect(el[0].querySelector('.my-template-wrapper')).to.not.exist; - }); + label: 'Text input', + }, + }, + ] + const el = compileAndDigest() + expect(el[0].querySelector('.my-template-wrapper')).to.not.exist + }) - }); + }) describe('api check', () => { beforeEach(() => { /* eslint no-console:0 */ - const originalWarn = console.warn; - console.warn = () => {}; + const originalWarn = console.warn + console.warn = () => {} formlyConfig.setType({ - name: 'text', template: `` - }); - scope.model = {}; - console.warn = originalWarn; - }); + name: 'text', template: ``, + }) + scope.model = {} + console.warn = originalWarn + }) it('should throw an error when a field has extra properties', () => { scope.fields = [ { type: 'text', - extraProp: 'whatever' - } - ]; + extraProp: 'whatever', + }, + ] - expect(() => compileAndDigest()).to.throw(/extra.*properties.*extraProp/); - }); - }); + expect(() => compileAndDigest()).to.throw(/extra.*properties.*extraProp/) + }) + }) describe('default type options', () => { beforeEach(() => { - scope.model = {}; + scope.model = {} formlyConfig.setType({ name: 'ipAddress', template: '', defaultOptions: { data: { - usingDefaultOptions: true + usingDefaultOptions: true, }, validators: { ipAddress: function(viewValue, modelValue) { - const value = modelValue || viewValue; - return /(\d{1,3}\.){3}\d{1,3}/.test(value); - } - } - } - }); + const value = modelValue || viewValue + return /(\d{1,3}\.){3}\d{1,3}/.test(value) + }, + }, + }, + }) formlyConfig.setType({ name: 'text', template: '', defaultOptions: { data: { - hasPropertiesFromTextType: true - } - } - }); + hasPropertiesFromTextType: true, + }, + }, + }) formlyConfig.setType({ name: 'phone', defaultOptions: { ngModelAttrs: { '/^1[2-9]\\d{2}[2-9]\\d{6}$/': { - value: 'ng-pattern' - } - } - } - }); + value: 'ng-pattern', + }, + }, + }, + }) formlyConfig.setType({ name: 'required', defaultOptions: { ngModelAttrs: { '/overwriting stuff is fun for tests/': { - value: 'ng-pattern' + value: 'ng-pattern', }, required: { bound: 'ng-required', - attribute: 'required' + attribute: 'required', }, myChange: { - statement: 'ng-change' - } + statement: 'ng-change', + }, }, templateOptions: { - required: true - } - } - }); - }); + required: true, + }, + }, + }) + }) it('should default to the ipAddress type options', () => { - const field = {type: 'ipAddress'}; - scope.fields = [field]; - compileAndDigest(); - expect(field.data.usingDefaultOptions).to.be.true; - expect(field.validators.ipAddress).to.be.a('function'); - }); + const field = {type: 'ipAddress'} + scope.fields = [field] + compileAndDigest() + expect(field.data.usingDefaultOptions).to.be.true + expect(field.validators.ipAddress).to.be.a('function') + }) it('should be possible to specify defaultOptions-only types (non-template types)', () => { const field = { - type: 'text', optionsTypes: ['phone', 'required'], templateOptions: {myChange: 'model.otherThing = true'} - }; - scope.fields = [field]; - const el = compileAndDigest(); - const input = el.find('input'); - expect(field.data.hasPropertiesFromTextType).to.be.true; - expect(input.attr('ng-pattern')).to.equal('/overwriting stuff is fun for tests/'); - expect(input.attr('ng-change')).to.contain('myChange'); - }); - }); + type: 'text', optionsTypes: ['phone', 'required'], templateOptions: {myChange: 'model.otherThing = true'}, + } + scope.fields = [field] + const el = compileAndDigest() + const input = el.find('input') + expect(field.data.hasPropertiesFromTextType).to.be.true + expect(input.attr('ng-pattern')).to.equal('/overwriting stuff is fun for tests/') + expect(input.attr('ng-change')).to.contain('myChange') + }) + }) describe('templateManipulators', () => { - testTemplateManipulators(true); - testTemplateManipulators(false); + testTemplateManipulators(true) + testTemplateManipulators(false) function testTemplateManipulators(isPre) { describe(isPre ? 'preWrapper' : 'postWrapper', () => { - let manipulators; - const textTemplate = ''; + let manipulators + const textTemplate = '' beforeEach(() => { - manipulators = formlyConfig.templateManipulators[isPre ? 'preWrapper' : 'postWrapper']; + manipulators = formlyConfig.templateManipulators[isPre ? 'preWrapper' : 'postWrapper'] formlyConfig.setWrapper([ { types: 'text', - template: '
' - } - ]); + template: '
', + }, + ]) formlyConfig.setType({ - name: 'text', template: textTemplate - }); - scope.model = {}; + name: 'text', template: textTemplate, + }) + scope.model = {} scope.fields = [ - {type: 'text'} - ]; - }); + {type: 'text'}, + ] + }) - const when = isPre ? 'before' : 'after'; + const when = isPre ? 'before' : 'after' it(`should call the manipulators when compiling a field ${when} the element is wrapped in wrappers`, () => { - let manipulatedTemplate; + let manipulatedTemplate manipulators.push((templateToManipulate, fieldOptions, scope) => { if (isPre) { - expect(templateToManipulate).to.contain('text-template'); + expect(templateToManipulate).to.contain('text-template') } - expect(fieldOptions).to.equal(scope.fields[0]); - expect(scope.options).to.equal(fieldOptions); + expect(fieldOptions).to.equal(scope.fields[0]) + expect(scope.options).to.equal(fieldOptions) if (isPre) { - expect(templateToManipulate).to.not.contain('my-template-wrapper'); + expect(templateToManipulate).to.not.contain('my-template-wrapper') } else { - expect(templateToManipulate).to.contain('my-template-wrapper'); + expect(templateToManipulate).to.contain('my-template-wrapper') } - manipulatedTemplate = angular.element(templateToManipulate).addClass('manipulated'); - return manipulatedTemplate; - }); + manipulatedTemplate = angular.element(templateToManipulate).addClass('manipulated') + return manipulatedTemplate + }) manipulators.push((templateToManipulate, fieldOptions, scope) => { if (isPre) { - expect(asHtml(manipulatedTemplate)).to.equal(templateToManipulate); + expect(asHtml(manipulatedTemplate)).to.equal(templateToManipulate) } - expect(fieldOptions).to.equal(scope.fields[0]); - expect(scope.options).to.equal(fieldOptions); + expect(fieldOptions).to.equal(scope.fields[0]) + expect(scope.options).to.equal(fieldOptions) if (isPre) { - expect(templateToManipulate).to.not.contain('my-template-wrapper'); + expect(templateToManipulate).to.not.contain('my-template-wrapper') } else { - expect(templateToManipulate).to.contain('my-template-wrapper'); + expect(templateToManipulate).to.contain('my-template-wrapper') } - expect(templateToManipulate).to.contain('manipulated'); - return angular.element(templateToManipulate).addClass('manipulated-twice'); - }); - compileAndDigest(); - scope.$digest(); - expect(el[0].querySelector('.manipulated')).to.exist; - expect(el[0].querySelector('.manipulated-twice')).to.exist; + expect(templateToManipulate).to.contain('manipulated') + return angular.element(templateToManipulate).addClass('manipulated-twice') + }) + compileAndDigest() + scope.$digest() + expect(el[0].querySelector('.manipulated')).to.exist + expect(el[0].querySelector('.manipulated-twice')).to.exist function asHtml(el) { - return angular.element('').append(el).html(); + return angular.element('').append(el).html() } - }); - }); + }) + }) } - }); + }) describe('type controllers and link functions', () => { - let controllerFn, linkFn; + let controllerFn, linkFn beforeEach(() => { controllerFn = function($scope) { - $scope.setInTypeController = true; - }; + $scope.setInTypeController = true + } linkFn = function(scope, el, attrs) { - scope.setInTypeLink = true; - scope.el = el; - scope.attrs = attrs; - }; + scope.setInTypeLink = true + scope.el = el + scope.attrs = attrs + } formlyConfig.setType({ name: 'text', template: ``, controller: ['$scope', controllerFn], - link: linkFn - }); - scope.model = {}; - }); + link: linkFn, + }) + scope.model = {} + }) it('should run the controller function of a type', () => { scope.fields = [ - {type: 'text'} - ]; - const el = compileAndDigest(); - const fieldEl = angular.element(el[0].querySelector('.formly-field')); - expect(fieldEl.isolateScope().setInTypeController).to.be.true; - }); + {type: 'text'}, + ] + const el = compileAndDigest() + const fieldEl = angular.element(el[0].querySelector('.formly-field')) + expect(fieldEl.isolateScope().setInTypeController).to.be.true + }) it('should run the link function of a type', () => { scope.fields = [ - {type: 'text'} - ]; - const el = compileAndDigest(); - const fieldEl = angular.element(el[0].querySelector('.formly-field')); - const fieldScope = fieldEl.isolateScope(); - expect(fieldScope.setInTypeLink).to.be.true; - expect(fieldScope.el[0]).to.equal(fieldEl[0]); - }); + {type: 'text'}, + ] + const el = compileAndDigest() + const fieldEl = angular.element(el[0].querySelector('.formly-field')) + const fieldScope = fieldEl.isolateScope() + expect(fieldScope.setInTypeLink).to.be.true + expect(fieldScope.el[0]).to.equal(fieldEl[0]) + }) it('should run the controller of the specific field', () => { scope.fields = [ - {template: 'sweet mercy', controller: ['$scope', controllerFn]} - ]; + {template: 'sweet mercy', controller: ['$scope', controllerFn]}, + ] - const el = compileAndDigest(); - const fieldEl = angular.element(el[0].querySelector('.formly-field')); - expect(fieldEl.isolateScope().setInTypeController).to.be.true; - }); + const el = compileAndDigest() + const fieldEl = angular.element(el[0].querySelector('.formly-field')) + expect(fieldEl.isolateScope().setInTypeController).to.be.true + }) it('should run the link function of a type', () => { scope.fields = [ - {template: 'sweet mercy', link: linkFn} - ]; - const el = compileAndDigest(); - const fieldEl = angular.element(el[0].querySelector('.formly-field')); - const fieldScope = fieldEl.isolateScope(); - expect(fieldScope.setInTypeLink).to.be.true; - expect(fieldScope.el[0]).to.equal(fieldEl[0]); - }); - }); + {template: 'sweet mercy', link: linkFn}, + ] + const el = compileAndDigest() + const fieldEl = angular.element(el[0].querySelector('.formly-field')) + const fieldScope = fieldEl.isolateScope() + expect(fieldScope.setInTypeLink).to.be.true + expect(fieldScope.el[0]).to.equal(fieldEl[0]) + }) + }) describe(`template and templateUrl properties`, () => { - let $templateCache; - const expectedTemplateText = 'sweet mercy'; + let $templateCache + const expectedTemplateText = 'sweet mercy' beforeEach(inject((_$templateCache_) => { - $templateCache = _$templateCache_; - $templateCache.put('templateUrlTest.html', expectedTemplateText); - })); + $templateCache = _$templateCache_ + $templateCache.put('templateUrlTest.html', expectedTemplateText) + })) it('should allow template property to be a function', () => { scope.fields = [ { template: function(options) { - expect(options).to.eq(scope.fields[0]); - return expectedTemplateText; - } - } - ]; + expect(options).to.eq(scope.fields[0]) + return expectedTemplateText + }, + }, + ] - const el = compileAndDigest(); + const el = compileAndDigest() - const fieldEl = angular.element(el[0].querySelector('.formly-field')); - expect(fieldEl.text()).to.equal(expectedTemplateText); - }); + const fieldEl = angular.element(el[0].querySelector('.formly-field')) + expect(fieldEl.text()).to.equal(expectedTemplateText) + }) it(`should allow template property to be a function that returns a promise`, () => { scope.fields = [ { template: function(options) { - expect(options).to.eq(scope.fields[0]); - return $q.when(expectedTemplateText); - } - } - ]; + expect(options).to.eq(scope.fields[0]) + return $q.when(expectedTemplateText) + }, + }, + ] - const el = compileAndDigest(); + const el = compileAndDigest() - const fieldEl = angular.element(el[0].querySelector('.formly-field')); - expect(fieldEl.text()).to.equal(expectedTemplateText); - }); + const fieldEl = angular.element(el[0].querySelector('.formly-field')) + expect(fieldEl.text()).to.equal(expectedTemplateText) + }) it('should allow template property to be a string', () => { scope.fields = [ - {template: expectedTemplateText} - ]; + {template: expectedTemplateText}, + ] - const el = compileAndDigest(); + const el = compileAndDigest() - const fieldEl = angular.element(el[0].querySelector('.formly-field')); - expect(fieldEl.text()).to.equal(expectedTemplateText); - }); + const fieldEl = angular.element(el[0].querySelector('.formly-field')) + expect(fieldEl.text()).to.equal(expectedTemplateText) + }) it('should allow template property to be an empty string', () => { scope.fields = [ {template: ''}, - ]; + ] - const el = compileAndDigest(); + const el = compileAndDigest() - const fieldEl = angular.element(el[0].querySelector('.formly-field')); - expect(fieldEl.text()).to.equal(''); - }); + const fieldEl = angular.element(el[0].querySelector('.formly-field')) + expect(fieldEl.text()).to.equal('') + }) it('should allow templateUrl property to be a function', () => { scope.fields = [ { templateUrl: function(options) { - expect(options).to.eq(scope.fields[0]); - return 'templateUrlTest.html'; - } - } - ]; + expect(options).to.eq(scope.fields[0]) + return 'templateUrlTest.html' + }, + }, + ] - const el = compileAndDigest(); + const el = compileAndDigest() - const fieldEl = angular.element(el[0].querySelector('.formly-field')); - expect(fieldEl.text()).to.equal(expectedTemplateText); - }); + const fieldEl = angular.element(el[0].querySelector('.formly-field')) + expect(fieldEl.text()).to.equal(expectedTemplateText) + }) it('should allow templateUrl property to be a function that returns a promise', () => { scope.fields = [ { templateUrl: function(options) { - expect(options).to.eq(scope.fields[0]); - return $q.when('templateUrlTest.html'); - } - } - ]; + expect(options).to.eq(scope.fields[0]) + return $q.when('templateUrlTest.html') + }, + }, + ] - const el = compileAndDigest(); + const el = compileAndDigest() - const fieldEl = angular.element(el[0].querySelector('.formly-field')); - expect(fieldEl.text()).to.equal(expectedTemplateText); - }); + const fieldEl = angular.element(el[0].querySelector('.formly-field')) + expect(fieldEl.text()).to.equal(expectedTemplateText) + }) it('should allow templateUrl property to be a string', () => { scope.fields = [ - {templateUrl: 'templateUrlTest.html'} - ]; + {templateUrl: 'templateUrlTest.html'}, + ] - const el = compileAndDigest(); + const el = compileAndDigest() - const fieldEl = angular.element(el[0].querySelector('.formly-field')); - expect(fieldEl.text()).to.equal(expectedTemplateText); - }); - }); + const fieldEl = angular.element(el[0].querySelector('.formly-field')) + expect(fieldEl.text()).to.equal(expectedTemplateText) + }) + }) describe(`defaultValue`, () => { - const key = 'foo'; - const defaultValue = '~=[,,_,,]:3'; + const key = 'foo' + const defaultValue = '~=[,,_,,]:3' beforeEach(() => { scope.fields = [ - {template: input, key, defaultValue} - ]; - scope.model = {}; - }); + {template: input, key, defaultValue}, + ] + scope.model = {} + }) it(`should default the model's value to the specified value if it is not defined`, () => { - compileAndDigest(); - expect(scope.model[key]).to.equal(defaultValue); - }); + compileAndDigest() + expect(scope.model[key]).to.equal(defaultValue) + }) it(`should not have a problem if the model starts out as undefined`, () => { - scope.model = undefined; - compileAndDigest(); - expect(scope.model[key]).to.equal(defaultValue); - }); + scope.model = undefined + compileAndDigest() + expect(scope.model[key]).to.equal(defaultValue) + }) it(`should not change the model's value if the specified value is defined`, () => { - const presetValue = 'ಠ_ರೃ'; - scope.model[key] = presetValue; + const presetValue = 'ಠ_ರೃ' + scope.model[key] = presetValue - compileAndDigest(); - expect(scope.model[key]).to.equal(presetValue); - }); + compileAndDigest() + expect(scope.model[key]).to.equal(presetValue) + }) it(`should be exactly equal to a non-primative`, () => { - const complexDefaultValue = {foo: 'bar'}; - scope.fields[0].defaultValue = complexDefaultValue; + const complexDefaultValue = {foo: 'bar'} + scope.fields[0].defaultValue = complexDefaultValue - compileAndDigest(); - expect(scope.model[key]).to.eq(complexDefaultValue); - }); + compileAndDigest() + expect(scope.model[key]).to.eq(complexDefaultValue) + }) it(`should be set even if the defaultValue is falsy`, () => { - const falsyValue = 0; - scope.fields[0].defaultValue = falsyValue; + const falsyValue = 0 + scope.fields[0].defaultValue = falsyValue - compileAndDigest(); - expect(scope.model[key]).to.eq(falsyValue); - }); + compileAndDigest() + expect(scope.model[key]).to.eq(falsyValue) + }) it(`should be set as the initialValue`, () => { - compileAndDigest(); + compileAndDigest() + + expect(scope.fields[0].initialValue).to.eq(defaultValue) + }) - expect(scope.fields[0].initialValue).to.eq(defaultValue); - }); + it(`should be set if the key is 0`, () => { + scope.fields[0].key = 0 + compileAndDigest() + + expect(scope.fields[0].initialValue).to.eq(defaultValue) + }) describe(`nested keys`, () => { - const nestedObject = 'foo.bar'; - const nestedArray = 'baz[0]'; + const nestedObject = 'foo.bar' + const nestedArray = 'baz[0]' beforeEach(() => { - const firstField = scope.fields[0]; - firstField.key = nestedObject; + const firstField = scope.fields[0] + firstField.key = nestedObject - const secondField = {template: input, key: nestedArray, defaultValue}; - scope.fields.push(secondField); - }); + const secondField = {template: input, key: nestedArray, defaultValue} + scope.fields.push(secondField) + }) it(`should set the default value for nested keys`, () => { - compileAndDigest(); - expect(scope.model.foo.bar).to.equal(defaultValue); - expect(scope.model.baz[0]).to.equal(defaultValue); - }); - }); - }); + compileAndDigest() + expect(scope.model.foo.bar).to.equal(defaultValue) + expect(scope.model.baz[0]).to.equal(defaultValue) + }) + }) + }) + + describe('getterSetters', () => { + it('should get and set values when key is all alpha', () => { + const key = 'foo' + const defaultValue = 'bar' + + scope.fields = [ + {template: input, key, defaultValue}, + ] + scope.model = {} + + compileAndDigest() + expect(scope.model[key]).to.eq(defaultValue) + }) + + it('should get and set values when key is all numeric', () => { + const key = '1333' + const defaultValue = 'bar' + + scope.fields = [ + {template: input, key, defaultValue}, + ] + scope.model = {} + + compileAndDigest() + expect(scope.model[key]).to.eq(defaultValue) + }) + + it('should get and set values when key is 0', () => { + const key = 0 + const defaultValue = 'bar' + + scope.fields = [ + {template: input, key, defaultValue}, + ] + scope.model = {} + + compileAndDigest() + expect(scope.model[key]).to.eq(defaultValue) + }) + + it('should handle arrays properly when formlyConfig.extras.parseKeyArrays is set', () => { + const key = 'foo[0]' + const defaultValue = 'bar' + + formlyConfig.extras.parseKeyArrays = true + scope.fields = [ + {template: input, key, defaultValue}, + ] + scope.model = {} + + compileAndDigest() + expect(scope.model.foo).to.be.instanceof(Array) + }) + + it('should get and set values when key is alpha numeric with alpha first', () => { + const key = 'A1' + const defaultValue = 'bar' + + scope.fields = [ + {template: input, key, defaultValue}, + ] + scope.model = {} + + compileAndDigest() + expect(scope.model[key]).to.eq(defaultValue) + }) + + it('should get and set values when key is alpha numeric with numeric first', () => { + const key = '1A' + const defaultValue = 'bar' + + scope.fields = [ + {template: input, key, defaultValue}, + ] + scope.model = {} + + compileAndDigest() + expect(scope.model[key]).to.eq(defaultValue) + }) + + it('should work with dashes in the key', () => { + const key = 'address-1st-line' + const defaultValue = 'baz' + scope.fields = [ + {template: input, key, defaultValue}, + ] + scope.model = {} + + compileAndDigest() + expect(scope.model[key]).to.eq(defaultValue) + }) + + it('should work with dashes and numerics in the key', () => { + const key = 'b141c66a-2857-4196-847b-b2096fa6170d' + const defaultValue = 'baz' + scope.fields = [ + {template: input, key, defaultValue}, + ] + scope.model = {} + + compileAndDigest() + expect(scope.model[key]).to.eq(defaultValue) + }) + + it('should work with nested keys with numbers in the key', () => { + const key = 'foo3bar.baz4foobar' + const defaultValue = 'baz' + scope.fields = [ + {template: input, key, defaultValue}, + ] + scope.model = {} + + compileAndDigest() + expect(scope.model.foo3bar.baz4foobar).to.eq(defaultValue) + }) + }) describe(`id property`, () => { it(`should default to a semi-random id that you cannot rely on and don't have to think about`, () => { - scope.fields = [getNewField()]; - compileAndDigest(); - const fieldNode = getFieldNgModelNode(); - expect(field.id).to.eq(fieldNode.id); - expect(fieldNode.id).to.match(/^formly_\d+_template_\d+_\d+$/); - expect(fieldNode.id).to.eq(fieldNode.getAttribute('name')); - }); + scope.fields = [getNewField()] + compileAndDigest() + const fieldNode = getFieldNgModelNode() + expect(field.id).to.eq(fieldNode.id) + expect(fieldNode.id).to.match(/^formly_\d+_template_\d+_\d+$/) + expect(fieldNode.id).to.eq(fieldNode.getAttribute('name')) + }) it(`should allow you to specify a custom id if you want to`, () => { - scope.fields = [getNewField({id: 'ᕕ( ᐛ )ᕗ'})]; - compileAndDigest(); - const fieldNode = getFieldNgModelNode(); - expect(field.id).to.eq(fieldNode.id); - expect(fieldNode.id).to.eq('ᕕ( ᐛ )ᕗ'); - expect(fieldNode.id).to.eq(fieldNode.getAttribute('name')); - }); - }); + scope.fields = [getNewField({id: 'ᕕ( ᐛ )ᕗ'})] + compileAndDigest() + const fieldNode = getFieldNgModelNode() + expect(field.id).to.eq(fieldNode.id) + expect(fieldNode.id).to.eq('ᕕ( ᐛ )ᕗ') + expect(fieldNode.id).to.eq(fieldNode.getAttribute('name')) + }) + }) describe(`modelOptions property`, () => { it(`should be able to handle modelOptions with debouce as a number`, () => { - scope.fields = [getNewField({modelOptions: {debounce: 500}})]; - compileAndDigest(); - const fieldNode = getFieldNgModelNode(); - expect(fieldNode.getAttribute('ng-model-options')).to.exist; - }); + scope.fields = [getNewField({modelOptions: {debounce: 500}})] + compileAndDigest() + const fieldNode = getFieldNgModelNode() + expect(fieldNode.getAttribute('ng-model-options')).to.exist + }) it(`should be able to compile modelOptions with debounce as an object of numbers`, () => { - scope.fields = [getNewField({modelOptions: {debounce: {blur: 500, default: 0}}})]; - compileAndDigest(); - const fieldNode = getFieldNgModelNode(); - expect(fieldNode.getAttribute('ng-model-options')).to.exist; - }); + scope.fields = [getNewField({modelOptions: {debounce: {blur: 500, default: 0}}})] + compileAndDigest() + const fieldNode = getFieldNgModelNode() + expect(fieldNode.getAttribute('ng-model-options')).to.exist + }) it(`should throw an error when modelOptions with debounce as a string`, () => { - scope.fields = [getNewField({modelOptions: {debounce: 'foo'}})]; - expect(() => compileAndDigest()).to.throw(); - }); + scope.fields = [getNewField({modelOptions: {debounce: 'foo'}})] + expect(() => compileAndDigest()).to.throw() + }) it(`should throw an error when modelOptions with debounce as an object of strings`, () => { - scope.fields = [getNewField({modelOptions: {debounce: {blur: 'foo', default: 'bar'}}})]; - expect(() => compileAndDigest()).to.throw(); - }); + scope.fields = [getNewField({modelOptions: {debounce: {blur: 'foo', default: 'bar'}}})] + expect(() => compileAndDigest()).to.throw() + }) describe(`value function`, () => { - let value; + let value it(`should be overrideable via value option`, () => { - compileDigestAndSetValueFunction({value: customGetterSetter}); - expect(value).to.eq(customGetterSetter); + compileDigestAndSetValueFunction({value: customGetterSetter}) + expect(value).to.eq(customGetterSetter) function customGetterSetter() { } - }); + }) it(`should be a getter/setter`, () => { - compileDigestAndSetValueFunction(); - expect(value()).to.eq(undefined); - expect(value('foo')).to.eq('foo'); - expect(value()).to.eq('foo'); - }); + compileDigestAndSetValueFunction() + expect(value()).to.eq(undefined) + expect(value('foo')).to.eq('foo') + expect(value()).to.eq('foo') + }) it(`should not throw an error when the model is undefined`, inject(($rootScope) => { const formlyField = `
-
`; - scope = $rootScope.$new(); + ` + scope = $rootScope.$new() _.assign(scope, { field: getNewField(), - model: undefined // <-- this is the key - }); - el = $compile(formlyField)(scope); - scope.$digest(); - expect(() => scope.field.value()).to.not.throw(); - })); + model: undefined, // <-- this is the key + }) + el = $compile(formlyField)(scope) + scope.$digest() + expect(() => scope.field.value()).to.not.throw() + })) function compileDigestAndSetValueFunction(fieldOverrides) { - scope.fields = [getNewField(_.merge({modelOptions: {getterSetter: true}}, fieldOverrides))]; - compileAndDigest(); - value = getIsolateScope().options.value; + scope.fields = [getNewField(_.merge({modelOptions: {getterSetter: true}}, fieldOverrides))] + compileAndDigest() + value = getIsolateScope().options.value } - }); + }) - }); + }) describe(`type apiCheck`, () => { - let inputType; - const type = 'input'; + let inputType + const type = 'input' beforeEach(() => { inputType = formlyConfig.setType({ name: type, @@ -616,57 +740,57 @@ describe('formly-field', function() { return { templateOptions: { label: check.string, - className: check.string - } - }; + className: check.string, + }, + } }, apiCheckInstance: apiCheck({ - output: {prefix: 'custom-api-check'} + output: {prefix: 'custom-api-check'}, }), apiCheckOptions: { - url: 'http://example.com/some-custom-url' - } - }); - scope.model = {}; - }); + url: 'http://example.com/some-custom-url', + }, + }) + scope.model = {} + }) it(`should default to the built-in formlyApiCheck`, inject((formlyApiCheck) => { const type = formlyConfig.setType({ name: 'someOtherType', template: '
', - apiCheck: sinon.spy() - }); - scope.fields = [{type: 'someOtherType'}]; - compileAndDigest(); - expect(type.apiCheck).to.have.been.calledWith(formlyApiCheck); - })); + apiCheck: sinon.spy(), + }) + scope.fields = [{type: 'someOtherType'}] + compileAndDigest() + expect(type.apiCheck).to.have.been.calledWith(formlyApiCheck) + })) it(`should not warn if everything's fine`, () => { scope.fields = [ - {type, templateOptions: {label: 'string', className: 'string'}} - ]; - shouldNotWarn(compileAndDigest); - }); + {type, templateOptions: {label: 'string', className: 'string'}}, + ] + shouldNotWarn(compileAndDigest) + }) it(`should warn if everything's not fine`, () => { scope.fields = [ - {type, templateOptions: {label: 'string'}} - ]; + {type, templateOptions: {label: 'string'}}, + ] shouldWarn( /custom-api-check formly-field type input for property templateOptions apiCheck failed.*?className.*?some-custom-url/, compileAndDigest - ); - }); + ) + }) it(`should throw if the apiCheckFunction is set to "throw" and everything's not fine`, () => { - formlyConfig.getType(type).apiCheckFunction = 'throw'; + formlyConfig.getType(type).apiCheckFunction = 'throw' scope.fields = [ - {type, templateOptions: {label: 'string'}} - ]; + {type, templateOptions: {label: 'string'}}, + ] expect(compileAndDigest).to.throw( /custom-api-check formly-field type input for property templateOptions apiCheck failed.*?.className.*?some-custom-url/ - ); - }); + ) + }) it(`should work with wrappers as well`, () => { formlyConfig.setWrapper({ @@ -675,75 +799,75 @@ describe('formly-field', function() { apiCheck(check) { return { templateOptions: { - foo: check.bool - } - }; + foo: check.bool, + }, + } }, apiCheckInstance: apiCheck({output: {prefix: 'my own'}}), - apiCheckOptions: {prefix: 'options prefix'} - }); + apiCheckOptions: {prefix: 'options prefix'}, + }) scope.fields = [ - {type, wrapper: 'mywrapper', templateOptions: {label: 'string', className: 'string'}} - ]; + {type, wrapper: 'mywrapper', templateOptions: {label: 'string', className: 'string'}}, + ] shouldWarn( /my own options prefix apiCheck failed/, compileAndDigest - ); - }); + ) + }) describe(`apiCheckInstance`, () => { describe(`disabled`, () => { it(`should not do anything if the given instance is disabled`, () => { - inputType.apiCheckInstance.config.disabled = true; + inputType.apiCheckInstance.config.disabled = true scope.fields = [ - {type, templateOptions: {label: 'string'}} - ]; - shouldNotWarn(compileAndDigest); - }); + {type, templateOptions: {label: 'string'}}, + ] + shouldNotWarn(compileAndDigest) + }) it(`should not do anything if no instance is provided and the formly instance is disabled`, inject((formlyApiCheck) => { - formlyApiCheck.config.disabled = true; + formlyApiCheck.config.disabled = true formlyConfig.setType({ name: 'someOtherType', template: '
', - apiCheck: checker => ({data: {foo: checker.bool}}) - }); - scope.fields = [{type: 'someOtherType'}]; - shouldNotWarn(compileAndDigest); - formlyApiCheck.config.disabled = false; - })); + apiCheck: checker => ({data: {foo: checker.bool}}), + }) + scope.fields = [{type: 'someOtherType'}] + shouldNotWarn(compileAndDigest) + formlyApiCheck.config.disabled = false + })) it(`should not do anything if the global instance is disabled`, () => { - apiCheck.globalConfig.disabled = true; + apiCheck.globalConfig.disabled = true scope.fields = [ - {type, templateOptions: {label: 'string'}} - ]; - shouldNotWarn(compileAndDigest); - apiCheck.globalConfig.disabled = false; - }); - }); + {type, templateOptions: {label: 'string'}}, + ] + shouldNotWarn(compileAndDigest) + apiCheck.globalConfig.disabled = false + }) + }) describe(`formlyConfig.extras.apiCheckInstance`, () => { it(`should default to this instance when specified and no specific type instance is specified`, () => { const globalApiCheckInstance = apiCheck({ - output: {prefix: 'custom-api-check'} - }); - const warnSpy = sinon.spy(globalApiCheckInstance, 'warn'); - formlyConfig.extras.apiCheckInstance = globalApiCheckInstance; - delete inputType.apiCheckInstance; + output: {prefix: 'custom-api-check'}, + }) + const warnSpy = sinon.spy(globalApiCheckInstance, 'warn') + formlyConfig.extras.apiCheckInstance = globalApiCheckInstance + delete inputType.apiCheckInstance scope.fields = [ - {type, templateOptions: {label: 'string', className: 'valid'}} - ]; - compileAndDigest(); - expect(warnSpy).to.have.been.calledOnce; - }); - }); - }); + {type, templateOptions: {label: 'string', className: 'valid'}}, + ] + compileAndDigest() + expect(warnSpy).to.have.been.calledOnce + }) + }) + }) describe(`extended scenario`, () => { - let childType, pristineOptions; + let childType, pristineOptions beforeEach(() => { - sinon.spy(inputType, 'apiCheck'); + sinon.spy(inputType, 'apiCheck') childType = formlyConfig.setType({ name: type + 'Child', extends: type, @@ -751,51 +875,51 @@ describe('formly-field', function() { apiCheck(check) { return { data: { - foo: check.string - } - }; + foo: check.string, + }, + } }, apiCheckFunction: 'throw', apiCheckInstance: apiCheck({ - output: {suffix: 'my own'} + output: {suffix: 'my own'}, }), - apiCheckOptions: {url: 'http://other-url.example.com', prefix: type + 'Child type checker'} - }); + apiCheckOptions: {url: 'http://other-url.example.com', prefix: type + 'Child type checker'}, + }) - sinon.spy(childType, 'apiCheck'); + sinon.spy(childType, 'apiCheck') - pristineOptions = {type: type + 'Child', templateOptions: {label: 'foo', className: 'bar'}, data: {foo: 'bar'}}; - }); + pristineOptions = {type: type + 'Child', templateOptions: {label: 'foo', className: 'bar'}, data: {foo: 'bar'}} + }) it(`should pass if everything is ok`, () => { - compileDigestAndMatchError(); - expect(childType.apiCheck).to.have.been.calledWith(childType.apiCheckInstance); - expect(inputType.apiCheck).to.have.been.calledWith(inputType.apiCheckInstance); - }); + compileDigestAndMatchError() + expect(childType.apiCheck).to.have.been.calledWith(childType.apiCheckInstance) + expect(inputType.apiCheck).to.have.been.calledWith(inputType.apiCheckInstance) + }) it(`should throw if the child has a problem`, () => { compileDigestAndMatchError( {data: {foo: false}}, /inputChild type checker apiCheck failed.*?`foo`.*?`String`.*?my own.*?other-url\.example\.com/ - ); - }); + ) + }) it(`should invoke the apiCheck for all extended types if an error is not thrown`, () => { - childType.apiCheckFunction = 'warn'; - compileDigestAndMatchError(); - expect(childType.apiCheck).to.have.been.calledWith(childType.apiCheckInstance); - expect(inputType.apiCheck).to.have.been.calledWith(inputType.apiCheckInstance); - }); + childType.apiCheckFunction = 'warn' + compileDigestAndMatchError() + expect(childType.apiCheck).to.have.been.calledWith(childType.apiCheckInstance) + expect(inputType.apiCheck).to.have.been.calledWith(inputType.apiCheckInstance) + }) function compileDigestAndMatchError(fieldOverrides, error) { - scope.fields = [_.merge(pristineOptions, fieldOverrides)]; + scope.fields = [_.merge(pristineOptions, fieldOverrides)] if (error) { - expect(compileAndDigest).to.throw(error); + expect(compileAndDigest).to.throw(error) } else { - expect(compileAndDigest).to.not.throw(); + expect(compileAndDigest).to.not.throw() } } - }); + }) it(`should have good default options`, () => { formlyConfig.setType({ @@ -805,27 +929,27 @@ describe('formly-field', function() { return { templateOptions: { label: check.string, - className: check.string - } - }; + className: check.string, + }, + } }, apiCheckInstance: apiCheck({ - output: {prefix: 'custom-api-check'} - }) - }); + output: {prefix: 'custom-api-check'}, + }), + }) scope.fields = [ - {type: 'someType', templateOptions: {label: 'string'}} - ]; + {type: 'someType', templateOptions: {label: 'string'}}, + ] shouldWarn( /custom-api-check formly-field type someType for property templateOptions apiCheck failed.*?Required.*?className/, compileAndDigest - ); - }); - }); + ) + }) + }) describe(`wrapper apiCheck`, () => { - const name = 'input'; - const wrapper = name; + const name = 'input' + const wrapper = name beforeEach(() => { formlyConfig.setWrapper({ name, @@ -834,63 +958,63 @@ describe('formly-field', function() { return { templateOptions: { label: check.string, - className: check.string - } - }; + className: check.string, + }, + } }, apiCheckInstance: apiCheck({ - output: {prefix: 'custom-api-check'} - }) - }); - scope.model = {}; + output: {prefix: 'custom-api-check'}, + }), + }) + scope.model = {} scope.fields = [ - {template: input, wrapper, templateOptions: {}} - ]; - }); + {template: input, wrapper, templateOptions: {}}, + ] + }) it(`should not warn if everything's fine`, () => { - scope.fields[0].templateOptions = {label: 'string', className: 'string'}; - shouldNotWarn(compileAndDigest); - }); + scope.fields[0].templateOptions = {label: 'string', className: 'string'} + shouldNotWarn(compileAndDigest) + }) it(`should warn if everything's not fine`, () => { - scope.fields[0].templateOptions = {label: 'string'}; - shouldWarn(/custom-api-check.*?formly-field(.|\n)*?className/, compileAndDigest); - }); + scope.fields[0].templateOptions = {label: 'string'} + shouldWarn(/custom-api-check.*?formly-field(.|\n)*?className/, compileAndDigest) + }) it(`should throw if the apiCheckFunction is set to "throw" and everything's not fine`, () => { - formlyConfig.getWrapper(name).apiCheckFunction = 'throw'; - scope.fields[0].templateOptions = {label: 'string'}; - expect(compileAndDigest).to.throw(/custom-api-check.*?formly-field(.|\n)*?className/); - }); - }); + formlyConfig.getWrapper(name).apiCheckFunction = 'throw' + scope.fields[0].templateOptions = {label: 'string'} + expect(compileAndDigest).to.throw(/custom-api-check.*?formly-field(.|\n)*?className/) + }) + }) describe(`formControl`, () => { beforeEach(() => { - scope.fields = [{template: input}]; - }); + scope.fields = [{template: input}] + }) it(`should be placed onto field's options`, () => { - compileAndDigest(); - expect(field.formControl).to.exist; - }); + compileAndDigest() + expect(field.formControl).to.exist + }) it(`should be placed onto the isolate scope for the formly-field`, () => { - compileAndDigest(); - expect(isolateScope.fc).to.exist; - }); + compileAndDigest() + expect(isolateScope.fc).to.exist + }) it(`should add a formControl even on a field with an ng-if on the ng-model`, () => { - const template = ''; - const field = {template, templateOptions: {if: false}}; - scope.fields = [field]; - compileAndDigest(); - expect(isolateScope.fc).to.not.exist; - field.templateOptions.if = true; - scope.$digest(); - expect(isolateScope.fc).to.exist; - }); + const template = '' + const field = {template, templateOptions: {if: false}} + scope.fields = [field] + compileAndDigest() + expect(isolateScope.fc).to.not.exist + field.templateOptions.if = true + scope.$digest() + expect(isolateScope.fc).to.exist + }) it(`should be used to add the formControl watcher if set to false even if there is no ng-model`, () => { const radioTemplate = ` @@ -906,259 +1030,259 @@ describe('formly-field', function() { - `; + ` scope.fields = [ { template: radioTemplate, templateOptions: { - options: [{name: 'Name', value: 'name'}] - } - } - ]; - compileAndDigest(); - expect(isolateScope.fc).to.exist; - }); + options: [{name: 'Name', value: 'name'}], + }, + }, + ] + compileAndDigest() + expect(isolateScope.fc).to.exist + }) describe(`noFormControl`, () => { it(`should skip adding the formControl if set to true`, () => { - scope.fields = [{template: input, noFormControl: true}]; - compileAndDigest(); - expect(isolateScope.fc).to.not.exist; - }); + scope.fields = [{template: input, noFormControl: true}] + compileAndDigest() + expect(isolateScope.fc).to.not.exist + }) - }); + }) describe(`name`, () => { it(`should be almost random`, () => { - compileAndDigest(); - expect(field.formControl.$name).to.match(/formly_\d+_template_.*?_\d+/); - }); + compileAndDigest() + expect(field.formControl.$name).to.match(/formly_\d+_template_.*?_\d+/) + }) it(`should be overrideable when a different name is specified`, () => { - scope.fields[0].template = ``; - compileAndDigest(); - makeNameExpectations('myCustomName'); - }); + scope.fields[0].template = `` + compileAndDigest() + makeNameExpectations('myCustomName') + }) it(`should handle interpolated names`, () => { - scope.fields[0].template = ``; - compileAndDigest(); - makeNameExpectations('myCustomName'); - }); + scope.fields[0].template = `` + compileAndDigest() + makeNameExpectations('myCustomName') + }) function makeNameExpectations(name) { - expect(field.formControl).to.exist; - expect(isolateScope.fc).to.exist; - expect(field.formControl.$name).to.eq(name); - expect(scope.theForm).to.have.property(name); + expect(field.formControl).to.exist + expect(isolateScope.fc).to.exist + expect(field.formControl.$name).to.eq(name) + expect(scope.theForm).to.have.property(name) } - }); + }) describe(`multiple ng-models`, () => { it(`should be an array`, () => { scope.fields = [{ - template: multiNgModelField - }]; + template: multiNgModelField, + }] - compileAndDigest(); - expect(isolateScope.fc).to.be.instanceof(Array); - }); - }); - }); + compileAndDigest() + expect(isolateScope.fc).to.be.instanceof(Array) + }) + }) + }) describe(`parsers/formatters`, () => { describe(`parsers`, () => { it(`should be merged in the right order`, () => { - testParsersOrFormatters('parsers'); - }); + testParsersOrFormatters('parsers') + }) it(`should handle a formlyExpression as a string`, () => { scope.fields = [getNewField({ key: 'myKey', parsers: ['$viewValue + options.data.extraThing'], - data: {extraThing: ' boo!'} - })]; - compileAndDigest(); - const ctrl = getNgModelCtrl(); - expect(ctrl.$parsers).to.have.length(1); - ctrl.$setViewValue('hello!'); - expect(scope.model.myKey).to.equal('hello! boo!'); - }); - }); + data: {extraThing: ' boo!'}, + })] + compileAndDigest() + const ctrl = getNgModelCtrl() + expect(ctrl.$parsers).to.have.length(1) + ctrl.$setViewValue('hello!') + expect(scope.model.myKey).to.equal('hello! boo!') + }) + }) describe(`formatters`, () => { it(`should be merged in the right order`, () => { - testParsersOrFormatters('formatters'); - }); + testParsersOrFormatters('formatters') + }) it(`should handle a formlyExpression as a string`, () => { scope.fields = [getNewField({ key: 'myKey', formatters: ['$viewValue + options.data.extraThing'], - data: {extraThing: ' boo!'} - })]; - compileAndDigest(); - scope.model.myKey = 'hello!'; - scope.$digest(); - const ctrl = getNgModelCtrl(); - expect(ctrl.$formatters).to.have.length(2); // ngModel adds one - expect(ctrl.$viewValue).to.equal('hello! boo!'); - }); + data: {extraThing: ' boo!'}, + })] + compileAndDigest() + scope.model.myKey = 'hello!' + scope.$digest() + const ctrl = getNgModelCtrl() + expect(ctrl.$formatters).to.have.length(2) // ngModel adds one + expect(ctrl.$viewValue).to.equal('hello! boo!') + }) it(`should format a model value right from the start and the controller should still be pristine`, () => { - scope.model = {myKey: 'hello'}; + scope.model = {myKey: 'hello'} scope.fields = [getNewField({ key: 'myKey', - formatters: ['"!" + $viewValue + "!"'] - })]; - compileAndDigest(); + formatters: ['"!" + $viewValue + "!"'], + })] + compileAndDigest() - const ctrl = getNgModelCtrl(); + const ctrl = getNgModelCtrl() - expect(ctrl.$viewValue).to.equal('!hello!'); - expect(ctrl.$dirty).to.equal(false); - expect(ctrl.$pristine).to.equal(true); - }); + expect(ctrl.$viewValue).to.equal('!hello!') + expect(ctrl.$dirty).to.equal(false) + expect(ctrl.$pristine).to.equal(true) + }) it(`should format a model value on initilization and keep the form state dirty if it was already dirty`, () => { - scope.model = {myKey: 'hello'}; + scope.model = {myKey: 'hello'} scope.fields = [getNewField({ key: 'myKey', - formatters: ['"!" + $viewValue + "!"'] - })]; - compileAndDigest(); - scope.theForm.$setDirty(); + formatters: ['"!" + $viewValue + "!"'], + })] + compileAndDigest() + scope.theForm.$setDirty() - const ctrl = getNgModelCtrl(); + const ctrl = getNgModelCtrl() - expect(ctrl.$viewValue).to.equal('!hello!'); - expect(scope.theForm.$dirty).to.equal(true); + expect(ctrl.$viewValue).to.equal('!hello!') + expect(scope.theForm.$dirty).to.equal(true) - }); + }) it(`should format a model value on initilization and keep the form state pristine if it was already pristine`, () => { - scope.model = {myKey: 'hello'}; + scope.model = {myKey: 'hello'} scope.fields = [getNewField({ key: 'myKey', - formatters: ['"!" + $viewValue + "!"'] - })]; - compileAndDigest(); + formatters: ['"!" + $viewValue + "!"'], + })] + compileAndDigest() - const ctrl = getNgModelCtrl(); + const ctrl = getNgModelCtrl() - expect(ctrl.$viewValue).to.equal('!hello!'); - expect(scope.theForm.$pristine).to.equal(true); + expect(ctrl.$viewValue).to.equal('!hello!') + expect(scope.theForm.$pristine).to.equal(true) - }); + }) it.skip(`should handle multiple form controllers when formatting a model value right from the start`, () => { scope.model = { multiNgModel: { start: 'start', - stop: 'stop' - } - }; + stop: 'stop', + }, + } const field = getNewField({ key: 'multiNgModel', template: multiNgModelField, - formatters: ['"!" + $viewValue + "!"'] - }); - scope.fields = [field]; + formatters: ['"!" + $viewValue + "!"'], + }) + scope.fields = [field] - compileAndDigest(); + compileAndDigest() - const ctrl1 = field.formControl[0]; - const ctrl2 = field.formControl[1]; + const ctrl1 = field.formControl[0] + const ctrl2 = field.formControl[1] - expect(ctrl1.$viewValue).to.equal('!start!'); - expect(ctrl2.$viewValue).to.equal('!stop!'); - }); + expect(ctrl1.$viewValue).to.equal('!start!') + expect(ctrl2.$viewValue).to.equal('!stop!') + }) - }); + }) function testParsersOrFormatters(which) { - let originalThingProp = 'originalParser'; + let originalThingProp = 'originalParser' if (which === 'formatters') { - originalThingProp = 'originalFormatter'; + originalThingProp = 'originalFormatter' } const parent1Thing1 = sinon.spy(function parent1Thing1() { - }); + }) const parent1Thing2 = sinon.spy(function parent1Thing2() { - }); + }) const parent2Thing1 = sinon.spy(function parent2Thing1() { - }); + }) const parent2Thing2 = sinon.spy(function parent2Thing2() { - }); + }) const childThing1 = sinon.spy(function childThing1() { - }); + }) const childThing2 = sinon.spy(function childThing2() { - }); + }) const optionType1Thing1 = sinon.spy(function optionType1Thing1() { - }); + }) const optionType1Thing2 = sinon.spy(function optionType1Thing2() { - }); + }) const optionType2Thing1 = sinon.spy(function optionType2Thing1() { - }); + }) const optionType2Thing2 = sinon.spy(function optionType2Thing2() { - }); + }) const fieldThing1 = sinon.spy(function fieldThing1() { - }); + }) const fieldThing2 = sinon.spy(function fieldThing2() { - }); + }) formlyConfig.setType({ name: 'parent1', defaultOptions: { - [which]: [parent1Thing1, parent1Thing2] - } - }); + [which]: [parent1Thing1, parent1Thing2], + }, + }) formlyConfig.setType({ name: 'parent2', defaultOptions: { - [which]: [parent2Thing1, parent2Thing2] - } - }); + [which]: [parent2Thing1, parent2Thing2], + }, + }) formlyConfig.setType({ name: 'child', template: '', extends: 'parent1', // <-- note this! defaultOptions: { - [which]: [childThing1, childThing2] - } - }); + [which]: [childThing1, childThing2], + }, + }) formlyConfig.setType({ name: 'optionType1', extends: 'parent2', // <-- note this! defaultOptions: { - [which]: [optionType1Thing1, optionType1Thing2] - } - }); + [which]: [optionType1Thing1, optionType1Thing2], + }, + }) formlyConfig.setType({ name: 'optionType2', defaultOptions: { - [which]: [optionType2Thing1, optionType2Thing2] - } - }); + [which]: [optionType2Thing1, optionType2Thing2], + }, + }) scope.fields = [ { type: 'child', optionsTypes: ['optionType1', 'optionType2'], - [which]: [fieldThing1, fieldThing2] - } - ]; + [which]: [fieldThing1, fieldThing2], + }, + ] - compileAndDigest(); - const ctrl = getNgModelCtrl(); - const originalThings = ctrl['$' + which].map(thing => thing[originalThingProp]); + compileAndDigest() + const ctrl = getNgModelCtrl() + const originalThings = ctrl['$' + which].map(thing => thing[originalThingProp]) if (which === 'formatters') { // all ngModelControllers have a default formatter, remove that from the originalThings for our test - originalThings.shift(); + originalThings.shift() } expect(originalThings).to.eql([ parent1Thing1, parent1Thing2, @@ -1166,354 +1290,504 @@ describe('formly-field', function() { parent2Thing1, parent2Thing2, optionType1Thing1, optionType1Thing2, optionType2Thing1, optionType2Thing2, - fieldThing1, fieldThing2 - ]); + fieldThing1, fieldThing2, + ]) } - }); + }) describe(`link`, () => { describe(`addClasses`, () => { it(`should add the type class`, () => { formlyConfig.setType({ name: 'input', - template: input - }); + template: input, + }) - scope.fields = [{type: 'input'}]; + scope.fields = [{type: 'input'}] - compileAndDigest(); - expect(el[0].querySelector('[formly-field].formly-field-input')).to.exist; - }); + compileAndDigest() + expect(el[0].querySelector('[formly-field].formly-field-input')).to.exist + }) it(`should add the className class`, () => { - scope.fields = [getNewField({className: 'classy'}), getNewField({className: 'very-classy'})]; - compileAndDigest(); - expect(el[0].querySelector('[formly-field].classy')).to.exist; - expect(el[0].querySelector('[formly-field].very-classy')).to.exist; - }); - }); - }); + scope.fields = [getNewField({className: 'classy'}), getNewField({className: 'very-classy'})] + compileAndDigest() + expect(el[0].querySelector('[formly-field].classy')).to.exist + expect(el[0].querySelector('[formly-field].very-classy')).to.exist + }) + }) + }) describe(`elementAttributes`, () => { it(`should allow fields to have attributes which will be applied to the [formly-field]`, () => { - scope.fields = [getNewField({elementAttributes: {foo: 'bar', baz: 'eggs'}})]; - compileAndDigest(); - expect(el[0].querySelector('[formly-field][foo=bar][baz=eggs]')).to.exist; - }); + scope.fields = [getNewField({elementAttributes: {foo: 'bar', baz: 'eggs'}})] + compileAndDigest() + expect(el[0].querySelector('[formly-field][foo=bar][baz=eggs]')).to.exist + }) it(`should allow fieldGroups to have attributes which will be applied to the ng-form`, () => { scope.fields = [ - {elementAttributes: {foo: 'bar', baz: 'eggs'}, fieldGroup: [getNewField()]} - ]; - compileAndDigest(); - expect(el[0].querySelector('ng-form[foo=bar][baz=eggs]')).to.exist; - }); - }); + {elementAttributes: {foo: 'bar', baz: 'eggs'}, fieldGroup: [getNewField()]}, + ] + compileAndDigest() + expect(el[0].querySelector('ng-form[foo=bar][baz=eggs]')).to.exist + }) + }) describe(`resetModel`, () => { it(`should reset the form state`, () => { - const field = getNewField({key: 'foo'}); - scope.fields = [field]; - compileAndDigest(); + const field = getNewField({key: 'foo'}) + scope.fields = [field] + compileAndDigest() // initial state - expect(field.formControl.$dirty).to.be.false; - expect(field.formControl.$touched).to.be.false; + expect(field.formControl.$dirty).to.be.false + expect(field.formControl.$touched).to.be.false + // modification + scope.model.foo = '~=[,,_,,]:3' + field.formControl.$setTouched() + field.formControl.$setDirty() + scope.$digest() + + // expect modification + expect(field.formControl.$dirty).to.be.true + expect(field.formControl.$touched).to.be.true + expect(field.formControl.$modelValue).to.eq('~=[,,_,,]:3') + + // reset state + field.resetModel() + + // expect reset + expect(field.formControl.$modelValue).to.be.empty + expect(field.formControl.$touched).to.be.false + expect(field.formControl.$dirty).to.be.false + }) + + it(`should reset the form state with a deep model`, () => { + const field = getNewField({key: 'foo.bar'}) + scope.fields = [field] + compileAndDigest() + + // initial state + expect(field.formControl.$dirty).to.be.false + expect(field.formControl.$touched).to.be.false // modification - scope.model.foo = '~=[,,_,,]:3'; - field.formControl.$setTouched(); - field.formControl.$setDirty(); - scope.$digest(); + scope.model.foo = { + bar: '~=[,,_,,]:3', + } + field.formControl.$setTouched() + field.formControl.$setDirty() + scope.$digest() // expect modification - expect(field.formControl.$dirty).to.be.true; - expect(field.formControl.$touched).to.be.true; - expect(field.formControl.$modelValue).to.eq('~=[,,_,,]:3'); + expect(field.formControl.$dirty).to.be.true + expect(field.formControl.$touched).to.be.true + expect(field.formControl.$modelValue).to.eq('~=[,,_,,]:3') + + // Set new initialValue + scope.options.updateInitialValue() + + // Modify again + scope.model.foo.bar = 'l33t' + field.formControl.$setTouched() + field.formControl.$setDirty() + scope.$digest() + + // expect modification + expect(field.formControl.$dirty).to.be.true + expect(field.formControl.$touched).to.be.true + expect(field.formControl.$modelValue).to.eq('l33t') // reset state - field.resetModel(); + scope.options.resetModel() // expect reset - expect(field.formControl.$modelValue).to.be.empty; - expect(field.formControl.$touched).to.be.false; - expect(field.formControl.$dirty).to.be.false; - }); + expect(field.formControl.$modelValue).to.eq('~=[,,_,,]:3') + expect(field.formControl.$touched).to.be.false + expect(field.formControl.$dirty).to.be.false + }) it(`should reset the form state for an field with multiple ng-models`, () => { const field = { key: 'multiNgModel', - template: multiNgModelField - }; - scope.fields = [field]; - compileAndDigest(); + template: multiNgModelField, + } + scope.fields = [field] + compileAndDigest() // initial state - expect(field.formControl[0].$dirty).to.be.false; - expect(field.formControl[0].$touched).to.be.false; - expect(field.formControl[1].$dirty).to.be.false; - expect(field.formControl[1].$touched).to.be.false; + expect(field.formControl[0].$dirty).to.be.false + expect(field.formControl[0].$touched).to.be.false + expect(field.formControl[1].$dirty).to.be.false + expect(field.formControl[1].$touched).to.be.false scope.model.multiNgModel = { start: 0, - stop: 20 - }; - field.formControl[0].$setDirty(); - field.formControl[0].$setTouched(); - field.formControl[1].$setDirty(); - field.formControl[1].$setTouched(); - scope.$digest(); + stop: 20, + } + field.formControl[0].$setDirty() + field.formControl[0].$setTouched() + field.formControl[1].$setDirty() + field.formControl[1].$setTouched() + scope.$digest() // expect modification - expect(field.formControl[0].$dirty).to.be.true; - expect(field.formControl[0].$touched).to.be.true; - expect(field.formControl[0].$modelValue).to.eq(0); - expect(field.formControl[1].$dirty).to.be.true; - expect(field.formControl[1].$touched).to.be.true; - expect(field.formControl[1].$modelValue).to.eq(20); + expect(field.formControl[0].$dirty).to.be.true + expect(field.formControl[0].$touched).to.be.true + expect(field.formControl[0].$modelValue).to.eq(0) + expect(field.formControl[1].$dirty).to.be.true + expect(field.formControl[1].$touched).to.be.true + expect(field.formControl[1].$modelValue).to.eq(20) // reset state - field.resetModel(); + field.resetModel() // expect reset - expect(field.formControl[0].$modelValue).to.be.empty; - expect(field.formControl[0].$touched).to.be.false; - expect(field.formControl[0].$dirty).to.be.false; - expect(field.formControl[1].$modelValue).to.be.empty; - expect(field.formControl[1].$touched).to.be.false; - expect(field.formControl[1].$dirty).to.be.false; - }); + expect(field.formControl[0].$modelValue).to.be.empty + expect(field.formControl[0].$touched).to.be.false + expect(field.formControl[0].$dirty).to.be.false + expect(field.formControl[1].$modelValue).to.be.empty + expect(field.formControl[1].$touched).to.be.false + expect(field.formControl[1].$dirty).to.be.false + }) it(`should work just fine to call resetModel on a field that has no formControl`, () => { - const field = {template: '
'}; - scope.fields = [field]; - compileAndDigest(); - expect(field.formControl).to.not.exist; - expect(() => field.resetModel()).to.not.throw(); - }); + const field = {template: '
'} + scope.fields = [field] + compileAndDigest() + expect(field.formControl).to.not.exist + expect(() => field.resetModel()).to.not.throw() + }) + + it('should reset the form state on the input and form both', () => { + const field = getNewField({key: 'foo'}) + scope.fields = [field] + compileAndDigest(` +
+ +
+`) + // initial state + expect(field.formControl.$dirty).to.be.false + expect(field.formControl.$touched).to.be.false + expect(scope.theForm.$dirty).to.be.false + expect(scope.theForm.$pristine).to.be.true + // modification + scope.model.foo = '~=[,,_,,]:3' + field.formControl.$setTouched() + field.formControl.$setDirty() + scope.$digest() + + // expect modification + expect(field.formControl.$dirty).to.be.true + expect(field.formControl.$touched).to.be.true + expect(scope.theForm.$dirty).to.be.true + expect(scope.theForm.$pristine).to.be.false + expect(field.formControl.$modelValue).to.eq('~=[,,_,,]:3') + + // reset state + field.resetModel() + + // expect reset + expect(field.formControl.$modelValue).to.be.empty + expect(field.formControl.$touched).to.be.false + expect(field.formControl.$dirty).to.be.false + expect(scope.theForm.$dirty).to.be.false + expect(scope.theForm.$pristine).to.be.true + }) it(`should not digest if there's a digest in progress`, () => { - scope.fields = [getNewField()]; - compileAndDigest(); - scope.$root.$$phase = '$digest'; - expect(() => field.resetModel()).to.not.throw(); - }); - }); + scope.fields = [getNewField()] + compileAndDigest() + scope.$root.$$phase = '$digest' + expect(() => field.resetModel()).to.not.throw() + }) + }) describe(`with a div ng-model`, () => { it(`should have a form-controller`, () => { - const template = `
`; - scope.fields = [getNewField({template})]; - compileAndDigest(); - expect(isolateScope.fc).to.exist; - expect(field.formControl).to.exist; - }); - }); + const template = `
` + scope.fields = [getNewField({template})] + compileAndDigest() + expect(isolateScope.fc).to.exist + expect(field.formControl).to.exist + }) + }) describe(`with a div data-ng-model`, () => { it(`should have a form-controller`, () => { - const template = `
`; - scope.fields = [getNewField({template})]; - compileAndDigest(); - expect(isolateScope.fc).to.exist; - expect(field.formControl).to.exist; - }); - }); - - describe(`with custom errorExistsAndShouldBeVisible expression`, () => { - beforeEach(() => { - scope.fields = [getNewField({validators: {foo: 'false'}})]; - }); - - it(`should set errorExistsAndShouldBeVisible to true when the expression function says so`, () => { - formlyConfig.extras.errorExistsAndShouldBeVisibleExpression = '!!options.data.customExpression'; - compileAndDigest(); - expect(field.validation.errorExistsAndShouldBeVisible).to.be.false; - field.data.customExpression = true; - scope.$digest(); - expect(field.validation.errorExistsAndShouldBeVisible).to.be.true; - }); - - it(`should be able to work with form.$submitted`, () => { - formlyConfig.extras.errorExistsAndShouldBeVisibleExpression = 'form.$submitted'; - compileAndDigest(` -
- -
- `); - expect(field.validation.errorExistsAndShouldBeVisible).to.be.false; - scope.theForm.$setSubmitted(true); - scope.$digest(); - expect(field.validation.errorExistsAndShouldBeVisible).to.be.true; - }); - - }); + const template = `
` + scope.fields = [getNewField({template})] + compileAndDigest() + expect(isolateScope.fc).to.exist + expect(field.formControl).to.exist + }) + }) + + describe(`options.validation.errorExistsAndShouldBeVisible`, () => { + describe(`multiple ng-model elements`, () => { + beforeEach(() => { + scope.fields = [ + { + template: ` + + + `, + // we'll just give it a validator that depends on a value we + // can change in our tests + validators: {foo: '!options.data.invalid'}, + }, + ] + }) + + it(`should set showError to true when one of them is invalid`, () => { + compileAndDigest() + expect(field.validation.errorExistsAndShouldBeVisible, 'initially false').to.be.false + invalidateAndTouchFields() + + expect(field.formControl[0].$error.foo, '$error on the first formControl').be.true + expect(field.validation.errorExistsAndShouldBeVisible, 'now true').to.be.true + }) + + it(`should work with a custom errorExistsAndShouldBeVisibleExpression`, () => { + const spy = sinon.spy() + formlyConfig.extras.errorExistsAndShouldBeVisibleExpression = spy + compileAndDigest() + + invalidateAndTouchFields() + expect(spy).to.have.been.calledTwice // once for each form control. + }) + + function invalidateAndTouchFields() { + field.data.invalid = true + // force $touched and revalidation of both form controls + field.formControl.forEach(fc => { + fc.$setTouched() + fc.$validate() + }) + + // redigest to set the showError prop + scope.$digest() + } + }) + + describe(`with custom errorExistsAndShouldBeVisible expression`, () => { + beforeEach(() => { + scope.fields = [getNewField({validators: {foo: 'false'}})] + }) + + it(`should set errorExistsAndShouldBeVisible to true when the expression function says so`, () => { + formlyConfig.extras.errorExistsAndShouldBeVisibleExpression = '!!options.data.customExpression' + compileAndDigest() + expect(field.validation.errorExistsAndShouldBeVisible).to.be.false + field.data.customExpression = true + scope.$digest() + expect(field.validation.errorExistsAndShouldBeVisible).to.be.true + }) + + it(`should be able to work with form.$submitted`, () => { + formlyConfig.extras.errorExistsAndShouldBeVisibleExpression = 'form.$submitted' + compileAndDigest(` +
+ +
+ `) + expect(field.validation.errorExistsAndShouldBeVisible).to.be.false + scope.theForm.$setSubmitted(true) + scope.$digest() + expect(field.validation.errorExistsAndShouldBeVisible).to.be.true + }) + + }) + }) describe(`with specified "model" property`, () => { it(`should use the specified model for the field which specifies it`, () => { const model = { - foo: 'bar' - }; + foo: 'bar', + } scope.fields = [ {template: input, model, key: 'foo'}, // do not use getNewField() here because _.merge creates a copy of model getNewField(), - getNewField() - ]; + getNewField(), + ] - compileAndDigest(); - expect(isolateScope.model).to.not.equal(scope.model); - expect(isolateScope.model).to.eq(model); - }); + compileAndDigest() + expect(isolateScope.model).to.not.equal(scope.model) + expect(isolateScope.model).to.eq(model) + }) it(`should allow you to specify "formState" and assign it to the formState property`, () => { scope.fields = [ getNewField({model: 'formState', data: {foo: 'bar'}}), getNewField(), - getNewField() - ]; + getNewField(), + ] - compileAndDigest(); - const field1 = getIsolateScope(0); - const field2 = getIsolateScope(1); - const field3 = getIsolateScope(2); + compileAndDigest() + const field1 = getIsolateScope(0) + const field2 = getIsolateScope(1) + const field3 = getIsolateScope(2) - expect(field1.model).to.not.equal(field2.model); - expect(field1.model).to.equal(field3.formState); - expect(field2.model).to.equal(field3.model); - }); + expect(field1.model).to.not.equal(field2.model) + expect(field1.model).to.equal(field3.formState) + expect(field2.model).to.equal(field3.model) + }) it(`should allow you to specify any expression which will be used to evaluate the model at compile-time`, () => { scope.fields = [ - getNewField({key: 'foobar', model: 'options.data.foo', data: {foo: {bar: 'foobar'}}}) - ]; + getNewField({key: 'foobar', model: 'options.data.foo', data: {foo: {bar: 'foobar'}}}), + ] - compileAndDigest(); - expect(isolateScope.model).to.equal(field.data.foo); - }); + compileAndDigest() + expect(isolateScope.model).to.equal(field.data.foo) + }) it(`should allow you to specify a model for a fieldGroup and have that apply to children fields`, () => { - scope.model = {child: {foo: 'bar', baz: {boo: {}}}}; + scope.model = {child: {foo: 'bar', baz: {boo: {}}}} scope.fields = [ { model: 'model.child', fieldGroup: [ getNewField({key: 'foo'}), - getNewField({key: 'bar', defaultValue: 'foobar', model: 'model.baz.boo'}) - ] - } - ]; + getNewField({key: 'bar', defaultValue: 'foobar', model: 'model.baz.boo'}), + ], + }, + ] - compileAndDigest(); - const fieldGroupNode = node.querySelector('.formly-field-group'); - expect(fieldGroupNode).to.exist; + compileAndDigest() + const fieldGroupNode = node.querySelector('.formly-field-group') + expect(fieldGroupNode).to.exist - const fieldNode1 = fieldGroupNode.querySelectorAll('.formly-field')[0]; - expect(fieldNode1).to.exist; + const fieldNode1 = fieldGroupNode.querySelectorAll('.formly-field')[0] + expect(fieldNode1).to.exist - const fieldNode2 = fieldGroupNode.querySelectorAll('.formly-field')[1]; - expect(fieldNode2).to.exist; + const fieldNode2 = fieldGroupNode.querySelectorAll('.formly-field')[1] + expect(fieldNode2).to.exist - const fieldGroup = getIsolateScope(0); - const field1 = getIsolateScope(1); - const field2 = getIsolateScope(2); + const fieldGroup = getIsolateScope(0) + const field1 = getIsolateScope(1) + const field2 = getIsolateScope(2) - expect(fieldGroup.model).to.eq(scope.model.child); - expect(field1.model).to.eq(scope.model.child); - expect(field2.model).to.eq(scope.model.child.baz.boo); - }); + expect(fieldGroup.model).to.eq(scope.model.child) + expect(field1.model).to.eq(scope.model.child) + expect(field2.model).to.eq(scope.model.child.baz.boo) + }) it(`should throw an error if the model does not exist`, () => { - scope.model = {child: {}}; + scope.model = {child: {}} scope.fields = [ { model: 'model.child', fieldGroup: [ - getNewField({model: 'model.baz'}) - ] - } - ]; + getNewField({model: 'model.baz'}), + ], + }, + ] - expect(() => compileAndDigest()).to.throw(); - }); + expect(() => compileAndDigest()).to.throw() + }) it(`should watch the model when it's not the direct child of a formly-form`, () => { scope.fields = [ - getNewField({key: 'foo', model: {}}) - ]; + getNewField({key: 'foo', model: {}}), + ] + + compileAndDigest('
') + $timeout.flush() + + const expressionPropertySpy = sinon.spy() + field.expressionProperties = {'data.dummy': expressionPropertySpy} - compileAndDigest('
'); - $timeout.flush(); + field.model.foo = 'hello' + scope.$digest() + $timeout.flush() - const expressionPropertySpy = sinon.spy(); - field.expressionProperties = {'data.dummy': expressionPropertySpy}; + expect(expressionPropertySpy).to.have.been.calledOnce + }) - field.model.foo = 'hello'; - scope.$digest(); - $timeout.flush(); + it(`should add watches on deep dive fields`, () => { + const formWithOptions = '' + scope.model = {} + scope.options = {} - expect(expressionPropertySpy).to.have.been.calledOnce; - }); + const deepLinkField = getNewField() + deepLinkField.key = 'foo.bar' + deepLinkField.watcher = { + listener: sinon.spy(), + } + + scope.fields = [deepLinkField] + compileAndDigest(formWithOptions) + expect(deepLinkField.watcher.listener).to.have.been.called + scope.model.foo = { + bar: 'brown', + } + scope.$digest() + expect(deepLinkField.watcher.listener).to.have.been.called + }) it('should make original model available on field scope, even another model has been set for field', () => { - scope.model = {foo: 'bar', child: {fox: 'jumps'}}; + scope.model = {foo: 'bar', child: {fox: 'jumps'}} scope.fields = [ getNewField({key: 'foo '}), - getNewField({key: 'bar', model: 'model.child'}) - ]; + getNewField({key: 'bar', model: 'model.child'}), + ] - compileAndDigest(); + compileAndDigest() - const field1 = getIsolateScope(0); - const field2 = getIsolateScope(1); + const field1 = getIsolateScope(0) + const field2 = getIsolateScope(1) - expect(field1.model).to.eq(scope.model); - expect(field1.originalModel).to.eq(field1.model); + expect(field1.model).to.eq(scope.model) + expect(field1.originalModel).to.eq(field1.model) - expect(field2.model).to.eq(scope.model.child); - expect(field2.originalModel).not.to.eq(scope.model.child); - expect(field2.originalModel).to.eq(scope.model); - }); + expect(field2.model).to.eq(scope.model.child) + expect(field2.originalModel).not.to.eq(scope.model.child) + expect(field2.originalModel).to.eq(scope.model) + }) it('should take field model as default for original model, if original value attributes has not been set', () => { scope.fields = [ - getNewField({key: 'foo', model: {foo: 'bar'}}) - ]; + getNewField({key: 'foo', model: {foo: 'bar'}}), + ] - compileAndDigest('
'); - $timeout.flush(); + compileAndDigest('
') + $timeout.flush() - expect(field.model).to.eq(scope.fields[0].model); - expect(field.originalModel).to.eql(scope.fields[0].model); + expect(field.model).to.eq(scope.fields[0].model) + expect(field.originalModel).to.eql(scope.fields[0].model) - }); + }) - }); + }) describe(`fieldGroup`, () => { it(`should share the form with a fieldGroup`, () => { - scope.model = {child: {foo: 'bar'}}; + scope.model = {child: {foo: 'bar'}} scope.fields = [ { model: 'model.child', fieldGroup: [ - getNewField({key: 'foo'}) - ] - } - ]; + getNewField({key: 'foo'}), + ], + }, + ] - compileAndDigest(); - const fieldGroupNode = node.querySelector('.formly-field-group'); - expect(fieldGroupNode).to.exist; + compileAndDigest() + const fieldGroupNode = node.querySelector('.formly-field-group') + expect(fieldGroupNode).to.exist - const fieldGroup = getIsolateScope(0); + const fieldGroup = getIsolateScope(0) - expect(fieldGroup.model).to.eq(scope.model.child); + expect(fieldGroup.model).to.eq(scope.model.child) - expect(fieldGroup.options.form).to.eq(fieldGroup.form); - }); + expect(fieldGroup.options.form).to.eq(fieldGroup.form) + }) it(`should allow you to specify a key which will be used for the model of the field-group`, () => { scope.fields = [ @@ -1524,104 +1798,104 @@ describe('formly-field', function() { { key: 'bar', fieldGroup: [ - getNewField({key: 'barChild', defaultValue: 'barVal'}) - ] - } - ] - } - ]; - compileAndDigest(); + getNewField({key: 'barChild', defaultValue: 'barVal'}), + ], + }, + ], + }, + ] + compileAndDigest() - expect(scope.model.foo.fooChild).to.eq('fooVal'); - expect(scope.model.foo.bar.barChild).to.eq('barVal'); - }); - }); + expect(scope.model.foo.fooChild).to.eq('fooVal') + expect(scope.model.foo.bar.barChild).to.eq('barVal') + }) + }) describe(`runExpressions`, () => { describe(`as functions`, () => { it(`should invoke the expressionProperties with the $viewValue, $modelValue, and scope`, () => { - const spy = sinon.spy(); + const spy = sinon.spy() scope.model = { - foo: 'bar' - }; + foo: 'bar', + } scope.fields = [ getNewField({ key: 'foo', expressionProperties: { - 'templateOptions.disabled': spy - } - }) - ]; - compileAndDigest(); - $timeout.flush(); // <-- runExpressions happens inside a $timeout - expect(spy).to.have.been.calledWith('bar', 'bar', isolateScope); - }); - }); - }); + 'templateOptions.disabled': spy, + }, + }), + ] + compileAndDigest() + $timeout.flush() // <-- runExpressions happens inside a $timeout + expect(spy).to.have.been.calledWith('bar', 'bar', isolateScope) + }) + }) + }) describe(`templateManipulators and wrappers`, () => { it(`should not cause a problem when you don't pass form-options`, () => { - const fieldScope = scope.$new(); - fieldScope.field = {template: 'foo', model: {}}; - fieldScope.fields = [fieldScope.field]; + const fieldScope = scope.$new() + fieldScope.field = {template: 'foo', model: {}} + fieldScope.fields = [fieldScope.field] expect(() => { compileAndDigest(`
`, - fieldScope); - }).to.not.throw(); - }); + fieldScope) + }).to.not.throw() + }) it(`should allow you to specify a templateManipulator on a field and form basis and they should be applied in the correct order`, () => { formlyConfig.setWrapper({ name: 'formWrapper1', - template: '__formWrapper1__' - }); + template: '__formWrapper1__', + }) formlyConfig.setWrapper({ name: 'formWrapper2', - template: '__formWrapper2__' - }); + template: '__formWrapper2__', + }) formlyConfig.setWrapper({ name: 'fieldWrapper1', - template: '__fieldWrapper1__' - }); + template: '__fieldWrapper1__', + }) formlyConfig.setWrapper({ name: 'fieldWrapper2', - template: '__fieldWrapper2__' - }); + template: '__fieldWrapper2__', + }) - const fieldPre1 = sinon.spy(template => `fieldPre1_${template}`); - const fieldPre2 = sinon.spy(template => `fieldPre2_${template}`); - const fieldPost1 = sinon.spy(template => `fieldPost1_${template}`); - const fieldPost2 = sinon.spy(template => `fieldPost2_${template}`); + const fieldPre1 = sinon.spy(template => `fieldPre1_${template}`) + const fieldPre2 = sinon.spy(template => `fieldPre2_${template}`) + const fieldPost1 = sinon.spy(template => `fieldPost1_${template}`) + const fieldPost2 = sinon.spy(template => `fieldPost2_${template}`) - const formPre1 = sinon.spy(template => `formPre1_${template}`); - const formPre2 = sinon.spy(template => `formPre2_${template}`); - const formPost1 = sinon.spy(template => `formPost1_${template}`); - const formPost2 = sinon.spy(template => `formPost2_${template}`); + const formPre1 = sinon.spy(template => `formPre1_${template}`) + const formPre2 = sinon.spy(template => `formPre2_${template}`) + const formPost1 = sinon.spy(template => `formPost1_${template}`) + const formPost2 = sinon.spy(template => `formPost2_${template}`) - const globalPre1 = sinon.spy(template => `globalPre1_${template}`); - const globalPre2 = sinon.spy(template => `globalPre2_${template}`); - const globalPost1 = sinon.spy(template => `globalPost1_${template}`); - const globalPost2 = sinon.spy(template => `globalPost2_${template}`); + const globalPre1 = sinon.spy(template => `globalPre1_${template}`) + const globalPre2 = sinon.spy(template => `globalPre2_${template}`) + const globalPost1 = sinon.spy(template => `globalPost1_${template}`) + const globalPost2 = sinon.spy(template => `globalPost2_${template}`) - formlyConfig.templateManipulators.preWrapper.push(globalPre1); - formlyConfig.templateManipulators.preWrapper.push(globalPre2); - formlyConfig.templateManipulators.postWrapper.push(globalPost1); - formlyConfig.templateManipulators.postWrapper.push(globalPost2); + formlyConfig.templateManipulators.preWrapper.push(globalPre1) + formlyConfig.templateManipulators.preWrapper.push(globalPre2) + formlyConfig.templateManipulators.postWrapper.push(globalPost1) + formlyConfig.templateManipulators.postWrapper.push(globalPost2) scope.options = { templateManipulators: { preWrapper: [formPre1, formPre2], - postWrapper: [formPost1, formPost2] + postWrapper: [formPost1, formPost2], }, - wrapper: ['formWrapper1', 'formWrapper2'] - }; + wrapper: ['formWrapper1', 'formWrapper2'], + } scope.fields = [ getNewField({ @@ -1629,106 +1903,156 @@ describe('formly-field', function() { wrapper: ['fieldWrapper1', 'fieldWrapper2'], templateManipulators: { preWrapper: [fieldPre1, fieldPre2], - postWrapper: [fieldPost1, fieldPost2] - } - }) - ]; + postWrapper: [fieldPost1, fieldPost2], + }, + }), + ] - compileAndDigest(); + compileAndDigest() // field pre - expect(fieldPre1).to.have.been.calledWith('foo', field, isolateScope); - expect(fieldPre1).to.have.returned('fieldPre1_foo'); + expect(fieldPre1).to.have.been.calledWith('foo', field, isolateScope) + expect(fieldPre1).to.have.returned('fieldPre1_foo') - expect(fieldPre2).to.have.been.calledWith('fieldPre1_foo', field, isolateScope); - expect(fieldPre2).to.have.returned('fieldPre2_fieldPre1_foo'); + expect(fieldPre2).to.have.been.calledWith('fieldPre1_foo', field, isolateScope) + expect(fieldPre2).to.have.returned('fieldPre2_fieldPre1_foo') // form pre - expect(formPre1).to.have.been.calledWith('fieldPre2_fieldPre1_foo', field, isolateScope); - expect(formPre1).to.have.returned('formPre1_fieldPre2_fieldPre1_foo'); + expect(formPre1).to.have.been.calledWith('fieldPre2_fieldPre1_foo', field, isolateScope) + expect(formPre1).to.have.returned('formPre1_fieldPre2_fieldPre1_foo') - expect(formPre2).to.have.been.calledWith('formPre1_fieldPre2_fieldPre1_foo', field, isolateScope); - expect(formPre2).to.have.returned('formPre2_formPre1_fieldPre2_fieldPre1_foo'); + expect(formPre2).to.have.been.calledWith('formPre1_fieldPre2_fieldPre1_foo', field, isolateScope) + expect(formPre2).to.have.returned('formPre2_formPre1_fieldPre2_fieldPre1_foo') // global pre - expect(globalPre1).to.have.been.calledWith('formPre2_formPre1_fieldPre2_fieldPre1_foo', field, isolateScope); - expect(globalPre1).to.have.returned('globalPre1_formPre2_formPre1_fieldPre2_fieldPre1_foo'); + expect(globalPre1).to.have.been.calledWith('formPre2_formPre1_fieldPre2_fieldPre1_foo', field, isolateScope) + expect(globalPre1).to.have.returned('globalPre1_formPre2_formPre1_fieldPre2_fieldPre1_foo') - expect(globalPre2).to.have.been.calledWith('globalPre1_formPre2_formPre1_fieldPre2_fieldPre1_foo', field, isolateScope); - expect(globalPre2).to.have.returned('globalPre2_globalPre1_formPre2_formPre1_fieldPre2_fieldPre1_foo'); + expect(globalPre2).to.have.been.calledWith('globalPre1_formPre2_formPre1_fieldPre2_fieldPre1_foo', field, isolateScope) + expect(globalPre2).to.have.returned('globalPre2_globalPre1_formPre2_formPre1_fieldPre2_fieldPre1_foo') // this is where the wrapper runs // field post - expect(fieldPost1).to.have.been.calledWith('__formWrapper2____formWrapper1____fieldWrapper2____fieldWrapper1__globalPre2_globalPre1_formPre2_formPre1_fieldPre2_fieldPre1_foo', field, isolateScope); - expect(fieldPost1).to.have.returned('fieldPost1___formWrapper2____formWrapper1____fieldWrapper2____fieldWrapper1__globalPre2_globalPre1_formPre2_formPre1_fieldPre2_fieldPre1_foo'); + expect(fieldPost1).to.have.been.calledWith('__formWrapper2____formWrapper1____fieldWrapper2____fieldWrapper1__globalPre2_globalPre1_formPre2_formPre1_fieldPre2_fieldPre1_foo', field, isolateScope) + expect(fieldPost1).to.have.returned('fieldPost1___formWrapper2____formWrapper1____fieldWrapper2____fieldWrapper1__globalPre2_globalPre1_formPre2_formPre1_fieldPre2_fieldPre1_foo') - expect(fieldPost2).to.have.been.calledWith('fieldPost1___formWrapper2____formWrapper1____fieldWrapper2____fieldWrapper1__globalPre2_globalPre1_formPre2_formPre1_fieldPre2_fieldPre1_foo', field, isolateScope); - expect(fieldPost2).to.have.returned('fieldPost2_fieldPost1___formWrapper2____formWrapper1____fieldWrapper2____fieldWrapper1__globalPre2_globalPre1_formPre2_formPre1_fieldPre2_fieldPre1_foo'); + expect(fieldPost2).to.have.been.calledWith('fieldPost1___formWrapper2____formWrapper1____fieldWrapper2____fieldWrapper1__globalPre2_globalPre1_formPre2_formPre1_fieldPre2_fieldPre1_foo', field, isolateScope) + expect(fieldPost2).to.have.returned('fieldPost2_fieldPost1___formWrapper2____formWrapper1____fieldWrapper2____fieldWrapper1__globalPre2_globalPre1_formPre2_formPre1_fieldPre2_fieldPre1_foo') // form post - expect(formPost1).to.have.been.calledWith('fieldPost2_fieldPost1___formWrapper2____formWrapper1____fieldWrapper2____fieldWrapper1__globalPre2_globalPre1_formPre2_formPre1_fieldPre2_fieldPre1_foo', field, isolateScope); - expect(formPost1).to.have.returned('formPost1_fieldPost2_fieldPost1___formWrapper2____formWrapper1____fieldWrapper2____fieldWrapper1__globalPre2_globalPre1_formPre2_formPre1_fieldPre2_fieldPre1_foo'); + expect(formPost1).to.have.been.calledWith('fieldPost2_fieldPost1___formWrapper2____formWrapper1____fieldWrapper2____fieldWrapper1__globalPre2_globalPre1_formPre2_formPre1_fieldPre2_fieldPre1_foo', field, isolateScope) + expect(formPost1).to.have.returned('formPost1_fieldPost2_fieldPost1___formWrapper2____formWrapper1____fieldWrapper2____fieldWrapper1__globalPre2_globalPre1_formPre2_formPre1_fieldPre2_fieldPre1_foo') - expect(formPost2).to.have.been.calledWith('formPost1_fieldPost2_fieldPost1___formWrapper2____formWrapper1____fieldWrapper2____fieldWrapper1__globalPre2_globalPre1_formPre2_formPre1_fieldPre2_fieldPre1_foo', field, isolateScope); - expect(formPost2).to.have.returned('formPost2_formPost1_fieldPost2_fieldPost1___formWrapper2____formWrapper1____fieldWrapper2____fieldWrapper1__globalPre2_globalPre1_formPre2_formPre1_fieldPre2_fieldPre1_foo'); + expect(formPost2).to.have.been.calledWith('formPost1_fieldPost2_fieldPost1___formWrapper2____formWrapper1____fieldWrapper2____fieldWrapper1__globalPre2_globalPre1_formPre2_formPre1_fieldPre2_fieldPre1_foo', field, isolateScope) + expect(formPost2).to.have.returned('formPost2_formPost1_fieldPost2_fieldPost1___formWrapper2____formWrapper1____fieldWrapper2____fieldWrapper1__globalPre2_globalPre1_formPre2_formPre1_fieldPre2_fieldPre1_foo') // global post - expect(globalPost1).to.have.been.calledWith('formPost2_formPost1_fieldPost2_fieldPost1___formWrapper2____formWrapper1____fieldWrapper2____fieldWrapper1__globalPre2_globalPre1_formPre2_formPre1_fieldPre2_fieldPre1_foo', field, isolateScope); - expect(globalPost1).to.have.returned('globalPost1_formPost2_formPost1_fieldPost2_fieldPost1___formWrapper2____formWrapper1____fieldWrapper2____fieldWrapper1__globalPre2_globalPre1_formPre2_formPre1_fieldPre2_fieldPre1_foo'); + expect(globalPost1).to.have.been.calledWith('formPost2_formPost1_fieldPost2_fieldPost1___formWrapper2____formWrapper1____fieldWrapper2____fieldWrapper1__globalPre2_globalPre1_formPre2_formPre1_fieldPre2_fieldPre1_foo', field, isolateScope) + expect(globalPost1).to.have.returned('globalPost1_formPost2_formPost1_fieldPost2_fieldPost1___formWrapper2____formWrapper1____fieldWrapper2____fieldWrapper1__globalPre2_globalPre1_formPre2_formPre1_fieldPre2_fieldPre1_foo') - expect(globalPost2).to.have.been.calledWith('globalPost1_formPost2_formPost1_fieldPost2_fieldPost1___formWrapper2____formWrapper1____fieldWrapper2____fieldWrapper1__globalPre2_globalPre1_formPre2_formPre1_fieldPre2_fieldPre1_foo', field, isolateScope); - expect(globalPost2).to.have.returned('globalPost2_globalPost1_formPost2_formPost1_fieldPost2_fieldPost1___formWrapper2____formWrapper1____fieldWrapper2____fieldWrapper1__globalPre2_globalPre1_formPre2_formPre1_fieldPre2_fieldPre1_foo'); - }); - }); + expect(globalPost2).to.have.been.calledWith('globalPost1_formPost2_formPost1_fieldPost2_fieldPost1___formWrapper2____formWrapper1____fieldWrapper2____fieldWrapper1__globalPre2_globalPre1_formPre2_formPre1_fieldPre2_fieldPre1_foo', field, isolateScope) + expect(globalPost2).to.have.returned('globalPost2_globalPost1_formPost2_formPost1_fieldPost2_fieldPost1___formWrapper2____formWrapper1____fieldWrapper2____fieldWrapper1__globalPre2_globalPre1_formPre2_formPre1_fieldPre2_fieldPre1_foo') + }) + }) describe(`extras`, () => { describe(`validateOnModelChange`, () => { it(`should invoke $validate on the field even when the field's model hasn't changed`, () => { - scope.fields = [getNewField({extras: {validateOnModelChange: true}})]; - compileAndDigest(); - const $validateSpy = sinon.spy(field.formControl, '$validate'); - scope.model.foo = 'bar'; - scope.$digest(); - $timeout.flush(); - expect($validateSpy).to.have.been.calledOnce; - }); - }); - }); + scope.fields = [getNewField({extras: {validateOnModelChange: true}})] + compileAndDigest() + const $validateSpy = sinon.spy(field.formControl, '$validate') + scope.model.foo = 'bar' + scope.$digest() + $timeout.flush() + expect($validateSpy).to.have.been.calledOnce + }) + + it(`should cope when field.formControl has been upgraded to an array`, () => { + scope.model = { + multiNgModel: { + start: 'start', + stop: 'stop', + }, + } + const field = getNewField({ + key: 'multiNgModel', + template: multiNgModelField, + extras: { + validateOnModelChange: true, + }, + }) + scope.fields = [field] + compileAndDigest() + const $validateSpy0 = sinon.spy(field.formControl[0], '$validate') + const $validateSpy1 = sinon.spy(field.formControl[1], '$validate') + scope.model.foo = 'bar' + scope.$digest() + $timeout.flush() + expect($validateSpy0).to.have.been.calledOnce + expect($validateSpy1).to.have.been.calledOnce + }) + + it.skip(`should run field expressions when form is initialised`, () => { + scope.model = {email: ''} + scope.fields = [getNewField({ + key: 'email', + templateOptions: { + required: true, + }, + extras: {validateOnModelChange: true}, + }), + getNewField({ + key: 'firstName', + templateOptions: { + required: true, + }, + extras: {validateOnModelChange: true}, + hideExpression: 'form.email.$invalid', + })] + + compileAndDigest() + $timeout.flush() + scope.$digest() + expect(scope.fields[1].formControl).to.exist + expect(scope.fields[1].hide).to.equal(true) + }) + }) + }) describe(`other things`, () => { it(`should warn if you specify 'hide' in expressionProperties`, inject(($log) => { - scope.fields = [getNewField({expressionProperties: {hide: 'foo'}})]; - compileAndDigest(); - const log = $log.warn.logs[0]; - expect($log.warn.logs).to.have.length(1); - expect(log[0]).to.equal('Formly Warning:'); + scope.fields = [getNewField({expressionProperties: {hide: 'foo'}})] + compileAndDigest() + const log = $log.warn.logs[0] + expect($log.warn.logs).to.have.length(1) + expect(log[0]).to.equal('Formly Warning:') expect(log[1]).to.equal( 'You have specified `hide` in `expressionProperties`. Use `hideExpression` instead' - ); - expect(log[2]).to.equal(field); - })); + ) + expect(log[2]).to.equal(field) + })) it(`should add a bunch of things to the formly field and it's scope`, () => { - scope.fields = [{template: ''}]; - compileAndDigest(); + scope.fields = [{template: ''}] + compileAndDigest() // here's a list of everything that angular-formly adds for you. expect(field).to.contain.all.keys([ 'key', 'extras', 'data', 'templateOptions', 'validation', 'value', 'runExpressions', - 'resetModel', 'updateInitialValue', 'id', 'name', 'initialValue', 'formControl' - ]); - }); + 'resetModel', 'updateInitialValue', 'id', 'name', 'initialValue', 'formControl', + ]) + }) it(`should add a bunch of things to the formly field and it's scope`, () => { - scope.fields = [{template: ''}]; - compileAndDigest(); + scope.fields = [{template: ''}] + compileAndDigest() // here's a list of everything that you have available on the scope for your templates expect(isolateScope).to.contain.all.keys([ 'options', 'model', 'formId', 'index', 'fields', 'formState', 'formOptions', - 'form', 'id', 'to', 'fc', 'name', 'showError' - ]); - }); - }); + 'form', 'id', 'to', 'fc', 'name', 'showError', + ]) + }) + }) describe(`merging of options`, () => { describe(`extends`, () => { @@ -1736,42 +2060,42 @@ describe('formly-field', function() { formlyConfig.setType({ name: 'hr', template: '
', - defaultOptions: () => ({data: {foo: 'bar', baz: 'foobar'}}) - }); - }); + defaultOptions: () => ({data: {foo: 'bar', baz: 'foobar'}}), + }) + }) it(`should merge the options properly`, () => { - const field = {type: 'hr', data: {baz: 'barfoo'}}; - scope.fields = [field]; - compileAndDigest(); - expect(field.data.baz).to.equal('barfoo'); - expect(field.data.foo).to.equal('bar'); - }); - }); - }); + const field = {type: 'hr', data: {baz: 'barfoo'}} + scope.fields = [field] + compileAndDigest() + expect(field.data.baz).to.equal('barfoo') + expect(field.data.foo).to.equal('bar') + }) + }) + }) function compileAndDigest(template = basicForm, context = scope) { - el = $compile(template)(context); - context.$digest(); - node = el[0]; - field = context.fields[0]; - isolateScope = getIsolateScope(); - return el; + el = $compile(template)(context) + context.$digest() + node = el[0] + field = context.fields[0] + isolateScope = getIsolateScope() + return el } function getIsolateScope(index = 0) { - return angular.element(getFieldNode(index)).isolateScope(); + return angular.element(getFieldNode(index)).isolateScope() } function getFieldNode(index = 0) { - return node.querySelectorAll('.formly-field')[index]; + return node.querySelectorAll('.formly-field')[index] } function getFieldNgModelNode(index = 0) { - return getFieldNode(index).querySelector('[ng-model]'); + return getFieldNode(index).querySelector('[ng-model]') } function getNgModelCtrl(index = 0) { - return angular.element(getFieldNgModelNode(index)).controller('ngModel'); + return angular.element(getFieldNgModelNode(index)).controller('ngModel') } -}); +}) diff --git a/src/directives/formly-focus.js b/src/directives/formly-focus.js index 5ed9d774..d39bdd25 100644 --- a/src/directives/formly-focus.js +++ b/src/directives/formly-focus.js @@ -1,29 +1,29 @@ -export default formlyFocus; +export default formlyFocus // @ngInject function formlyFocus($timeout, $document) { return { restrict: 'A', link: function formlyFocusLink(scope, element, attrs) { - let previousEl = null; - const el = element[0]; - const doc = $document[0]; + let previousEl = null + const el = element[0] + const doc = $document[0] attrs.$observe('formlyFocus', function respondToFocusExpressionChange(value) { /* eslint no-bitwise:0 */ // I know what I'm doing. I promise... if (value === 'true') { $timeout(function setElementFocus() { - previousEl = doc.activeElement; - el.focus(); - }, ~~attrs.focusWait); + previousEl = doc.activeElement + el.focus() + }, ~~attrs.focusWait) } else if (value === 'false') { if (doc.activeElement === el) { - el.blur(); + el.blur() if (attrs.hasOwnProperty('refocus') && previousEl) { - previousEl.focus(); + previousEl.focus() } } } - }); - } - }; + }) + }, + } } diff --git a/src/directives/formly-focus.test.js b/src/directives/formly-focus.test.js index 3c4e25ba..c30a11fa 100644 --- a/src/directives/formly-focus.test.js +++ b/src/directives/formly-focus.test.js @@ -1,99 +1,99 @@ /* eslint max-len:[2,200] */ -import _ from 'lodash'; +import _ from 'lodash' describe('formly-form', () => { - let $compile, scope, el, node, input, textarea, $timeout; + let $compile, scope, el, node, input, textarea, $timeout - const basicTemplate = '
'; - beforeEach(window.module('formly')); + const basicTemplate = '
' + beforeEach(window.module('formly')) beforeEach(inject((_$compile_, _$timeout_, $rootScope) => { - $compile = _$compile_; - $timeout = _$timeout_; - scope = $rootScope.$new(); - })); + $compile = _$compile_ + $timeout = _$timeout_ + scope = $rootScope.$new() + })) it(`focus the element when focus is set to "true" and then blurred when it's set to "false"`, () => { - compileAndDigest(); - expectFocus(false); - scope.focus = true; - scope.$digest(); - $timeout.flush(); - expectFocus(true); - - scope.focus = false; - scope.$digest(); - expectFocus(false); - }); + compileAndDigest() + expectFocus(false) + scope.focus = true + scope.$digest() + $timeout.flush() + expectFocus(true) + + scope.focus = false + scope.$digest() + expectFocus(false) + }) it(`should not bother unfocusing the element if it doesn't have focus to begin with`, () => { - compileAndDigest(); - expectFocus(false); - scope.focus = false; - scope.$digest(); - expectFocus(false); - }); + compileAndDigest() + expectFocus(false) + scope.focus = false + scope.$digest() + expectFocus(false) + }) it(`should refocus the previously focused element`, () => { - compileAndDigest(); - textarea.focus(); - scope.focus = true; - scope.$digest(); - $timeout.flush(); - expectFocus(true); - - scope.focus = false; - scope.$digest(); - expectFocus(false); - expectFocus(true, textarea); - }); + compileAndDigest() + textarea.focus() + scope.focus = true + scope.$digest() + $timeout.flush() + expectFocus(true) + + scope.focus = false + scope.$digest() + expectFocus(false) + expectFocus(true, textarea) + }) it(`should not refocus the previously focused element when the refocus attribute doesn't exist`, () => { compileAndDigest( '
' - ); - textarea.focus(); - scope.focus = true; - scope.$digest(); - $timeout.flush(); - expectFocus(true); - - scope.focus = false; - scope.$digest(); - expectFocus(false); - expectFocus(false, textarea); - }); + ) + textarea.focus() + scope.focus = true + scope.$digest() + $timeout.flush() + expectFocus(true) + + scope.focus = false + scope.$digest() + expectFocus(false) + expectFocus(false, textarea) + }) it(`should not refocus if the previously active element has not been set`, () => { - compileAndDigest(undefined, {focus: false}); - expectFocus(false); - }); + compileAndDigest(undefined, {focus: false}) + expectFocus(false) + }) afterEach(() => { if (document.body.contains(node)) { - node.parentNode.removeChild(node); + node.parentNode.removeChild(node) } - }); + }) function expectFocus(focus, focusedNode = input) { - const isFocused = document.activeElement === focusedNode; + const isFocused = document.activeElement === focusedNode if (focus) { - expect(isFocused, 'expected focused element to be: ' + focusedNode + ' but it was ' + document.activeElement).to.be.true; + expect(isFocused, 'expected focused element to be: ' + focusedNode + ' but it was ' + document.activeElement).to.be.true } else { - expect(isFocused, 'expected focused element to not be: ' + focusedNode + ' but it was ' + document.activeElement).to.be.false; + expect(isFocused, 'expected focused element to not be: ' + focusedNode + ' but it was ' + document.activeElement).to.be.false } } function compileAndDigest(template = basicTemplate, scopeOverrides = {}) { - _.assign(scope, scopeOverrides); - el = $compile(template)(scope); - scope.$digest(); - node = el[0]; - document.body.appendChild(node); - input = document.getElementById('main-input'); - textarea = document.getElementById('textarea'); - return el; + _.assign(scope, scopeOverrides) + el = $compile(template)(scope) + scope.$digest() + node = el[0] + document.body.appendChild(node) + input = document.getElementById('main-input') + textarea = document.getElementById('textarea') + return el } -}); +}) diff --git a/src/directives/formly-form.controller.js b/src/directives/formly-form.controller.js new file mode 100644 index 00000000..94fa4fe0 --- /dev/null +++ b/src/directives/formly-form.controller.js @@ -0,0 +1,301 @@ +import angular from 'angular-fix' + +function isFieldGroup(field) { + return field && !!field.fieldGroup +} + +// @ngInject +export default function FormlyFormController( + formlyUsability, formlyWarn, formlyConfig, $parse, $scope, formlyApiCheck, formlyUtil) { + + setupOptions() + $scope.model = $scope.model || {} + setupFields() + + // watch the model and evaluate watch expressions that depend on it. + if (!$scope.options.manualModelWatcher) { + $scope.$watch('model', onModelOrFormStateChange, true) + } else if (angular.isFunction($scope.options.manualModelWatcher)) { + $scope.$watch($scope.options.manualModelWatcher, onModelOrFormStateChange, true) + } + + if ($scope.options.formState) { + $scope.$watch('options.formState', onModelOrFormStateChange, true) + } + + function onModelOrFormStateChange() { + angular.forEach($scope.fields, runFieldExpressionProperties) + } + + function validateFormControl(formControl, promise) { + const validate = formControl.$validate + if (promise) { + promise.then(() => validate.apply(formControl)) + } else { + validate() + } + } + + function runFieldExpressionProperties(field, index) { + const model = field.model || $scope.model + const promise = field.runExpressions && field.runExpressions() + if (field.hideExpression) { // can't use hide with expressionProperties reliably + const val = model[field.key] + field.hide = evalCloseToFormlyExpression(field.hideExpression, val, field, index, {model}) + } + if (field.extras && field.extras.validateOnModelChange && field.formControl) { + if (angular.isArray(field.formControl)) { + angular.forEach(field.formControl, function(formControl) { + validateFormControl(formControl, promise) + }) + } else { + validateFormControl(field.formControl, promise) + } + } + } + + function setupFields() { + $scope.fields = $scope.fields || [] + + checkDeprecatedOptions($scope.options) + + let fieldTransforms = $scope.options.fieldTransform || formlyConfig.extras.fieldTransform + + if (!angular.isArray(fieldTransforms)) { + fieldTransforms = [fieldTransforms] + } + + angular.forEach(fieldTransforms, function transformFields(fieldTransform) { + if (fieldTransform) { + $scope.fields = fieldTransform($scope.fields, $scope.model, $scope.options, $scope.form) + if (!$scope.fields) { + throw formlyUsability.getFormlyError('fieldTransform must return an array of fields') + } + } + }) + + setupModels() + + if ($scope.options.watchAllExpressions) { + angular.forEach($scope.fields, setupHideExpressionWatcher) + } + + angular.forEach($scope.fields, attachKey) // attaches a key based on the index if a key isn't specified + angular.forEach($scope.fields, setupWatchers) // setup watchers for all fields + } + + function checkDeprecatedOptions(options) { + if (formlyConfig.extras.fieldTransform && angular.isFunction(formlyConfig.extras.fieldTransform)) { + formlyWarn( + 'fieldtransform-as-a-function-deprecated', + 'fieldTransform as a function has been deprecated.', + `Attempted for formlyConfig.extras: ${formlyConfig.extras.fieldTransform.name}`, + formlyConfig.extras + ) + } else if (options.fieldTransform && angular.isFunction(options.fieldTransform)) { + formlyWarn( + 'fieldtransform-as-a-function-deprecated', + 'fieldTransform as a function has been deprecated.', + `Attempted for form`, + options + ) + } + } + + function setupOptions() { + formlyApiCheck.throw( + [formlyApiCheck.formOptionsApi.optional], [$scope.options], {prefix: 'formly-form options check'} + ) + $scope.options = $scope.options || {} + $scope.options.formState = $scope.options.formState || {} + + angular.extend($scope.options, { + updateInitialValue, + resetModel, + }) + + } + + function updateInitialValue() { + angular.forEach($scope.fields, field => { + if (isFieldGroup(field) && field.options) { + field.options.updateInitialValue() + } else { + field.updateInitialValue() + } + }) + } + + function resetModel() { + angular.forEach($scope.fields, field => { + if (isFieldGroup(field) && field.options) { + field.options.resetModel() + } else if (field.resetModel) { + field.resetModel() + } + }) + } + + function setupModels() { + // a set of field models that are already watched (the $scope.model will have its own watcher) + const watchedModels = [$scope.model] + // we will not set up automatic model watchers if manual mode is set + const manualModelWatcher = $scope.options.manualModelWatcher + + if ($scope.options.formState) { + // $scope.options.formState will have its own watcher + watchedModels.push($scope.options.formState) + } + + angular.forEach($scope.fields, (field) => { + const isNewModel = initModel(field) + + if (field.model && isNewModel && watchedModels.indexOf(field.model) === -1 && !manualModelWatcher) { + $scope.$watch(() => field.model, onModelOrFormStateChange, true) + watchedModels.push(field.model) + } + }) + } + + function setupHideExpressionWatcher(field, index) { + if (field.hideExpression) { // can't use hide with expressionProperties reliably + const model = field.model || $scope.model + $scope.$watch(function hideExpressionWatcher() { + const val = model[field.key] + return evalCloseToFormlyExpression(field.hideExpression, val, field, index, {model}) + }, (hide) => field.hide = hide, true) + } + } + + function initModel(field) { + let isNewModel = true + + if (angular.isString(field.model)) { + const expression = field.model + + isNewModel = !referencesCurrentlyWatchedModel(expression) + + field.model = resolveStringModel(expression) + + $scope.$watch(() => resolveStringModel(expression), (model) => field.model = model) + } + + return isNewModel + + function resolveStringModel(expression) { + const index = $scope.fields.indexOf(field) + const model = evalCloseToFormlyExpression(expression, undefined, field, index, {model: $scope.model}) + + if (!model) { + throw formlyUsability.getFieldError( + 'field-model-must-be-initialized', + 'Field model must be initialized. When specifying a model as a string for a field, the result of the' + + ' expression must have been initialized ahead of time.', + field) + } + + return model + } + } + + function referencesCurrentlyWatchedModel(expression) { + return ['model', 'formState'].some(item => { + return formlyUtil.startsWith(expression, `${item}.`) || formlyUtil.startsWith(expression, `${item}[`) + }) + } + + function attachKey(field, index) { + if (!isFieldGroup(field)) { + field.key = field.key || index || 0 + } + } + + function setupWatchers(field, index) { + if (!angular.isDefined(field.watcher)) { + return + } + let watchers = field.watcher + if (!angular.isArray(watchers)) { + watchers = [watchers] + } + angular.forEach(watchers, function setupWatcher(watcher) { + if (!angular.isDefined(watcher.listener) && !watcher.runFieldExpressions) { + throw formlyUsability.getFieldError( + 'all-field-watchers-must-have-a-listener', + 'All field watchers must have a listener', field + ) + } + const watchExpression = getWatchExpression(watcher, field, index) + const watchListener = getWatchListener(watcher, field, index) + + const type = watcher.type || '$watch' + watcher.stopWatching = $scope[type](watchExpression, watchListener, watcher.watchDeep) + }) + } + + function getWatchExpression(watcher, field, index) { + let watchExpression + if (!angular.isUndefined(watcher.expression)) { + watchExpression = watcher.expression + } else if (field.key) { + watchExpression = 'model[\'' + field.key.toString().split('.').join('\'][\'') + '\']' + } + if (angular.isFunction(watchExpression)) { + // wrap the field's watch expression so we can call it with the field as the first arg + // and the stop function as the last arg as a helper + const originalExpression = watchExpression + watchExpression = function formlyWatchExpression() { + const args = modifyArgs(watcher, index, ...arguments) + return originalExpression(...args) + } + watchExpression.displayName = `Formly Watch Expression for field for ${field.key}` + } else if (field.model) { + watchExpression = $parse(watchExpression).bind(null, $scope, {model: field.model}) + } + return watchExpression + } + + function getWatchListener(watcher, field, index) { + let watchListener = watcher.listener + if (angular.isFunction(watchListener) || watcher.runFieldExpressions) { + // wrap the field's watch listener so we can call it with the field as the first arg + // and the stop function as the last arg as a helper + const originalListener = watchListener + watchListener = function formlyWatchListener() { + let value + if (originalListener) { + const args = modifyArgs(watcher, index, ...arguments) + value = originalListener(...args) + } + if (watcher.runFieldExpressions) { + runFieldExpressionProperties(field, index) + } + return value + } + watchListener.displayName = `Formly Watch Listener for field for ${field.key}` + } + return watchListener + } + + function modifyArgs(watcher, index, ...originalArgs) { + return [$scope.fields[index], ...originalArgs, watcher.stopWatching] + } + + function evalCloseToFormlyExpression(expression, val, field, index, extraLocals = {}) { + extraLocals = angular.extend(getFormlyFieldLikeLocals(field, index), extraLocals) + return formlyUtil.formlyEval($scope, expression, val, val, extraLocals) + } + + function getFormlyFieldLikeLocals(field, index) { + // this makes it closer to what a regular formlyExpression would be + return { + model: field.model, + options: field, + index, + formState: $scope.options.formState, + originalModel: $scope.model, + formOptions: $scope.options, + formId: $scope.formId, + } + } +} diff --git a/src/directives/formly-form.controller.test.js b/src/directives/formly-form.controller.test.js new file mode 100644 index 00000000..eb01be9f --- /dev/null +++ b/src/directives/formly-form.controller.test.js @@ -0,0 +1,122 @@ +/* eslint no-shadow:0 */ +/* eslint no-console:0 */ +/* eslint max-len:0 */ +/* eslint max-nested-callbacks:0 */ +import {getNewField, shouldWarnWithLog} from '../test.utils.js' +import _ from 'lodash' + +describe('FormlyFormController', () => { + let $controller, formlyConfig, scope + beforeEach(window.module('formly')) + beforeEach(inject((_formlyConfig_, $rootScope, _$controller_) => { + formlyConfig = _formlyConfig_ + scope = $rootScope.$new() + scope.model = {} + scope.fields = [] + scope.options = {} + $controller = _$controller_ + })) + + describe(`fieldTransform`, () => { + beforeEach(() => { + formlyConfig.extras.fieldTransform = fieldTransform + }) + + it(`should give a deprecation warning when formlyConfig.extras.fieldTransform is a function rather than an array`, inject(($log) => { + + shouldWarnWithLog( + $log, + [ + 'Formly Warning:', + 'fieldTransform as a function has been deprecated.', + /Attempted for formlyConfig.extras/, + ], + () => $controller('FormlyFormController', {$scope: scope}) + ) + })) + + it(`should give a deprecation warning when options.fieldTransform function rather than an array`, inject(($log) => { + formlyConfig.extras.fieldTransform = undefined + scope.fields = [getNewField()] + scope.options.fieldTransform = fields => fields + shouldWarnWithLog( + $log, + [ + 'Formly Warning:', + 'fieldTransform as a function has been deprecated.', + 'Attempted for form', + ], + () => $controller('FormlyFormController', {$scope: scope}) + ) + })) + + it(`should throw an error if something is passed in and nothing is returned`, () => { + scope.fields = [getNewField()] + scope.options.fieldTransform = function() { + // I return nothing... + } + expect(() => $controller('FormlyFormController', {$scope: scope})).to.throw(/^Formly Error: fieldTransform must return an array of fields/) + }) + + it(`should allow you to transform field configuration`, () => { + scope.options.fieldTransform = fieldTransform + const spy = sinon.spy(scope.options, 'fieldTransform') + doExpectations(spy) + }) + + it(`should use formlyConfig.extras.fieldTransform when not specified on options`, () => { + const spy = sinon.spy(formlyConfig.extras, 'fieldTransform') + doExpectations(spy) + }) + + it(`should allow you to use an array of transform functions`, () => { + scope.fields = [getNewField({ + customThing: 'foo', + otherCustomThing: { + whatever: '|-o-|', + }})] + scope.options.fieldTransform = [fieldTransform] + expect(() => $controller('FormlyFormController', {$scope: scope})).to.not.throw() + + const field = scope.fields[0] + expect(field).to.have.deep.property('data.customThing') + expect(field).to.have.deep.property('data.otherCustomThing') + }) + + function doExpectations(spy) { + const originalFields = [{ + key: 'keyProp', + template: '
', + customThing: 'foo', + otherCustomThing: { + whatever: '|-o-|', + }, + }] + scope.fields = originalFields + $controller('FormlyFormController', {$scope: scope}) + expect(spy).to.have.been.calledWith(originalFields, scope.model, scope.options, scope.form) + const field = scope.fields[0] + + expect(field).to.not.have.property('customThing') + expect(field).to.not.have.property('otherCustomThing') + expect(field).to.have.deep.property('data.customThing') + expect(field).to.have.deep.property('data.otherCustomThing') + } + + function fieldTransform(fields) { + const extraKeys = ['customThing', 'otherCustomThing'] + return _.map(fields, field => { + const newField = {data: {}} + _.each(field, (val, name) => { + if (_.includes(extraKeys, name)) { + newField.data[name] = val + } else { + newField[name] = val + } + }) + return newField + }) + } + }) + +}) diff --git a/src/directives/formly-form.js b/src/directives/formly-form.js index 541b1b9c..74f411c6 100644 --- a/src/directives/formly-form.js +++ b/src/directives/formly-form.js @@ -1,6 +1,6 @@ -import angular from 'angular-fix'; +import angular from 'angular-fix' -export default formlyForm; +export default formlyForm /** * @ngdoc directive @@ -9,7 +9,7 @@ export default formlyForm; */ // @ngInject function formlyForm(formlyUsability, formlyWarn, $parse, formlyConfig, $interpolate) { - let currentFormId = 1; + let currentFormId = 1 return { restrict: 'AE', template: formlyFormGetTemplate, @@ -19,19 +19,19 @@ function formlyForm(formlyUsability, formlyWarn, $parse, formlyConfig, $interpol fields: '=', model: '=', form: '=?', - options: '=?' + options: '=?', }, - controller: FormlyFormController, - link: formlyFormLink - }; + controller: 'FormlyFormController', + link: formlyFormLink, + } function formlyFormGetTemplate(el, attrs) { - const rootEl = getRootEl(); - const fieldRootEl = getFieldRootEl(); - const formId = `formly_${currentFormId++}`; - let parentFormAttributes = ''; + const rootEl = getRootEl() + const fieldRootEl = getFieldRootEl() + const formId = `formly_${currentFormId++}` + let parentFormAttributes = '' if (attrs.hasOwnProperty('isFieldGroup') && el.parent().parent().hasClass('formly')) { - parentFormAttributes = copyAttributes(el.parent().parent()[0].attributes); + parentFormAttributes = copyAttributes(el.parent().parent()[0].attributes) } return ` <${rootEl} class="formly" @@ -53,308 +53,74 @@ function formlyForm(formlyUsability, formlyWarn, $parse, formlyConfig, $interpol
- `; + ` function getRootEl() { - return attrs.rootEl || 'ng-form'; + return attrs.rootEl || 'ng-form' } function getFieldRootEl() { - return attrs.fieldRootEl || 'div'; + return attrs.fieldRootEl || 'div' } function getHideDirective() { - return attrs.hideDirective || formlyConfig.extras.defaultHideDirective || 'ng-if'; + return attrs.hideDirective || formlyConfig.extras.defaultHideDirective || 'ng-if' } function getTrackBy() { if (!attrs.trackBy) { - return ''; + return '' } else { - return `track by ${attrs.trackBy}`; + return `track by ${attrs.trackBy}` } } function getFormName() { - let formName = formId; - const bindName = attrs.bindName; + let formName = formId + const bindName = attrs.bindName if (bindName) { if (angular.version.minor < 3) { - throw formlyUsability.getFormlyError('bind-name attribute on formly-form not allowed in < angular 1.3'); + throw formlyUsability.getFormlyError('bind-name attribute on formly-form not allowed in < angular 1.3') } // we can do a one-time binding here because we know we're in 1.3.x territory - formName = `${$interpolate.startSymbol()}::'formly_' + ${bindName}${$interpolate.endSymbol()}`; + formName = `${$interpolate.startSymbol()}::'formly_' + ${bindName}${$interpolate.endSymbol()}` } - return formName; + return formName } function getTranscludeClass() { - return attrs.transcludeClass || ''; + return attrs.transcludeClass || '' } function copyAttributes(attributes) { const excluded = ['model', 'form', 'fields', 'options', 'name', 'role', 'class', - 'data-model', 'data-form', 'data-fields', 'data-options', 'data-name']; - const arrayAttrs = []; + 'data-model', 'data-form', 'data-fields', 'data-options', 'data-name'] + const arrayAttrs = [] angular.forEach(attributes, ({nodeName, value}) => { if (nodeName !== 'undefined' && excluded.indexOf(nodeName) === -1) { - arrayAttrs.push(`${toKebabCase(nodeName)}="${value}"`); + arrayAttrs.push(`${toKebabCase(nodeName)}="${value}"`) } - }); - return arrayAttrs.join(' '); - } - } - - // @ngInject - function FormlyFormController($scope, formlyApiCheck, formlyUtil) { - setupOptions(); - $scope.model = $scope.model || {}; - setupFields(); - - // watch the model and evaluate watch expressions that depend on it. - $scope.$watch('model', onModelOrFormStateChange, true); - if ($scope.options.formState) { - $scope.$watch('options.formState', onModelOrFormStateChange, true); - } - - function onModelOrFormStateChange() { - angular.forEach($scope.fields, function runFieldExpressionProperties(field, index) { - const model = field.model || $scope.model; - const promise = field.runExpressions && field.runExpressions(); - if (field.hideExpression) { // can't use hide with expressionProperties reliably - const val = model[field.key]; - field.hide = evalCloseToFormlyExpression(field.hideExpression, val, field, index); - } - if (field.extras && field.extras.validateOnModelChange && field.formControl) { - const validate = field.formControl.$validate; - if (promise) { - promise.then(validate); - } else { - validate(); - } - } - }); - } - - function setupFields() { - $scope.fields = $scope.fields || []; - - checkDeprecatedOptions($scope.options); - - let fieldTransforms = $scope.options.fieldTransform || formlyConfig.extras.fieldTransform; - - if (!angular.isArray(fieldTransforms)) { - fieldTransforms = [fieldTransforms]; - } - - angular.forEach(fieldTransforms, function transformFields(fieldTransform) { - if (fieldTransform) { - $scope.fields = fieldTransform($scope.fields, $scope.model, $scope.options, $scope.form); - if (!$scope.fields) { - throw formlyUsability.getFormlyError('fieldTransform must return an array of fields'); - } - } - }); - - setupModels(); - - angular.forEach($scope.fields, attachKey); // attaches a key based on the index if a key isn't specified - angular.forEach($scope.fields, setupWatchers); // setup watchers for all fields - } - - function checkDeprecatedOptions(options) { - if (formlyConfig.extras.fieldTransform && angular.isFunction(formlyConfig.extras.fieldTransform)) { - formlyWarn( - 'fieldtransform-as-a-function-deprecated', - 'fieldTransform as a function has been deprecated.', - `Attempted for formlyConfig.extras: ${formlyConfig.extras.fieldTransform.name}`, - formlyConfig.extras - ); - } else if (options.fieldTransform && angular.isFunction(options.fieldTransform)) { - formlyWarn( - 'fieldtransform-as-a-function-deprecated', - 'fieldTransform as a function has been deprecated.', - `Attempted for form`, - options - ); - } - } - - function setupOptions() { - formlyApiCheck.throw( - [formlyApiCheck.formOptionsApi.optional], [$scope.options], {prefix: 'formly-form options check'} - ); - $scope.options = $scope.options || {}; - $scope.options.formState = $scope.options.formState || {}; - - angular.extend($scope.options, { - updateInitialValue, - resetModel - }); - - } - - function updateInitialValue() { - angular.forEach($scope.fields, field => { - if (isFieldGroup(field) && field.options) { - field.options.updateInitialValue(); - } else { - field.updateInitialValue(); - } - }); - } - - function resetModel() { - angular.forEach($scope.fields, field => { - if (isFieldGroup(field) && field.options) { - field.options.resetModel(); - } else if (field.resetModel) { - field.resetModel(); - } - }); - } - - function setupModels() { - // a set of field models that are already watched (the $scope.model will have its own watcher) - const watchedModels = [$scope.model]; - - if ($scope.options.formState) { - // $scope.options.formState will have its own watcher - watchedModels.push($scope.options.formState); - } - - angular.forEach($scope.fields, (field) => { - const isNewModel = initModel(field); - - if (field.model && isNewModel && watchedModels.indexOf(field.model) === -1) { - $scope.$watch(() => field.model, onModelOrFormStateChange, true); - watchedModels.push(field.model); - } - }); - } - - function initModel(field) { - let isNewModel = true; - - if (angular.isString(field.model)) { - const expression = field.model; - const index = $scope.fields.indexOf(field); - - isNewModel = !refrencesCurrentlyWatchedModel(expression); - - field.model = evalCloseToFormlyExpression(expression, undefined, field, index); - if (!field.model) { - throw formlyUsability.getFieldError( - 'field-model-must-be-initialized', - 'Field model must be initialized. When specifying a model as a string for a field, the result of the' + - ' expression must have been initialized ahead of time.', - field); - } - } - return isNewModel; - } - - function refrencesCurrentlyWatchedModel(expression) { - return ['model', 'formState'].some(item => { - return formlyUtil.startsWith(expression, `${item}.`) || formlyUtil.startsWith(expression, `${item}[`); - }); - } - - function attachKey(field, index) { - if (!isFieldGroup(field)) { - field.key = field.key || index || 0; - } - } - - function setupWatchers(field, index) { - if (isFieldGroup(field) || !angular.isDefined(field.watcher)) { - return; - } - let watchers = field.watcher; - if (!angular.isArray(watchers)) { - watchers = [watchers]; - } - angular.forEach(watchers, function setupWatcher(watcher) { - if (!angular.isDefined(watcher.listener)) { - throw formlyUsability.getFieldError( - 'all-field-watchers-must-have-a-listener', - 'All field watchers must have a listener', field - ); - } - const watchExpression = getWatchExpression(watcher, field, index); - const watchListener = getWatchListener(watcher, field, index); - - const type = watcher.type || '$watch'; - watcher.stopWatching = $scope[type](watchExpression, watchListener, watcher.watchDeep); - }); - } - - function getWatchExpression(watcher, field, index) { - let watchExpression = watcher.expression || `model['${field.key}']`; - if (angular.isFunction(watchExpression)) { - // wrap the field's watch expression so we can call it with the field as the first arg - // and the stop function as the last arg as a helper - const originalExpression = watchExpression; - watchExpression = function formlyWatchExpression() { - const args = modifyArgs(watcher, index, ...arguments); - return originalExpression(...args); - }; - watchExpression.displayName = `Formly Watch Expression for field for ${field.key}`; - } - return watchExpression; - } - - function getWatchListener(watcher, field, index) { - let watchListener = watcher.listener; - if (angular.isFunction(watchListener)) { - // wrap the field's watch listener so we can call it with the field as the first arg - // and the stop function as the last arg as a helper - const originalListener = watchListener; - watchListener = function formlyWatchListener() { - const args = modifyArgs(watcher, index, ...arguments); - return originalListener(...args); - }; - watchListener.displayName = `Formly Watch Listener for field for ${field.key}`; - } - return watchListener; - } - - function modifyArgs(watcher, index, ...originalArgs) { - return [$scope.fields[index], ...originalArgs, watcher.stopWatching]; - } - - function evalCloseToFormlyExpression(expression, val, field, index) { - const extraLocals = getFormlyFieldLikeLocals(field, index); - return formlyUtil.formlyEval($scope, expression, val, val, extraLocals); - } - - function getFormlyFieldLikeLocals(field, index) { - // this makes it closer to what a regular formlyExpression would be - return { - options: field, - index, - formState: $scope.options.formState, - formId: $scope.formId - }; + }) + return arrayAttrs.join(' ') } } function formlyFormLink(scope, el, attrs) { - setFormController(); - fixChromeAutocomplete(); + setFormController() + fixChromeAutocomplete() function setFormController() { - const formId = attrs.name; - scope.formId = formId; - scope.theFormlyForm = scope[formId]; + const formId = attrs.name + scope.formId = formId + scope.theFormlyForm = scope[formId] if (attrs.form) { - const getter = $parse(attrs.form); - const setter = getter.assign; - const parentForm = getter(scope.$parent); + const getter = $parse(attrs.form) + const setter = getter.assign + const parentForm = getter(scope.$parent) if (parentForm) { - scope.theFormlyForm = parentForm; + scope.theFormlyForm = parentForm if (scope[formId]) { - scope.theFormlyForm.$removeControl(scope[formId]); + scope.theFormlyForm.$removeControl(scope[formId]) } // this next line is probably one of the more dangerous things that angular-formly does to improve the @@ -366,9 +132,9 @@ function formlyForm(formlyUsability, formlyWarn, $parse, formlyConfig, $interpol // https://github.com/formly-js/angular-formly/issues/287 // luckily, this is how the formController has been accessed by the NgModelController since angular 1.0.0 // so I expect it will remain this way for the life of angular 1.x - el.removeData('$formController'); + el.removeData('$formController') } else { - setter(scope.$parent, scope[formId]); + setter(scope.$parent, scope[formId]) } } if (!scope.theFormlyForm && !formlyConfig.disableWarnings) { @@ -378,7 +144,7 @@ function formlyForm(formlyUsability, formlyWarn, $parse, formlyConfig, $interpol 'Your formly-form does not have a `form` property. Many functions of the form (like validation) may not work', el, scope - ); + ) } } @@ -388,14 +154,14 @@ function formlyForm(formlyUsability, formlyWarn, $parse, formlyConfig, $interpol * ლ(ಠ益ಠლ) (╯°□°)╯︵ ┻━┻ (◞‸◟;) */ function fixChromeAutocomplete() { - const global = formlyConfig.extras.removeChromeAutoComplete === true; - const offInstance = scope.options && scope.options.removeChromeAutoComplete === false; - const onInstance = scope.options && scope.options.removeChromeAutoComplete === true; + const global = formlyConfig.extras.removeChromeAutoComplete === true + const offInstance = scope.options && scope.options.removeChromeAutoComplete === false + const onInstance = scope.options && scope.options.removeChromeAutoComplete === true if ((global && !offInstance) || onInstance) { - const input = document.createElement('input'); - input.setAttribute('autocomplete', 'address-level4'); - input.setAttribute('hidden', 'true'); - el[0].appendChild(input); + const input = document.createElement('input') + input.setAttribute('autocomplete', 'address-level4') + input.setAttribute('hidden', 'true') + el[0].appendChild(input) } } @@ -405,13 +171,10 @@ function formlyForm(formlyUsability, formlyWarn, $parse, formlyConfig, $interpol // stateless util functions function toKebabCase(string) { if (string) { - return string.replace(/([A-Z])/g, $1 => '-' + $1.toLowerCase()); + return string.replace(/([A-Z])/g, $1 => '-' + $1.toLowerCase()) } else { - return ''; + return '' } } - function isFieldGroup(field) { - return field && !!field.fieldGroup; - } } diff --git a/src/directives/formly-form.test.js b/src/directives/formly-form.test.js index dbe5c801..fb767c2c 100644 --- a/src/directives/formly-form.test.js +++ b/src/directives/formly-form.test.js @@ -2,99 +2,98 @@ /* eslint no-console:0 */ /* eslint max-len:0 */ /* eslint max-nested-callbacks:0 */ -import testUtils from '../test.utils.js'; -import angular from 'angular-fix'; -import _ from 'lodash'; +import testUtils from '../test.utils.js' +import angular from 'angular-fix' -const {getNewField, input, basicForm, shouldWarnWithLog} = testUtils; +const {getNewField, input, basicForm} = testUtils describe('formly-form', () => { - let $compile, formlyConfig, scope, el, $timeout; + let $compile, formlyConfig, scope, el, $timeout - beforeEach(window.module('formly')); + beforeEach(window.module('formly')) beforeEach(inject((_$compile_, _formlyConfig_, _$timeout_, $rootScope) => { - formlyConfig = _formlyConfig_; - $compile = _$compile_; - $timeout = _$timeout_; - scope = $rootScope.$new(); - scope.model = {}; - scope.fields = []; - })); + formlyConfig = _formlyConfig_ + $compile = _$compile_ + $timeout = _$timeout_ + scope = $rootScope.$new() + scope.model = {} + scope.fields = [] + })) it(`should be possible to use it as an attribute directive`, () => { const el = compileAndDigest(`
- `); - expect(el.length).to.equal(1); - expect(el.prop('nodeName').toLowerCase()).to.equal('ng-form'); - }); + `) + expect(el.length).to.equal(1) + expect(el.prop('nodeName').toLowerCase()).to.equal('ng-form') + }) it('should use ng-form as the default root tag', () => { const el = compileAndDigest(` - `); - expect(el.length).to.equal(1); - expect(el.prop('nodeName').toLowerCase()).to.equal('ng-form'); - }); + `) + expect(el.length).to.equal(1) + expect(el.prop('nodeName').toLowerCase()).to.equal('ng-form') + }) it('should use a different root tag when specified', () => { const el = compileAndDigest(` - `); - expect(el.length).to.equal(1); - expect(el.prop('nodeName').toLowerCase()).to.equal('form'); - }); + `) + expect(el.length).to.equal(1) + expect(el.prop('nodeName').toLowerCase()).to.equal('form') + }) it(`should use a different root tag for formly-fields when specified`, () => { - scope.fields = [getNewField()]; + scope.fields = [getNewField()] const el = compileAndDigest(` - `); - expect(el[0].querySelector('area.formly-field')).to.exist; - }); + `) + expect(el[0].querySelector('area.formly-field')).to.exist + }) it(`should assign the scope's "form" property to the given FormController if it has a value`, () => { const el = compileAndDigest(`
- `); - const isolateScope = angular.element(el[0].querySelector('#my-formly-form')).isolateScope(); - expect(scope.theForm).to.eq(isolateScope.form); - expect(scope.theForm.$name).to.eq('theForm'); - }); + `) + const isolateScope = angular.element(el[0].querySelector('#my-formly-form')).isolateScope() + expect(scope.theForm).to.eq(isolateScope.form) + expect(scope.theForm.$name).to.eq('theForm') + }) it(`should assign the scope's "form" property to its own FormController if it doesn't have a value`, () => { const el = compileAndDigest(`
- `); - const isolateScope = angular.element(el[0].querySelector('#my-formly-form')).isolateScope(); - expect(scope.theForm).to.eq(isolateScope.theFormlyForm); - }); + `) + const isolateScope = angular.element(el[0].querySelector('#my-formly-form')).isolateScope() + expect(scope.theForm).to.eq(isolateScope.theFormlyForm) + }) it(`should warn if there's no FormController to be assigned`, inject(($log) => { compileAndDigest(` - `); - const log = $log.warn.logs[0]; - expect($log.warn.logs).to.have.length(1); - expect(log[0]).to.equal('Formly Warning:'); - expect(log[1]).to.equal('Your formly-form does not have a `form` property. Many functions of the form (like validation) may not work'); - })); + `) + const log = $log.warn.logs[0] + expect($log.warn.logs).to.have.length(1) + expect(log[0]).to.equal('Formly Warning:') + expect(log[1]).to.equal('Your formly-form does not have a `form` property. Many functions of the form (like validation) may not work') + })) it(`should put the formControl on the field's scope when using a different form root element`, () => { - scope.fields = [getNewField()]; + scope.fields = [getNewField()] const el = compileAndDigest(`
- `); + `) - const fieldScope = angular.element(el[0].querySelector('.formly-field')).isolateScope(); - expect(fieldScope.fc).to.exist; - }); + const fieldScope = angular.element(el[0].querySelector('.formly-field')).isolateScope() + expect(fieldScope.fc).to.exist + }) it(`should not allow sibling forms to override each other on a parent form`, () => { compileAndDigest(` @@ -102,36 +101,36 @@ describe('formly-form', () => { - `); - expect(scope.parent).to.have.property('formly_1'); - expect(scope.parent).to.have.property('formly_2'); - }); + `) + expect(scope.parent).to.have.property('formly_1') + expect(scope.parent).to.have.property('formly_2') + }) it(`should place the form control on the scope property defined by the form attribute`, () => { compileAndDigest(` - `); - expect(scope.vm).to.have.property('myForm'); - expect(scope.vm.myForm).to.have.property('$name'); - }); + `) + expect(scope.vm).to.have.property('myForm') + expect(scope.vm.myForm).to.have.property('$name') + }) it(`should initialize the model and the fields if not provided`, () => { compileAndDigest(` - `); - expect(scope.model).to.exist; - expect(scope.fields).to.exist; - }); + `) + expect(scope.model).to.exist + expect(scope.fields).to.exist + }) it(`should initialize the model and fields if they are null`, () => { - scope.model = null; - scope.fields = null; + scope.model = null + scope.fields = null compileAndDigest(` - `); - expect(scope.model).to.exist; - expect(scope.fields).to.exist; - }); + `) + expect(scope.model).to.exist + expect(scope.fields).to.exist + }) it(`should allow the user to specify their own name for the form`, () => { compileAndDigest(` @@ -140,57 +139,57 @@ describe('formly-form', () => { - `); + `) - expect(scope.parent).to.have.property('formly_0_in_my_ng_repeat'); - expect(scope.parent).to.have.property('formly_1_in_my_ng_repeat'); - const firstForm = el[0].querySelector('ng-form'); - const firstFormScope = angular.element(firstForm).isolateScope(); - expect(firstFormScope.formId).to.eq('formly_0_in_my_ng_repeat'); - }); + expect(scope.parent).to.have.property('formly_0_in_my_ng_repeat') + expect(scope.parent).to.have.property('formly_1_in_my_ng_repeat') + const firstForm = el[0].querySelector('ng-form') + const firstFormScope = angular.element(firstForm).isolateScope() + expect(firstFormScope.formId).to.eq('formly_0_in_my_ng_repeat') + }) it(`should allow you to completely swap out the fields`, () => { - scope.fields = [getNewField(), getNewField()]; - compileAndDigest(basicForm); - scope.fields = [getNewField(), getNewField()]; + scope.fields = [getNewField(), getNewField()] + compileAndDigest(basicForm) + scope.fields = [getNewField(), getNewField()] - expect(() => scope.$digest()).to.not.throw(); - }); + expect(() => scope.$digest()).to.not.throw() + }) describe(`ngTransclude element`, () => { it(`should have the specified className`, () => { const el = compileAndDigest(` - `); - expect(el[0].querySelector('.foo.yeah')).to.exist; - }); + `) + expect(el[0].querySelector('.foo.yeah')).to.exist + }) it(`should not have a className when one is unspecified`, () => { // this test is to avoid giving it a class of "undefined" const el = compileAndDigest(` - `); - const transcludedDiv = el[0].querySelector('div[ng-transclude]'); - expect(transcludedDiv.classList).to.have.length(0); - }); - }); + `) + const transcludedDiv = el[0].querySelector('div[ng-transclude]') + expect(transcludedDiv.classList).to.have.length(0) + }) + }) describe(`fieldGroup`, () => { beforeEach(() => { - scope.user = {}; + scope.user = {} formlyConfig.setType({ name: 'input', - template: input - }); - let key = 0; + template: input, + }) + let key = 0 scope.fields = [ { className: 'bar', fieldGroup: [ {type: 'input', key: key++}, - {type: 'input', key: key++} - ] + {type: 'input', key: key++}, + ], }, {type: 'input', key: key++}, {type: 'input', key: key++}, @@ -200,22 +199,22 @@ describe('formly-form', () => { fieldGroup: [ {type: 'input', key: key++}, {type: 'input', key: key++, className: 'specific-field'}, - {type: 'input', key: key++} - ] - } - ]; - }); + {type: 'input', key: key++}, + ], + }, + ] + }) it(`should allow you to specify a fieldGroup which will use the formly-form directive internally`, () => { - compileAndDigest(); + compileAndDigest() - expect(el[0].querySelectorAll('[formly-field].formly-field-input')).to.have.length(7); + expect(el[0].querySelectorAll('[formly-field].formly-field-input')).to.have.length(7) - expect(el[0].querySelectorAll('ng-form')).to.have.length(2); - expect(el[0].querySelectorAll('ng-form.foo')).to.have.length(1); - expect(el[0].querySelectorAll('ng-form.foo [formly-field].formly-field-input')).to.have.length(3); - expect(el[0].querySelectorAll('.formly-field-group')).to.have.length(2); - }); + expect(el[0].querySelectorAll('ng-form')).to.have.length(2) + expect(el[0].querySelectorAll('ng-form.foo')).to.have.length(1) + expect(el[0].querySelectorAll('ng-form.foo [formly-field].formly-field-input')).to.have.length(3) + expect(el[0].querySelectorAll('.formly-field-group')).to.have.length(2) + }) it(`should copy the parent's attributes in the template`, () => { scope.fields = [ @@ -223,94 +222,144 @@ describe('formly-form', () => { className: 'field-group', fieldGroup: [ getNewField(), - getNewField() - ] - } - ]; + getNewField(), + ], + }, + ] - compileAndDigest(''); + compileAndDigest('') - const fieldGroupNode = el[0].querySelector('.field-group'); - expect(fieldGroupNode).to.exist; + const fieldGroupNode = el[0].querySelector('.field-group') + expect(fieldGroupNode).to.exist - expect(fieldGroupNode.getAttribute('some-extra-attr')).to.eq('someValue'); - }); + expect(fieldGroupNode.getAttribute('some-extra-attr')).to.eq('someValue') + }) describe(`options`, () => { - const formWithOptions = ''; + const formWithOptions = '' beforeEach(() => { scope.fields = [ { className: 'field-group', fieldGroup: [ getNewField(), - getNewField() - ] + getNewField(), + ], }, { className: 'field-group', fieldGroup: [ getNewField(), - getNewField() - ] - } - ]; + getNewField(), + ], + }, + ] - scope.options = {}; - }); + scope.options = {} + }) it(`should allow you to call the child's updateInitialValue and resetModel from the parent`, () => { - const field = scope.fields[0].fieldGroup[0]; - compileAndDigest(formWithOptions); - expect(field.initialValue).to.not.exist; - scope.model[field.key] = 'foo'; - scope.options.updateInitialValue(); - expect(field.initialValue).to.eq('foo'); - scope.model[field.key] = 'bar'; - scope.options.resetModel(); - expect(scope.model[field.key]).to.eq('foo'); - }); + const field = scope.fields[0].fieldGroup[0] + compileAndDigest(formWithOptions) + expect(field.initialValue).to.not.exist + scope.model[field.key] = 'foo' + scope.options.updateInitialValue() + expect(field.initialValue).to.eq('foo') + scope.model[field.key] = 'bar' + scope.options.resetModel() + expect(scope.model[field.key]).to.eq('foo') + }) it(`should have the same formState`, () => { - compileAndDigest(formWithOptions); - const fieldGroup1 = scope.fields[0]; - const fieldGroup2 = scope.fields[1]; - expect(fieldGroup1.options.formState).to.eq(fieldGroup2.options.formState); - expect(scope.options.formState).to.eq(fieldGroup1.options.formState); - }); - }); + compileAndDigest(formWithOptions) + const fieldGroup1 = scope.fields[0] + const fieldGroup2 = scope.fields[1] + expect(fieldGroup1.options.formState).to.eq(fieldGroup2.options.formState) + expect(scope.options.formState).to.eq(fieldGroup1.options.formState) + }) + }) + + it(`should be possible to use a wrapper & templateOptions in a fieldGroup`, () => { + formlyConfig.setWrapper({ + name: 'panel', + template: `
+
+ Panel Title: {{options.templateOptions.title}} +
+
+ Subtitle: {{to.subtitle}} +
+
+ +
+
+ `, + }) + + scope.fields = [ + { + className: 'field-group', + wrapper: 'panel', + templateOptions: { + title: 'My Panel', + subtitle: 'is awesome', + }, + fieldGroup: [ + getNewField(), + getNewField(), + ], + }, + ] + + scope.options = {} + + compileAndDigest() + + const panelNode = el[0].querySelector('.panel') + expect(panelNode).to.exist + const bodyNode = panelNode.querySelector('.body') + expect(bodyNode).to.exist + const headingNode = panelNode.querySelector('.heading') + expect(headingNode).to.exist + const headingEl = angular.element(headingNode) + expect(headingEl.text().trim()).to.eq('Panel Title: My Panel') + const subHeadingNode = panelNode.querySelector('.sub-heading') + expect(subHeadingNode).to.exist + const subHeadingEl = angular.element(subHeadingNode) + expect(subHeadingEl.text().trim()).to.eq('Subtitle: is awesome') + }) it(`should be possible to hide a fieldGroup with the hide property`, () => { - compileAndDigest(); + compileAndDigest() - expect(el[0].querySelectorAll('ng-form.bar')).to.have.length(1); + expect(el[0].querySelectorAll('ng-form.bar')).to.have.length(1) - const fieldGroup1 = scope.fields[0]; - fieldGroup1.hide = true; + const fieldGroup1 = scope.fields[0] + fieldGroup1.hide = true - scope.$digest(); + scope.$digest() - expect(el[0].querySelectorAll('ng-form.bar')).to.have.length(0); - }); + expect(el[0].querySelectorAll('ng-form.bar')).to.have.length(0) + }) it(`should pass the model to it's children fields`, () => { - compileAndDigest(); - - const specificGroup = scope.fields[3]; - const specificField = specificGroup.fieldGroup[1]; - const specificFieldNode = el[0].querySelector('.specific-field'); - expect(specificFieldNode).to.exist; - specificField.formControl.$setViewValue('foo'); - expect(specificGroup.model[specificField.key]).to.eq('foo'); - expect(specificGroup.model).to.eq(scope.user); - expect(scope.user[specificField.key]).to.eq('foo'); - expect(angular.element(specificFieldNode).isolateScope().model).to.eq(scope.user); - }); + compileAndDigest() + + const specificGroup = scope.fields[3] + const specificField = specificGroup.fieldGroup[1] + const specificFieldNode = el[0].querySelector('.specific-field') + expect(specificFieldNode).to.exist + specificField.formControl.$setViewValue('foo') + expect(specificGroup.model[specificField.key]).to.eq('foo') + expect(specificGroup.model).to.eq(scope.user) + expect(scope.user[specificField.key]).to.eq('foo') + expect(angular.element(specificFieldNode).isolateScope().model).to.eq(scope.user) + }) it(`should have a form property`, () => { - compileAndDigest(); - expect(scope.fields[0].form).to.have.property('$$parentForm'); - }); + compileAndDigest() + expect(scope.fields[0].form).to.have.property('$$parentForm') + }) it(`should be able to be dynamically hidden with a hideExpression`, () => { scope.fields = [ @@ -318,26 +367,26 @@ describe('formly-form', () => { hideExpression: 'model.foo === "bar"', fieldGroup: [ getNewField(), - getNewField() - ] + getNewField(), + ], }, getNewField({ - key: 'foo', hideExpression: 'options.data.canHide && model.baz === "foobar"', data: {canHide: true} - }) - ]; + key: 'foo', hideExpression: 'options.data.canHide && model.baz === "foobar"', data: {canHide: true}, + }), + ] - compileAndDigest(); + compileAndDigest() - expect(scope.fields[0].hide).to.be.false; - expect(scope.fields[1].hide).to.be.false; + expect(scope.fields[0].hide).to.be.false + expect(scope.fields[1].hide).to.be.false - scope.model.foo = 'bar'; - scope.model.baz = 'foobar'; - scope.$digest(); + scope.model.foo = 'bar' + scope.model.baz = 'foobar' + scope.$digest() - expect(scope.fields[0].hide).to.be.true; - expect(scope.fields[1].hide).to.be.true; - }); + expect(scope.fields[0].hide).to.be.true + expect(scope.fields[1].hide).to.be.true + }) it(`should allow a field group inside a field group`, () => { scope.fields = scope.fields = [ @@ -350,15 +399,15 @@ describe('formly-form', () => { className: 'field-group', fieldGroup: [ getNewField(), - getNewField() - ] - } - ] - } - ]; + getNewField(), + ], + }, + ], + }, + ] - expect(() => compileAndDigest()).to.not.throw(); - }); + expect(() => compileAndDigest()).to.not.throw() + }) it(`should validate fields in a fieldGroup`, () => { scope.fields = [ @@ -372,535 +421,940 @@ describe('formly-form', () => { fieldGroup: [ getNewField({extra: 'property'}), getNewField(), - getNewField() - ] - } - ] - } - ]; - expect(() => compileAndDigest()).to.throw(); - }); + getNewField(), + ], + }, + ], + }, + ] + expect(() => compileAndDigest()).to.throw() + }) - }); + }) describe('an instance of model', () => { - const spy1 = sinon.spy(); - const spy2 = sinon.spy(); + const spy1 = sinon.spy() + const spy2 = sinon.spy() beforeEach(() => { - scope.model = {}; - scope.fieldModel1 = {}; + scope.model = {} + scope.fieldModel1 = {} scope.fields = [ {template: input, key: 'foo', model: scope.fieldModel1}, {template: input, key: 'bar', model: scope.fieldModel1}, - {template: input, key: 'zoo', model: scope.fieldModel1} - ]; - }); + {template: input, key: 'zoo', model: scope.fieldModel1}, + {template: input, key: 'test'}, + ] + }) it('should be assigned with only one watcher', () => { - compileAndDigest(); - $timeout.flush(); + compileAndDigest() + $timeout.flush() + + scope.fields[0].expressionProperties = {'data.dummy': spy1} + scope.fields[1].expressionProperties = {'data.dummy': spy2} + + scope.fieldModel1.foo = 'value' + scope.$digest() + $timeout.flush() + + expect(spy1).to.have.been.calledOnce + expect(spy2).to.have.been.calledOnce + }) + + it('should be updated when the reference to the model changes', () => { + scope.model = {test: 'bar'} + scope.fields[3].expressionProperties = {'data.test': 'model.test'} - scope.fields[0].expressionProperties = {'data.dummy': spy1}; - scope.fields[1].expressionProperties = {'data.dummy': spy2}; + compileAndDigest() + $timeout.flush() - scope.fieldModel1.foo = 'value'; - scope.$digest(); - $timeout.flush(); + scope.model = {test: 'baz'} - expect(spy1).to.have.been.calledOnce; - expect(spy2).to.have.been.calledOnce; - }); - }); + scope.$digest() + $timeout.flush() + + expect(scope.fields[3].data.test).to.equal('baz') + }) + }) describe('nested model as string', () => { - let spy; + let spy beforeEach(() => { - spy = sinon.spy(); + spy = sinon.spy() scope.model = { - nested: {} - }; + nested: {}, + } scope.fields = [ - {template: input, key: 'foo'} - ]; - }); + {template: input, key: 'foo'}, + ] + }) it('starting with "model." should be assigned with only one watcher', () => { - testModelAccessor('model.nested'); - }); + testModelAccessor('model.nested') + }) it('starting with "model[" should be assigned with only one watcher', () => { - testModelAccessor('model["nested"]'); - }); + testModelAccessor('model["nested"]') + }) it('starting with "formState." should be assigned with only one watcher', () => { - testFormStateAccessor('formState.nested'); - }); + testFormStateAccessor('formState.nested') + }) it('starting with "formState[" should be assigned with only one watcher', () => { - testFormStateAccessor('formState["nested"]'); - }); + testFormStateAccessor('formState["nested"]') + }) + + it('should be updated when the reference to the outer model changes', () => { + scope.model.nested.foo = 'bar' + scope.fields[0].model = 'model.nested' + scope.fields[0].expressionProperties = {'data.foo': 'model.foo'} + + compileAndDigest() + $timeout.flush() + + scope.model = { + nested: { + foo: 'baz', + }, + } + + scope.$digest() + $timeout.flush() + + expect(scope.fields[0].data.foo).to.equal('baz') + }) function testModelAccessor(accessor) { - scope.fields[0].model = accessor; + scope.fields[0].model = accessor - compileAndDigest(); - $timeout.flush(); + compileAndDigest() + $timeout.flush() - scope.fields[0].expressionProperties = {'data.dummy': spy}; + scope.fields[0].expressionProperties = {'data.dummy': spy} - scope.model.nested.foo = 'value'; - scope.$digest(); - $timeout.flush(); + scope.model.nested.foo = 'value' + scope.$digest() + $timeout.flush() - expect(spy).to.have.been.calledOnce; + expect(spy).to.have.been.calledOnce } function testFormStateAccessor(accessor) { - const formWithOptions = ''; + const formWithOptions = '' scope.options = { formState: { - nested: {} - } - }; - scope.fields[0].model = accessor; + nested: {}, + }, + } + scope.fields[0].model = accessor - compileAndDigest(formWithOptions); - $timeout.flush(); + compileAndDigest(formWithOptions) + $timeout.flush() - scope.fields[0].expressionProperties = {'data.dummy': spy}; + scope.fields[0].expressionProperties = {'data.dummy': spy} - scope.options.formState.nested.foo = 'value'; - scope.$digest(); - $timeout.flush(); + scope.options.formState.nested.foo = 'value' + scope.$digest() + $timeout.flush() - expect(spy).to.have.been.calledOnce; + expect(spy).to.have.been.calledOnce } - }); + }) describe('hideExpression', () => { beforeEach(() => { - scope.model = {}; - scope.fieldModel = {}; + scope.model = {} + scope.fieldModel = {} + }) + describe('behaviour when model changes', () => { + + describe('when a string is passed to hideExpression', () => { + beforeEach(() => { + scope.fields = [ + {template: input, key: 'foo', model: scope.fieldModel}, + {template: input, key: 'bar', model: scope.fieldModel, hideExpression: () => !!scope.fieldModel.foo}, + ] + }) - scope.fields = [ - {template: input, key: 'foo', model: scope.fieldModel}, - {template: input, key: 'bar', model: scope.fieldModel, hideExpression: () => !!scope.fieldModel.foo} - ]; - }); - - it('should be called and resolve to true when field model changes', () => { - compileAndDigest(); - expect(scope.fields[1].hide).be.false; - scope.fields[0].formControl.$setViewValue('value'); - expect(scope.fields[1].hide).be.true; - }); - - it('should be called and resolve to false when field model changes', () => { - scope.fieldModel.foo = 'value'; - compileAndDigest(); - expect(scope.fields[1].hide).be.true; - scope.fields[0].formControl.$setViewValue(''); - expect(scope.fields[1].hide).be.false; - }); - }); + it('should be called and should resolve to true when field model changes', () => { + compileAndDigest() + expect(scope.fields[1].hide).be.false + scope.fields[0].formControl.$setViewValue('value') + expect(scope.fields[1].hide).be.true + }) + + it('should be called and should resolve to false when field model changes', () => { + scope.fieldModel.foo = 'value' + compileAndDigest() + expect(scope.fields[1].hide).be.true + scope.fields[0].formControl.$setViewValue('') + expect(scope.fields[1].hide).be.false + }) + }) + describe('when a function is passed to hideExpression', () => { + beforeEach(() => { + scope.fields = [ + {template: input, key: 'foo', model: scope.fieldModel}, + { + template: input, key: 'bar', + model: scope.fieldModel, + hideExpression: ($viewValue, $modelValue, scope) => { + return !!scope.fields[1].data.parentScope.fieldModel.foo //since the scope passed to the function belongs to the form, + }, //we store the outer(parent) scope in 'data' property to access + data: { //the template named 'foo' stored in the fields array + parentScope: scope, //the parent scope(one used to compile the form) + }, + }, + ] + }) + + it('should be called and should resolve to true when field model changes', () => { + compileAndDigest() + expect(scope.fields[1].hide).be.false + scope.fields[0].formControl.$setViewValue('value') + expect(scope.fields[1].hide).be.true + }) + + it('should be called and should resolve to false when field model changes', () => { + scope.fieldModel.foo = 'value' + compileAndDigest() + expect(scope.fields[1].hide).be.true + scope.fields[0].formControl.$setViewValue('') + expect(scope.fields[1].hide).be.false + }) + }) + }) + + it('ensures that hideExpression has all the expressionProperties values', () => { + scope.model = {nested: {foo: 'bar', baz: []}} + scope.options = {formState: {}} + scope.fields = [{ + template: input, + key: 'test', + model: 'model.nested', + hideExpression: ` + model === options.data.model && + options === options.data.field && + index === 0 && + formState === options.data.formOptions.formState && + originalModel === options.data.originalModel && + formOptions === options.data.formOptions`, + data: { + model: scope.model.nested, + originalModel: scope.model, + formOptions: scope.options, + }, + }] + scope.fields[0].data.field = scope.fields[0] + compileAndDigest() + expect(scope.fields[0].hide).be.true + }) + }) describe(`options`, () => { beforeEach(() => { scope.model = { foo: 'myFoo', bar: 123, - foobar: 'ab@cd.com' - }; + foobar: 'ab@cd.com', + } scope.fields = [ {template: input, key: 'foo'}, {template: input, key: 'bar', templateOptions: {type: 'numaber'}}, - {template: input, key: 'foobar', templateOptions: {type: 'email'}} - ]; + {template: input, key: 'foobar', templateOptions: {type: 'email'}}, + ] scope.options = { formState: { - foo: 'bar' - } - }; - }); + foo: 'bar', + }, + } + }) it(`should throw an error with extra options`, () => { expect(() => { - scope.options = {extra: true}; - compileAndDigest(); - }).to.throw(); - }); + scope.options = {extra: true} + compileAndDigest() + }).to.throw() + }) it(`should run expressionProperties when the formState changes`, () => { - const spy = sinon.spy(); + const spy = sinon.spy() const field = { template: input, key: 'foo', expressionProperties: { - 'templateOptions.label': spy - } - }; - scope.fields = [field]; - compileAndDigest(); - scope.options.formState.foo = 'eggs'; - scope.$digest(); - $timeout.flush(); - expect(spy).to.have.been.called; - }); + 'templateOptions.label': spy, + }, + } + scope.fields = [field] + compileAndDigest() + scope.options.formState.foo = 'eggs' + scope.$digest() + $timeout.flush() + expect(spy).to.have.been.called + }) describe(`resetModel`, () => { it(`should reset the model that's given`, () => { - compileAndDigest(); - expect(typeof scope.options.resetModel).to.eq('function'); - const previousFoo = scope.model.foo; - scope.model.foo = 'newFoo'; - scope.options.resetModel(); - expect(scope.model.foo).to.eq(previousFoo); - }); + compileAndDigest() + expect(typeof scope.options.resetModel).to.eq('function') + const previousFoo = scope.model.foo + scope.model.foo = 'newFoo' + scope.options.resetModel() + expect(scope.model.foo).to.eq(previousFoo) + }) it(`should reset the $viewValue of fields`, () => { - compileAndDigest(); - const previousFoobar = scope.model.foobar; - scope.fields[2].formControl.$setViewValue('not-an-email'); - scope.options.resetModel(); - expect(scope.fields[2].formControl.$viewValue).to.equal(previousFoobar); - }); + compileAndDigest() + const previousFoobar = scope.model.foobar + scope.fields[2].formControl.$setViewValue('not-an-email') + scope.options.resetModel() + expect(scope.fields[2].formControl.$viewValue).to.equal(previousFoobar) + }) it(`should reset the $viewValue and $modelValue to undefined if the value was not originally defined`, () => { scope.fields.push({ - template: input, key: 'baz', templateOptions: {required: true} - }); - compileAndDigest(); - const fc = scope.fields[scope.fields.length - 1].formControl; - scope.model.baz = 'hello world'; - scope.$digest(); - expect(fc.$viewValue).to.eq('hello world'); - expect(fc.$modelValue).to.eq('hello world'); - scope.options.resetModel(); - expect(scope.model.baz).to.be.undefined; - expect(fc.$viewValue).to.be.undefined; - expect(fc.$modelValue).to.be.undefined; - }); + template: input, key: 'baz', templateOptions: {required: true}, + }) + compileAndDigest() + const fc = scope.fields[scope.fields.length - 1].formControl + scope.model.baz = 'hello world' + scope.$digest() + expect(fc.$viewValue).to.eq('hello world') + expect(fc.$modelValue).to.eq('hello world') + scope.options.resetModel() + expect(scope.model.baz).to.be.undefined + expect(fc.$viewValue).to.be.undefined + expect(fc.$modelValue).to.be.undefined + }) it(`should rerender the ng-model element`, () => { - const el = compileAndDigest(); - const ngModelNode = el[0].querySelector('[ng-model]'); - scope.model.foo = 'hey there!'; - scope.$digest(); - scope.options.resetModel(); - expect(ngModelNode.value).to.eq('myFoo'); - }); + const el = compileAndDigest() + const ngModelNode = el[0].querySelector('[ng-model]') + scope.model.foo = 'hey there!' + scope.$digest() + scope.options.resetModel() + expect(ngModelNode.value).to.eq('myFoo') + }) it(`should reset models of fields`, () => { - scope.fieldModel = {baz: false}; + scope.fieldModel = {baz: false} scope.fields.push({ - template: input, key: 'baz', model: scope.fieldModel - }); + template: input, key: 'baz', model: scope.fieldModel, + }) - compileAndDigest(); + compileAndDigest() - scope.fieldModel.baz = true; - scope.options.resetModel(); - expect(scope.fieldModel.baz).to.be.false; - }); + scope.fieldModel.baz = true + scope.options.resetModel() + expect(scope.fieldModel.baz).to.be.false + }) it(`should not break if a fieldGroup has yet to be initialized`, () => { scope.fields = [ - {fieldGroup: [getNewField()], hide: true} - ]; - compileAndDigest(); - expect(() => scope.options.resetModel()).to.not.throw(); - }); + {fieldGroup: [getNewField()], hide: true}, + ] + compileAndDigest() + expect(() => scope.options.resetModel()).to.not.throw() + }) it(`should not break if a field has yet to be initialized`, () => { - scope.fields = [getNewField({hide: true})]; - compileAndDigest(); - expect(() => scope.options.resetModel()).to.not.throw(); - }); - }); + scope.fields = [getNewField({hide: true})] + compileAndDigest() + expect(() => scope.options.resetModel()).to.not.throw() + }) + }) describe(`hide-directive attribute`, () => { beforeEach(() => { - scope.fields = [{template: input, key: 'foo'}]; - }); + scope.fields = [{template: input, key: 'foo'}] + }) it(`should default to ng-if`, () => { - compileAndDigest(basicForm); - const fieldNode = el[0].querySelector('.formly-field'); - expect(fieldNode.getAttribute('ng-if')).to.exist; - }); + compileAndDigest(basicForm) + const fieldNode = el[0].querySelector('.formly-field') + expect(fieldNode.getAttribute('ng-if')).to.exist + }) it(`should allow custom directive for hiding`, () => { compileAndDigest(` - `); - const fieldNode = el[0].querySelector('.formly-field'); - expect(fieldNode.getAttribute('ng-if')).to.not.exist; - expect(fieldNode.getAttribute('ng-show')).to.exist; - }); + `) + const fieldNode = el[0].querySelector('.formly-field') + expect(fieldNode.getAttribute('ng-if')).to.not.exist + expect(fieldNode.getAttribute('ng-show')).to.exist + }) - }); + }) describe(`track-by attribute`, () => { - const template = ``; + const template = `` beforeEach(() => { - scope.fields = [getNewField(), getNewField(), getNewField()]; - }); + scope.fields = [getNewField(), getNewField(), getNewField()] + }) it(`should default to track by $$hashKey when the attribute is not present`, () => { - compileAndDigest(basicForm); - expect(scope.fields[0].$$hashKey).to.exist; - }); + compileAndDigest(basicForm) + expect(scope.fields[0].$$hashKey).to.exist + }) it(`should track by the specified value`, () => { - compileAndDigest(template); - expectTrackBy('field.key'); - }); + compileAndDigest(template) + expectTrackBy('field.key') + }) it(`should allow you to track by $index`, () => { - compileAndDigest(``); - expectTrackBy('$index'); - }); + compileAndDigest(``) + expectTrackBy('$index') + }) it(`should throw an error when the field's specified values are not unique`, () => { - scope.fields.push({template: input, key: 'foo'}); - scope.fields.push({template: input, key: 'foo'}); - expect(compileAndDigest.bind(null, template)).to.throw('ngRepeat:dupes'); - }); + scope.fields.push({template: input, key: 'foo'}) + scope.fields.push({template: input, key: 'foo'}) + expect(compileAndDigest.bind(null, template)).to.throw('ngRepeat:dupes') + }) it(`should allow you to push a field after initial compile`, () => { - expectFieldChange(scope.fields.push.bind(scope.fields, getNewField())); - }); + expectFieldChange(scope.fields.push.bind(scope.fields, getNewField())) + }) it(`should allow you to pop a field after initial compile`, () => { - expectFieldChange(scope.fields.pop.bind(scope.fields)); - }); + expectFieldChange(scope.fields.pop.bind(scope.fields)) + }) it(`should allow you to splice out a field after initial compile`, () => { - expectFieldChange(scope.fields.splice.bind(scope.fields, 1, 1)); - }); + expectFieldChange(scope.fields.splice.bind(scope.fields, 1, 1)) + }) it(`should allow you splice in a field after initial compile`, () => { - expectFieldChange(scope.fields.splice.bind(scope.fields, 1, 0, getNewField())); - }); + expectFieldChange(scope.fields.splice.bind(scope.fields, 1, 0, getNewField())) + }) function expectTrackBy(trackBy) { - expect(el[0].innerHTML).to.contain(`field in fields track by ${trackBy}`); + expect(el[0].innerHTML).to.contain(`field in fields track by ${trackBy}`) } function expectFieldChange(change) { - compileAndDigest(template); - change(); - expect(() => scope.$digest()).to.not.throw(); + compileAndDigest(template) + change() + expect(() => scope.$digest()).to.not.throw() } - }); + }) describe(`updateInitialValue`, () => { it(`should update the initial value of the fields`, () => { - compileAndDigest(); - const field = scope.fields[0]; - expect(field.initialValue).to.equal('myFoo'); - scope.model.foo = 'otherValue'; - scope.options.updateInitialValue(); - expect(field.initialValue).to.equal('otherValue'); - }); + compileAndDigest() + const field = scope.fields[0] + expect(field.initialValue).to.equal('myFoo') + scope.model.foo = 'otherValue' + scope.options.updateInitialValue() + expect(field.initialValue).to.equal('otherValue') + }) it(`should reset to the updated initial value`, () => { - compileAndDigest(); - const field = scope.fields[0]; - scope.model.foo = 'otherValue'; - scope.options.updateInitialValue(); - scope.model.foo = 'otherValueAgain'; - scope.options.resetModel(); - expect(field.initialValue).to.equal('otherValue'); - expect(scope.model.foo).to.equal('otherValue'); - }); - }); + compileAndDigest() + const field = scope.fields[0] + scope.model.foo = 'otherValue' + scope.options.updateInitialValue() + scope.model.foo = 'otherValueAgain' + scope.options.resetModel() + expect(field.initialValue).to.equal('otherValue') + expect(scope.model.foo).to.equal('otherValue') + }) + }) describe(`removeChromeAutoComplete`, () => { it(`should not have a hidden input when nothing is specified`, () => { - const el = compileAndDigest(); - const autoCompleteFixEl = el[0].querySelector('[autocomplete="address-level4"]'); - expect(autoCompleteFixEl).to.be.null; - }); + const el = compileAndDigest() + const autoCompleteFixEl = el[0].querySelector('[autocomplete="address-level4"]') + expect(autoCompleteFixEl).to.be.null + }) it(`should add a hidden input when specified as true`, () => { - scope.options.removeChromeAutoComplete = true; - const el = compileAndDigest(); - const autoCompleteFixEl = el[0].querySelector('[autocomplete="address-level4"]'); - expect(autoCompleteFixEl).to.exist; - }); + scope.options.removeChromeAutoComplete = true + const el = compileAndDigest() + const autoCompleteFixEl = el[0].querySelector('[autocomplete="address-level4"]') + expect(autoCompleteFixEl).to.exist + }) it(`should override the 'true' global configuration`, inject((formlyConfig) => { - formlyConfig.extras.removeChromeAutoComplete = true; - scope.options.removeChromeAutoComplete = false; - const el = compileAndDigest(); - const autoCompleteFixEl = el[0].querySelector('[autocomplete="address-level4"]'); - expect(autoCompleteFixEl).to.be.null; - })); + formlyConfig.extras.removeChromeAutoComplete = true + scope.options.removeChromeAutoComplete = false + const el = compileAndDigest() + const autoCompleteFixEl = el[0].querySelector('[autocomplete="address-level4"]') + expect(autoCompleteFixEl).to.be.null + })) it(`should be added regardless of the option if the global config is set`, inject((formlyConfig) => { - formlyConfig.extras.removeChromeAutoComplete = true; - const el = compileAndDigest(); - const autoCompleteFixEl = el[0].querySelector('[autocomplete="address-level4"]'); - expect(autoCompleteFixEl).to.exist; - })); - }); - - describe(`fieldTransform`, () => { - beforeEach(() => { - formlyConfig.extras.fieldTransform = fieldTransform; - }); - - it(`should give a deprecation warning when formlyConfig.extras.fieldTransform is a function rather than an array`, inject(($log) => { - shouldWarnWithLog( - $log, - [ - 'Formly Warning:', - 'fieldTransform as a function has been deprecated.', - /Attempted for formlyConfig.extras/ - ], - compileAndDigest - ); - })); - - it(`should give a deprecation warning when options.fieldTransform function rather than an array`, inject(($log) => { - formlyConfig.extras.fieldTransform = undefined; - scope.fields = [getNewField()]; - scope.options.fieldTransform = fields => fields; - shouldWarnWithLog( - $log, - [ - 'Formly Warning:', - 'fieldTransform as a function has been deprecated.', - 'Attempted for form' - ], - compileAndDigest - ); - })); - - it(`should throw an error if something is passed in and nothing is returned`, () => { - scope.fields = [getNewField()]; - scope.options.fieldTransform = function() { - // I return nothing... - }; - expect(() => compileAndDigest()).to.throw(/^Formly Error: fieldTransform must return an array of fields/); - }); - - it(`should allow you to transform field configuration`, () => { - scope.options.fieldTransform = fieldTransform; - const spy = sinon.spy(scope.options, 'fieldTransform'); - doExpectations(spy); - }); - - it(`should use formlyConfig.extras.fieldTransform when not specified on options`, () => { - const spy = sinon.spy(formlyConfig.extras, 'fieldTransform'); - doExpectations(spy); - }); - - it(`should allow you to use an array of transform functions`, () => { - scope.fields = [getNewField({ - customThing: 'foo', - otherCustomThing: { - whatever: '|-o-|' - }})]; - scope.options.fieldTransform = [fieldTransform]; - expect(() => compileAndDigest()).to.not.throw(); - - const field = scope.fields[0]; - expect(field).to.have.deep.property('data.customThing'); - expect(field).to.have.deep.property('data.otherCustomThing'); - }); - - function doExpectations(spy) { - const originalFields = [{ - key: 'keyProp', - template: '
', - customThing: 'foo', - otherCustomThing: { - whatever: '|-o-|' - } - }]; - scope.fields = originalFields; - compileAndDigest(); - expect(spy).to.have.been.calledWith(originalFields, scope.model, scope.options, scope.form); - const field = scope.fields[0]; - - expect(field).to.not.have.property('customThing'); - expect(field).to.not.have.property('otherCustomThing'); - expect(field).to.have.deep.property('data.customThing'); - expect(field).to.have.deep.property('data.otherCustomThing'); - } - - function fieldTransform(fields) { - const extraKeys = ['customThing', 'otherCustomThing']; - return _.map(fields, field => { - const newField = {data: {}}; - _.each(field, (val, name) => { - if (_.contains(extraKeys, name)) { - newField.data[name] = val; - } else { - newField[name] = val; - } - }); - return newField; - }); - } - }); + formlyConfig.extras.removeChromeAutoComplete = true + const el = compileAndDigest() + const autoCompleteFixEl = el[0].querySelector('[autocomplete="address-level4"]') + expect(autoCompleteFixEl).to.exist + })) + }) describe(`data`, () => { it(`should allow you to put whatever you want in data`, () => { - scope.options.data = {foo: 'bar'}; - expect(compileAndDigest).to.not.throw(); - }); - }); - }); + scope.options.data = {foo: 'bar'} + expect(compileAndDigest).to.not.throw() + }) + }) + }) function compileAndDigest(template) { - el = $compile(template || basicForm)(scope); - scope.$digest(); - return el; + el = $compile(template || basicForm)(scope) + scope.$digest() + return el } describe(`field watchers`, () => { it('should throw for a watcher with no listener', () => { scope.fields = [getNewField({ - watcher: {} - })]; + watcher: {}, + })] - expect(compileAndDigest).to.throw(); - }); + expect(compileAndDigest).to.throw() + }) it(`should setup any watchers specified on a field`, () => { - scope.model = {}; + scope.model = {} - const listener = sinon.spy(); - const expression = sinon.spy(); + const listener = sinon.spy() + const expression = sinon.spy() scope.fields = [getNewField({ watcher: { - listener: '' - } + listener: '', + }, }), getNewField({ watcher: [{ listener: '', - expression: '' + expression: '', }, { listener, - expression + expression, + }], + })] + + expect(compileAndDigest).to.not.throw() + expect(listener).to.have.been.called + expect(expression).to.have.been.called + }) + + it(`should setup any watchers specified on a fieldgroup`, () => { + scope.model = {} + + const listener = sinon.spy() + const expression = sinon.spy() + + scope.fields = [{ + watcher: [{ + listener: '', + expression: '', + }, { + listener, + expression, + }], + fieldGroup: [ + getNewField({}), + getNewField({}), + ], + }] + + expect(compileAndDigest).to.not.throw() + expect(listener).to.have.been.called + expect(expression).to.have.been.called + }) + }) + + describe(`manualModelWatcher option`, () => { + beforeEach(() => { + scope.model = { + foo: 'myFoo', + bar: 123, + baz: {buzz: 'myBuzz'}, + } + + scope.fields = [ + {template: input, key: 'foo'}, + {template: input, key: 'bar', templateOptions: {type: 'number'}}, + ] + }) + + describe('declared as a boolean', () => { + beforeEach(() => { + scope.options = { + manualModelWatcher: true, + } + }) + + it(`should block a global model watcher`, () => { + const spy = sinon.spy() + + scope.fields[0].expressionProperties = { + 'templateOptions.label': spy, + } + + compileAndDigest() + $timeout.flush() + + spy.reset() + + scope.model.foo = 'bar' + + scope.$digest() + $timeout.verifyNoPendingTasks() + + expect(spy).to.not.have.been.called + }) + + it(`should watch manually selected model property`, () => { + const spy = sinon.spy() + + scope.fields[0].watcher = [{ + expression: 'model.foo', + runFieldExpressions: true, + }] + scope.fields[0].expressionProperties = { + 'templateOptions.label': spy, + } + + compileAndDigest() + $timeout.flush() + + spy.reset() + + scope.model.foo = 'bar' + + scope.$digest() + $timeout.flush() + + expect(spy).to.have.been.called + }) + + it(`should not watch model properties that do not have manual watcher defined`, () => { + const spy = sinon.spy() + + scope.fields[0].watcher = [{ + expression: 'model.foo', + runFieldExpressions: true, + }] + scope.fields[0].expressionProperties = { + 'templateOptions.label': spy, + } + + compileAndDigest() + $timeout.flush() + + spy.reset() + + scope.model.bar = 123 + + scope.$digest() + $timeout.verifyNoPendingTasks() + + expect(spy).to.not.have.been.called + }) + + it(`should run manual watchers defined as a function`, () => { + const spy = sinon.spy() + const stub = sinon.stub() + + scope.fields[0].watcher = [{ + expression: stub, + runFieldExpressions: true, }] - })]; - - expect(compileAndDigest).to.not.throw(); - expect(listener).to.have.been.called; - expect(expression).to.have.been.called; - }); - }); -}); + scope.fields[0].expressionProperties = { + 'templateOptions.label': spy, + } + + compileAndDigest() + $timeout.flush() + + stub.reset() + spy.reset() + + // set random stub value so it triggers watcher function + stub.returns(Math.random()) + + scope.$digest() + $timeout.flush() + + expect(stub).to.have.been.called + expect(spy).to.have.been.called + }) + + it('should not trigger watches on other fields', () => { + const spy1 = sinon.spy() + const spy2 = sinon.spy() + + scope.fields[0].watcher = [{ + expression: 'model.foo', + runFieldExpressions: true, + }] + scope.fields[0].expressionProperties = { + 'templateOptions.label': spy1, + } + scope.fields[1].expressionProperties = { + 'templateOptions.label': spy2, + } + + compileAndDigest() + $timeout.flush() + + spy1.reset() + spy2.reset() + + scope.model.foo = 'asd' + + scope.$digest() + $timeout.flush() + + expect(spy1).to.have.been.called + expect(spy2).to.not.have.been.called + }) + + it('works with models that are declared as string (relative model)', () => { + const spy = sinon.spy() + const model = 'model.nested' + + scope.model = { + nested: { + foo: 'foo', + }, + } + scope.fields[0].model = model + scope.fields[0].watcher = [{ + expression: 'model.foo', + runFieldExpressions: true, + }] + scope.fields[0].expressionProperties = { + 'templateOptions.label': spy, + } + + compileAndDigest() + $timeout.flush() + + spy.reset() + + scope.model.nested.foo = 'bar' + + scope.$digest() + $timeout.flush() + + expect(spy).to.have.been.called + }) + }) + + describe('declared as a function', () => { + beforeEach(() => { + scope.options = { + manualModelWatcher: () => scope.model.baz, + } + }) + + it('works as a form-wide watcher', () => { + const spy = sinon.spy() + + scope.options = { + manualModelWatcher: () => scope.model.baz, + } + + scope.fields[1].expressionProperties = { + 'templateOptions.label': spy, + } + + compileAndDigest() + $timeout.flush() + + spy.reset() + + scope.model.foo = 'random string' + + scope.$digest() + $timeout.verifyNoPendingTasks() + + expect(spy).to.not.have.been.called + + spy.reset() + + scope.model.baz.buzz = 'random buzz string' + + scope.$digest() + $timeout.flush() + + expect(spy).to.have.been.called + }) + + it('still fires manual field watchers', () => { + const spy = sinon.spy() + + scope.fields[0].watcher = [{ + expression: 'model.foo', + runFieldExpressions: true, + }] + scope.fields[0].expressionProperties = { + 'templateOptions.label': spy, + } + + compileAndDigest() + $timeout.flush() + + spy.reset() + + scope.model.foo = 'bar' + + scope.$digest() + $timeout.flush() + + expect(spy).to.have.been.called + }) + + }) + + describe('enabled with watchAllExpressions option', () => { + beforeEach(() => { + scope.options = { + manualModelWatcher: true, + watchAllExpressions: true, + } + }) + + it('watches and evaluates string template expressions', () => { + const field = scope.fields[0] + + field.expressionProperties = { + 'templateOptions.label': 'model.foo', + } + + compileAndDigest() + $timeout.flush() + + scope.model.foo = 'bar' + + scope.$digest() + + expect(field.templateOptions.label).to.equal(scope.model.foo) + }) + + it('watches and evaluates string template expressions with custom string model', () => { + const field = scope.fields[0] + + field.model = 'model.baz' + field.expressionProperties = { + 'templateOptions.label': 'model.buzz', + } + + compileAndDigest() + $timeout.flush() + + scope.model.baz.buzz = 'bar' + + scope.$digest() + + expect(field.templateOptions.label).to.equal(scope.model.baz.buzz) + }) + + it('watches and evaluates string template expressions with custom object model', () => { + const field = scope.fields[0] + + field.model = {customFoo: 'customBar'} + field.expressionProperties = { + 'templateOptions.label': 'model.customFoo', + } + + compileAndDigest() + $timeout.flush() + + field.model.customFoo = 'bar' + + scope.$digest() + + expect(field.templateOptions.label).to.equal(field.model.customFoo) + }) + + it('watches and evaluates hideExpression', () => { + const field = scope.fields[0] + + field.hideExpression = 'model.foo === "bar"' + + compileAndDigest() + $timeout.flush() + + scope.model.foo = 'bar' + + scope.$digest() + + expect(field.hide).to.equal(true) + }) + + it('watches and evaluates hideExpression with custom string model', () => { + const field = scope.fields[0] + + field.model = 'model.baz' + field.hideExpression = 'model.buzz === "bar"' + + compileAndDigest() + $timeout.flush() + + scope.model.baz.buzz = 'bar' + + scope.$digest() + + expect(field.hide).to.equal(true) + }) + }) + }) + describe('extras', () => { + describe('validateOnModelChange', () => { + it('should run validators after expressions are set', () => { + let inputs, invalidInputs, el + + scope.model = { + foo: null, + bar: 123, + } + + scope.fields = [ + {template: input, key: 'foo', extras: {validateOnModelChange: true}}, + {template: input, key: 'bar', templateOptions: {type: 'number'}}, + ] + // First Field isn't valid when second field is 1 + scope.fields[0].expressionProperties = { + 'templateOptions.isValid': 'model.bar !== 1', + } + // validator to use isValid attribute + scope.fields[0].validators = {isValid: {expression: (viewValue, modelValue, fieldScope) => { + return fieldScope.to.isValid + }}} + + el = compileAndDigest() + + // Input state before + inputs = el[0].querySelectorAll('input') + invalidInputs = el[0].querySelectorAll('input.ng-invalid') + expect(inputs.length).to.equal(2) + expect(invalidInputs.length).to.equal(0) + + // Enter '1' into second field + angular.element(inputs[1]).val(1).triggerHandler('change') + $timeout.flush() + + // Input state after + inputs = el[0].querySelectorAll('input') + invalidInputs = el[0].querySelectorAll('input.ng-invalid') + expect(inputs.length).to.equal(2) + expect(invalidInputs.length).to.equal(1) + }) + }) + }) +}) diff --git a/src/index.common.js b/src/index.common.js index 99a9d32d..7d98f541 100644 --- a/src/index.common.js +++ b/src/index.common.js @@ -1,42 +1,44 @@ -import angular from 'angular-fix'; +import angular from 'angular-fix' -import formlyApiCheck from './providers/formlyApiCheck'; -import formlyErrorAndWarningsUrlPrefix from './other/docsBaseUrl'; -import formlyUsability from './providers/formlyUsability'; -import formlyConfig from './providers/formlyConfig'; -import formlyValidationMessages from './providers/formlyValidationMessages'; -import formlyUtil from './services/formlyUtil'; -import formlyWarn from './services/formlyWarn'; +import formlyApiCheck from './providers/formlyApiCheck' +import formlyErrorAndWarningsUrlPrefix from './other/docsBaseUrl' +import formlyUsability from './providers/formlyUsability' +import formlyConfig from './providers/formlyConfig' +import formlyValidationMessages from './providers/formlyValidationMessages' +import formlyUtil from './services/formlyUtil' +import formlyWarn from './services/formlyWarn' -import formlyCustomValidation from './directives/formly-custom-validation'; -import formlyField from './directives/formly-field'; -import formlyFocus from './directives/formly-focus'; -import formlyForm from './directives/formly-form'; +import formlyCustomValidation from './directives/formly-custom-validation' +import formlyField from './directives/formly-field' +import formlyFocus from './directives/formly-focus' +import formlyForm from './directives/formly-form' +import FormlyFormController from './directives/formly-form.controller' -import formlyNgModelAttrsManipulator from './run/formlyNgModelAttrsManipulator'; -import formlyCustomTags from './run/formlyCustomTags'; +import formlyNgModelAttrsManipulator from './run/formlyNgModelAttrsManipulator' +import formlyCustomTags from './run/formlyCustomTags' -const ngModuleName = 'formly'; +const ngModuleName = 'formly' -export default ngModuleName; +export default ngModuleName -const ngModule = angular.module(ngModuleName, []); +const ngModule = angular.module(ngModuleName, []) -ngModule.constant('formlyApiCheck', formlyApiCheck); -ngModule.constant('formlyErrorAndWarningsUrlPrefix', formlyErrorAndWarningsUrlPrefix); -ngModule.constant('formlyVersion', VERSION); // <-- webpack variable +ngModule.constant('formlyApiCheck', formlyApiCheck) +ngModule.constant('formlyErrorAndWarningsUrlPrefix', formlyErrorAndWarningsUrlPrefix) +ngModule.constant('formlyVersion', VERSION) // <-- webpack variable -ngModule.provider('formlyUsability', formlyUsability); -ngModule.provider('formlyConfig', formlyConfig); +ngModule.provider('formlyUsability', formlyUsability) +ngModule.provider('formlyConfig', formlyConfig) -ngModule.factory('formlyValidationMessages', formlyValidationMessages); -ngModule.factory('formlyUtil', formlyUtil); -ngModule.factory('formlyWarn', formlyWarn); +ngModule.factory('formlyValidationMessages', formlyValidationMessages) +ngModule.factory('formlyUtil', formlyUtil) +ngModule.factory('formlyWarn', formlyWarn) -ngModule.directive('formlyCustomValidation', formlyCustomValidation); -ngModule.directive('formlyField', formlyField); -ngModule.directive('formlyFocus', formlyFocus); -ngModule.directive('formlyForm', formlyForm); +ngModule.directive('formlyCustomValidation', formlyCustomValidation) +ngModule.directive('formlyField', formlyField) +ngModule.directive('formlyFocus', formlyFocus) +ngModule.directive('formlyForm', formlyForm) +ngModule.controller('FormlyFormController', FormlyFormController) -ngModule.run(formlyNgModelAttrsManipulator); -ngModule.run(formlyCustomTags); +ngModule.run(formlyNgModelAttrsManipulator) +ngModule.run(formlyCustomTags) diff --git a/src/index.js b/src/index.js index 9ce67349..063ed087 100644 --- a/src/index.js +++ b/src/index.js @@ -1,2 +1,2 @@ -import index from './index.common'; -export default index; +import index from './index.common' +export default index diff --git a/src/index.test.js b/src/index.test.js index 1600bb06..12e8f30f 100644 --- a/src/index.test.js +++ b/src/index.test.js @@ -1,16 +1,17 @@ // Load up the angular formly module -import index from './index.common'; +import index from './index.common' // Bring in the test suites -import './providers/formlyApiCheck.test'; -import './providers/formlyConfig.test'; -import './services/formlyUtil.test'; -import './directives/formly-custom-validation.test'; -import './directives/formly-field.test'; -import './directives/formly-focus.test'; -import './directives/formly-form.test'; -import './run/formlyCustomTags.test'; -import './run/formlyNgModelAttrsManipulator.test'; -import './other/utils.test'; +import './providers/formlyApiCheck.test' +import './providers/formlyConfig.test' +import './services/formlyUtil.test' +import './directives/formly-custom-validation.test' +import './directives/formly-field.test' +import './directives/formly-focus.test' +import './directives/formly-form.test' +import './directives/formly-form.controller.test' +import './run/formlyCustomTags.test' +import './run/formlyNgModelAttrsManipulator.test' +import './other/utils.test' -export default index; +export default index diff --git a/src/other/docsBaseUrl.js b/src/other/docsBaseUrl.js index c75a144d..5db163ce 100644 --- a/src/other/docsBaseUrl.js +++ b/src/other/docsBaseUrl.js @@ -1 +1 @@ -export default `https://github.com/formly-js/angular-formly/blob/${VERSION}/other/ERRORS_AND_WARNINGS.md#`; +export default `https://github.com/formly-js/angular-formly/blob/${VERSION}/other/ERRORS_AND_WARNINGS.md#` diff --git a/src/other/utils.js b/src/other/utils.js index e330d9e6..aa6f1aa4 100644 --- a/src/other/utils.js +++ b/src/other/utils.js @@ -1,68 +1,81 @@ -import angular from 'angular-fix'; +import angular from 'angular-fix' export default { - formlyEval, getFieldId, reverseDeepMerge, findByNodeName, arrayify, extendFunction, extendArray, startsWith, contains -}; + containsSelector, containsSpecialChar, formlyEval, getFieldId, reverseDeepMerge, findByNodeName, + arrayify, extendFunction, extendArray, startsWith, contains, +} + +function containsSelector(string) { + return containsSpecialChar(string, '.') || (containsSpecialChar(string, '[') && containsSpecialChar(string, ']')) +} + +function containsSpecialChar(a, b) { + if (!a || !a.indexOf) { + return false + } + return a.indexOf(b) !== -1 +} + function formlyEval(scope, expression, $modelValue, $viewValue, extraLocals) { if (angular.isFunction(expression)) { - return expression($viewValue, $modelValue, scope, extraLocals); + return expression($viewValue, $modelValue, scope, extraLocals) } else { - return scope.$eval(expression, angular.extend({$viewValue, $modelValue}, extraLocals)); + return scope.$eval(expression, angular.extend({$viewValue, $modelValue}, extraLocals)) } } function getFieldId(formId, options, index) { if (options.id) { - return options.id; + return options.id } - let type = options.type; + let type = options.type if (!type && options.template) { - type = 'template'; + type = 'template' } else if (!type && options.templateUrl) { - type = 'templateUrl'; + type = 'templateUrl' } - return [formId, type, options.key, index].join('_'); + return [formId, type, options.key, index].join('_') } function reverseDeepMerge(dest) { angular.forEach(arguments, (src, index) => { if (!index) { - return; + return } angular.forEach(src, (val, prop) => { if (!angular.isDefined(dest[prop])) { - dest[prop] = angular.copy(val); + dest[prop] = angular.copy(val) } else if (objAndSameType(dest[prop], val)) { - reverseDeepMerge(dest[prop], val); + reverseDeepMerge(dest[prop], val) } - }); - }); - return dest; + }) + }) + return dest } function objAndSameType(obj1, obj2) { return angular.isObject(obj1) && angular.isObject(obj2) && - Object.getPrototypeOf(obj1) === Object.getPrototypeOf(obj2); + Object.getPrototypeOf(obj1) === Object.getPrototypeOf(obj2) } // recurse down a node tree to find a node with matching nodeName, for custom tags jQuery.find doesn't work in IE8 function findByNodeName(el, nodeName) { if (!el.prop) { // not a jQuery or jqLite object -> wrap it - el = angular.element(el); + el = angular.element(el) } if (el.prop('nodeName') === nodeName.toUpperCase()) { - return el; + return el } - const c = el.children(); + const c = el.children() for (let i = 0; c && i < c.length; i++) { - const node = findByNodeName(c[i], nodeName); + const node = findByNodeName(c[i], nodeName) if (node) { - return node; + return node } } } @@ -70,52 +83,52 @@ function findByNodeName(el, nodeName) { function arrayify(obj) { if (obj && !angular.isArray(obj)) { - obj = [obj]; + obj = [obj] } else if (!obj) { - obj = []; + obj = [] } - return obj; + return obj } function extendFunction(...fns) { return function extendedFunction() { - const args = arguments; - fns.forEach(fn => fn.apply(null, args)); - }; + const args = arguments + fns.forEach(fn => fn.apply(null, args)) + } } function extendArray(primary, secondary, property) { if (property) { - primary = primary[property]; - secondary = secondary[property]; + primary = primary[property] + secondary = secondary[property] } if (secondary && primary) { angular.forEach(secondary, function(item) { if (primary.indexOf(item) === -1) { - primary.push(item); + primary.push(item) } - }); - return primary; + }) + return primary } else if (secondary) { - return secondary; + return secondary } else { - return primary; + return primary } } function startsWith(str, search) { if (angular.isString(str) && angular.isString(search)) { - return str.length >= search.length && str.substring(0, search.length) === search; + return str.length >= search.length && str.substring(0, search.length) === search } else { - return false; + return false } } function contains(str, search) { if (angular.isString(str) && angular.isString(search)) { - return str.length >= search.length && str.indexOf(search) !== -1; + return str.length >= search.length && str.indexOf(search) !== -1 } else { - return false; + return false } } diff --git a/src/other/utils.test.js b/src/other/utils.test.js index 20a78b7b..4b0b2624 100644 --- a/src/other/utils.test.js +++ b/src/other/utils.test.js @@ -1,44 +1,44 @@ /* eslint no-unused-vars:0 */ -import utils from './utils.js'; +import utils from './utils.js' // gotta do this because webstorm/jshint doesn't like destructuring imports :-( -const {extendFunction, startsWith} = utils; +const {extendFunction, startsWith} = utils describe(`utils`, () => { describe(`extendFunction`, () => { - let fn1, fn2, fn3; + let fn1, fn2, fn3 beforeEach(() => { - fn1 = sinon.spy(); - fn2 = sinon.spy(); - fn3 = sinon.spy(); - }); + fn1 = sinon.spy() + fn2 = sinon.spy() + fn3 = sinon.spy() + }) it(`should call all functions with the given`, () => { - const extended = extendFunction(fn1, fn2); - extended('foo'); + const extended = extendFunction(fn1, fn2) + extended('foo') - expect(fn1).to.have.been.calledWith('foo'); - }); - }); + expect(fn1).to.have.been.calledWith('foo') + }) + }) describe(`startsWith`, () => { it(`should return true if a string has a given prefix`, () => { - expect(startsWith('fooBar', 'foo')).to.be.true; - }); + expect(startsWith('fooBar', 'foo')).to.be.true + }) it(`should return false if a string does not have a given prefix`, () => { - expect(startsWith('fooBar', 'nah')).to.be.false; - }); + expect(startsWith('fooBar', 'nah')).to.be.false + }) it(`should return false if no a string`, () => { - expect(startsWith(undefined, 'foo')).to.be.false; - expect(startsWith(5, 'foo')).to.be.false; - expect(startsWith('foo', undefined)).to.be.false; - expect(startsWith('foo', 5)).to.be.false; - expect(startsWith(undefined, undefined)).to.be.false; - }); - }); - -}); + expect(startsWith(undefined, 'foo')).to.be.false + expect(startsWith(5, 'foo')).to.be.false + expect(startsWith('foo', undefined)).to.be.false + expect(startsWith('foo', 5)).to.be.false + expect(startsWith(undefined, undefined)).to.be.false + }) + }) + +}) diff --git a/src/providers/formlyApiCheck.js b/src/providers/formlyApiCheck.js index ecf5d98e..cb1f32b8 100644 --- a/src/providers/formlyApiCheck.js +++ b/src/providers/formlyApiCheck.js @@ -1,47 +1,47 @@ -import angular from 'angular-fix'; -import apiCheckFactory from 'api-check'; +import angular from 'angular-fix' +import apiCheckFactory from 'api-check' const apiCheck = apiCheckFactory({ output: { prefix: 'angular-formly:', - docsBaseUrl: require('../other/docsBaseUrl') - } -}); + docsBaseUrl: require('../other/docsBaseUrl'), + }, +}) function shapeRequiredIfNot(otherProps, propChecker) { if (!angular.isArray(otherProps)) { - otherProps = [otherProps]; + otherProps = [otherProps] } - const type = `specified if these are not specified: \`${otherProps.join(', ')}\` (otherwise it's optional)`; + const type = `specified if these are not specified: \`${otherProps.join(', ')}\` (otherwise it's optional)` function shapeRequiredIfNotDefinition(prop, propName, location, obj) { - const propExists = obj && obj.hasOwnProperty(propName); + const propExists = obj && obj.hasOwnProperty(propName) const otherPropsExist = otherProps.some(function(otherProp) { - return obj && obj.hasOwnProperty(otherProp); - }); + return obj && obj.hasOwnProperty(otherProp) + }) if (!otherPropsExist && !propExists) { - return apiCheck.utils.getError(propName, location, type); + return apiCheck.utils.getError(propName, location, type) } else if (propExists) { - return propChecker(prop, propName, location, obj); + return propChecker(prop, propName, location, obj) } } - shapeRequiredIfNotDefinition.type = type; - return apiCheck.utils.checkerHelpers.setupChecker(shapeRequiredIfNotDefinition); + shapeRequiredIfNotDefinition.type = type + return apiCheck.utils.checkerHelpers.setupChecker(shapeRequiredIfNotDefinition) } -const formlyExpression = apiCheck.oneOfType([apiCheck.string, apiCheck.func]); -const specifyWrapperType = apiCheck.typeOrArrayOf(apiCheck.string).nullable; +const formlyExpression = apiCheck.oneOfType([apiCheck.string, apiCheck.func]) +const specifyWrapperType = apiCheck.typeOrArrayOf(apiCheck.string).nullable -const apiCheckProperty = apiCheck.func; +const apiCheckProperty = apiCheck.func const apiCheckInstanceProperty = apiCheck.shape.onlyIf('apiCheck', apiCheck.func.withProperties({ warn: apiCheck.func, throw: apiCheck.func, - shape: apiCheck.func -})); + shape: apiCheck.func, +})) -const apiCheckFunctionProperty = apiCheck.shape.onlyIf('apiCheck', apiCheck.oneOf(['throw', 'warn'])); +const apiCheckFunctionProperty = apiCheck.shape.onlyIf('apiCheck', apiCheck.oneOf(['throw', 'warn'])) const formlyWrapperType = apiCheck.shape({ name: shapeRequiredIfNot('types', apiCheck.string).optional, @@ -52,30 +52,38 @@ const formlyWrapperType = apiCheck.shape({ apiCheck: apiCheckProperty.optional, apiCheckInstance: apiCheckInstanceProperty.optional, apiCheckFunction: apiCheckFunctionProperty.optional, - apiCheckOptions: apiCheck.object.optional -}).strict; + apiCheckOptions: apiCheck.object.optional, +}).strict const expressionProperties = apiCheck.objectOf(apiCheck.oneOfType([ formlyExpression, apiCheck.shape({ expression: formlyExpression, - message: formlyExpression.optional - }).strict -])); + message: formlyExpression.optional, + }).strict, +])) -const modelChecker = apiCheck.oneOfType([apiCheck.string, apiCheck.object]); +const modelChecker = apiCheck.oneOfType([apiCheck.string, apiCheck.object]) const templateManipulators = apiCheck.shape({ preWrapper: apiCheck.arrayOf(apiCheck.func).nullable.optional, - postWrapper: apiCheck.arrayOf(apiCheck.func).nullable.optional -}).strict.nullable; + postWrapper: apiCheck.arrayOf(apiCheck.func).nullable.optional, +}).strict.nullable const validatorChecker = apiCheck.objectOf(apiCheck.oneOfType([ formlyExpression, apiCheck.shape({ expression: formlyExpression, - message: formlyExpression.optional - }).strict -])); + message: formlyExpression.optional, + }).strict, +])) + +const watcherChecker = apiCheck.typeOrArrayOf( + apiCheck.shape({ + expression: formlyExpression.optional, + listener: formlyExpression.optional, + runFieldExpressions: apiCheck.bool.optional, + }) +) const fieldOptionsApiShape = { $$hashKey: apiCheck.any.optional, @@ -98,8 +106,8 @@ const fieldOptionsApiShape = { extras: apiCheck.shape({ validateOnModelChange: apiCheck.bool.optional, skipNgModelAttrsManipulator: apiCheck.oneOfType([ - apiCheck.string, apiCheck.bool - ]).optional + apiCheck.string, apiCheck.bool, + ]).optional, }).strict.optional, data: apiCheck.object.optional, templateOptions: apiCheck.object.optional, @@ -107,18 +115,13 @@ const fieldOptionsApiShape = { modelOptions: apiCheck.shape({ updateOn: apiCheck.string.optional, debounce: apiCheck.oneOfType([ - apiCheck.objectOf(apiCheck.number), apiCheck.number + apiCheck.objectOf(apiCheck.number), apiCheck.number, ]).optional, allowInvalid: apiCheck.bool.optional, getterSetter: apiCheck.bool.optional, - timezone: apiCheck.string.optional + timezone: apiCheck.string.optional, }).optional, - watcher: apiCheck.typeOrArrayOf( - apiCheck.shape({ - expression: formlyExpression.optional, - listener: formlyExpression - }) - ).optional, + watcher: watcherChecker.optional, validators: validatorChecker.optional, asyncValidators: validatorChecker.optional, parsers: apiCheck.arrayOf(formlyExpression).optional, @@ -132,18 +135,18 @@ const fieldOptionsApiShape = { value: apiCheck.shape.ifNot('statement', apiCheck.any).optional, attribute: apiCheck.shape.ifNot('statement', apiCheck.any).optional, bound: apiCheck.shape.ifNot('statement', apiCheck.any).optional, - boolean: apiCheck.shape.ifNot('statement', apiCheck.any).optional + boolean: apiCheck.shape.ifNot('statement', apiCheck.any).optional, }).strict).optional, elementAttributes: apiCheck.objectOf(apiCheck.string).optional, optionsTypes: apiCheck.typeOrArrayOf(apiCheck.string).optional, link: apiCheck.func.optional, controller: apiCheck.oneOfType([ - apiCheck.string, apiCheck.func, apiCheck.array + apiCheck.string, apiCheck.func, apiCheck.array, ]).optional, validation: apiCheck.shape({ show: apiCheck.bool.nullable.optional, messages: apiCheck.objectOf(formlyExpression).optional, - errorExistsAndShouldBeVisible: apiCheck.bool.optional + errorExistsAndShouldBeVisible: apiCheck.bool.optional, }).optional, formControl: apiCheck.typeOrArrayOf(apiCheck.object).optional, value: apiCheck.func.optional, @@ -152,24 +155,27 @@ const fieldOptionsApiShape = { resetModel: apiCheck.func.optional, updateInitialValue: apiCheck.func.optional, initialValue: apiCheck.any.optional, - defaultValue: apiCheck.any.optional -}; + defaultValue: apiCheck.any.optional, +} -const formlyFieldOptions = apiCheck.shape(fieldOptionsApiShape).strict; +const formlyFieldOptions = apiCheck.shape(fieldOptionsApiShape).strict const formOptionsApi = apiCheck.shape({ formState: apiCheck.object.optional, resetModel: apiCheck.func.optional, updateInitialValue: apiCheck.func.optional, removeChromeAutoComplete: apiCheck.bool.optional, + parseKeyArrays: apiCheck.bool.optional, templateManipulators: templateManipulators.optional, + manualModelWatcher: apiCheck.oneOfType([apiCheck.bool, apiCheck.func]).optional, + watchAllExpressions: apiCheck.bool.optional, wrapper: specifyWrapperType.optional, fieldTransform: apiCheck.oneOfType([ - apiCheck.func, apiCheck.array + apiCheck.func, apiCheck.array, ]).optional, - data: apiCheck.object.optional -}).strict; + data: apiCheck.object.optional, +}).strict const fieldGroup = apiCheck.shape({ @@ -179,27 +185,30 @@ const fieldGroup = apiCheck.shape({ fieldGroup: apiCheck.arrayOf(apiCheck.oneOfType([formlyFieldOptions, apiCheck.object])), className: apiCheck.string.optional, options: formOptionsApi.optional, + templateOptions: apiCheck.object.optional, + wrapper: specifyWrapperType.optional, + watcher: watcherChecker.optional, hide: apiCheck.bool.optional, hideExpression: formlyExpression.optional, data: apiCheck.object.optional, model: modelChecker.optional, form: apiCheck.object.optional, - elementAttributes: apiCheck.objectOf(apiCheck.string).optional -}).strict; + elementAttributes: apiCheck.objectOf(apiCheck.string).optional, +}).strict -const typeOptionsDefaultOptions = angular.copy(fieldOptionsApiShape); -typeOptionsDefaultOptions.key = apiCheck.string.optional; +const typeOptionsDefaultOptions = angular.copy(fieldOptionsApiShape) +typeOptionsDefaultOptions.key = apiCheck.string.optional const formlyTypeOptions = apiCheck.shape({ name: apiCheck.string, template: apiCheck.shape.ifNot('templateUrl', apiCheck.oneOfType([apiCheck.string, apiCheck.func])).optional, templateUrl: apiCheck.shape.ifNot('template', apiCheck.oneOfType([apiCheck.string, apiCheck.func])).optional, controller: apiCheck.oneOfType([ - apiCheck.func, apiCheck.string, apiCheck.array + apiCheck.func, apiCheck.string, apiCheck.array, ]).optional, link: apiCheck.func.optional, defaultOptions: apiCheck.oneOfType([ - apiCheck.func, apiCheck.shape(typeOptionsDefaultOptions) + apiCheck.func, apiCheck.shape(typeOptionsDefaultOptions), ]).optional, extends: apiCheck.string.optional, wrapper: specifyWrapperType.optional, @@ -208,11 +217,11 @@ const formlyTypeOptions = apiCheck.shape({ apiCheckInstance: apiCheckInstanceProperty.optional, apiCheckFunction: apiCheckFunctionProperty.optional, apiCheckOptions: apiCheck.object.optional, - overwriteOk: apiCheck.bool.optional -}).strict; + overwriteOk: apiCheck.bool.optional, +}).strict angular.extend(apiCheck, { - formlyTypeOptions, formlyFieldOptions, formlyExpression, formlyWrapperType, fieldGroup, formOptionsApi -}); + formlyTypeOptions, formlyFieldOptions, formlyExpression, formlyWrapperType, fieldGroup, formOptionsApi, +}) -export default apiCheck; +export default apiCheck diff --git a/src/providers/formlyApiCheck.test.js b/src/providers/formlyApiCheck.test.js index 13bd1552..ac7aff7b 100644 --- a/src/providers/formlyApiCheck.test.js +++ b/src/providers/formlyApiCheck.test.js @@ -1,13 +1,13 @@ /* jshint maxlen:false */ describe('formlyApiCheck', () => { - beforeEach(window.module('formly')); + beforeEach(window.module('formly')) - let formlyApiCheck; + let formlyApiCheck beforeEach(inject((_formlyApiCheck_) => { - formlyApiCheck = _formlyApiCheck_; - })); + formlyApiCheck = _formlyApiCheck_ + })) describe('formlyFieldOptions', () => { it(`should pass when validation.messages is an object of functions or strings`, () => { @@ -18,19 +18,19 @@ describe('formlyApiCheck', () => { messages: { thing1() { }, - thing2: '"Formly Expression"' - } - } - }, 'formlyFieldOptions'); - }); + thing2: '"Formly Expression"', + }, + }, + }, 'formlyFieldOptions') + }) it(`should allow $$hashKey`, () => { expectPass({ $$hashKey: 'object:1', template: 'hello', - key: 'whatevs' - }, 'formlyFieldOptions'); - }); + key: 'whatevs', + }, 'formlyFieldOptions') + }) describe('ngModelAttrs', () => { it(`should allow property of 'boolean'`, () => { @@ -38,67 +38,67 @@ describe('formlyApiCheck', () => { template: 'hello', key: 'whatevs', templateOptions: { - foo: 'bar' + foo: 'bar', }, ngModelAttrs: { foo: { - boolean: 'foo-bar' - } - } - }, 'formlyFieldOptions'); - }); - }); - }); + boolean: 'foo-bar', + }, + }, + }, 'formlyFieldOptions') + }) + }) + }) describe(`fieldGroup`, () => { it(`should pass when specifying data`, () => { expectPass({ fieldGroup: [], - data: {foo: 'bar'} - }, 'fieldGroup'); - }); - }); + data: {foo: 'bar'}, + }, 'fieldGroup') + }) + }) describe(`extras`, () => { describe(`skipNgModelAttrsManipulator`, () => { it(`should pass with a boolean`, () => { expectPass({ template: 'foo', - extras: {skipNgModelAttrsManipulator: true} - }, 'formlyFieldOptions'); - }); + extras: {skipNgModelAttrsManipulator: true}, + }, 'formlyFieldOptions') + }) it(`should pass with a string`, () => { expectPass({ template: 'foo', - extras: {skipNgModelAttrsManipulator: '.selector'} - }, 'formlyFieldOptions'); - }); + extras: {skipNgModelAttrsManipulator: '.selector'}, + }, 'formlyFieldOptions') + }) it(`should pass with nothing`, () => { expectPass({ template: 'foo', - extras: {skipNgModelAttrsManipulator: '.selector'} - }, 'formlyFieldOptions'); - }); + extras: {skipNgModelAttrsManipulator: '.selector'}, + }, 'formlyFieldOptions') + }) it(`should fail with anything else`, () => { expectFail({ template: 'foo', - extras: {skipNgModelAttrsManipulator: 32} - }, 'formlyFieldOptions'); - }); - }); - }); + extras: {skipNgModelAttrsManipulator: 32}, + }, 'formlyFieldOptions') + }) + }) + }) function expectPass(options, checker) { - const result = formlyApiCheck[checker](options); - expect(result).to.be.undefined; + const result = formlyApiCheck[checker](options) + expect(result).to.be.undefined } function expectFail(options, checker) { - const result = formlyApiCheck[checker](options); - expect(result).to.be.an.instanceOf(Error); + const result = formlyApiCheck[checker](options) + expect(result).to.be.an.instanceOf(Error) } -}); +}) diff --git a/src/providers/formlyConfig.js b/src/providers/formlyConfig.js index 2035f4da..8737c5d3 100644 --- a/src/providers/formlyConfig.js +++ b/src/providers/formlyConfig.js @@ -1,20 +1,21 @@ -import angular from 'angular-fix'; -import utils from '../other/utils'; +import angular from 'angular-fix' +import utils from '../other/utils' -export default formlyConfig; +export default formlyConfig // @ngInject function formlyConfig(formlyUsabilityProvider, formlyErrorAndWarningsUrlPrefix, formlyApiCheck) { - const typeMap = {}; - const templateWrappersMap = {}; - const defaultWrapperName = 'default'; - const _this = this; - const getError = formlyUsabilityProvider.getFormlyError; + const typeMap = {} + const templateWrappersMap = {} + const defaultWrapperName = 'default' + const _this = this + const getError = formlyUsabilityProvider.getFormlyError angular.extend(this, { setType, getType, + getTypes, getTypeHeritage, setWrapper, getWrapper, @@ -24,208 +25,213 @@ function formlyConfig(formlyUsabilityProvider, formlyErrorAndWarningsUrlPrefix, disableWarnings: false, extras: { disableNgModelAttrsManipulator: false, + fieldTransform: [], ngModelAttrsManipulatorPreferUnbound: false, removeChromeAutoComplete: false, + parseKeyArrays: false, defaultHideDirective: 'ng-if', getFieldId: null, - explicitAsync: false }, templateManipulators: { preWrapper: [], - postWrapper: [] + postWrapper: [], }, - $get: () => this - }); + $get: () => this, + }) function setType(options) { if (angular.isArray(options)) { - const allTypes = []; + const allTypes = [] angular.forEach(options, item => { - allTypes.push(setType(item)); - }); - return allTypes; + allTypes.push(setType(item)) + }) + return allTypes } else if (angular.isObject(options)) { - checkType(options); + checkType(options) if (options.extends) { - extendTypeOptions(options); + extendTypeOptions(options) } - typeMap[options.name] = options; - return typeMap[options.name]; + typeMap[options.name] = options + return typeMap[options.name] } else { - throw getError(`You must provide an object or array for setType. You provided: ${JSON.stringify(arguments)}`); + throw getError(`You must provide an object or array for setType. You provided: ${JSON.stringify(arguments)}`) } } function checkType(options) { formlyApiCheck.throw(formlyApiCheck.formlyTypeOptions, options, { prefix: 'formlyConfig.setType', - url: 'settype-validation-failed' - }); + url: 'settype-validation-failed', + }) if (!options.overwriteOk) { - checkOverwrite(options.name, typeMap, options, 'types'); + checkOverwrite(options.name, typeMap, options, 'types') } else { - options.overwriteOk = undefined; + options.overwriteOk = undefined } } function extendTypeOptions(options) { - const extendsType = getType(options.extends, true, options); - extendTypeControllerFunction(options, extendsType); - extendTypeLinkFunction(options, extendsType); - extendTypeDefaultOptions(options, extendsType); - utils.reverseDeepMerge(options, extendsType); - extendTemplate(options, extendsType); + const extendsType = getType(options.extends, true, options) + extendTypeControllerFunction(options, extendsType) + extendTypeLinkFunction(options, extendsType) + extendTypeDefaultOptions(options, extendsType) + utils.reverseDeepMerge(options, extendsType) + extendTemplate(options, extendsType) } function extendTemplate(options, extendsType) { if (options.template && extendsType.templateUrl) { - delete options.templateUrl; + delete options.templateUrl } else if (options.templateUrl && extendsType.template) { - delete options.template; + delete options.template } } function extendTypeControllerFunction(options, extendsType) { - const extendsCtrl = extendsType.controller; + const extendsCtrl = extendsType.controller if (!angular.isDefined(extendsCtrl)) { - return; + return } - const optionsCtrl = options.controller; + const optionsCtrl = options.controller if (angular.isDefined(optionsCtrl)) { options.controller = function($scope, $controller) { - $controller(extendsCtrl, {$scope}); - $controller(optionsCtrl, {$scope}); - }; - options.controller.$inject = ['$scope', '$controller']; + $controller(extendsCtrl, {$scope}) + $controller(optionsCtrl, {$scope}) + } + options.controller.$inject = ['$scope', '$controller'] } else { - options.controller = extendsCtrl; + options.controller = extendsCtrl } } function extendTypeLinkFunction(options, extendsType) { - const extendsFn = extendsType.link; + const extendsFn = extendsType.link if (!angular.isDefined(extendsFn)) { - return; + return } - const optionsFn = options.link; + const optionsFn = options.link if (angular.isDefined(optionsFn)) { options.link = function() { - extendsFn(...arguments); - optionsFn(...arguments); - }; + extendsFn(...arguments) + optionsFn(...arguments) + } } else { - options.link = extendsFn; + options.link = extendsFn } } function extendTypeDefaultOptions(options, extendsType) { - const extendsDO = extendsType.defaultOptions; + const extendsDO = extendsType.defaultOptions if (!angular.isDefined(extendsDO)) { - return; + return } - const optionsDO = options.defaultOptions; - const optionsDOIsFn = angular.isFunction(optionsDO); - const extendsDOIsFn = angular.isFunction(extendsDO); + const optionsDO = options.defaultOptions || {} + const optionsDOIsFn = angular.isFunction(optionsDO) + const extendsDOIsFn = angular.isFunction(extendsDO) if (extendsDOIsFn) { options.defaultOptions = function defaultOptions(opts, scope) { - const extendsDefaultOptions = extendsDO(opts, scope); - const mergedDefaultOptions = {}; - utils.reverseDeepMerge(mergedDefaultOptions, opts, extendsDefaultOptions); - let extenderOptionsDefaultOptions = optionsDO; + const extendsDefaultOptions = extendsDO(opts, scope) + const mergedDefaultOptions = {} + utils.reverseDeepMerge(mergedDefaultOptions, opts, extendsDefaultOptions) + let extenderOptionsDefaultOptions = optionsDO if (optionsDOIsFn) { - extenderOptionsDefaultOptions = extenderOptionsDefaultOptions(mergedDefaultOptions, scope); + extenderOptionsDefaultOptions = extenderOptionsDefaultOptions(mergedDefaultOptions, scope) } - utils.reverseDeepMerge(extendsDefaultOptions, extenderOptionsDefaultOptions); - return extendsDefaultOptions; - }; + utils.reverseDeepMerge(extenderOptionsDefaultOptions, extendsDefaultOptions) + return extenderOptionsDefaultOptions + } } else if (optionsDOIsFn) { options.defaultOptions = function defaultOptions(opts, scope) { - const newDefaultOptions = {}; - utils.reverseDeepMerge(newDefaultOptions, opts, extendsDO); - return optionsDO(newDefaultOptions, scope); - }; + const newDefaultOptions = {} + utils.reverseDeepMerge(newDefaultOptions, opts, extendsDO) + return optionsDO(newDefaultOptions, scope) + } } } function getType(name, throwError, errorContext) { if (!name) { - return undefined; + return undefined } - const type = typeMap[name]; + const type = typeMap[name] if (!type && throwError === true) { throw getError( `There is no type by the name of "${name}": ${JSON.stringify(errorContext)}` - ); + ) } else { - return type; + return type } } + function getTypes() { + return typeMap + } + function getTypeHeritage(parent) { - const heritage = []; - let type = parent; + const heritage = [] + let type = parent if (angular.isString(type)) { - type = getType(parent); + type = getType(parent) } - parent = type.extends; + parent = type.extends while (parent) { - type = getType(parent); - heritage.push(type); - parent = type.extends; + type = getType(parent) + heritage.push(type) + parent = type.extends } - return heritage; + return heritage } function setWrapper(options, name) { if (angular.isArray(options)) { - return options.map(wrapperOptions => setWrapper(wrapperOptions)); + return options.map(wrapperOptions => setWrapper(wrapperOptions)) } else if (angular.isObject(options)) { - options.types = getOptionsTypes(options); - options.name = getOptionsName(options, name); - checkWrapperAPI(options); - templateWrappersMap[options.name] = options; - return options; + options.types = getOptionsTypes(options) + options.name = getOptionsName(options, name) + checkWrapperAPI(options) + templateWrappersMap[options.name] = options + return options } else if (angular.isString(options)) { return setWrapper({ template: options, - name - }); + name, + }) } } function getOptionsTypes(options) { if (angular.isString(options.types)) { - return [options.types]; + return [options.types] } if (!angular.isDefined(options.types)) { - return []; + return [] } else { - return options.types; + return options.types } } function getOptionsName(options, name) { - return options.name || name || options.types.join(' ') || defaultWrapperName; + return options.name || name || options.types.join(' ') || defaultWrapperName } function checkWrapperAPI(options) { - formlyUsabilityProvider.checkWrapper(options); + formlyUsabilityProvider.checkWrapper(options) if (options.template) { - formlyUsabilityProvider.checkWrapperTemplate(options.template, options); + formlyUsabilityProvider.checkWrapperTemplate(options.template, options) } if (!options.overwriteOk) { - checkOverwrite(options.name, templateWrappersMap, options, 'templateWrappers'); + checkOverwrite(options.name, templateWrappersMap, options, 'templateWrappers') } else { - delete options.overwriteOk; + delete options.overwriteOk } - checkWrapperTypes(options); + checkWrapperTypes(options) } function checkWrapperTypes(options) { - const shouldThrow = !angular.isArray(options.types) || !options.types.every(angular.isString); + const shouldThrow = !angular.isArray(options.types) || !options.types.every(angular.isString) if (shouldThrow) { - throw getError(`Attempted to create a template wrapper with types that is not a string or an array of strings`); + throw getError(`Attempted to create a template wrapper with types that is not a string or an array of strings`) } } @@ -234,44 +240,44 @@ function formlyConfig(formlyUsabilityProvider, formlyErrorAndWarningsUrlPrefix, warn('overwriting-types-or-wrappers', [ `Attempting to overwrite ${property} on ${objectName} which is currently`, `${JSON.stringify(object[property])} with ${JSON.stringify(newValue)}`, - `To supress this warning, specify the property "overwriteOk: true"` - ].join(' ')); + `To supress this warning, specify the property "overwriteOk: true"`, + ].join(' ')) } } function getWrapper(name) { - return templateWrappersMap[name || defaultWrapperName]; + return templateWrappersMap[name || defaultWrapperName] } function getWrapperByType(type) { /* eslint prefer-const:0 */ - const wrappers = []; + const wrappers = [] for (let name in templateWrappersMap) { if (templateWrappersMap.hasOwnProperty(name)) { if (templateWrappersMap[name].types && templateWrappersMap[name].types.indexOf(type) !== -1) { - wrappers.push(templateWrappersMap[name]); + wrappers.push(templateWrappersMap[name]) } } } - return wrappers; + return wrappers } function removeWrapperByName(name) { - const wrapper = templateWrappersMap[name]; - delete templateWrappersMap[name]; - return wrapper; + const wrapper = templateWrappersMap[name] + delete templateWrappersMap[name] + return wrapper } function removeWrappersForType(type) { - const wrappers = getWrapperByType(type); + const wrappers = getWrapperByType(type) if (!wrappers) { - return undefined; + return undefined } if (!angular.isArray(wrappers)) { - return removeWrapperByName(wrappers.name); + return removeWrapperByName(wrappers.name) } else { - wrappers.forEach((wrapper) => removeWrapperByName(wrapper.name)); - return wrappers; + wrappers.forEach((wrapper) => removeWrapperByName(wrapper.name)) + return wrappers } } @@ -279,11 +285,11 @@ function formlyConfig(formlyUsabilityProvider, formlyErrorAndWarningsUrlPrefix, function warn() { if (!_this.disableWarnings && console.warn) { /* eslint no-console:0 */ - const args = Array.prototype.slice.call(arguments); - const warnInfoSlug = args.shift(); - args.unshift('Formly Warning:'); - args.push(`${formlyErrorAndWarningsUrlPrefix}${warnInfoSlug}`); - console.warn(...args); + const args = Array.prototype.slice.call(arguments) + const warnInfoSlug = args.shift() + args.unshift('Formly Warning:') + args.push(`${formlyErrorAndWarningsUrlPrefix}${warnInfoSlug}`) + console.warn(...args) } } } diff --git a/src/providers/formlyConfig.test.js b/src/providers/formlyConfig.test.js index 27025956..988a050e 100644 --- a/src/providers/formlyConfig.test.js +++ b/src/providers/formlyConfig.test.js @@ -3,229 +3,238 @@ /* eslint no-shadow:0 */ /* eslint no-console:0 */ /* eslint no-unused-vars:0 */ -import angular from 'angular-fix'; -import testUtils from '../test.utils.js'; +import angular from 'angular-fix' +import testUtils from '../test.utils.js' -const {getNewField, basicForm, shouldWarn, shouldNotWarn} = testUtils; +const {getNewField, basicForm, shouldWarn, shouldNotWarn} = testUtils describe('formlyConfig', () => { - beforeEach(window.module('formly')); + beforeEach(window.module('formly')) - let formlyConfig; + let formlyConfig beforeEach(inject((_formlyConfig_) => { - formlyConfig = _formlyConfig_; - })); + formlyConfig = _formlyConfig_ + })) describe('setWrapper/getWrapper', () => { - let getterFn, setterFn, $log; - const template = 'This is my template'; - const templateUrl = '/path/to/my/template.html'; - const typesString = 'checkbox'; - const types = ['text', 'textarea']; - const name = 'hi'; - const name2 = 'name2'; - const template2 = template + '2'; + let getterFn, setterFn, $log + const template = 'This is my template' + const templateUrl = '/path/to/my/template.html' + const typesString = 'checkbox' + const types = ['text', 'textarea'] + const name = 'hi' + const name2 = 'name2' + const template2 = template + '2' beforeEach(inject(function(_$log_) { - getterFn = formlyConfig.getWrapper; - setterFn = formlyConfig.setWrapper; - $log = _$log_; - })); + getterFn = formlyConfig.getWrapper + setterFn = formlyConfig.setWrapper + $log = _$log_ + })) describe('\(^O^)/ path', () => { describe('the default template', () => { it('can be a string without a name', () => { - setterFn(template); - expect(getterFn()).to.eql({name: 'default', template, types: []}); - }); + setterFn(template) + expect(getterFn()).to.eql({name: 'default', template, types: []}) + }) it('can be a string with a name', () => { - setterFn(template, name); - expect(getterFn(name)).to.eql({name, template, types: []}); - }); + setterFn(template, name) + expect(getterFn(name)).to.eql({name, template, types: []}) + }) it('can be an object with a template', () => { - setterFn({template}); - expect(getterFn()).to.eql({name: 'default', template, types: []}); - }); + setterFn({template}) + expect(getterFn()).to.eql({name: 'default', template, types: []}) + }) it('can be an object with a template and a name', () => { - setterFn({template, name}); - expect(getterFn(name)).to.eql({name, template, types: []}); - }); + setterFn({template, name}) + expect(getterFn(name)).to.eql({name, template, types: []}) + }) it('can be an object with a templateUrl', () => { - setterFn({templateUrl}); - expect(getterFn()).to.eql({name: 'default', templateUrl, types: []}); - }); + setterFn({templateUrl}) + expect(getterFn()).to.eql({name: 'default', templateUrl, types: []}) + }) it('can be an object with a templateUrl and a name', () => { - setterFn({name, templateUrl}); - expect(getterFn(name)).to.eql({name, templateUrl, types: []}); - }); + setterFn({name, templateUrl}) + expect(getterFn(name)).to.eql({name, templateUrl, types: []}) + }) it('can be an array of objects with names, urls, and/or templates', () => { setterFn([ {templateUrl}, {name, template}, - {name: name2, template: template2} - ]); - expect(getterFn()).to.eql({templateUrl, name: 'default', types: []}); - expect(getterFn(name)).to.eql({template, name, types: []}); - expect(getterFn(name2)).to.eql({template: template2, name: name2, types: []}); - }); + {name: name2, template: template2}, + ]) + expect(getterFn()).to.eql({templateUrl, name: 'default', types: []}) + expect(getterFn(name)).to.eql({template, name, types: []}) + expect(getterFn(name2)).to.eql({template: template2, name: name2, types: []}) + }) it('can specify types as a string (using types as the name when not specified)', () => { - setterFn({types: typesString, template}); - expect(getterFn(typesString)).to.eql({template, name: typesString, types: [typesString]}); - }); + setterFn({types: typesString, template}) + expect(getterFn(typesString)).to.eql({template, name: typesString, types: [typesString]}) + }) it('can specify types as an array (using types as the name when not specified)', () => { - setterFn({types, template}); - expect(getterFn(types.join(' '))).to.eql({template, name: types.join(' '), types}); - }); - }); - }); + setterFn({types, template}) + expect(getterFn(types.join(' '))).to.eql({template, name: types.join(' '), types}) + }) + }) + }) describe('(◞‸◟;) path', () => { it('should throw an error when providing both a template and templateUrl', () => { - expect(() => setterFn({template, templateUrl}, name)).to.throw(/`template` must be `ifNot\[templateUrl]`/i); - }); + expect(() => setterFn({template, templateUrl}, name)).to.throw(/`template` must be `ifNot\[templateUrl]`/i) + }) it('should throw an error when the template does not use formly-transclude', () => { - const error = /templates.*?must.*?<\/formly-transclude>/; - expect(() => setterFn({template: 'no formly-transclude'})).to.throw(error); - }); + const error = /templates.*?must.*?<\/formly-transclude>/ + expect(() => setterFn({template: 'no formly-transclude'})).to.throw(error) + }) it('should throw an error when specifying an array type where not all items are strings', () => { - const error = /types.*?typeOrArrayOf.*?String.*?/i; - expect(() => setterFn({template, types: ['hi', 2, false, 'cool']})).to.throw(error); - }); + const error = /types.*?typeOrArrayOf.*?String.*?/i + expect(() => setterFn({template, types: ['hi', 2, false, 'cool']})).to.throw(error) + }) it('should warn when attempting to override a template wrapper', () => { shouldWarn(/overwrite/, function() { - setterFn({template}); - setterFn({template}); - }); - }); + setterFn({template}) + setterFn({template}) + }) + }) it('should not warn when attempting to override a template wrapper if overwriteOk is true', () => { shouldNotWarn(() => { - setterFn({template}); - setterFn({template, overwriteOk: true}); - }); - }); - }); + setterFn({template}) + setterFn({template, overwriteOk: true}) + }) + }) + }) describe(`apiCheck`, () => { - testApiCheck('setWrapper', 'getWrapper'); - }); + testApiCheck('setWrapper', 'getWrapper') + }) - }); + }) describe('getWrapperByType', () => { - let getterFn, setterFn; - const types = ['input', 'checkbox']; - const types2 = ['input', 'select']; - const templateUrl = '/path/to/file.html'; + let getterFn, setterFn + const types = ['input', 'checkbox'] + const types2 = ['input', 'select'] + const templateUrl = '/path/to/file.html' beforeEach(inject(function(formlyConfig) { - setterFn = formlyConfig.setWrapper; - getterFn = formlyConfig.getWrapperByType; - })); + setterFn = formlyConfig.setWrapper + getterFn = formlyConfig.getWrapperByType + })) describe('\(^O^)/ path', () => { it('should return a template wrapper that has the same type', () => { - const option = setterFn({templateUrl, types}); - expect(getterFn(types[0])).to.eql([option]); - }); + const option = setterFn({templateUrl, types}) + expect(getterFn(types[0])).to.eql([option]) + }) it('should return an array when multiple wrappers have the same time', () => { - setterFn({templateUrl, types}); - setterFn({templateUrl, types: types2}); - const inputWrappers = getterFn('input'); - expect(inputWrappers).to.be.instanceOf(Array); - expect(inputWrappers).to.have.length(2); - }); + setterFn({templateUrl, types}) + setterFn({templateUrl, types: types2}) + const inputWrappers = getterFn('input') + expect(inputWrappers).to.be.instanceOf(Array) + expect(inputWrappers).to.have.length(2) + }) - }); - }); + }) + }) describe('removeWrapper', () => { - let remove, removeForType, setterFn, getterFn, getByTypeFn; - const template = '
Something cool
'; - const name = 'name'; - const types = ['input', 'checkbox']; - const types2 = ['input', 'something else']; - const types3 = ['checkbox', 'something else']; + let remove, removeForType, setterFn, getterFn, getByTypeFn + const template = '
Something cool
' + const name = 'name' + const types = ['input', 'checkbox'] + const types2 = ['input', 'something else'] + const types3 = ['checkbox', 'something else'] beforeEach(inject((formlyConfig) => { - remove = formlyConfig.removeWrapperByName; - removeForType = formlyConfig.removeWrappersForType; - setterFn = formlyConfig.setWrapper; - getterFn = formlyConfig.getWrapper; - getByTypeFn = formlyConfig.getWrapperByType; - })); + remove = formlyConfig.removeWrapperByName + removeForType = formlyConfig.removeWrappersForType + setterFn = formlyConfig.setWrapper + getterFn = formlyConfig.getWrapper + getByTypeFn = formlyConfig.getWrapperByType + })) it('should allow you to remove a wrapper', () => { - setterFn(template, name); - remove(name); - expect(getterFn(name)).to.be.empty; - }); + setterFn(template, name) + remove(name) + expect(getterFn(name)).to.be.empty + }) it('should allow you to remove a wrapper for a type', () => { - setterFn({types, template}); - setterFn({types: types2, template}); - const checkboxAndSomethingElseWrapper = setterFn({types: types3, template}); - removeForType('input'); - expect(getByTypeFn('input')).to.be.empty; - const checkboxWrappers = getByTypeFn('checkbox'); - expect(checkboxWrappers).to.eql([checkboxAndSomethingElseWrapper]); - }); - }); - - - describe('setType/getType', () => { - let getterFn, setterFn; - const name = 'input'; - const template = ''; - const templateUrl = '/input.html'; - const wrapper = 'input'; - const wrapper2 = 'input2'; + setterFn({types, template}) + setterFn({types: types2, template}) + const checkboxAndSomethingElseWrapper = setterFn({types: types3, template}) + removeForType('input') + expect(getByTypeFn('input')).to.be.empty + const checkboxWrappers = getByTypeFn('checkbox') + expect(checkboxWrappers).to.eql([checkboxAndSomethingElseWrapper]) + }) + }) + + + describe('setType/getType/getTypes', () => { + let getterFn, setterFn, getTypesFn + const name = 'input' + const template = '' + const templateUrl = '/input.html' + const wrapper = 'input' + const wrapper2 = 'input2' beforeEach(inject(function(formlyConfig) { - getterFn = formlyConfig.getType; - setterFn = formlyConfig.setType; - })); + getterFn = formlyConfig.getType + setterFn = formlyConfig.setType + getTypesFn = formlyConfig.getTypes + })) describe('\(^O^)/ path', () => { it('should accept an object with a name and a template', () => { - setterFn({name, template}); - expect(getterFn(name).template).to.equal(template); - }); + setterFn({name, template}) + expect(getterFn(name).template).to.equal(template) + }) it('should accept an object with a name and a templateUrl', () => { - setterFn({name, templateUrl}); - expect(getterFn(name).templateUrl).to.equal(templateUrl); - }); + setterFn({name, templateUrl}) + expect(getterFn(name).templateUrl).to.equal(templateUrl) + }) it('should accept an array of objects', () => { setterFn([ {name, template}, - {name: 'type2', templateUrl} - ]); - expect(getterFn(name).template).to.equal(template); - expect(getterFn('type2').templateUrl).to.equal(templateUrl); - }); + {name: 'type2', templateUrl}, + ]) + expect(getterFn(name).template).to.equal(template) + expect(getterFn('type2').templateUrl).to.equal(templateUrl) + }) + + it('should expose the mapping from type name to config', () => { + setterFn([ + {name, template}, + {name: 'type2', templateUrl}, + ]) + expect(getTypesFn()).to.eql({[name]: getterFn(name), type2: getterFn('type2')}) + }) it('should allow you to set a wrapper as a string', () => { - setterFn({name, template, wrapper}); - expect(getterFn(name).wrapper).to.equal(wrapper); - }); + setterFn({name, template, wrapper}) + expect(getterFn(name).wrapper).to.equal(wrapper) + }) it('should allow you to set a wrapper as an array', () => { - setterFn({name, template, wrapper: [wrapper, wrapper2]}); - expect(getterFn(name).wrapper).to.eql([wrapper, wrapper2]); - }); + setterFn({name, template, wrapper: [wrapper, wrapper2]}) + expect(getterFn(name).wrapper).to.eql([wrapper, wrapper2]) + }) describe(`extends`, () => { describe(`object case`, () => { @@ -237,9 +246,9 @@ describe('formlyConfig', () => { defaultOptions: { templateOptions: { required: true, - min: 3 - } - } + min: 3, + }, + }, }, { name: 'type2', @@ -247,337 +256,399 @@ describe('formlyConfig', () => { defaultOptions: { templateOptions: { required: false, - max: 4 + max: 4, }, data: { - extraStuff: [1, 2, 3] - } - } - } - ]); - }); + extraStuff: [1, 2, 3], + }, + }, + }, + ]) + }) it(`should inherit all fields that it does not have itself`, () => { - expect(getterFn('type2').template).to.eql(template); - }); + expect(getterFn('type2').template).to.eql(template) + }) it(`should merge objects that it shares`, () => { expect(getterFn('type2').defaultOptions).to.eql({ templateOptions: { required: false, min: 3, - max: 4 + max: 4, }, data: { - extraStuff: [1, 2, 3] - } - }); - }); + extraStuff: [1, 2, 3], + }, + }) + }) it(`should not error when extends is specified without a template, templateUrl, or defaultOptions`, () => { - expect(() => setterFn({name: 'type3', extends: 'type2'})).to.not.throw(); - }); + expect(() => setterFn({name: 'type3', extends: 'type2'})).to.not.throw() + }) + + }) + + describe(`abstractType function case`, () => { + beforeEach(() => { + setterFn([ + { + name, + template, + defaultOptions: function(options) { + return { + templateOptions: { + required: true, + min: 3, + }, + } + }, + }, + { + name: 'type2', + extends: name, + defaultOptions: function(options) { + return { + templateOptions: { + required: false, + max: 4, + }, + } + }, + }, + { + name: 'type3', + extends: name, + defaultOptions: { + templateOptions: { + required: false, + max: 4, + }, + }, + }, + ]) + }) - }); + it(`should merge options when extending defaultOptions is a function`, () => { + expect(getterFn('type2').defaultOptions({})).to.eql({ + templateOptions: { + required: false, + min: 3, + max: 4, + }, + }) + }) + + it(`should merge options when extending defaultOptions is an object`, () => { + expect(getterFn('type3').defaultOptions({})).to.eql({ + templateOptions: { + required: false, + min: 3, + max: 4, + }, + }) + }) + + }) describe(`template/templateUrl Cases`, () => { it('should use templateUrl if type defines it and its parent has template defined', function() { setterFn([ { name, - template + template, }, { name: 'type2', extends: name, - templateUrl - } - ]); + templateUrl, + }, + ]) - expect(getterFn('type2').templateUrl).not.to.be.undefined; - expect(getterFn('type2').template).to.be.undefined; - }); + expect(getterFn('type2').templateUrl).not.to.be.undefined + expect(getterFn('type2').template).to.be.undefined + }) it('should use template if type defines it and its parent had templateUrl defined', function() { setterFn([ { name, - templateUrl + templateUrl, }, { name: 'type2', extends: name, - template - } - ]); + template, + }, + ]) - expect(getterFn('type2').template).not.to.be.undefined; - expect(getterFn('type2').templateUrl).to.be.undefined; - }); - }); + expect(getterFn('type2').template).not.to.be.undefined + expect(getterFn('type2').templateUrl).to.be.undefined + }) + }) describe(`function cases`, () => { - let args, fakeScope, parentFn, childFn, parentDefaultOptions, childDefaultOptions, argsAndParent; + let args, fakeScope, parentFn, childFn, parentDefaultOptions, childDefaultOptions, argsAndParent beforeEach(() => { - args = {data: {someData: true}}; - fakeScope = {}; + args = {data: {someData: true}} + fakeScope = {} parentDefaultOptions = { data: {extraOptions: true}, - templateOptions: {placeholder: 'hi'} - }; + templateOptions: {placeholder: 'hi'}, + } childDefaultOptions = { - templateOptions: {placeholder: 'hey', required: true} - }; - parentFn = sinon.stub().returns(parentDefaultOptions); - childFn = sinon.stub().returns(childDefaultOptions); + templateOptions: {placeholder: 'hey', required: true}, + } + parentFn = sinon.stub().returns(parentDefaultOptions) + childFn = sinon.stub().returns(childDefaultOptions) argsAndParent = { data: {someData: true, extraOptions: true}, - templateOptions: {placeholder: 'hi'} - }; - }); + templateOptions: {placeholder: 'hi'}, + } + }) it(`should call the extended parent's defaultOptions function and its own defaultOptions function`, () => { setterFn([ {name, defaultOptions: parentFn}, - {name: 'type2', extends: name, defaultOptions: childFn} - ]); - getterFn('type2').defaultOptions(args, fakeScope); - expect(parentFn).to.have.been.calledWith(args, fakeScope); - expect(childFn).to.have.been.calledWith(argsAndParent, fakeScope); - }); + {name: 'type2', extends: name, defaultOptions: childFn}, + ]) + getterFn('type2').defaultOptions(args, fakeScope) + expect(parentFn).to.have.been.calledWith(args, fakeScope) + expect(childFn).to.have.been.calledWith(argsAndParent, fakeScope) + }) it(`should call the extended parent's defaultOptions function when it doesn't have one of its own`, () => { setterFn([ {name, defaultOptions: parentFn}, - {name: 'type2', extends: name} - ]); - getterFn('type2').defaultOptions(args, fakeScope); - expect(parentFn).to.have.been.calledWith(args, fakeScope); - }); + {name: 'type2', extends: name}, + ]) + getterFn('type2').defaultOptions(args, fakeScope) + expect(parentFn).to.have.been.calledWith(args, fakeScope) + }) it(`should call its own defaultOptions function when the parent doesn't have one`, () => { setterFn([ {name, template}, - {name: 'type2', extends: name, defaultOptions: childFn} - ]); - getterFn('type2').defaultOptions(args, fakeScope); - expect(childFn).to.have.been.calledWith(args, fakeScope); - }); + {name: 'type2', extends: name, defaultOptions: childFn}, + ]) + getterFn('type2').defaultOptions(args, fakeScope) + expect(childFn).to.have.been.calledWith(args, fakeScope) + }) it(`should extend its defaultOptions object with the parent's defaultOptions object`, () => { const objectMergedDefaultOptions = { data: {extraOptions: true}, - templateOptions: {placeholder: 'hey', required: true} - }; + templateOptions: {placeholder: 'hey', required: true}, + } setterFn([ {name, defaultOptions: parentDefaultOptions}, - {name: 'type2', extends: name, defaultOptions: childDefaultOptions} - ]); - expect(getterFn('type2').defaultOptions).to.eql(objectMergedDefaultOptions); - }); + {name: 'type2', extends: name, defaultOptions: childDefaultOptions}, + ]) + expect(getterFn('type2').defaultOptions).to.eql(objectMergedDefaultOptions) + }) it(`should call its defaultOptions with the parent's defaultOptions object merged with the given args`, () => { setterFn([ {name, defaultOptions: parentDefaultOptions}, - {name: 'type2', extends: name, defaultOptions: childFn} - ]); - const returned = getterFn('type2').defaultOptions(args, fakeScope); - expect(childFn).to.have.been.calledWith(argsAndParent, fakeScope); - expect(returned).to.eql(childDefaultOptions); - }); - }); + {name: 'type2', extends: name, defaultOptions: childFn}, + ]) + const returned = getterFn('type2').defaultOptions(args, fakeScope) + expect(childFn).to.have.been.calledWith(argsAndParent, fakeScope) + expect(returned).to.eql(childDefaultOptions) + }) + }) describe(`link functions`, () => { - let linkArgs, parentFn, childFn; + let linkArgs, parentFn, childFn beforeEach(inject(($rootScope) => { - linkArgs = [$rootScope.$new(), angular.element('
'), {}]; - parentFn = sinon.spy(); - childFn = sinon.spy(); - })); + linkArgs = [$rootScope.$new(), angular.element('
'), {}] + parentFn = sinon.spy() + childFn = sinon.spy() + })) it(`should call the parent link function when there is no child function`, () => { setterFn([ {name, template, link: parentFn}, - {name: 'type2', extends: name} - ]); - getterFn('type2').link(...linkArgs); - expect(parentFn).to.have.been.calledWith(...linkArgs); - }); + {name: 'type2', extends: name}, + ]) + getterFn('type2').link(...linkArgs) + expect(parentFn).to.have.been.calledWith(...linkArgs) + }) it(`should call the child link function when there is no parent function`, () => { setterFn([ {name, template}, - {name: 'type2', extends: name, link: childFn} - ]); - getterFn('type2').link(...linkArgs); - expect(childFn).to.have.been.calledWith(...linkArgs); - }); + {name: 'type2', extends: name, link: childFn}, + ]) + getterFn('type2').link(...linkArgs) + expect(childFn).to.have.been.calledWith(...linkArgs) + }) it(`should call the child link function and the parent link function when they are both present`, () => { setterFn([ {name, template, link: parentFn}, - {name: 'type2', extends: name, link: childFn} - ]); - getterFn('type2').link(...linkArgs); - expect(parentFn).to.have.been.calledWith(...linkArgs); - expect(childFn).to.have.been.calledWith(...linkArgs); - }); + {name: 'type2', extends: name, link: childFn}, + ]) + getterFn('type2').link(...linkArgs) + expect(parentFn).to.have.been.calledWith(...linkArgs) + expect(childFn).to.have.been.calledWith(...linkArgs) + }) - }); + }) describe(`controller functions`, () => { - let parentFn, childFn, $controller, $scope; + let parentFn, childFn, $controller, $scope beforeEach(inject(($rootScope, _$controller_) => { - $scope = $rootScope.$new(); - $controller = _$controller_; - parentFn = sinon.spy(); - parentFn.$inject = ['$log']; - childFn = sinon.spy(); - childFn.$inject = ['$http']; - })); + $scope = $rootScope.$new() + $controller = _$controller_ + parentFn = sinon.spy() + parentFn.$inject = ['$log'] + childFn = sinon.spy() + childFn.$inject = ['$http'] + })) it(`should call the parent controller function when there is no child controller function`, inject(($log) => { setterFn([ {name, template, controller: parentFn}, - {name: 'type2', extends: name} - ]); - $controller(getterFn('type2').controller, {$scope}); - expect(parentFn).to.have.been.calledWith($log); - })); + {name: 'type2', extends: name}, + ]) + $controller(getterFn('type2').controller, {$scope}) + expect(parentFn).to.have.been.calledWith($log) + })) it(`should call the parent controller function and the child's when there is a child controller function`, inject(($log, $http) => { setterFn([ {name, template, controller: parentFn}, - {name: 'type2', extends: name, controller: childFn} - ]); - $controller(getterFn('type2').controller, {$scope}); - expect(parentFn).to.have.been.calledWith($log); - expect(childFn).to.have.been.calledWith($http); - })); + {name: 'type2', extends: name, controller: childFn}, + ]) + $controller(getterFn('type2').controller, {$scope}) + expect(parentFn).to.have.been.calledWith($log) + expect(childFn).to.have.been.calledWith($http) + })) it(`should call the child controller function when there's no parent controller`, inject(($http) => { setterFn([ {name, template}, - {name: 'type2', extends: name, controller: childFn} - ]); - $controller(getterFn('type2').controller, {$scope}); - expect(childFn).to.have.been.calledWith($http); - })); + {name: 'type2', extends: name, controller: childFn}, + ]) + $controller(getterFn('type2').controller, {$scope}) + expect(childFn).to.have.been.calledWith($http) + })) - }); - }); - }); + }) + }) + }) describe('(◞‸◟;) path', () => { it('should throw an error when the first argument is not an object or an array', () => { - expect(() => setterFn('string')).to.throw(/must.*provide.*object.*array/); - expect(() => setterFn(324)).to.throw(/must.*provide.*object.*array/); - expect(() => setterFn(false)).to.throw(/must.*provide.*object.*array/); - }); + expect(() => setterFn('string')).to.throw(/must.*provide.*object.*array/) + expect(() => setterFn(324)).to.throw(/must.*provide.*object.*array/) + expect(() => setterFn(false)).to.throw(/must.*provide.*object.*array/) + }) it('should throw an error when a name is not provided', () => { - expect(() => setterFn({templateUrl})).to.throw(/formlyConfig\.setType/); - }); + expect(() => setterFn({templateUrl})).to.throw(/formlyConfig\.setType/) + }) it(`should throw an error when specifying both a template and a templateUrl`, () => { - expect(() => setterFn({name, template, templateUrl})).to.throw(/formlyConfig\.setType/); - }); + expect(() => setterFn({name, template, templateUrl})).to.throw(/formlyConfig\.setType/) + }) it(`should throw an error when an extra property is provided`, () => { - expect(() => setterFn({name, templateUrl, extra: true})).to.throw(/formlyConfig\.setType/); - }); + expect(() => setterFn({name, templateUrl, extra: true})).to.throw(/formlyConfig\.setType/) + }) it('should warn when attempting to override a type', () => { shouldWarn(/overwrite/, function() { - setterFn({name, template}); - setterFn({name, template}); - }); - }); - }); + setterFn({name, template}) + setterFn({name, template}) + }) + }) + }) describe(`apiCheck`, () => { - testApiCheck('setType', 'getType'); - }); - }); + testApiCheck('setType', 'getType') + }) + }) function testApiCheck(setterName, getterName) { - const template = 'something with '; - const name = 'input'; - let setterFn, getterFn, formlyApiCheck; + const template = 'something with ' + const name = 'input' + let setterFn, getterFn, formlyApiCheck beforeEach(inject((_formlyApiCheck_, formlyConfig) => { - formlyApiCheck = _formlyApiCheck_; - setterFn = formlyConfig[setterName]; - getterFn = formlyConfig[getterName]; - })); + formlyApiCheck = _formlyApiCheck_ + setterFn = formlyConfig[setterName] + getterFn = formlyConfig[getterName] + })) it(`should allow you to specify an apiCheck function that will be used to validate your options`, () => { expect(() => { setterFn({ name, apiCheck, - template - }); - }).to.not.throw(); + template, + }) + }).to.not.throw() - expect(getterFn(name).apiCheck).to.equal(apiCheck); + expect(getterFn(name).apiCheck).to.equal(apiCheck) function apiCheck() { return { templateOptions: {}, - data: {} - }; + data: {}, + } } - }); + }) describe(`apiCheckInstance`, () => { - let apiCheckInstance; + let apiCheckInstance beforeEach(() => { - apiCheckInstance = require('api-check')(); - }); + apiCheckInstance = require('api-check')() + }) it(`should allow you to specify an instance of your own apiCheck so messaging will be custom`, () => { expect(() => { - setterFn({name, apiCheck, apiCheckInstance, template}); - }).to.not.throw(); - expect(getterFn(name).apiCheckInstance).to.equal(apiCheckInstance); - }); + setterFn({name, apiCheck, apiCheckInstance, template}) + }).to.not.throw() + expect(getterFn(name).apiCheckInstance).to.equal(apiCheckInstance) + }) it(`should throw an error if you specify an instance without specifying an apiCheck`, () => { expect(() => { - setterFn({name, apiCheckInstance, template}); - }).to.throw(); - }); + setterFn({name, apiCheckInstance, template}) + }).to.throw() + }) function apiCheck() { return { templateOptions: {}, - data: {} - }; + data: {}, + } } - }); + }) describe(`apiCheckFunction`, () => { it(`should allow you to specify warn or throw as the `, () => { expect(() => { - setterFn({name, apiCheck, apiCheckFunction: 'warn', template}); - }).to.not.throw(); - expect(getterFn(name).apiCheckFunction).to.equal('warn'); + setterFn({name, apiCheck, apiCheckFunction: 'warn', template}) + }).to.not.throw() + expect(getterFn(name).apiCheckFunction).to.equal('warn') expect(() => { - setterFn({name: 'name2', apiCheck, apiCheckFunction: 'throw', template}); - }).to.not.throw(); - expect(getterFn('name2').apiCheckFunction).to.equal('throw'); - }); + setterFn({name: 'name2', apiCheck, apiCheckFunction: 'throw', template}) + }).to.not.throw() + expect(getterFn('name2').apiCheckFunction).to.equal('throw') + }) it(`should throw an error if you specify anything other than warn or throw`, () => { expect(() => { - setterFn({name, apiCheckFunction: 'other', template}); - }).to.throw(); - }); + setterFn({name, apiCheckFunction: 'other', template}) + }).to.throw() + }) function apiCheck() { return { templateOptions: {}, - data: {} - }; + data: {}, + } } - }); + }) } @@ -585,89 +656,89 @@ describe('formlyConfig', () => { describe(`that impact field rendering`, () => { - let scope, $compile, el, field; + let scope, $compile, el, field beforeEach(inject(($rootScope, _$compile_) => { - scope = $rootScope.$new(); - $compile = _$compile_; - scope.fields = [{template: ''}]; - })); + scope = $rootScope.$new() + $compile = _$compile_ + scope.fields = [{template: ''}] + })) describe(`defaultHideDirective`, () => { it(`should default formly-form to use ng-if when not specified`, () => { compileAndDigest(` - `); - const fieldNode = getFieldNode(); - expect(fieldNode.getAttribute('ng-if')).to.exist; - }); + `) + const fieldNode = getFieldNode() + expect(fieldNode.getAttribute('ng-if')).to.exist + }) it(`should default formly-form to use the specified directive for hiding and showing`, () => { - formlyConfig.extras.defaultHideDirective = 'ng-show'; + formlyConfig.extras.defaultHideDirective = 'ng-show' compileAndDigest(` - `); - const fieldNode = getFieldNode(); - expect(fieldNode.getAttribute('ng-show')).to.exist; - }); + `) + const fieldNode = getFieldNode() + expect(fieldNode.getAttribute('ng-show')).to.exist + }) it(`should be overrideable on a per-form basis`, () => { - formlyConfig.extras.defaultHideDirective = '(╯°□°)╯︵ ┻━┻'; + formlyConfig.extras.defaultHideDirective = '(╯°□°)╯︵ ┻━┻' compileAndDigest(` - `); - const fieldNode = getFieldNode(); - expect(fieldNode.getAttribute('ng-show')).to.exist; - expect(fieldNode.getAttribute('(╯°□°)╯︵ ┻━┻')).to.not.exist; - }); + `) + const fieldNode = getFieldNode() + expect(fieldNode.getAttribute('ng-show')).to.exist + expect(fieldNode.getAttribute('(╯°□°)╯︵ ┻━┻')).to.not.exist + }) - }); + }) describe(`getFieldId`, () => { it(`should allow you to specify your own function for generating the IDs for a field`, () => { scope.fields = [ getNewField({id: 'custom'}), getNewField({model: {foo: 'bar', id: '1234'}, key: 'foo'}), - getNewField({key: 'bar'}) - ]; + getNewField({key: 'bar'}), + ] formlyConfig.extras.getFieldId = function(options, model, scope) { if (options.id) { - return options.id; + return options.id } - return [scope.index, (model && model.id) || 'new-model', options.key].join('_'); - }; - compileAndDigest(); + return [scope.index, (model && model.id) || 'new-model', options.key].join('_') + } + compileAndDigest() - const field0 = getFieldNgModelNode(0); - const field1 = getFieldNgModelNode(1); - const field2 = getFieldNgModelNode(2); + const field0 = getFieldNgModelNode(0) + const field1 = getFieldNgModelNode(1) + const field2 = getFieldNgModelNode(2) - expect(field0.id).to.eq('custom'); - expect(field1.id).to.eq('1_1234_foo'); - expect(field2.id).to.eq('2_new-model_bar'); - }); - }); + expect(field0.id).to.eq('custom') + expect(field1.id).to.eq('1_1234_foo') + expect(field2.id).to.eq('2_new-model_bar') + }) + }) function compileAndDigest(template) { - el = $compile(template || basicForm)(scope); - scope.$digest(); - field = scope.fields[0]; - return el; + el = $compile(template || basicForm)(scope) + scope.$digest() + field = scope.fields[0] + return el } function getFieldNode(index = 0) { - return el[0].querySelectorAll('.formly-field')[index]; + return el[0].querySelectorAll('.formly-field')[index] } function getFieldNgModelNode(index = 0) { - return getFieldNode(index).querySelector('[ng-model]'); + return getFieldNode(index).querySelector('[ng-model]') } - }); + }) - }); + }) describe(`getTypeHeritage`, () => { it(`should get the heritage of all type extensions`, () => { @@ -676,11 +747,11 @@ describe('formlyConfig', () => { {name: 'parent', extends: 'grandparent'}, {name: 'child', extends: 'parent'}, {name: 'extra', extends: 'grandparent'}, - {name: 'extra2'} - ]); + {name: 'extra2'}, + ]) expect(formlyConfig.getTypeHeritage('child')).to.eql([ - formlyConfig.getType('parent'), formlyConfig.getType('grandparent') - ]); - }); - }); -}); + formlyConfig.getType('parent'), formlyConfig.getType('grandparent'), + ]) + }) + }) +}) diff --git a/src/providers/formlyUsability.js b/src/providers/formlyUsability.js index f9ac7363..87e9af4f 100644 --- a/src/providers/formlyUsability.js +++ b/src/providers/formlyUsability.js @@ -1,6 +1,6 @@ -import angular from 'angular-fix'; +import angular from 'angular-fix' -export default formlyUsability; +export default formlyUsability // @ngInject function formlyUsability(formlyApiCheck, formlyErrorAndWarningsUrlPrefix) { @@ -10,49 +10,49 @@ function formlyUsability(formlyApiCheck, formlyErrorAndWarningsUrlPrefix) { checkWrapper, checkWrapperTemplate, getErrorMessage, - $get: () => this - }); + $get: () => this, + }) function getFieldError(errorInfoSlug, message, field) { if (arguments.length < 3) { - field = message; - message = errorInfoSlug; - errorInfoSlug = null; + field = message + message = errorInfoSlug + errorInfoSlug = null } - return new Error(getErrorMessage(errorInfoSlug, message) + ` Field definition: ${angular.toJson(field)}`); + return new Error(getErrorMessage(errorInfoSlug, message) + ` Field definition: ${angular.toJson(field)}`) } function getFormlyError(errorInfoSlug, message) { if (!message) { - message = errorInfoSlug; - errorInfoSlug = null; + message = errorInfoSlug + errorInfoSlug = null } - return new Error(getErrorMessage(errorInfoSlug, message)); + return new Error(getErrorMessage(errorInfoSlug, message)) } function getErrorMessage(errorInfoSlug, message) { - let url = ''; + let url = '' if (errorInfoSlug !== null) { - url = `${formlyErrorAndWarningsUrlPrefix}${errorInfoSlug}`; + url = `${formlyErrorAndWarningsUrlPrefix}${errorInfoSlug}` } - return `Formly Error: ${message}. ${url}`; + return `Formly Error: ${message}. ${url}` } function checkWrapper(wrapper) { formlyApiCheck.throw(formlyApiCheck.formlyWrapperType, wrapper, { prefix: 'formlyConfig.setWrapper', - urlSuffix: 'setwrapper-validation-failed' - }); + urlSuffix: 'setwrapper-validation-failed', + }) } function checkWrapperTemplate(template, additionalInfo) { - const formlyTransclude = ''; + const formlyTransclude = '' if (template.indexOf(formlyTransclude) === -1) { throw getFormlyError( `Template wrapper templates must use "${formlyTransclude}" somewhere in them. ` + `This one does not have "" in it: ${template}` + '\n' + `Additional information: ${JSON.stringify(additionalInfo)}` - ); + ) } } } diff --git a/src/providers/formlyValidationMessages.js b/src/providers/formlyValidationMessages.js index ef3067af..4b5edc01 100644 --- a/src/providers/formlyValidationMessages.js +++ b/src/providers/formlyValidationMessages.js @@ -1,4 +1,4 @@ -export default formlyValidationMessages; +export default formlyValidationMessages // @ngInject @@ -7,27 +7,27 @@ function formlyValidationMessages() { const validationMessages = { addTemplateOptionValueMessage, addStringMessage, - messages: {} - }; + messages: {}, + } - return validationMessages; + return validationMessages function addTemplateOptionValueMessage(name, prop, prefix, suffix, alternate) { - validationMessages.messages[name] = templateOptionValue(prop, prefix, suffix, alternate); + validationMessages.messages[name] = templateOptionValue(prop, prefix, suffix, alternate) } function addStringMessage(name, string) { - validationMessages.messages[name] = () => string; + validationMessages.messages[name] = () => string } function templateOptionValue(prop, prefix, suffix, alternate) { return function getValidationMessage(viewValue, modelValue, scope) { - if (scope.options.templateOptions[prop]) { - return `${prefix} ${scope.options.templateOptions[prop]} ${suffix}`; + if (typeof scope.options.templateOptions[prop] !== 'undefined') { + return `${prefix} ${scope.options.templateOptions[prop]} ${suffix}` } else { - return alternate; + return alternate } - }; + } } } diff --git a/src/run/formlyCustomTags.js b/src/run/formlyCustomTags.js index ff2a4988..e0714a8f 100644 --- a/src/run/formlyCustomTags.js +++ b/src/run/formlyCustomTags.js @@ -1,24 +1,18 @@ -import angular from 'angular-fix'; -export default addCustomTags; +import angular from 'angular-fix' +export default addCustomTags // @ngInject function addCustomTags($document) { - if ($document && $document.get) { - // IE8 check -> - // http://stackoverflow.com/questions/10964966/detect-ie-version-prior-to-v9-in-javascript/10965203#10965203 - const document = $document.get(0); - const div = document.createElement('div'); - div.innerHTML = ''; - const isIeLessThan9 = (div.getElementsByTagName('i').length === 1); - - if (isIeLessThan9) { - // add the custom elements that we need for formly - const customElements = [ - 'formly-field', 'formly-form', 'formly-custom-validation', 'formly-focus', 'formly-transpose' - ]; - angular.forEach(customElements, el => { - document.createElement(el); - }); - } + // IE8 check -> + // https://msdn.microsoft.com/en-us/library/cc196988(v=vs.85).aspx + if ($document && $document.documentMode < 9) { + const document = $document.get(0) + // add the custom elements that we need for formly + const customElements = [ + 'formly-field', 'formly-form', + ] + angular.forEach(customElements, el => { + document.createElement(el) + }) } } diff --git a/src/run/formlyCustomTags.test.js b/src/run/formlyCustomTags.test.js index e9b38fcc..c176e7ff 100644 --- a/src/run/formlyCustomTags.test.js +++ b/src/run/formlyCustomTags.test.js @@ -1,35 +1,32 @@ -import angular from 'angular'; +import angular from 'angular' describe(`formlyCustomTags`, () => { beforeEach(window.module(`formly`, $provide => { - const docStub = { + $provide.value(`$document`, { + documentMode: 8, get: sinon.stub().withArgs(0).returnsThis(), - createElement: sinon.stub().withArgs(`div`).returns({ - getElementsByTagName: sinon.stub().withArgs(`i`).returns([1]) - }) - }; - - $provide.value(`$document`, docStub); - })); + createElement: sinon.spy(), + }) + })) - let $document; + let $document beforeEach(inject((_$document_) => { - $document = _$document_; - })); + $document = _$document_ + })) describe(`addCustomTags`, () => { it(`should create custom formly tags`, () => { const customElements = [ - `div`, `formly-field`, `formly-form`, `formly-custom-validation`, `formly-focus`, `formly-transpose` - ]; + `formly-field`, `formly-form`, + ] - expect($document.get).to.have.been.calledOnce; + expect($document.get).to.have.been.calledOnce angular.forEach(customElements, el => { - expect($document.createElement).to.have.been.calledWith(el); - }); - }); - }); -}); + expect($document.createElement).to.have.been.calledWith(el) + }) + }) + }) +}) diff --git a/src/run/formlyNgModelAttrsManipulator.js b/src/run/formlyNgModelAttrsManipulator.js index 1db56893..e780b9ee 100644 --- a/src/run/formlyNgModelAttrsManipulator.js +++ b/src/run/formlyNgModelAttrsManipulator.js @@ -1,59 +1,59 @@ -import angular from 'angular-fix'; -import {contains} from '../other/utils'; +import angular from 'angular-fix' +import {contains} from '../other/utils' -export default addFormlyNgModelAttrsManipulator; +export default addFormlyNgModelAttrsManipulator // @ngInject function addFormlyNgModelAttrsManipulator(formlyConfig, $interpolate) { if (formlyConfig.extras.disableNgModelAttrsManipulator) { - return; + return } - formlyConfig.templateManipulators.preWrapper.push(ngModelAttrsManipulator); + formlyConfig.templateManipulators.preWrapper.push(ngModelAttrsManipulator) function ngModelAttrsManipulator(template, options, scope) { - const node = document.createElement('div'); - const skip = options.extras && options.extras.skipNgModelAttrsManipulator; + const node = document.createElement('div') + const skip = options.extras && options.extras.skipNgModelAttrsManipulator if (skip === true) { - return template; + return template } - node.innerHTML = template; + node.innerHTML = template - const modelNodes = getNgModelNodes(node, skip); + const modelNodes = getNgModelNodes(node, skip) if (!modelNodes || !modelNodes.length) { - return template; + return template } - addIfNotPresent(modelNodes, 'id', scope.id); - addIfNotPresent(modelNodes, 'name', scope.name || scope.id); + addIfNotPresent(modelNodes, 'id', scope.id) + addIfNotPresent(modelNodes, 'name', scope.name || scope.id) - addValidation(); - alterNgModelAttr(); - addModelOptions(); - addTemplateOptionsAttrs(); - addNgModelElAttrs(); + addValidation() + alterNgModelAttr() + addModelOptions() + addTemplateOptionsAttrs() + addNgModelElAttrs() - return node.innerHTML; + return node.innerHTML function addValidation() { if (angular.isDefined(options.validators) || angular.isDefined(options.validation.messages)) { - addIfNotPresent(modelNodes, 'formly-custom-validation', ''); + addIfNotPresent(modelNodes, 'formly-custom-validation', '') } } function alterNgModelAttr() { if (isPropertyAccessor(options.key)) { - addRegardlessOfPresence(modelNodes, 'ng-model', 'model.' + options.key); + addRegardlessOfPresence(modelNodes, 'ng-model', 'model.' + options.key) } } function addModelOptions() { if (angular.isDefined(options.modelOptions)) { - addIfNotPresent(modelNodes, 'ng-model-options', 'options.modelOptions'); + addIfNotPresent(modelNodes, 'ng-model-options', 'options.modelOptions') if (options.modelOptions.getterSetter) { - addRegardlessOfPresence(modelNodes, 'ng-model', 'options.value'); + addRegardlessOfPresence(modelNodes, 'ng-model', 'options.value') } } } @@ -61,178 +61,178 @@ function addFormlyNgModelAttrsManipulator(formlyConfig, $interpolate) { function addTemplateOptionsAttrs() { if (!options.templateOptions && !options.expressionProperties) { // no need to run these if there are no templateOptions or expressionProperties - return; + return } - const to = options.templateOptions || {}; - const ep = options.expressionProperties || {}; + const to = options.templateOptions || {} + const ep = options.expressionProperties || {} - const ngModelAttributes = getBuiltInAttributes(); + const ngModelAttributes = getBuiltInAttributes() // extend with the user's specifications winning - angular.extend(ngModelAttributes, options.ngModelAttrs); + angular.extend(ngModelAttributes, options.ngModelAttrs) // Feel free to make this more simple :-) angular.forEach(ngModelAttributes, (val, name) => { /* eslint complexity:[2, 14] */ - let attrVal, attrName; - const ref = `options.templateOptions['${name}']`; - const toVal = to[name]; - const epVal = getEpValue(ep, name); + let attrVal, attrName + const ref = `options.templateOptions['${name}']` + const toVal = to[name] + const epVal = getEpValue(ep, name) - const inTo = angular.isDefined(toVal); - const inEp = angular.isDefined(epVal); + const inTo = angular.isDefined(toVal) + const inEp = angular.isDefined(epVal) if (val.value) { // I realize this looks backwards, but it's right, trust me... - attrName = val.value; - attrVal = name; + attrName = val.value + attrVal = name } else if (val.statement && inTo) { - attrName = val.statement; + attrName = val.statement if (angular.isString(to[name])) { - attrVal = `$eval(${ref})`; + attrVal = `$eval(${ref})` } else if (angular.isFunction(to[name])) { - attrVal = `${ref}(model[options.key], options, this, $event)`; + attrVal = `${ref}(model[options.key], options, this, $event)` } else { throw new Error( `options.templateOptions.${name} must be a string or function: ${JSON.stringify(options)}` - ); + ) } } else if (val.bound && inEp) { - attrName = val.bound; - attrVal = ref; + attrName = val.bound + attrVal = ref } else if ((val.attribute || val.boolean) && inEp) { - attrName = val.attribute || val.boolean; - attrVal = `${$interpolate.startSymbol()}${ref}${$interpolate.endSymbol()}`; + attrName = val.attribute || val.boolean + attrVal = `${$interpolate.startSymbol()}${ref}${$interpolate.endSymbol()}` } else if (val.attribute && inTo) { - attrName = val.attribute; - attrVal = toVal; + attrName = val.attribute + attrVal = toVal } else if (val.boolean) { if (inTo && !inEp && toVal) { - attrName = val.boolean; - attrVal = true; + attrName = val.boolean + attrVal = true } else { /* eslint no-empty:0 */ // empty to illustrate that a boolean will not be added via val.bound // if you want it added via val.bound, then put it in expressionProperties } } else if (val.bound && inTo) { - attrName = val.bound; - attrVal = ref; + attrName = val.bound + attrVal = ref } if (angular.isDefined(attrName) && angular.isDefined(attrVal)) { - addIfNotPresent(modelNodes, attrName, attrVal); + addIfNotPresent(modelNodes, attrName, attrVal) } - }); + }) } function addNgModelElAttrs() { angular.forEach(options.ngModelElAttrs, (val, name) => { - addRegardlessOfPresence(modelNodes, name, val); - }); + addRegardlessOfPresence(modelNodes, name, val) + }) } } // Utility functions function getNgModelNodes(node, skip) { - const selectorNot = angular.isString(skip) ? `:not(${skip})` : ''; - const skipNot = ':not([formly-skip-ng-model-attrs-manipulator])'; - const query = `[ng-model]${selectorNot}${skipNot}, [data-ng-model]${selectorNot}${skipNot}`; + const selectorNot = angular.isString(skip) ? `:not(${skip})` : '' + const skipNot = ':not([formly-skip-ng-model-attrs-manipulator])' + const query = `[ng-model]${selectorNot}${skipNot}, [data-ng-model]${selectorNot}${skipNot}` try { - return node.querySelectorAll(query); + return node.querySelectorAll(query) } catch (e) { //this code is needed for IE8, as it does not support the CSS3 ':not' selector //it should be removed when IE8 support is dropped - return getNgModelNodesFallback(node, skip); + return getNgModelNodesFallback(node, skip) } } function getNgModelNodesFallback(node, skip) { - const allNgModelNodes = node.querySelectorAll('[ng-model], [data-ng-model]'); - const matchingNgModelNodes = []; + const allNgModelNodes = node.querySelectorAll('[ng-model], [data-ng-model]') + const matchingNgModelNodes = [] //make sure this array is compatible with NodeList type by adding an 'item' function matchingNgModelNodes.item = function(i) { - return this[i]; - }; + return this[i] + } for (let i = 0; i < allNgModelNodes.length; i++) { - const ngModelNode = allNgModelNodes[i]; + const ngModelNode = allNgModelNodes[i] if (!ngModelNode.hasAttribute('formly-skip-ng-model-attrs-manipulator') && !(angular.isString(skip) && nodeMatches(ngModelNode, skip))) { - matchingNgModelNodes.push(ngModelNode); + matchingNgModelNodes.push(ngModelNode) } } - return matchingNgModelNodes; + return matchingNgModelNodes } function nodeMatches(node, selector) { - const div = document.createElement('div'); - div.innerHTML = node.outerHTML; - return div.querySelector(selector); + const div = document.createElement('div') + div.innerHTML = node.outerHTML + return div.querySelector(selector) } function getBuiltInAttributes() { const ngModelAttributes = { focus: { - attribute: 'formly-focus' - } - }; - const boundOnly = []; - const bothBooleanAndBound = ['required', 'disabled']; - const bothAttributeAndBound = ['pattern', 'minlength']; - const statementOnly = ['change', 'keydown', 'keyup', 'keypress', 'click', 'focus', 'blur']; - const attributeOnly = ['placeholder', 'min', 'max', 'tabindex', 'type']; + attribute: 'formly-focus', + }, + } + const boundOnly = [] + const bothBooleanAndBound = ['required', 'disabled'] + const bothAttributeAndBound = ['pattern', 'minlength'] + const statementOnly = ['change', 'keydown', 'keyup', 'keypress', 'click', 'focus', 'blur'] + const attributeOnly = ['placeholder', 'min', 'max', 'step', 'tabindex', 'type'] if (formlyConfig.extras.ngModelAttrsManipulatorPreferUnbound) { - bothAttributeAndBound.push('maxlength'); + bothAttributeAndBound.push('maxlength') } else { - boundOnly.push('maxlength'); + boundOnly.push('maxlength') } angular.forEach(boundOnly, item => { - ngModelAttributes[item] = {bound: 'ng-' + item}; - }); + ngModelAttributes[item] = {bound: 'ng-' + item} + }) angular.forEach(bothBooleanAndBound, item => { - ngModelAttributes[item] = {boolean: item, bound: 'ng-' + item}; - }); + ngModelAttributes[item] = {boolean: item, bound: 'ng-' + item} + }) angular.forEach(bothAttributeAndBound, item => { - ngModelAttributes[item] = {attribute: item, bound: 'ng-' + item}; - }); + ngModelAttributes[item] = {attribute: item, bound: 'ng-' + item} + }) angular.forEach(statementOnly, item => { - const propName = 'on' + item.substr(0, 1).toUpperCase() + item.substr(1); - ngModelAttributes[propName] = {statement: 'ng-' + item}; - }); + const propName = 'on' + item.substr(0, 1).toUpperCase() + item.substr(1) + ngModelAttributes[propName] = {statement: 'ng-' + item} + }) angular.forEach(attributeOnly, item => { - ngModelAttributes[item] = {attribute: item}; - }); - return ngModelAttributes; + ngModelAttributes[item] = {attribute: item} + }) + return ngModelAttributes } function getEpValue(ep, name) { return ep['templateOptions.' + name] || ep[`templateOptions['${name}']`] || - ep[`templateOptions["${name}"]`]; + ep[`templateOptions["${name}"]`] } function addIfNotPresent(nodes, attr, val) { angular.forEach(nodes, node => { if (!node.getAttribute(attr)) { - node.setAttribute(attr, val); + node.setAttribute(attr, val) } - }); + }) } function addRegardlessOfPresence(nodes, attr, val) { angular.forEach(nodes, node => { - node.setAttribute(attr, val); - }); + node.setAttribute(attr, val) + }) } function isPropertyAccessor(key) { - return contains(key, '.') || (contains(key, '[') && contains(key, ']')); + return contains(key, '.') || (contains(key, '[') && contains(key, ']')) } } diff --git a/src/run/formlyNgModelAttrsManipulator.test.js b/src/run/formlyNgModelAttrsManipulator.test.js index 008a44b0..b4f0fc3b 100644 --- a/src/run/formlyNgModelAttrsManipulator.test.js +++ b/src/run/formlyNgModelAttrsManipulator.test.js @@ -1,375 +1,375 @@ /* eslint max-len:0 */ -import angular from 'angular'; -import _ from 'lodash'; +import angular from 'angular' +import _ from 'lodash' describe('formlyNgModelAttrsManipulator', () => { - beforeEach(window.module('formly')); + beforeEach(window.module('formly')) - let formlyConfig, manipulator, scope, field, result, resultEl, resultNode; - const template = ''; + let formlyConfig, manipulator, scope, field, result, resultEl, resultNode + const template = '' beforeEach(inject((_formlyConfig_, $rootScope) => { - formlyConfig = _formlyConfig_; - manipulator = formlyConfig.templateManipulators.preWrapper[0]; - scope = $rootScope.$new(); - scope.id = 'id'; + formlyConfig = _formlyConfig_ + manipulator = formlyConfig.templateManipulators.preWrapper[0] + scope = $rootScope.$new() + scope.id = 'id' field = { extras: {}, data: {}, validation: {}, - templateOptions: {} - }; - })); + templateOptions: {}, + } + })) describe(`skipping`, () => { it(`should allow you to skip the manipulator wholesale for the field`, () => { - field.extras.skipNgModelAttrsManipulator = true; - manipulate(); - expect(result).to.equal(template); - }); + field.extras.skipNgModelAttrsManipulator = true + manipulate() + expect(result).to.equal(template) + }) - const skipWithSelectorTitle = `should allow you to specify a selector for specific elements to skip`; + const skipWithSelectorTitle = `should allow you to specify a selector for specific elements to skip` function skipWithSelector() { - const className = 'ignored-thing' + _.random(0, 10); - field.templateOptions.required = true; - field.extras.skipNgModelAttrsManipulator = `.${className}`; + const className = 'ignored-thing' + _.random(0, 10) + field.templateOptions.required = true + field.extras.skipNgModelAttrsManipulator = `.${className}` manipulate(`
- `); - const firstInput = angular.element(resultNode.querySelector('.first-thing')); - const secondInput = angular.element(resultNode.querySelector(`.${className}`)); - expect(firstInput.attr('required')).to.exist; - expect(secondInput.attr('required')).to.not.exist; + `) + const firstInput = angular.element(resultNode.querySelector('.first-thing')) + const secondInput = angular.element(resultNode.querySelector(`.${className}`)) + expect(firstInput.attr('required')).to.exist + expect(secondInput.attr('required')).to.not.exist } - it(skipWithSelectorTitle, skipWithSelector); + it(skipWithSelectorTitle, skipWithSelector) - const skipWithAttributeTitle = `should allow you to place the attribute formly-skip-ng-model-attrs-manipulator on an ng-model to have it skip`; + const skipWithAttributeTitle = `should allow you to place the attribute formly-skip-ng-model-attrs-manipulator on an ng-model to have it skip` function skipWithAttribute() { - field.templateOptions.required = true; + field.templateOptions.required = true manipulate(`
- `); - const firstInput = angular.element(resultNode.querySelector('.first-thing')); - const secondInput = angular.element(resultNode.querySelector('[formly-skip-ng-model-attrs-manipulator]')); - expect(firstInput.attr('required')).to.exist; - expect(secondInput.attr('required')).to.not.exist; + `) + const firstInput = angular.element(resultNode.querySelector('.first-thing')) + const secondInput = angular.element(resultNode.querySelector('[formly-skip-ng-model-attrs-manipulator]')) + expect(firstInput.attr('required')).to.exist + expect(secondInput.attr('required')).to.not.exist } - it(skipWithAttributeTitle, skipWithAttribute); + it(skipWithAttributeTitle, skipWithAttribute) - const dontSkipWithBooleanTitle = `should not skip by selector if skipNgModelAttrsManipulator is a boolean value`; + const dontSkipWithBooleanTitle = `should not skip by selector if skipNgModelAttrsManipulator is a boolean value` function dontSkipWithBoolean() { - field.templateOptions.required = true; - field.extras.skipNgModelAttrsManipulator = false; + field.templateOptions.required = true + field.extras.skipNgModelAttrsManipulator = false manipulate(`
- `); - const firstInput = angular.element(resultNode.querySelector('.first-thing')); - const secondInput = angular.element(resultNode.querySelector('.second-thing')); - expect(firstInput.attr('required')).to.exist; - expect(secondInput.attr('required')).to.exist; + `) + const firstInput = angular.element(resultNode.querySelector('.first-thing')) + const secondInput = angular.element(resultNode.querySelector('.second-thing')) + expect(firstInput.attr('required')).to.exist + expect(secondInput.attr('required')).to.exist } - it(dontSkipWithBooleanTitle, dontSkipWithBoolean); + it(dontSkipWithBooleanTitle, dontSkipWithBoolean) - const skipWithAttributeAndSelectorTitle = `should allow you to skip using both the special attribute and the custom selector`; + const skipWithAttributeAndSelectorTitle = `should allow you to skip using both the special attribute and the custom selector` function skipWithAttributeAndSelector() { - const className = 'ignored-thing' + _.random(0, 10); - field.templateOptions.required = true; - field.extras.skipNgModelAttrsManipulator = `.${className}`; + const className = 'ignored-thing' + _.random(0, 10) + field.templateOptions.required = true + field.extras.skipNgModelAttrsManipulator = `.${className}` manipulate(`
- `); - const firstInput = angular.element(resultNode.querySelector('.first-thing')); - const secondInput = angular.element(resultNode.querySelector(`.${className}`)); - const thirdInput = angular.element(resultNode.querySelector('[formly-skip-ng-model-attrs-manipulator]')); - expect(firstInput.attr('required')).to.exist; - expect(secondInput.attr('required')).to.not.exist; - expect(thirdInput.attr('required')).to.not.exist; + `) + const firstInput = angular.element(resultNode.querySelector('.first-thing')) + const secondInput = angular.element(resultNode.querySelector(`.${className}`)) + const thirdInput = angular.element(resultNode.querySelector('[formly-skip-ng-model-attrs-manipulator]')) + expect(firstInput.attr('required')).to.exist + expect(secondInput.attr('required')).to.not.exist + expect(thirdInput.attr('required')).to.not.exist } - it(skipWithAttributeAndSelectorTitle, skipWithAttributeAndSelector); + it(skipWithAttributeAndSelectorTitle, skipWithAttributeAndSelector) //repeat a few skipping tests with a broken Element.querySelectorAll function describe('node search fallback', () => { - let origQuerySelectorAll; + let origQuerySelectorAll //deliberately break querySelectorAll to mimic IE8's behaviour beforeEach(() => { - origQuerySelectorAll = Element.prototype.querySelectorAll; + origQuerySelectorAll = Element.prototype.querySelectorAll Element.prototype.querySelectorAll = function brokenQuerySelectorAll(selector) { if (selector && selector.indexOf(':not') >= 0) { - throw new Error(':not selector not supported'); + throw new Error(':not selector not supported') } - return origQuerySelectorAll.apply(this, arguments); - }; - }); + return origQuerySelectorAll.apply(this, arguments) + } + }) afterEach(() => { - Element.prototype.querySelectorAll = origQuerySelectorAll; - }); + Element.prototype.querySelectorAll = origQuerySelectorAll + }) - it(skipWithSelectorTitle, skipWithSelector); - it(skipWithAttributeTitle, skipWithAttribute); - it(dontSkipWithBooleanTitle, dontSkipWithBoolean); - it(skipWithAttributeAndSelectorTitle, skipWithAttributeAndSelector); - }); + it(skipWithSelectorTitle, skipWithSelector) + it(skipWithAttributeTitle, skipWithAttribute) + it(dontSkipWithBooleanTitle, dontSkipWithBoolean) + it(skipWithAttributeAndSelectorTitle, skipWithAttributeAndSelector) + }) - }); + }) it(`should have a limited number of automatically added attributes without any specific options`, () => { - manipulate(); + manipulate() // because different browsers place attributes in different places... - const spaces = ''.split(' ').length; - expect(result.split(' ').length).to.equal(spaces); - attrExists('ng-model'); - attrExists('id'); - attrExists('name'); - }); + const spaces = ''.split(' ').length + expect(result.split(' ').length).to.equal(spaces) + attrExists('ng-model') + attrExists('id') + attrExists('name') + }) it(`should automatically add an id and name`, () => { - manipulate(); - expect(resultEl.attr('name')).to.eq('id'); - expect(resultEl.attr('id')).to.eq('id'); - }); + manipulate() + expect(resultEl.attr('name')).to.eq('id') + expect(resultEl.attr('id')).to.eq('id') + }) describe(`name`, () => { it(`should automatically be added when id is specified`, () => { - scope.id = 'some_random_id'; - manipulate(); - expect(resultEl.attr('name')).to.eq('some_random_id'); - expect(resultEl.attr('id')).to.eq('some_random_id'); - }); + scope.id = 'some_random_id' + manipulate() + expect(resultEl.attr('name')).to.eq('some_random_id') + expect(resultEl.attr('id')).to.eq('some_random_id') + }) it(`should allow to be set in scope`, () => { - scope.id = 'some_random_id'; - scope.name = 'some_random_name'; - manipulate(); - expect(resultEl.attr('name')).to.eq('some_random_name'); - expect(resultEl.attr('id')).to.eq('some_random_id'); - }); - }); + scope.id = 'some_random_id' + scope.name = 'some_random_name' + manipulate() + expect(resultEl.attr('name')).to.eq('some_random_name') + expect(resultEl.attr('id')).to.eq('some_random_id') + }) + }) describe(`ng-model-options`, () => { it(`should be added if modelOptions is specified`, () => { - field.modelOptions = {}; - manipulate(); - attrExists('ng-model-options'); - }); + field.modelOptions = {} + manipulate() + attrExists('ng-model-options') + }) it(`should change the value of ng-model if getterSetter is specified`, () => { - field.modelOptions = {getterSetter: true}; - manipulate(); - expect(resultEl.attr('ng-model')).to.equal('options.value'); - }); - }); + field.modelOptions = {getterSetter: true} + manipulate() + expect(resultEl.attr('ng-model')).to.equal('options.value') + }) + }) describe(`selector key notation`, () => { it(`should change the ng-model when the key is a dot property accessor`, () => { - field.key = 'bar.foo'; - manipulate(); - expect(resultEl.attr('ng-model')).to.equal('model.' + field.key); - }); + field.key = 'bar.foo' + manipulate() + expect(resultEl.attr('ng-model')).to.equal('model.' + field.key) + }) it(`should change the ng-model when the key is a bracket property accessor`, () => { - field.key = 'bar["foo-bar"]'; - manipulate(); - expect(resultEl.attr('ng-model')).to.equal('model.' + field.key); - }); - }); + field.key = 'bar["foo-bar"]' + manipulate() + expect(resultEl.attr('ng-model')).to.equal('model.' + field.key) + }) + }) describe(`formly-custom-validation`, () => { it(`shouldn't be added if there aren't validators or messages`, () => { - formlyCustomValidationPresence(false); - }); + formlyCustomValidationPresence(false) + }) it(`should be added if there are validators`, () => { - field.validators = {foo: 'bar'}; - formlyCustomValidationPresence(true); - }); + field.validators = {foo: 'bar'} + formlyCustomValidationPresence(true) + }) it(`should be added if there are messages`, () => { - field.validators = {foo: 'bar'}; - field.validation.messages = {foo: '"bar"'}; - formlyCustomValidationPresence(true); - }); + field.validators = {foo: 'bar'} + field.validation.messages = {foo: '"bar"'} + formlyCustomValidationPresence(true) + }) it(`should be added if there are validators and messages`, () => { - field.validators = {foo: 'bar'}; - field.validation.messages = {foo: '"bar"'}; - formlyCustomValidationPresence(true); - }); + field.validators = {foo: 'bar'} + field.validation.messages = {foo: '"bar"'} + formlyCustomValidationPresence(true) + }) function formlyCustomValidationPresence(present) { - manipulate(); - attrExists('formly-custom-validation', !present); + manipulate() + attrExists('formly-custom-validation', !present) } - }); + }) describe(`templateOptions attributes`, () => { describe(`boolean attributes`, () => { - testAttribute('required'); - testAttribute('disabled'); + testAttribute('required') + testAttribute('disabled') function testAttribute(name) { it(`should allow you to specify 'true' for ${name}`, () => { field.templateOptions = { - [name]: true - }; - manipulate(); - attrExists(name); - }); + [name]: true, + } + manipulate() + attrExists(name) + }) it(`should allow you to specify 'false' for ${name}`, () => { field.templateOptions = { - [name]: false - }; - manipulate(); - attrExists(name, false); - attrExists(`ng-${name}`, false); - }); + [name]: false, + } + manipulate() + attrExists(name, false) + attrExists(`ng-${name}`, false) + }) it(`should allow you to specify expressionProperties for ${name}`, () => { field.expressionProperties = { - [`templateOptions.${name}`]: 'someExpression' - }; - manipulate(); - attrExists(name, false); - attrExists(`ng-${name}`); - expect(resultEl.attr(`ng-${name}`)).to.eq(`options.templateOptions['${name}']`); - }); + [`templateOptions.${name}`]: 'someExpression', + } + manipulate() + attrExists(name, false) + attrExists(`ng-${name}`) + expect(resultEl.attr(`ng-${name}`)).to.eq(`options.templateOptions['${name}']`) + }) } - }); + }) describe(`attributeOnly`, () => { - ['placeholder', 'min', 'max', 'tabindex', 'type'].forEach(testAttribute); + ['placeholder', 'min', 'max', 'step', 'tabindex', 'type'].forEach(testAttribute) function testAttribute(name) { it(`should be placed as an attribute if it is present in the templateOptions`, () => { field.templateOptions = { - [name]: 'Ammon' - }; - manipulate(); - expect(resultEl.attr(name)).to.eq('Ammon'); - }); + [name]: 'Ammon', + } + manipulate() + expect(resultEl.attr(name)).to.eq('Ammon') + }) it(`should be placed as an attribute with {{expression}} if it is present in the expressionProperties`, () => { field.expressionProperties = { - ['templateOptions.' + name]: 'Ammon' - }; - manipulate(); - expect(resultEl.attr(name)).to.eq(`{{options.templateOptions['${name}']}}`); - }); + ['templateOptions.' + name]: 'Ammon', + } + manipulate() + expect(resultEl.attr(name)).to.eq(`{{options.templateOptions['${name}']}}`) + }) } - }); + }) describe(`preferUnbound`, () => { it(`should prefer to specify maxlength as ng-maxlegnth even when it's not in expressionProperties`, () => { field.templateOptions = { - maxlength: 3 - }; - manipulate(); - expect(resultEl.attr('ng-maxlength')).to.eq(`options.templateOptions['maxlength']`); - attrExists('maxlength', false); - }); + maxlength: 3, + } + manipulate() + expect(resultEl.attr('ng-maxlength')).to.eq(`options.templateOptions['maxlength']`) + attrExists('maxlength', false) + }) it(`should allow you to specify maxlength that gets set to maxlength if it's not in expressionProperties`, () => { - formlyConfig.extras.ngModelAttrsManipulatorPreferUnbound = true; + formlyConfig.extras.ngModelAttrsManipulatorPreferUnbound = true field.templateOptions = { - maxlength: 3 - }; - manipulate(); - attrExists('ng-maxlength', false); - expect(resultEl.attr('maxlength')).to.eq('3'); - formlyConfig.extras.ngModelAttrsManipulatorPreferUnbound = false; - }); + maxlength: 3, + } + manipulate() + attrExists('ng-maxlength', false) + expect(resultEl.attr('maxlength')).to.eq('3') + formlyConfig.extras.ngModelAttrsManipulatorPreferUnbound = false + }) it(`should still allow maxlength work with expressionProperties`, () => { field.expressionProperties = { - 'templateOptions.maxlength': '3' - }; - manipulate(); - expect(resultEl.attr('ng-maxlength')).to.eq(`options.templateOptions['maxlength']`); - attrExists('maxlength', false); - }); + 'templateOptions.maxlength': '3', + } + manipulate() + expect(resultEl.attr('ng-maxlength')).to.eq(`options.templateOptions['maxlength']`) + attrExists('maxlength', false) + }) - }); - }); + }) + }) describe(`ngModelElAttrs`, () => { it(`should place the attributes you specify on the ng-model element`, () => { _.assign(field, { - ngModelElAttrs: {foo: '{{::to.bar}}'} - }); - manipulate(); + ngModelElAttrs: {foo: '{{::to.bar}}'}, + }) + manipulate() expect( resultNode.getAttribute('foo'), 'foo attribute should equal the value of foo in ngModelElAttrs' - ).to.equal('{{::to.bar}}'); - }); + ).to.equal('{{::to.bar}}') + }) it(`should work with multiple ng-models`, () => { _.assign(field, { - ngModelElAttrs: {foo: '{{::to.bar}}'} - }); + ngModelElAttrs: {foo: '{{::to.bar}}'}, + }) manipulate(`
- `); + `) expect( resultNode.querySelector('.first').getAttribute('foo'), 'foo attribute should equal the value of foo in ngModelElAttrs' - ).to.equal('{{::to.bar}}'); + ).to.equal('{{::to.bar}}') expect( resultNode.querySelector('.second').getAttribute('foo'), 'foo attribute should equal the value of foo in ngModelElAttrs' - ).to.equal('{{::to.bar}}'); + ).to.equal('{{::to.bar}}') - }); + }) it(`should override existing attributes`, () => { field.ngModelElAttrs = { - 'ng-model': 'formState.foo.bar' - }; - manipulate(); - expect(resultEl.attr('ng-model')).to.equal('formState.foo.bar'); - }); - }); + 'ng-model': 'formState.foo.bar', + } + manipulate() + expect(resultEl.attr('ng-model')).to.equal('formState.foo.bar') + }) + }) function manipulate(theTemplate = template) { - result = manipulator(theTemplate, field, scope); - resultEl = angular.element(result); - resultNode = resultEl[0]; + result = manipulator(theTemplate, field, scope) + resultEl = angular.element(result) + resultNode = resultEl[0] } function attrExists(name, notExists) { - const attr = resultNode.getAttribute(name); + const attr = resultNode.getAttribute(name) if (notExists) { - expect(attr).to.be.null; + expect(attr).to.be.null } else { - expect(attr).to.be.defined; + expect(attr).to.be.defined } } -}); +}) diff --git a/src/services/formlyUtil.js b/src/services/formlyUtil.js index 246e6c49..4ce02f5e 100644 --- a/src/services/formlyUtil.js +++ b/src/services/formlyUtil.js @@ -1,8 +1,8 @@ -import utils from '../other/utils'; +import utils from '../other/utils' -export default formlyUtil; +export default formlyUtil // @ngInject function formlyUtil() { - return utils; + return utils } diff --git a/src/services/formlyUtil.test.js b/src/services/formlyUtil.test.js index ccec71c6..e44ee7bf 100644 --- a/src/services/formlyUtil.test.js +++ b/src/services/formlyUtil.test.js @@ -1,12 +1,12 @@ describe('formlyUtil', () => { - beforeEach(window.module('formly')); + beforeEach(window.module('formly')) describe('reverseDeepMerge', () => { - let merge; + let merge beforeEach(inject(function(formlyUtil) { - merge = formlyUtil.reverseDeepMerge; - })); + merge = formlyUtil.reverseDeepMerge + })) it(`should modify and prefer the first object`, () => { const firstObj = { @@ -14,13 +14,13 @@ describe('formlyUtil', () => { obj2a: { string3a: 'Hello world', number3a: 4, - bool3a: false - } + bool3a: false, + }, }, arry1a: [ - 1, 2, 3, 4 - ] - }; + 1, 2, 3, 4, + ], + } const secondObj = { obj1a: { obj2a: { @@ -28,17 +28,17 @@ describe('formlyUtil', () => { string3b: 'Should exist', number3a: 5, bool3a: true, - bool3b: false - } - } - }; + bool3b: false, + }, + }, + } const thirdObj = { obj1a: 'false', arry1a: [ - 4, 3, 2, 1, 0 - ] - }; + 4, 3, 2, 1, 0, + ], + } const result = { obj1a: { @@ -47,77 +47,77 @@ describe('formlyUtil', () => { string3b: 'Should exist', number3a: 4, bool3a: false, - bool3b: false - } + bool3b: false, + }, }, arry1a: [ - 1, 2, 3, 4, 0 - ] - }; + 1, 2, 3, 4, 0, + ], + } - merge(firstObj, secondObj, thirdObj); - expect(firstObj).to.eql(result); - }); + merge(firstObj, secondObj, thirdObj) + expect(firstObj).to.eql(result) + }) it(`should allow for adding of empty objects`, () => { const firstObj = { a: 'a', - b: 'b' - }; + b: 'b', + } const secondObj = { data: {}, templateOptions: {}, - validation: {} - }; + validation: {}, + } const result = { a: 'a', b: 'b', data: {}, templateOptions: {}, - validation: {} - }; + validation: {}, + } - merge(firstObj, secondObj); - expect(firstObj).to.eql(result); - }); - }); + merge(firstObj, secondObj) + expect(firstObj).to.eql(result) + }) + }) describe('findByNodeName', () => { - let find, $compile, scope; + let find, $compile, scope beforeEach(inject(function(_$compile_, $rootScope, formlyUtil) { - $compile = _$compile_; - scope = $rootScope; - find = formlyUtil.findByNodeName; - })); + $compile = _$compile_ + scope = $rootScope + find = formlyUtil.findByNodeName + })) it('should find an element by nodeName from a single root', () => { const template = - '
'; - const el = $compile(template)(scope); - const found = find(el, 'input'); - expect(found.length).to.equal(1); - expect(found.prop('nodeName')).to.equal('INPUT'); - }); + '
' + const el = $compile(template)(scope) + const found = find(el, 'input') + expect(found.length).to.equal(1) + expect(found.prop('nodeName')).to.equal('INPUT') + }) it('should find an element by nodeName from multiple root', () => { const template = '
' + - ''; - const el = $compile(template)(scope); - const found = find(el, 'i'); - expect(found.length).to.equal(1); - expect(found.prop('nodeName')).to.equal('I'); - }); + '' + const el = $compile(template)(scope) + const found = find(el, 'i') + expect(found.length).to.equal(1) + expect(found.prop('nodeName')).to.equal('I') + }) it('should return undefined when a node can\'t be found', () => { const template = - '
'; - const el = $compile(template)(scope); - const found = find(el, 'bla'); - expect(found).to.be.undefined; - }); + '
' + const el = $compile(template)(scope) + const found = find(el, 'bla') + expect(found).to.be.undefined + }) - }); -}); + }) +}) diff --git a/src/services/formlyWarn.js b/src/services/formlyWarn.js index 40c5c86d..4c33c876 100644 --- a/src/services/formlyWarn.js +++ b/src/services/formlyWarn.js @@ -1,14 +1,14 @@ -export default formlyWarn; +export default formlyWarn // @ngInject function formlyWarn(formlyConfig, formlyErrorAndWarningsUrlPrefix, $log) { return function warn() { if (!formlyConfig.disableWarnings) { - const args = Array.prototype.slice.call(arguments); - const warnInfoSlug = args.shift(); - args.unshift('Formly Warning:'); - args.push(`${formlyErrorAndWarningsUrlPrefix}${warnInfoSlug}`); - $log.warn(...args); + const args = Array.prototype.slice.call(arguments) + const warnInfoSlug = args.shift() + args.unshift('Formly Warning:') + args.push(`${formlyErrorAndWarningsUrlPrefix}${warnInfoSlug}`) + $log.warn(...args) } - }; + } } diff --git a/src/test.utils.js b/src/test.utils.js index 76db583c..94629e3a 100644 --- a/src/test.utils.js +++ b/src/test.utils.js @@ -1,60 +1,60 @@ /* eslint no-console:0 */ -import _ from 'lodash'; +import _ from 'lodash' -let key = 0; +let key = 0 -const input = ''; +const input = '' const multiNgModelField = ` -`; -const basicForm = ''; +` +const basicForm = '' export default { - getNewField, input, multiNgModelField, basicForm, shouldWarn, shouldNotWarn, shouldWarnWithLog -}; + getNewField, input, multiNgModelField, basicForm, shouldWarn, shouldNotWarn, shouldWarnWithLog, +} function getNewField(options) { - return _.merge({template: input, key: key++}, options); + return _.merge({template: input, key: key++}, options) } function shouldWarn(match, test) { - const originalWarn = console.warn; - let calledArgs; + const originalWarn = console.warn + let calledArgs console.warn = function() { - calledArgs = arguments; - }; - test(); - expect(calledArgs, 'expected warning and there was none').to.exist; - expect(Array.prototype.join.call(calledArgs, ' ')).to.match(match); - console.warn = originalWarn; + calledArgs = arguments + } + test() + expect(calledArgs, 'expected warning and there was none').to.exist + expect(Array.prototype.join.call(calledArgs, ' ')).to.match(match) + console.warn = originalWarn } function shouldNotWarn(test) { - const originalWarn = console.warn; - let calledArgs; + const originalWarn = console.warn + let calledArgs console.warn = function() { - calledArgs = arguments; - }; - test(); + calledArgs = arguments + } + test() if (calledArgs) { - console.log(calledArgs); - throw new Error('Expected no warning, but there was one', calledArgs); + console.log(calledArgs) + throw new Error('Expected no warning, but there was one', calledArgs) } - console.warn = originalWarn; + console.warn = originalWarn } function shouldWarnWithLog($log, logArgs, test) { /* eslint no-console:0 */ - test(); - expect($log.warn.logs, '$log should have only been called once').to.have.length(1); - const log = $log.warn.logs[0]; + test() + expect($log.warn.logs, '$log should have only been called once').to.have.length(1) + const log = $log.warn.logs[0] _.each(logArgs, (arg, index) => { if (_.isRegExp(arg)) { - expect(log[index]).to.match(arg); + expect(log[index]).to.match(arg) } else { - expect(log[index]).to.equal(arg); + expect(log[index]).to.equal(arg) } - }); + }) }