├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── PATRONS.md ├── README.md ├── bower.json ├── gulpfile.js ├── karma.conf.js ├── other ├── ERRORS_AND_WARNINGS.md ├── common.eslintrc ├── karma.conf.es6.js ├── logo │ ├── angular-formly-logo-1200px.png │ ├── angular-formly-logo-128px.png │ ├── angular-formly-logo-256px.png │ ├── angular-formly-logo-512px.png │ └── angular-formly-logo-64px.png ├── ng-nl-talk.png ├── src.eslintrc ├── test.eslintrc └── webpack.config.es6.js ├── package.js ├── package.json ├── src ├── angular-fix │ └── index.js ├── directives │ ├── formly-custom-validation.js │ ├── formly-custom-validation.test.js │ ├── formly-field.js │ ├── formly-field.test.js │ ├── formly-focus.js │ ├── formly-focus.test.js │ ├── formly-form.controller.js │ ├── formly-form.controller.test.js │ ├── formly-form.js │ └── formly-form.test.js ├── index.common.js ├── index.js ├── index.test.js ├── other │ ├── docsBaseUrl.js │ ├── utils.js │ └── utils.test.js ├── providers │ ├── formlyApiCheck.js │ ├── formlyApiCheck.test.js │ ├── formlyConfig.js │ ├── formlyConfig.test.js │ ├── formlyUsability.js │ └── formlyValidationMessages.js ├── run │ ├── formlyCustomTags.js │ ├── formlyCustomTags.test.js │ ├── formlyNgModelAttrsManipulator.js │ └── formlyNgModelAttrsManipulator.test.js ├── services │ ├── formlyUtil.js │ ├── formlyUtil.test.js │ └── formlyWarn.js └── test.utils.js └── webpack.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # all files 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | indent_style = space 11 | indent_size = 2 12 | charset = utf-8 13 | trim_trailing_whitespace = true 14 | max_line_length = 120 15 | 16 | [*.js] 17 | quote_type = single 18 | curly_bracket_next_line = false 19 | spaces_around_operators = true 20 | spaces_around_brackets = inside 21 | indent_brace_style = BSD KNF 22 | 23 | # HTML 24 | [*.html] 25 | quote_type = double 26 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | dist 4 | local-examples 5 | other 6 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | // The purpose of this .eslintrc is just for editors and IDEs to pick it up. 3 | // Webpack tells eslint which files to use explicitly. 4 | "extends": "./other/test.eslintrc" 5 | } 6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | *.png binary 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 27 | 28 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 10 | 11 | ## What 12 | 13 | 14 | 15 | ## Why 16 | 17 | 18 | 19 | ## How 20 | 21 | 22 | 23 | For issue # 24 | 25 | Checklist: 26 | 27 | * [ ] Follows the commit message [conventions](https://github.com/stevemao/conventional-changelog-angular/blob/master/convention.md) 28 | * [ ] 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) 29 | * [ ] Is [only one (maybe two) commits](https://egghead.io/lessons/javascript-how-to-squash-multiple-git-commits) 30 | 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.log 3 | *.iml 4 | *.DS_Store 5 | 6 | node_modules 7 | coverage 8 | nohup.out 9 | 10 | *.ignored.* 11 | *.ignored/ 12 | *.ignored 13 | dist 14 | 15 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | coverage 4 | local-examples 5 | demo 6 | .editorconfig 7 | .gitignore 8 | .travis.yml 9 | CONTRIBUTING.md 10 | karma.conf.js 11 | webpack.config.js 12 | other 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | cache: 4 | directories: 5 | - node_modules 6 | branches: 7 | only: 8 | - master 9 | notifications: 10 | email: false 11 | node_js: 12 | - 4.2 13 | before_install: 14 | - npm i -g npm@^3.0.0 15 | - "export DISPLAY=:99.0" 16 | - "sh -e /etc/init.d/xvfb start" 17 | before_script: 18 | - npm prune 19 | script: 20 | - npm run eslint 21 | - npm run test 22 | - npm run check-coverage 23 | after_success: 24 | - npm run report-coverage 25 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | The changelog is automatically updated using [semantic-release](https://github.com/semantic-release/semantic-release). 4 | You can see it on the [releases page](https://github.com/formly-js/angular-formly/releases). (Shortcut: 5 | [changelog.angular-formly.com](http://changelog.angular-formly.com)) 6 | 7 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 4 | 5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion. 6 | 7 | Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 8 | 9 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. 10 | 11 | This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. 12 | 13 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 14 | 15 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.1.0, available at [http://contributor-covenant.org/version/1/1/0/](http://contributor-covenant.org/version/1/1/0/) 16 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Watch the videos 4 | 5 | I've recorded several screencasts to demonstrate how to contribute. 6 | Here's [a playlist](https://www.youtube.com/playlist?list=PLV5CVI1eNcJi7lVVIuNyRhEuck1Z007BH) of them all. You'll find 7 | individual links by the respective sections 8 | 9 | ## Questions/Help 10 | 11 | [Watch video](https://www.youtube.com/watch?v=NXqFiSeBE-M&list=PLV5CVI1eNcJi7lVVIuNyRhEuck1Z007BH&index=2) 12 | 13 | An example will get you help faster than anything else you do. Create an example by going to 14 | [help.angular-formly.com](http://help.angular-formly.com) 15 | 16 | Please post questions to [StackOverflow](http://stackoverflow.com/) using the 17 | [angular-formly](http://stackoverflow.com/tags/angular-formly/info) tag. There's also the 18 | [mailing list](https://groups.io/org/groupsio/formly-js) where you can ask/answer questions. Also join us on 19 | [gitter](https://gitter.im/formly-js/angular-formly). 20 | 21 | If you file an issue with a question, it will be closed. I'm not trying to be mean. I'm just trying to stay sane. :-) 22 | 23 | ## Reporting Bugs / Requesting Features 24 | 25 | [Watch video](https://www.youtube.com/watch?v=Kw9fVgc3Tzk&index=6&list=PLV5CVI1eNcJi7lVVIuNyRhEuck1Z007BH) 26 | 27 | If you've found an issue, please submit it in [the issues](https://github.com/formly-js/angular-formly/issues) 28 | with a link to a jsbin that demonstrates the issue with [issue.angular-formly.com](http://issue.angular-formly.com) 29 | 30 | ## Pull Requests 31 | 32 | **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) 33 | 34 | ‼️‼️‼️ 👉**Please follow our commit message conventions** even if you're making a small change! This repository follows the 35 | [How to Write an Open Source JavaScript Library](https://egghead.io/series/how-to-write-an-open-source-javascript-library) 36 | series on egghead.io (by yours truly). See 37 | [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) 38 | and [this repo](https://github.com/stevemao/conventional-changelog-angular/blob/master/convention.md) 39 | to learn more about the commit message conventions. 40 | 41 | [Watch video](https://www.youtube.com/watch?v=QOchwBm9W-g&list=PLV5CVI1eNcJi7lVVIuNyRhEuck1Z007BH&index=1) (slightly out of date) 42 | 43 | If you would like to add functionality, please submit [an issue](https://github.com/formly-js/angular-formly/issues) 44 | first to make sure it's a direction we want to take. 45 | 46 | Please do the following: 47 | * Follow the existing styles (we have an `.editorconfig` file) 48 | * Document your changes in the README (try to follow the convention you see in the rest of the file) 49 | * Create an example for the website that demonstrates your changes so people can see how your changes work 50 | 51 | ### Development 52 | 53 | 1. run `npm install` 54 | 2. run `npm start` (if you're on a windows machine, see [this issue](https://github.com/formly-js/angular-formly/issues/305)) 55 | 3. write tests & code in ES6 goodness :-) 56 | 4. run `git add src/` 57 | 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)). 58 | 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 :-) 59 | 7. push your changes 60 | 8. create a PR with a link to the original issue 61 | 9. wait patiently :-) 62 | 63 | #### Notes 64 | 65 | - Please don't commit any changes to the `dist/` directory. This is only committed for releases. 66 | - Due to some inconsistencies with angular versions, always use `require('angular-fix')` rather than simply `require('angular')` 67 | - If you wish to view your changes visually, you can play around with it in the `local-examples` directory. Don't commit anything in this directory, but it's a good sanity check. It's just straight JavaScript with an `index.html`. I recommend `http-server`. 68 | 69 | ### What do you need help with? 70 | 71 | #### Helping others! 72 | 73 | There are a lot of questions from people as they get started using angular-formly. If you could **please do the following things**, that would really help: 74 | 75 | - Subscribe to the `angular-formly` questions [RSS Feed](http://stackoverflow.com/feeds/tag?tagnames=angular-formly&sort=newest) on StackOverflow. You can use this free service: [Blogtrottr](https://blogtrottr.com) to have it email you with new questions. 76 | - Hang out on [the chat](http://chat.angular-formly.com) 77 | - Sign up on [the mailing list](http://mailing-list.angular-formly.com) 78 | - Watch the [angular-formly repositories](https://github.com/formly-js) for issues or requests that you could help with (like [angular-formly-website](https://github.com/formly-js/angular-formly-website) for requests for examples). 79 | 80 | #### Contributing to community 81 | 82 | - Create plugins! [ideas](http://docs.angular-formly.com/v7.0.1/page/plugins) 83 | - Write blog posts! Like [these](http://docs.angular-formly.com/docs/tips#blog-articles) 84 | - Record screencasts 85 | - Write examples. The website is driven by examples. [Watch video](https://www.youtube.com/watch?v=4dsXXTPET4A&list=PLV5CVI1eNcJi7lVVIuNyRhEuck1Z007BH&index=3) 86 | 87 | #### Contributing to the core 88 | 89 | - Tests are always helpful! [Watch video](https://youtu.be/CQ766-miGQ4?list=PLV5CVI1eNcJi7lVVIuNyRhEuck1Z007BH) 90 | - Any of the issues in GitHub, let us know if you have some time to fix one. Especially those labeled [up-for-grabs](https://github.com/formly-js/angular-formly/labels/up-for-grabs) 91 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /PATRONS.md: -------------------------------------------------------------------------------- 1 | # Patrons 2 | 3 | The work on angular-formly is [funded by the community](https://www.patreon.com/kentcdodds). 4 | Meet some of the outstanding companies and individuals that made it possible: 5 | 6 | - [Pascal DeMilly](https://twitter.com/pdemilly) 7 | - [Benjamin Orozco](https://twitter.com/benoror) 8 | 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | angular-formly logo 2 | 3 | ## [angular-formly](http://docs.angular-formly.com) 4 | 5 | [THIS PROJECT NEEDS A MAINTAINER](https://github.com/formly-js/angular-formly/issues/638) 6 | 7 | Status: 8 | [![npm version](https://img.shields.io/npm/v/angular-formly.svg?style=flat-square)](https://www.npmjs.org/package/angular-formly) 9 | [![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) 10 | [![Build Status](https://img.shields.io/travis/formly-js/angular-formly.svg?style=flat-square)](https://travis-ci.org/formly-js/angular-formly) 11 | [![Code Coverage](https://img.shields.io/codecov/c/github/formly-js/angular-formly.svg?style=flat-square)](https://codecov.io/github/formly-js/angular-formly) 12 | 13 | Links: 14 | [![Documentation](https://img.shields.io/badge/API-Docs-red.svg?style=flat-square)](http://docs.angular-formly.com) 15 | [![Examples](https://img.shields.io/badge/formly-examples-green.svg?style=flat-square)](http://angular-formly.com) 16 | [![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) 17 | [![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) 18 | [![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) 19 | [![PRs Welcome](https://img.shields.io/badge/prs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) 20 | 21 | Sponsor 22 | 23 | angular-formly is an AngularJS module which has a directive to help customize and render JavaScript/JSON configured forms. 24 | The `formly-form` directive and the `formlyConfig` service are very powerful and bring unmatched maintainability to your 25 | application's forms. 26 | 27 | ```html 28 |
29 | 30 | 31 | 32 | 33 |
34 | ``` 35 | 36 | From there, it's just JavaScript. Allowing for DRY, maintainable, reusable forms. 37 | 38 | > **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. 39 | 40 | 41 | ## [Learning](http://learn.angular-formly.com) 42 | 43 | ### Egghead.io Lessons 44 | 45 | I'm an [egghead.io](https://egghead.io/) author and I have made a handful of lessons available there for free [here](https://egghead.io/playlists/advanced-angular-forms-with-angular-formly) 46 | 47 | ### NG-NL Talk 48 | 49 | [![JavaScript Powered Forms](other/ng-nl-talk.png)](http://youtu.be/o90TMDL3OYc) 50 | 51 | ### Examples 52 | 53 | [The website](http://angular-formly.com/) is full of tons of examples. 54 | 55 | ### More 56 | 57 | Find more resources at [learn.angular-formly.com](http://learn.angular-formly.com) 58 | 59 | ## Documentation 60 | 61 | Find all the documentation at [docs.angular-formly.com](http://docs.angular-formly.com). 62 | 63 | ## Plugins 64 | 65 | Find all the plugins at [docs.angular-formly.com/page/plugins](http://docs.angular-formly.com/page/plugins) 66 | 67 | ## Getting Help 68 | 69 | Please don't file an issue unless you feel like you've found a bug or have a feature request. Instead, go to [help.angular-formly.com](http://help.angular-formly.com) and follow the instructions. 70 | 71 | ## Roadmap 72 | 73 | See the [issues labeled enhancement](https://github.com/formly-js/angular-formly/labels/enhancement) 74 | 75 | ## Contributing 76 | 77 | Please see the [CONTRIBUTING Guidelines](CONTRIBUTING.md). 78 | 79 | **Note**: This projects adheres to a [Code of Conduct](CODE_OF_CONDUCT.md). 80 | 81 | ## Financial Support 82 | 83 | Some have expressed a desire to contribute financially as a way of expressing gratitude. I appreciate anything you (or 84 | your company) would be willing to contribute :-) You can support me [here](https://www.patreon.com/kentcdodds). Thanks! 85 | 86 | ## Bookmark Links 87 | 88 | You can bookmark these :-) 89 | 90 | - [http://help.angular-formly.com](http://help.angular-formly.com) 91 | - [http://question.angular-formly.com](http://question.angular-formly.com) 92 | - [http://issue.angular-formly.com](http://issue.angular-formly.com) 93 | - [http://new-example.angular-formly.com](http://new-example.angular-formly.com) 94 | - [http://egghead.angular-formly.com](http://egghead.angular-formly.com) 95 | - [http://chat.angular-formly.com](http://chat.angular-formly.com) 96 | - [http://mailing-list.angular-formly.com](http://mailing-list.angular-formly.com) 97 | - [http://learn.angular-formly.com](http://learn.angular-formly.com) 98 | - [http://questions.angular-formly.com](http://questions.angular-formly.com) 99 | 100 | ## Thanks 101 | 102 | A special thanks to [Nimbly](http://gonimbly.com) for creating angular-formly. 103 | This library is maintained (with love) by me, [Kent C. Dodds](https://twitter.com/kentcdodds). 104 | Thanks to all [contributors](https://github.com/formly-js/angular-formly/graphs/contributors)! 105 | 106 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-formly", 3 | "authors": [ 4 | "Astrism " 5 | ], 6 | "contributors": [ 7 | "Astrism ", 8 | "Kent C. Dodds " 9 | ], 10 | "main": "dist/formly.js", 11 | "description": "AngularJS directive which takes JSON representing a form and renders to HTML", 12 | "keywords": [ 13 | "AngularJs", 14 | "form", 15 | "formly", 16 | "json", 17 | "html" 18 | ], 19 | "license": "MIT", 20 | "ignore": [ 21 | "node_modules", 22 | "tests", 23 | "local-examples", 24 | ".editorconfig", 25 | ".gitignore", 26 | ".eslintrc", 27 | ".npmignore", 28 | ".travis.yml", 29 | "CONTRIBUTING.md", 30 | "karma.conf.js", 31 | "src", 32 | "webpack.config.js", 33 | "coverage", 34 | "other", 35 | ".test", 36 | "scripts" 37 | ], 38 | "dependencies": { 39 | "angular": "^1.2.x", 40 | "api-check": "^7.0.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp') 2 | const replace = require('gulp-replace') 3 | 4 | // bump npm package version into package.js 5 | gulp.task('meteor', function() { 6 | const pkg = require('./package.json') 7 | const versionRegex = /(version\:\s*\')([^\']+)\'/gi 8 | 9 | return gulp.src('package.js') 10 | .pipe(replace(versionRegex, '$1' + pkg.version + "'")) 11 | .pipe(gulp.dest('./')) 12 | }) 13 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | require('babel/register'); 3 | module.exports = require('./other/karma.conf.es6'); 4 | -------------------------------------------------------------------------------- /other/ERRORS_AND_WARNINGS.md: -------------------------------------------------------------------------------- 1 | Angular formly provides you with some handy warnings and errors to keep you from using it incorrectly. It will direct 2 | you to these for additional help. If these don't help you, please reproduce the issue using 3 | [this jsbin template](http://jsbin.com/biqesi/edit) and 4 | [file an issue on GitHub](https://github.com/formly-js/angular-formly/issues) or 5 | [join us on Gitter](https://gitter.im/formly-js/angular-formly). 6 | 7 | # apiCheck.js dependency required 8 | 9 | angular-formly has two dependencies: angular and [apiCheck.js](https://github.com/kentcdodds/apiCheck.js). You need to 10 | make sure you include apiCheck on the page for angular-formly to work. If you're using something like webpack or 11 | browserify, this should just work and you shouldn't be seeing this. If you are in that case, please file a bug. However 12 | if you're using just script tags, make sure to include the apiCheck.js script on the page. 13 | 14 | # Problem loading template for {templateUrl} 15 | 16 | If you provide a `templateUrl` as a property for a formly field or for a preconfigured template type, formly will 17 | attempt to load that template using `$http` with the `$templateCache`. If this request fails for some reason, you will 18 | see this warning. 19 | 20 | Fixes for this warning are: 21 | 22 | 1. Make sure that the URL is correct and that you're able to reach that URL (paste it into your browser to test) 23 | 2. Use `template` instead of `templateUrl`. This can be a little crazy for bigger templates. However, if you use 24 | Webpack's `raw-loader` then it's as simple as `require('./my-formly-template.html')`. This is what I do, and it's a 25 | beautiful thing :-) 26 | 27 | # Overwriting types or wrappers 28 | 29 | If you wish to override an existing type or wrapper, you must specify `overwriteOk: true`. 30 | 31 | # Type {type} has no template 32 | 33 | This means that you're specifying a type for a field that doesn't not have a template or templateUrl. For a field to 34 | be shown it must either have a `template`, `templateUrl`, or a `type` that has a `template` or `templateUrl`. 35 | 36 | # You must provide one of type, template, or templateUrl for a field 37 | 38 | I feel like this is fairly self explanatory. Make sure you're only specifying one of the three options there for each 39 | field. 40 | 41 | # You must only provide a type, template, or templateUrl for a field 42 | 43 | Same as above. 44 | 45 | # setType validation failed 46 | 47 | Basically you're calling `setType` and you aren't passing all the required data or you're passing the wrong 48 | properties/types. `name` is the only required property. If you're passing this, then it's likely that you're either 49 | passing an extra property that isn't allowed or you're passing the wrong data type for a property. Look at the output 50 | for what you passed and compare it with what you're allowed to pass. 51 | 52 | # setWrapper validation failed 53 | 54 | Similar to [setType validation failed](#settype-validation-failed) above, however the `name` property is only required 55 | if no `types` property is specified. 56 | 57 | # formly-field directive validation failed 58 | 59 | You need to make sure that the field config for all of your fields is correct according the specification. apiCheck 60 | should print out something that helps you understand what this is. If it's not good enough, file an issue and I'll look 61 | and improving it! 62 | 63 | # All field watchers must have a listener 64 | 65 | If you're using the `watcher` property, it can be an object (called a `watcher` object) or an array of `watcher` 66 | objects. Either way, all watchers must have a `listener` property which is a string (expression) or a function. The 67 | `expression` property is optional and you will not receive a warning for this property. See the documentation for more 68 | information. 69 | 70 | # formly-field {type} apiCheck failed 71 | 72 | Formly types can specify an `apiCheck` property which uses the `apiCheck.js` library to validate the options you 73 | provide (after they have been merged with the `optionsTypes` but before the `defaultOptions` of the type). You will see 74 | this warning if your options fail the apiCheck validation. Look at the warning, there's generally something there to 75 | indicate what you're missing. If you can't figure it out, or it's not clear, please file an issue! 76 | 77 | # There was a problem setting the template for this field 78 | 79 | In the `link` function of the `formly-field` directive, a number of things happen. If any of these goes wrong, you'll 80 | see this warning logged to the console and your field will never be added. Here are a few things to check if you see 81 | this error: 82 | 83 | - Make sure you haven't seen any other warnings. If you have, fix those first. 84 | - If you have any [`templateManipulators`](https://github.com/formly-js/angular-formly/#templatemanipulators) running, 85 | make sure those aren't throwing errors. Formly ships with one `templateManipulator` built-in. If you believe this is the 86 | reason you're seeing the error, please 87 | [file an issue with a reproducible example](https://github.com/formly-js/angular-formly/blob/master/CONTRIBUTING.md#issues). 88 | - If your field specifies [`wrappers`](https://github.com/formly-js/angular-formly#wrapper-stringarray-of-strings), 89 | or the template for your field type specifies any wrappers, or you register any wrappers for that field type (or with 90 | the name `default`) then make sure that there is nothing wrong with these wrappers. It is likely that formly will let 91 | you know that something is wrong with the wrappers if there is. 92 | - Make sure that there's not an issue in the template itself. Angular will throw a parse error if this is the case, so 93 | you should know if this is the issue. 94 | 95 | # formly-form has no FormController 96 | 97 | This can happen when you are specifying your own `root-el` on the `formly-form` and you don't pass an existing 98 | `FormController` (from another `
` or ``) to the `formly-form`. To fix the problem, make sure that if you 99 | must use a different `root-el` that you pass it an existing `FormController` like so: 100 | 101 | ```javascript 102 | 103 | 104 | 105 | ``` 106 | 107 | # Field model must be initialized 108 | 109 | Because of how the `model` property is evaluated when it's a string, it is not possible for angular-formly to initialize 110 | your model to an empty object for you. Because of this, if your expression evaluates to an undefined value, then this 111 | will be overridden by the value you specify for `formly-form`. Hence, you must initialize the actual `model` to which 112 | your expression evaluates. 113 | 114 | # Don't use expressionProperties.hide, use hideExpression instead 115 | 116 | This is due to limitations with expressionProperties and ng-if not working together very nicely. Essentially, if you are 117 | initializing your field to be hidden, then its controller function never runs, which means its `runExpressions` function 118 | is never initialized, which means the condition you specify in `expressionProperties.hide` will never be evaluated and 119 | the field will always remain hidden. 120 | 121 | To get around this, use `hideExpression` instead which is run on the `formly-form` level rather than the `formly-field` 122 | level. It's almost exactly the same API as a normal `formly-field` `expressionProperty` and you should never notice a 123 | difference. The main difference is that the `$scope` on which it is evaluated is not the `formly-field` scope like a 124 | normal `expressionProperties` expression, but rather the `formly-form` scope, which means that in the function version 125 | of the expression, the scope you're passed wont have all the properties you may be expecting. 126 | 127 | See documentation [here](http://docs.angular-formly.com/docs/field-configuration-object#hideexpression-string--function) 128 | and an example [here](http://angular-formly.com/#/example/field-options/hide-fields) 129 | 130 | # FieldTransform as a function deprecated 131 | 132 | To allow for plugin like functionality, `fieldTransform` functions on `formlyConfig.extras` and `formly-form.options` 133 | are now deprecated. Moving forward fieldTransform will accept an array of `fieldTransform` functions. This makes it possible 134 | to have multiple fieldTransforms. Note, `fieldTransform` functions will be removed in a major release. 135 | 136 | # Notes 137 | 138 | It is recommended to disable warnings in production using `formlyConfigProvider.disableWarnings = true`. Note: This will 139 | not disable thrown errors, only the `console.warn` messages. 140 | -------------------------------------------------------------------------------- /other/common.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-invalid-this": 0, // meh... 4 | "func-names": 0, // I wish, but doing this right now would be a royal pain 5 | "new-cap": [ 6 | 2, 7 | { 8 | "newIsCap": true, 9 | "capIsNew": true 10 | } 11 | ], 12 | "max-params": [2, 10], 13 | "max-statements": [2, 30], // TODO bring this down 14 | }, 15 | "globals": { 16 | "VERSION": false 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /other/karma.conf.es6.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | require('argv-set-env')(); 3 | const path = require('path'); 4 | 5 | process.env.NODE_ENV = process.env.NODE_ENV || 'test'; 6 | 7 | const coverage = process.env.COVERAGE === 'true'; 8 | const ci = process.env.CI; 9 | if (coverage) { 10 | console.log('-- recording coverage --'); // eslint-disable-line no-console 11 | } 12 | 13 | const webpackConfig = require('./webpack.config.es6'); 14 | const entry = path.join(webpackConfig.context, webpackConfig.entry); 15 | const preprocessors = {}; 16 | preprocessors[entry] = ['webpack']; 17 | 18 | module.exports = function(config) { 19 | config.set({ 20 | basePath: './', 21 | frameworks: ['sinon-chai', 'chai', 'mocha', 'sinon'], 22 | files: [ 23 | 'node_modules/lodash/lodash.js', 24 | 'node_modules/api-check/dist/api-check.js', 25 | 'node_modules/angular/angular.js', 26 | 'node_modules/angular-mocks/angular-mocks.js', 27 | entry 28 | ], 29 | preprocessors, 30 | reporters: getReporters(), 31 | webpack: webpackConfig, 32 | webpackMiddleware: {noInfo: true}, 33 | coverageReporter: { 34 | reporters: [ 35 | {type: 'lcov', dir: 'coverage/', subdir: '.'}, 36 | {type: 'json', dir: 'coverage/', subdir: '.'}, 37 | {type: 'text-summary'} 38 | ] 39 | }, 40 | port: 9876, 41 | colors: true, 42 | logLevel: config.LOG_INFO, 43 | autoWatch: !ci, 44 | browsers: ['Firefox'], 45 | singleRun: ci, 46 | browserNoActivityTimeout: 180000 47 | }); 48 | }; 49 | 50 | function getReporters() { 51 | const reps = ['progress']; 52 | if (coverage) { 53 | reps.push('coverage'); 54 | } 55 | return reps; 56 | } 57 | -------------------------------------------------------------------------------- /other/logo/angular-formly-logo-1200px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formly-js/angular-formly/c1bd043dc56bed4eecb573717c60804e993e1ab0/other/logo/angular-formly-logo-1200px.png -------------------------------------------------------------------------------- /other/logo/angular-formly-logo-128px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formly-js/angular-formly/c1bd043dc56bed4eecb573717c60804e993e1ab0/other/logo/angular-formly-logo-128px.png -------------------------------------------------------------------------------- /other/logo/angular-formly-logo-256px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formly-js/angular-formly/c1bd043dc56bed4eecb573717c60804e993e1ab0/other/logo/angular-formly-logo-256px.png -------------------------------------------------------------------------------- /other/logo/angular-formly-logo-512px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formly-js/angular-formly/c1bd043dc56bed4eecb573717c60804e993e1ab0/other/logo/angular-formly-logo-512px.png -------------------------------------------------------------------------------- /other/logo/angular-formly-logo-64px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formly-js/angular-formly/c1bd043dc56bed4eecb573717c60804e993e1ab0/other/logo/angular-formly-logo-64px.png -------------------------------------------------------------------------------- /other/ng-nl-talk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formly-js/angular-formly/c1bd043dc56bed4eecb573717c60804e993e1ab0/other/ng-nl-talk.png -------------------------------------------------------------------------------- /other/src.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["kentcdodds", "./common.eslintrc"] 3 | } 4 | -------------------------------------------------------------------------------- /other/test.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["kentcdodds/test-angular", "./common.eslintrc"] 3 | } 4 | -------------------------------------------------------------------------------- /other/webpack.config.es6.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | require('argv-set-env')(); 3 | const packageJson = require('../package.json'); 4 | 5 | const here = require('path-here'); 6 | const _ = require('lodash'); 7 | const webpack = require('webpack'); 8 | const deindent = require('deindent'); 9 | const WebpackNotifierPlugin = require('webpack-notifier'); 10 | 11 | module.exports = getConfig(); 12 | 13 | function getConfig() { 14 | let config = getCommonConfig(); 15 | 16 | switch (process.env.NODE_ENV) { 17 | case 'development': 18 | config = _.merge(config, getDevConfig()); 19 | break; 20 | case 'production': 21 | config = _.merge(config, getProdConfig()); 22 | break; 23 | case 'test': 24 | config = _.merge(config, getTestConfig()); 25 | break; 26 | default: 27 | throw new Error(`NODE_ENV not equal to development, production, or test. It is equal to ${process.env.NODE_ENV}`); 28 | } 29 | return config; 30 | } 31 | 32 | 33 | function getCommonConfig() { 34 | return { 35 | context: here('src'), 36 | entry: './index.js', 37 | output: { 38 | libraryTarget: 'umd', 39 | library: 'ngFormly' 40 | }, 41 | stats: { 42 | colors: true, 43 | reasons: true 44 | }, 45 | resolve: { 46 | extensions: ['', '.js'], 47 | alias: { 48 | 'angular-fix': here('src/angular-fix') 49 | } 50 | }, 51 | eslint: { 52 | emitError: true, 53 | failOnError: true, 54 | failOnWarning: false, 55 | quiet: true 56 | }, 57 | externals: { 58 | angular: 'angular', 59 | 'api-check': { 60 | root: 'apiCheck', 61 | amd: 'api-check', 62 | commonjs2: 'api-check', 63 | commonjs: 'api-check' 64 | } 65 | } 66 | }; 67 | } 68 | 69 | function getDevConfig() { 70 | return { 71 | output: { 72 | filename: 'dist/formly.js' 73 | }, 74 | module: { 75 | loaders: [ 76 | getJavaScriptLoader(), 77 | getHtmlLoader() 78 | ] 79 | }, 80 | plugins: getCommonPlugins() 81 | }; 82 | } 83 | 84 | function getProdConfig() { 85 | return { 86 | output: { 87 | filename: 'dist/formly.min.js' 88 | }, 89 | devtool: 'source-map', 90 | module: { 91 | loaders: [ 92 | getJavaScriptLoader(), 93 | getHtmlLoader() 94 | ] 95 | }, 96 | plugins: _.union(getCommonPlugins(), [ 97 | new webpack.optimize.DedupePlugin(), 98 | new webpack.optimize.OccurenceOrderPlugin(), 99 | new webpack.optimize.AggressiveMergingPlugin(), 100 | new webpack.optimize.UglifyJsPlugin() 101 | ]) 102 | }; 103 | } 104 | 105 | function getTestConfig() { 106 | const coverage = process.env.COVERAGE === 'true'; 107 | const ci = process.env.CI === 'true'; 108 | return { 109 | entry: './index.test.js', 110 | module: { 111 | loaders: _.flatten([ 112 | coverage ? getCoverageLoaders() : getJavaScriptLoader(), 113 | getHtmlLoader() 114 | ]) 115 | }, 116 | plugins: getCommonPlugins(), 117 | eslint: { 118 | emitError: ci, 119 | failOnError: ci 120 | } 121 | }; 122 | 123 | function getCoverageLoaders() { 124 | return [ 125 | { 126 | test: /\.test\.js$|\.mock\.js$/, // include only mock and test files 127 | loaders: ['ng-annotate', 'babel', 'eslint?configFile=./other/test.eslintrc'], 128 | exclude: /node_modules/ 129 | }, 130 | { 131 | test: /\.js$/, 132 | loaders: ['ng-annotate', 'isparta', 'eslint?configFile=./other/src.eslintrc'], 133 | exclude: /node_modules|\.test.js$|\.mock\.js$/ // exclude node_modules and test files 134 | } 135 | ]; 136 | } 137 | } 138 | 139 | 140 | function getJavaScriptLoader() { 141 | return {test: /\.js$/, loaders: ['ng-annotate', 'babel', 'eslint?configFile=./other/src.eslintrc'], exclude: /node_modules/}; 142 | } 143 | 144 | function getHtmlLoader() { 145 | return {test: /\.html$/, loaders: ['raw'], exclude: /node_modules/}; 146 | } 147 | 148 | function getCommonPlugins() { 149 | return _.filter([ 150 | new webpack.BannerPlugin(getBanner(process.env.NODE_ENV), {raw: true}), 151 | new webpack.DefinePlugin({ 152 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), 153 | VERSION: JSON.stringify(packageJson.version) 154 | }), 155 | process.env.CI ? undefined : new WebpackNotifierPlugin({ 156 | title: 'angular-formly', 157 | contentImage: here('other/logo/angular-formly-logo-64px.png') 158 | }) 159 | ]); 160 | } 161 | 162 | function getBanner(env) { 163 | if (env === 'production') { 164 | return deindent` 165 | /*! ${packageJson.name} v${packageJson.version} | MIT | built with ♥ by ${packageJson.contributors.join(', ')} (ó ì_í)=óò=(ì_í ò) */ 166 | `.trim(); 167 | } else { 168 | return deindent` 169 | /*! 170 | * ${packageJson.name} JavaScript Library v${packageJson.version} 171 | * 172 | * @license MIT (http://license.angular-formly.com) 173 | * 174 | * built with ♥ by ${packageJson.contributors.join(', ')} 175 | * (ó ì_í)=óò=(ì_í ò) 176 | */ 177 | `.trim(); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | /* global Package:false */ 2 | // package metadata file for AtmosphereJS 3 | 4 | try { 5 | Package.describe({ 6 | name: 'formly:angular-formly', 7 | summary: 'angular-formly (official): forms for AngularJS', 8 | version: '0.0.0-semantically-released.0', 9 | git: 'https://github.com/formly-js/angular-formly.git', 10 | }) 11 | 12 | Package.onUse(function(api) { 13 | api.versionsFrom(['METEOR@1.0']) 14 | // api-check 15 | api.use('wieldo:api-check@7.5.5') 16 | api.imply('wieldo:api-check') 17 | // angular 18 | api.use('angular:angular@1.4.0') 19 | api.addFiles('dist/formly.js', 'client') 20 | }) 21 | } catch (e) { 22 | // 23 | } 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-formly", 3 | "version": "0.0.0-semantically-released.0", 4 | "author": "Astrism ", 5 | "contributors": [ 6 | "Astrism ", 7 | "Kent C. Dodds " 8 | ], 9 | "homepage": "http://formly-js.github.io/angular-formly/", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/formly-js/angular-formly.git" 13 | }, 14 | "keywords": [ 15 | "angular", 16 | "forms", 17 | "angular-formly", 18 | "formly", 19 | "angularjs", 20 | "angular forms", 21 | "json forms", 22 | "form library" 23 | ], 24 | "main": "dist/formly.js", 25 | "license": "MIT", 26 | "scripts": { 27 | "build:dist": "webpack --progress --colors --set-env-NODE_ENV=development", 28 | "build:prod": "webpack --progress --colors --set-env-NODE_ENV=production", 29 | "build": "npm run build:dist & npm run build:prod", 30 | "eslint:test": "eslint -c other/test.eslintrc --ignore-pattern **/*.{test,mock}.js src/", 31 | "eslint:src": "eslint -c other/src.eslintrc --ignore-pattern !**/*.{test,mock}.js src/", 32 | "eslint": "npm run eslint:test -s && npm run eslint:src -s", 33 | "test": "karma start --single-run --set-env-COVERAGE=true --set-env-NODE_ENV=test", 34 | "test:watch": "karma start --set-env-COVERAGE=true --set-env-NODE_ENV=test", 35 | "test:debug": "karma start --browsers Chrome --set-env-NODE_ENV=test", 36 | "start": "npm run test:watch", 37 | "check-coverage": "istanbul check-coverage --statements 93 --branches 89 --functions 92 --lines 92", 38 | "report-coverage": "cat ./coverage/lcov.info | node_modules/.bin/codecov", 39 | "commit": "git-cz", 40 | "publish-latest": "publish-latest --user-email kent+formly-bot@doddsfamily.us --user-name formly-bot", 41 | "meteor": "gulp meteor", 42 | "semantic-release": "semantic-release pre && npm run build && npm run meteor && npm publish && npm run publish-latest && semantic-release post" 43 | }, 44 | "config": { 45 | "ghooks": { 46 | "commit-msg": "validate-commit-msg && npm run eslint && npm t && npm run check-coverage" 47 | }, 48 | "commitizen": { 49 | "path": "node_modules/cz-conventional-changelog" 50 | } 51 | }, 52 | "description": "AngularJS directive which takes JSON representing a form and renders to HTML", 53 | "peerDependencies": { 54 | "angular": "^1.2.x || >= 1.4.0-beta.0 || >= 1.5.0-beta.0", 55 | "api-check": "^7.0.0" 56 | }, 57 | "devDependencies": { 58 | "angular": "1.5.0", 59 | "angular-mocks": "1.5.0", 60 | "api-check": "7.5.5", 61 | "argv-set-env": "1.0.1", 62 | "babel": "5.8.23", 63 | "babel-eslint": "4.1.3", 64 | "babel-loader": "5.3.2", 65 | "chai": "3.5.0", 66 | "codecov.io": "0.1.6", 67 | "commitizen": "2.7.2", 68 | "cracks": "3.1.2", 69 | "cz-conventional-changelog": "1.1.5", 70 | "deindent": "0.1.0", 71 | "eslint": "1.7.3", 72 | "eslint-config-kentcdodds": "5.0.0", 73 | "eslint-loader": "1.1.0", 74 | "eslint-plugin-mocha": "1.0.0", 75 | "ghooks": "1.0.3", 76 | "gulp": "3.9.1", 77 | "gulp-replace": "0.5.4", 78 | "http-server": "0.9.0", 79 | "isparta": "3.1.0", 80 | "isparta-loader": "1.0.0", 81 | "istanbul": "0.4.2", 82 | "karma": "0.13.22", 83 | "karma-chai": "0.1.0", 84 | "karma-chrome-launcher": "0.2.2", 85 | "karma-coverage": "0.5.5", 86 | "karma-firefox-launcher": "0.1.7", 87 | "karma-mocha": "0.2.2", 88 | "karma-sinon": "1.0.4", 89 | "karma-sinon-chai": "1.2.0", 90 | "karma-webpack": "1.7.0", 91 | "lodash": "4.6.1", 92 | "lolex": "1.4.0", 93 | "mocha": "2.4.5", 94 | "ng-annotate": "1.2.1", 95 | "ng-annotate-loader": "0.1.0", 96 | "node-libs-browser": "1.0.0", 97 | "path-here": "1.1.0", 98 | "publish-latest": "1.1.2", 99 | "raw-loader": "0.5.1", 100 | "semantic-release": "4.3.5", 101 | "sinon": "1.17.3", 102 | "sinon-chai": "2.8.0", 103 | "validate-commit-msg": "2.3.1", 104 | "webpack": "1.12.14", 105 | "webpack-notifier": "1.3.0" 106 | }, 107 | "jspm": { 108 | "peerDependencies": { 109 | "angular": "*" 110 | } 111 | }, 112 | "release": { 113 | "verfiyRelease": { 114 | "path": "cracks", 115 | "paths": [ 116 | "src", 117 | "package.json" 118 | ], 119 | "silent": false 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/angular-fix/index.js: -------------------------------------------------------------------------------- 1 | // some versions of angular don't export the angular module properly, 2 | // so we get it from window in this case. 3 | let angular = require('angular') 4 | 5 | /* istanbul ignore next */ 6 | if (!angular.version) { 7 | angular = window.angular 8 | } 9 | export default angular 10 | -------------------------------------------------------------------------------- /src/directives/formly-custom-validation.js: -------------------------------------------------------------------------------- 1 | import angular from 'angular-fix' 2 | export default formlyCustomValidation 3 | 4 | // @ngInject 5 | function formlyCustomValidation(formlyUtil) { 6 | return { 7 | restrict: 'A', 8 | require: 'ngModel', 9 | link: function formlyCustomValidationLink(scope, el, attrs, ctrl) { 10 | const opts = scope.options 11 | opts.validation.messages = opts.validation.messages || {} 12 | angular.forEach(opts.validation.messages, (message, key) => { 13 | opts.validation.messages[key] = () => { 14 | return formlyUtil.formlyEval(scope, message, ctrl.$modelValue, ctrl.$viewValue) 15 | } 16 | }) 17 | 18 | 19 | const useNewValidatorsApi = ctrl.hasOwnProperty('$validators') && !attrs.hasOwnProperty('useParsers') 20 | angular.forEach(opts.validators, angular.bind(null, addValidatorToPipeline, false)) 21 | angular.forEach(opts.asyncValidators, angular.bind(null, addValidatorToPipeline, true)) 22 | 23 | function addValidatorToPipeline(isAsync, validator, name) { 24 | setupMessage(validator, name) 25 | validator = angular.isObject(validator) ? validator.expression : validator 26 | if (useNewValidatorsApi) { 27 | setupWithValidators(validator, name, isAsync) 28 | } else { 29 | setupWithParsers(validator, name, isAsync) 30 | } 31 | } 32 | 33 | function setupMessage(validator, name) { 34 | const message = validator.message 35 | if (message) { 36 | opts.validation.messages[name] = () => { 37 | return formlyUtil.formlyEval(scope, message, ctrl.$modelValue, ctrl.$viewValue) 38 | } 39 | } 40 | } 41 | 42 | function setupWithValidators(validator, name, isAsync) { 43 | const validatorCollection = isAsync ? '$asyncValidators' : '$validators' 44 | 45 | ctrl[validatorCollection][name] = function evalValidity(modelValue, viewValue) { 46 | return formlyUtil.formlyEval(scope, validator, modelValue, viewValue) 47 | } 48 | } 49 | 50 | function setupWithParsers(validator, name, isAsync) { 51 | let inFlightValidator 52 | ctrl.$parsers.unshift(function evalValidityOfParser(viewValue) { 53 | const isValid = formlyUtil.formlyEval(scope, validator, ctrl.$modelValue, viewValue) 54 | if (isAsync) { 55 | ctrl.$pending = ctrl.$pending || {} 56 | ctrl.$pending[name] = true 57 | inFlightValidator = isValid 58 | isValid.then(() => { 59 | if (inFlightValidator === isValid) { 60 | ctrl.$setValidity(name, true) 61 | } 62 | }).catch(() => { 63 | if (inFlightValidator === isValid) { 64 | ctrl.$setValidity(name, false) 65 | } 66 | }).finally(() => { 67 | const $pending = ctrl.$pending || {} 68 | if (Object.keys($pending).length === 1) { 69 | delete ctrl.$pending 70 | } else { 71 | delete ctrl.$pending[name] 72 | } 73 | }) 74 | } else { 75 | ctrl.$setValidity(name, isValid) 76 | } 77 | return viewValue 78 | }) 79 | } 80 | }, 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/directives/formly-custom-validation.test.js: -------------------------------------------------------------------------------- 1 | /* eslint no-unused-vars:0, max-len:0 */ 2 | import _ from 'lodash' 3 | import angular from 'angular-fix' 4 | 5 | import testUtils from '../test.utils.js' 6 | 7 | const {shouldWarnWithLog} = testUtils 8 | 9 | describe(`formly-custom-validation`, function() { 10 | let $compile, $timeout, $q, scope, $log, formlyConfig 11 | const formTemplate = `
TEMPLATE
` 12 | beforeEach(window.module('formly')) 13 | beforeEach(inject((_$compile_, _$timeout_, _$q_, $rootScope, _$log_, _formlyConfig_) => { 14 | $compile = _$compile_ 15 | $timeout = _$timeout_ 16 | $q = _$q_ 17 | scope = $rootScope.$new() 18 | scope.options = {validation: {}, validators: {}, asyncValidators: {}} 19 | $log = _$log_ 20 | formlyConfig = _formlyConfig_ 21 | })) 22 | 23 | describe(`using parsers`, () => { 24 | checkApi(formTemplate.replace( 25 | `TEMPLATE`, `` 26 | ), angular.version.minor >= 3) 27 | }) 28 | 29 | describe(`using $validators`, () => { 30 | checkApi(formTemplate.replace( 31 | `TEMPLATE`, `` 32 | )) 33 | }) 34 | 35 | describe(`options.validation.messages`, () => { 36 | it(`should convert all strings to functions`, () => { 37 | scope.options.validation = { 38 | messages: { 39 | isHello: `'"' + $viewValue + '" is not "hello"'`, 40 | }, 41 | } 42 | $compile(formTemplate.replace( 43 | `TEMPLATE`, `` 44 | ))(scope) 45 | 46 | expect(typeof scope.options.validation.messages.isHello).to.eq('function') 47 | const field = scope.myForm.field 48 | field.$setViewValue('sup') 49 | expect(scope.options.validation.messages.isHello()).to.eq('"sup" is not "hello"') 50 | }) 51 | }) 52 | 53 | function checkApi(template, versionThreeOrBetterAndEmulating) { 54 | const value = `hello` 55 | describe(`validators`, () => { 56 | const validate = doValidation.bind(null, template, 'hello', false) 57 | it(`should pass if returning a string that passes`, () => { 58 | validate(`$viewValue === "${value}"`, true) 59 | }) 60 | 61 | it(`should fail if returning a string that fails`, () => { 62 | validate(`$viewValue !== "${value}"`, false) 63 | }) 64 | 65 | it(`should pass if it's a function that passes`, () => { 66 | validate(viewValue => viewValue === value, true) 67 | }) 68 | 69 | it(`should fail if it's a function that fails`, () => { 70 | validate(viewValue => viewValue !== value, false) 71 | }) 72 | }) 73 | 74 | describe(`asyncValidators`, () => { 75 | const validate = doValidation.bind(null, template, 'hello', true) 76 | it(`should pass if it's a function that returns a promise that resolves`, () => { 77 | validate(() => $q.when(), true) 78 | }) 79 | 80 | it(`should fail if it's a function that returns a promise that rejects`, () => { 81 | validate(() => $q.reject(), false) 82 | }) 83 | 84 | it(`should be pending until the promise is resolved`, () => { 85 | const deferred = $q.defer() 86 | const deferred2 = $q.defer() 87 | scope.options.asyncValidators.isHello = () => deferred.promise 88 | scope.options.asyncValidators.isHey = () => deferred2.promise 89 | $compile(template)(scope) 90 | const field = scope.myForm.field 91 | scope.$digest() 92 | field.$setViewValue(value) 93 | 94 | expect(field.$pending).to.exist 95 | expect(field.$pending.isHello).to.be.true 96 | expect(field.$pending.isHey).to.be.true 97 | 98 | // because in angular 1.3 they do some interesting stuff with $pending, so can only test $pending in 1.2 99 | if (!versionThreeOrBetterAndEmulating) { 100 | deferred.resolve() 101 | scope.$digest() 102 | 103 | expect(field.$pending).to.exist 104 | expect(field.$pending.isHey).to.be.true 105 | expect(field.$pending.isHello).to.not.exist 106 | 107 | deferred2.reject() 108 | scope.$digest() 109 | expect(field.$pending).to.not.exist 110 | } 111 | }) 112 | }) 113 | } 114 | 115 | function doValidation(template, value, isAsync, validator, pass) { 116 | if (isAsync) { 117 | scope.options.asyncValidators.isHello = validator 118 | } else { 119 | scope.options.validators.isHello = validator 120 | } 121 | $compile(template)(scope) 122 | const field = scope.myForm.field 123 | scope.$digest() 124 | field.$setViewValue(value) 125 | scope.$digest() 126 | expect(field.$valid).to.eq(pass) 127 | } 128 | }) 129 | -------------------------------------------------------------------------------- /src/directives/formly-field.js: -------------------------------------------------------------------------------- 1 | import angular from 'angular-fix' 2 | import apiCheckFactory from 'api-check' 3 | 4 | export default formlyField 5 | 6 | /** 7 | * @ngdoc directive 8 | * @name formlyField 9 | * @restrict AE 10 | */ 11 | // @ngInject 12 | function formlyField($http, $q, $compile, $templateCache, $interpolate, formlyConfig, 13 | formlyApiCheck, formlyUtil, formlyUsability, formlyWarn) { 14 | const {arrayify} = formlyUtil 15 | 16 | return { 17 | restrict: 'AE', 18 | transclude: true, 19 | require: '?^formlyForm', 20 | scope: { 21 | options: '=', 22 | model: '=', 23 | originalModel: '=?', 24 | formId: '@', // TODO remove formId in a breaking release 25 | index: '=?', 26 | fields: '=?', 27 | formState: '=?', 28 | formOptions: '=?', 29 | form: '=?', // TODO require form in a breaking release 30 | }, 31 | controller: FormlyFieldController, 32 | link: fieldLink, 33 | } 34 | 35 | 36 | // @ngInject 37 | function FormlyFieldController($scope, $timeout, $parse, $controller, formlyValidationMessages) { 38 | /* eslint max-statements:[2, 37] */ 39 | if ($scope.options.fieldGroup) { 40 | setupFieldGroup() 41 | return 42 | } 43 | 44 | const fieldType = getFieldType($scope.options) 45 | simplifyLife($scope.options) 46 | mergeFieldOptionsWithTypeDefaults($scope.options, fieldType) 47 | extendOptionsWithDefaults($scope.options, $scope.index) 48 | checkApi($scope.options) 49 | // set field id to link labels and fields 50 | 51 | // initalization 52 | setFieldIdAndName() 53 | setDefaultValue() 54 | setInitialValue() 55 | runExpressions() 56 | watchExpressions() 57 | addValidationMessages($scope.options) 58 | invokeControllers($scope, $scope.options, fieldType) 59 | 60 | // function definitions 61 | function runExpressions() { 62 | const deferred = $q.defer() 63 | // must run on next tick to make sure that the current value is correct. 64 | $timeout(function runExpressionsOnNextTick() { 65 | const promises = [] 66 | const field = $scope.options 67 | const currentValue = valueGetterSetter() 68 | angular.forEach(field.expressionProperties, function runExpression(expression, prop) { 69 | const setter = $parse(prop).assign 70 | const promise = $q.when(formlyUtil.formlyEval($scope, expression, currentValue, currentValue)) 71 | .then(function setFieldValue(value) { 72 | setter(field, value) 73 | }) 74 | promises.push(promise) 75 | }) 76 | $q.all(promises).then(function() { 77 | deferred.resolve() 78 | }) 79 | }, 0, false) 80 | return deferred.promise 81 | } 82 | 83 | function watchExpressions() { 84 | if ($scope.formOptions.watchAllExpressions) { 85 | const field = $scope.options 86 | const currentValue = valueGetterSetter() 87 | angular.forEach(field.expressionProperties, function watchExpression(expression, prop) { 88 | const setter = $parse(prop).assign 89 | $scope.$watch(function expressionPropertyWatcher() { 90 | return formlyUtil.formlyEval($scope, expression, currentValue, currentValue) 91 | }, function expressionPropertyListener(value) { 92 | setter(field, value) 93 | }, true) 94 | }) 95 | } 96 | } 97 | 98 | function valueGetterSetter(newVal) { 99 | if (!$scope.model || !$scope.options.key) { 100 | return undefined 101 | } 102 | if (angular.isDefined(newVal)) { 103 | parseSet($scope.options.key, $scope.model, newVal) 104 | } 105 | return parseGet($scope.options.key, $scope.model) 106 | } 107 | 108 | function shouldNotUseParseKey(key) { 109 | return angular.isNumber(key) || !formlyUtil.containsSelector(key) 110 | } 111 | 112 | function keyContainsArrays(key) { 113 | return /\[\d{1,}\]/.test(key) 114 | } 115 | 116 | function deepAssign(obj, prop, value) { 117 | if (angular.isString(prop)) { 118 | prop = prop.replace(/\[(\w+)\]/g, '.$1').split('.') 119 | } 120 | 121 | if (prop.length > 1) { 122 | const e = prop.shift() 123 | obj[e] = obj[e] || (isNaN(prop[0])) ? {} : [] 124 | deepAssign(obj[e], prop, value) 125 | } else { 126 | obj[prop[0]] = value 127 | } 128 | } 129 | 130 | function parseSet(key, model, newVal) { 131 | // If either of these are null/undefined then just return undefined 132 | if ((!key && key !== 0) || !model) { 133 | return 134 | } 135 | // If we are working with a number then $parse wont work, default back to the old way for now 136 | if (shouldNotUseParseKey(key)) { 137 | // TODO: Fix this so we can get several levels instead of just one with properties that are numeric 138 | model[key] = newVal 139 | } else if (formlyConfig.extras.parseKeyArrays && keyContainsArrays(key)) { 140 | deepAssign($scope.model, key, newVal) 141 | } else { 142 | const setter = $parse($scope.options.key).assign 143 | if (setter) { 144 | setter($scope.model, newVal) 145 | } 146 | } 147 | } 148 | 149 | function parseGet(key, model) { 150 | // If either of these are null/undefined then just return undefined 151 | if ((!key && key !== 0) || !model) { 152 | return undefined 153 | } 154 | 155 | // If we are working with a number then $parse wont work, default back to the old way for now 156 | if (shouldNotUseParseKey(key)) { 157 | // TODO: Fix this so we can get several levels instead of just one with properties that are numeric 158 | return model[key] 159 | } else { 160 | return $parse(key)(model) 161 | } 162 | } 163 | 164 | function simplifyLife(options) { 165 | // add a few empty objects (if they don't already exist) so you don't have to undefined check everywhere 166 | formlyUtil.reverseDeepMerge(options, { 167 | originalModel: options.model, 168 | extras: {}, 169 | data: {}, 170 | templateOptions: {}, 171 | validation: {}, 172 | }) 173 | // create $scope.to so template authors can reference to instead of $scope.options.templateOptions 174 | $scope.to = $scope.options.templateOptions 175 | $scope.formOptions = $scope.formOptions || {} 176 | } 177 | 178 | function setFieldIdAndName() { 179 | if (angular.isFunction(formlyConfig.extras.getFieldId)) { 180 | $scope.id = formlyConfig.extras.getFieldId($scope.options, $scope.model, $scope) 181 | } else { 182 | const formName = ($scope.form && $scope.form.$name) || $scope.formId 183 | $scope.id = formlyUtil.getFieldId(formName, $scope.options, $scope.index) 184 | } 185 | $scope.options.id = $scope.id 186 | $scope.name = $scope.options.name || $scope.options.id 187 | $scope.options.name = $scope.name 188 | } 189 | 190 | function setDefaultValue() { 191 | if (angular.isDefined($scope.options.defaultValue) && 192 | !angular.isDefined(parseGet($scope.options.key, $scope.model))) { 193 | parseSet($scope.options.key, $scope.model, $scope.options.defaultValue) 194 | } 195 | } 196 | 197 | function setInitialValue() { 198 | $scope.options.initialValue = $scope.model && parseGet($scope.options.key, $scope.model) 199 | } 200 | 201 | function mergeFieldOptionsWithTypeDefaults(options, type) { 202 | if (type) { 203 | mergeOptions(options, type.defaultOptions) 204 | } 205 | const properOrder = arrayify(options.optionsTypes).reverse() // so the right things are overridden 206 | angular.forEach(properOrder, typeName => { 207 | mergeOptions(options, formlyConfig.getType(typeName, true, options).defaultOptions) 208 | }) 209 | } 210 | 211 | function mergeOptions(options, extraOptions) { 212 | if (extraOptions) { 213 | if (angular.isFunction(extraOptions)) { 214 | extraOptions = extraOptions(options, $scope) 215 | } 216 | formlyUtil.reverseDeepMerge(options, extraOptions) 217 | } 218 | } 219 | 220 | function extendOptionsWithDefaults(options, index) { 221 | const key = options.key || index || 0 222 | angular.extend(options, { 223 | // attach the key in case the formly-field directive is used directly 224 | key, 225 | value: options.value || valueGetterSetter, 226 | runExpressions, 227 | resetModel, 228 | updateInitialValue, 229 | }) 230 | } 231 | 232 | function resetModel() { 233 | parseSet($scope.options.key, $scope.model, $scope.options.initialValue) 234 | if ($scope.options.formControl) { 235 | if (angular.isArray($scope.options.formControl)) { 236 | angular.forEach($scope.options.formControl, function(formControl) { 237 | resetFormControl(formControl, true) 238 | }) 239 | } else { 240 | resetFormControl($scope.options.formControl) 241 | } 242 | } 243 | if ($scope.form) { 244 | $scope.form.$setUntouched && $scope.form.$setUntouched() 245 | $scope.form.$setPristine() 246 | } 247 | } 248 | 249 | function resetFormControl(formControl, isMultiNgModel) { 250 | if (!isMultiNgModel) { 251 | formControl.$setViewValue(parseGet($scope.options.key, $scope.model)) 252 | } 253 | 254 | formControl.$render() 255 | formControl.$setUntouched && formControl.$setUntouched() 256 | formControl.$setPristine() 257 | 258 | // To prevent breaking change requiring a digest to reset $viewModel 259 | if (!$scope.$root.$$phase) { 260 | $scope.$digest() 261 | } 262 | } 263 | 264 | function updateInitialValue() { 265 | $scope.options.initialValue = parseGet($scope.options.key, $scope.model) 266 | } 267 | 268 | function addValidationMessages(options) { 269 | options.validation.messages = options.validation.messages || {} 270 | angular.forEach(formlyValidationMessages.messages, function createFunctionForMessage(expression, name) { 271 | if (!options.validation.messages[name]) { 272 | options.validation.messages[name] = function evaluateMessage(viewValue, modelValue, scope) { 273 | return formlyUtil.formlyEval(scope, expression, modelValue, viewValue) 274 | } 275 | } 276 | }) 277 | } 278 | 279 | function invokeControllers(scope, options = {}, type = {}) { 280 | angular.forEach([type.controller, options.controller], controller => { 281 | if (controller) { 282 | $controller(controller, {$scope: scope}) 283 | } 284 | }) 285 | } 286 | 287 | function setupFieldGroup() { 288 | $scope.options.options = $scope.options.options || {} 289 | $scope.options.options.formState = $scope.formState 290 | $scope.to = $scope.options.templateOptions 291 | } 292 | } 293 | 294 | 295 | // link function 296 | function fieldLink(scope, el, attrs, formlyFormCtrl) { 297 | if (scope.options.fieldGroup) { 298 | setFieldGroupTemplate() 299 | return 300 | } 301 | 302 | // watch the field model (if exists) if there is no parent formly-form directive (that would watch it instead) 303 | if (!formlyFormCtrl && scope.options.model) { 304 | scope.$watch('options.model', () => scope.options.runExpressions(), true) 305 | } 306 | 307 | addAttributes() 308 | addClasses() 309 | 310 | const type = getFieldType(scope.options) 311 | const args = arguments 312 | const thusly = this 313 | let fieldCount = 0 314 | const fieldManipulators = getManipulators(scope.options, scope.formOptions) 315 | getFieldTemplate(scope.options) 316 | .then(runManipulators(fieldManipulators.preWrapper)) 317 | .then(transcludeInWrappers(scope.options, scope.formOptions)) 318 | .then(runManipulators(fieldManipulators.postWrapper)) 319 | .then(setElementTemplate) 320 | .then(watchFormControl) 321 | .then(callLinkFunctions) 322 | .catch(error => { 323 | formlyWarn( 324 | 'there-was-a-problem-setting-the-template-for-this-field', 325 | 'There was a problem setting the template for this field ', 326 | scope.options, 327 | error 328 | ) 329 | }) 330 | 331 | function setFieldGroupTemplate() { 332 | checkFieldGroupApi(scope.options) 333 | el.addClass('formly-field-group') 334 | let extraAttributes = '' 335 | if (scope.options.elementAttributes) { 336 | extraAttributes = Object.keys(scope.options.elementAttributes).map(key => { 337 | return `${key}="${scope.options.elementAttributes[key]}"` 338 | }).join(' ') 339 | } 340 | let modelValue = 'model' 341 | scope.options.form = scope.form 342 | if (scope.options.key) { 343 | modelValue = `model['${scope.options.key}']` 344 | } 345 | getTemplate(` 346 | 353 | 354 | `) 355 | .then(transcludeInWrappers(scope.options, scope.formOptions)) 356 | .then(setElementTemplate) 357 | } 358 | 359 | function addAttributes() { 360 | if (scope.options.elementAttributes) { 361 | el.attr(scope.options.elementAttributes) 362 | } 363 | } 364 | 365 | function addClasses() { 366 | if (scope.options.className) { 367 | el.addClass(scope.options.className) 368 | } 369 | if (scope.options.type) { 370 | el.addClass(`formly-field-${scope.options.type}`) 371 | } 372 | } 373 | 374 | function setElementTemplate(templateString) { 375 | el.html(asHtml(templateString)) 376 | $compile(el.contents())(scope) 377 | return templateString 378 | } 379 | 380 | function watchFormControl(templateString) { 381 | let stopWatchingShowError = angular.noop 382 | if (scope.options.noFormControl) { 383 | return 384 | } 385 | const templateEl = angular.element(`
${templateString}
`) 386 | const ngModelNodes = templateEl[0].querySelectorAll('[ng-model],[data-ng-model]') 387 | 388 | 389 | if (ngModelNodes.length) { 390 | angular.forEach(ngModelNodes, function(ngModelNode) { 391 | fieldCount++ 392 | watchFieldNameOrExistence(ngModelNode.getAttribute('name')) 393 | }) 394 | } 395 | 396 | function watchFieldNameOrExistence(name) { 397 | const nameExpressionRegex = /\{\{(.*?)}}/ 398 | const nameExpression = nameExpressionRegex.exec(name) 399 | if (nameExpression) { 400 | name = $interpolate(name)(scope) 401 | } 402 | watchFieldExistence(name) 403 | } 404 | 405 | function watchFieldExistence(name) { 406 | scope.$watch(`form["${name}"]`, function formControlChange(formControl) { 407 | if (formControl) { 408 | if (fieldCount > 1) { 409 | if (!scope.options.formControl) { 410 | scope.options.formControl = [] 411 | } 412 | scope.options.formControl.push(formControl) 413 | } else { 414 | scope.options.formControl = formControl 415 | } 416 | scope.fc = scope.options.formControl // shortcut for template authors 417 | stopWatchingShowError() 418 | addShowMessagesWatcher() 419 | addParsers() 420 | addFormatters() 421 | } 422 | }) 423 | } 424 | 425 | function addShowMessagesWatcher() { 426 | stopWatchingShowError = scope.$watch(function watchShowValidationChange() { 427 | const customExpression = formlyConfig.extras.errorExistsAndShouldBeVisibleExpression 428 | const options = scope.options 429 | const formControls = arrayify(scope.fc) 430 | if (!formControls.some(fc => fc.$invalid)) { 431 | return false 432 | } else if (typeof options.validation.show === 'boolean') { 433 | return options.validation.show 434 | } else if (customExpression) { 435 | return formControls.some(fc => 436 | formlyUtil.formlyEval(scope, customExpression, fc.$modelValue, fc.$viewValue)) 437 | } else { 438 | return formControls.some(fc => { 439 | const noTouchedButDirty = (angular.isUndefined(fc.$touched) && fc.$dirty) 440 | return (fc.$touched || noTouchedButDirty) 441 | }) 442 | } 443 | }, function onShowValidationChange(show) { 444 | scope.options.validation.errorExistsAndShouldBeVisible = show 445 | scope.showError = show // shortcut for template authors 446 | }) 447 | } 448 | 449 | function addParsers() { 450 | setParsersOrFormatters('parsers') 451 | } 452 | 453 | function addFormatters() { 454 | setParsersOrFormatters('formatters') 455 | const ctrl = scope.fc 456 | const formWasPristine = scope.form.$pristine 457 | if (scope.options.formatters) { 458 | let value = ctrl.$modelValue 459 | ctrl.$formatters.forEach((formatter) => { 460 | value = formatter(value) 461 | }) 462 | 463 | ctrl.$setViewValue(value) 464 | ctrl.$render() 465 | ctrl.$setPristine() 466 | if (formWasPristine) { 467 | scope.form.$setPristine() 468 | } 469 | } 470 | } 471 | 472 | function setParsersOrFormatters(which) { 473 | let originalThingProp = 'originalParser' 474 | if (which === 'formatters') { 475 | originalThingProp = 'originalFormatter' 476 | } 477 | 478 | // init with type's parsers 479 | let things = getThingsFromType(type) 480 | 481 | // get optionsTypes things 482 | things = formlyUtil.extendArray(things, getThingsFromOptionsTypes(scope.options.optionsTypes)) 483 | 484 | // get field's things 485 | things = formlyUtil.extendArray(things, scope.options[which]) 486 | 487 | // convert things into formlyExpression things 488 | angular.forEach(things, (thing, index) => { 489 | things[index] = getFormlyExpressionThing(thing) 490 | }) 491 | 492 | let ngModelCtrls = scope.fc 493 | if (!angular.isArray(ngModelCtrls)) { 494 | ngModelCtrls = [ngModelCtrls] 495 | } 496 | 497 | angular.forEach(ngModelCtrls, ngModelCtrl => { 498 | ngModelCtrl['$' + which] = ngModelCtrl['$' + which].concat(...things) 499 | }) 500 | 501 | function getThingsFromType(theType) { 502 | if (!theType) { 503 | return [] 504 | } 505 | if (angular.isString(theType)) { 506 | theType = formlyConfig.getType(theType, true, scope.options) 507 | } 508 | let typeThings = [] 509 | 510 | // get things from parent 511 | if (theType.extends) { 512 | typeThings = formlyUtil.extendArray(typeThings, getThingsFromType(theType.extends)) 513 | } 514 | 515 | // get own type's things 516 | typeThings = formlyUtil.extendArray(typeThings, getDefaultOptionsProperty(theType, which, [])) 517 | 518 | // get things from optionsTypes 519 | typeThings = formlyUtil.extendArray( 520 | typeThings, 521 | getThingsFromOptionsTypes(getDefaultOptionsOptionsTypes(theType)) 522 | ) 523 | 524 | return typeThings 525 | } 526 | 527 | function getThingsFromOptionsTypes(optionsTypes = []) { 528 | let optionsTypesThings = [] 529 | angular.forEach(angular.copy(arrayify(optionsTypes)).reverse(), optionsTypeName => { 530 | optionsTypesThings = formlyUtil.extendArray(optionsTypesThings, getThingsFromType(optionsTypeName)) 531 | }) 532 | return optionsTypesThings 533 | } 534 | 535 | function getFormlyExpressionThing(thing) { 536 | formlyExpressionParserOrFormatterFunction[originalThingProp] = thing 537 | return formlyExpressionParserOrFormatterFunction 538 | 539 | function formlyExpressionParserOrFormatterFunction($viewValue) { 540 | const $modelValue = scope.options.value() 541 | return formlyUtil.formlyEval(scope, thing, $modelValue, $viewValue) 542 | } 543 | } 544 | 545 | } 546 | } 547 | 548 | function callLinkFunctions() { 549 | if (type && type.link) { 550 | type.link.apply(thusly, args) 551 | } 552 | if (scope.options.link) { 553 | scope.options.link.apply(thusly, args) 554 | } 555 | } 556 | 557 | 558 | function runManipulators(manipulators) { 559 | return function runManipulatorsOnTemplate(templateToManipulate) { 560 | let chain = $q.when(templateToManipulate) 561 | angular.forEach(manipulators, manipulator => { 562 | chain = chain.then(template => { 563 | return $q.when(manipulator(template, scope.options, scope)).then(newTemplate => { 564 | return angular.isString(newTemplate) ? newTemplate : asHtml(newTemplate) 565 | }) 566 | }) 567 | }) 568 | return chain 569 | } 570 | } 571 | } 572 | 573 | // sort-of stateless util functions 574 | function asHtml(el) { 575 | const wrapper = angular.element('') 576 | return wrapper.append(el).html() 577 | } 578 | 579 | function getFieldType(options) { 580 | return options.type && formlyConfig.getType(options.type) 581 | } 582 | 583 | function getManipulators(options, formOptions) { 584 | let preWrapper = [] 585 | let postWrapper = [] 586 | addManipulators(options.templateManipulators) 587 | addManipulators(formOptions.templateManipulators) 588 | addManipulators(formlyConfig.templateManipulators) 589 | return {preWrapper, postWrapper} 590 | 591 | function addManipulators(manipulators) { 592 | /* eslint-disable */ // it doesn't understand this :-( 593 | const {preWrapper:pre = [], postWrapper:post = []} = (manipulators || {}); 594 | preWrapper = preWrapper.concat(pre); 595 | postWrapper = postWrapper.concat(post); 596 | /* eslint-enable */ 597 | } 598 | } 599 | 600 | function getFieldTemplate(options) { 601 | function fromOptionsOrType(key, fieldType) { 602 | if (angular.isDefined(options[key])) { 603 | return options[key] 604 | } else if (fieldType && angular.isDefined(fieldType[key])) { 605 | return fieldType[key] 606 | } 607 | } 608 | 609 | const type = formlyConfig.getType(options.type, true, options) 610 | const template = fromOptionsOrType('template', type) 611 | const templateUrl = fromOptionsOrType('templateUrl', type) 612 | if (angular.isUndefined(template) && !templateUrl) { 613 | throw formlyUsability.getFieldError( 614 | 'type-type-has-no-template', 615 | `Type '${options.type}' has no template. On element:`, options 616 | ) 617 | } 618 | 619 | return getTemplate(templateUrl || template, angular.isUndefined(template), options) 620 | } 621 | 622 | 623 | function getTemplate(template, isUrl, options) { 624 | let templatePromise 625 | if (angular.isFunction(template)) { 626 | templatePromise = $q.when(template(options)) 627 | } else { 628 | templatePromise = $q.when(template) 629 | } 630 | 631 | if (!isUrl) { 632 | return templatePromise 633 | } else { 634 | const httpOptions = {cache: $templateCache} 635 | return templatePromise 636 | .then((url) => $http.get(url, httpOptions)) 637 | .then((response) => response.data) 638 | .catch(function handleErrorGettingATemplate(error) { 639 | formlyWarn( 640 | 'problem-loading-template-for-templateurl', 641 | 'Problem loading template for ' + template, 642 | error 643 | ) 644 | }) 645 | } 646 | } 647 | 648 | function transcludeInWrappers(options, formOptions) { 649 | const wrapper = getWrapperOption(options, formOptions) 650 | 651 | return function transcludeTemplate(template) { 652 | if (!wrapper.length) { 653 | return $q.when(template) 654 | } 655 | 656 | wrapper.forEach((aWrapper) => { 657 | formlyUsability.checkWrapper(aWrapper, options) 658 | runApiCheck(aWrapper, options) 659 | }) 660 | const promises = wrapper.map(w => getTemplate(w.template || w.templateUrl, !w.template)) 661 | return $q.all(promises).then(wrappersTemplates => { 662 | wrappersTemplates.forEach((wrapperTemplate, index) => { 663 | formlyUsability.checkWrapperTemplate(wrapperTemplate, wrapper[index]) 664 | }) 665 | wrappersTemplates.reverse() // wrapper 0 is wrapped in wrapper 1 and so on... 666 | let totalWrapper = wrappersTemplates.shift() 667 | wrappersTemplates.forEach(wrapperTemplate => { 668 | totalWrapper = doTransclusion(totalWrapper, wrapperTemplate) 669 | }) 670 | return doTransclusion(totalWrapper, template) 671 | }) 672 | } 673 | } 674 | 675 | function doTransclusion(wrapper, template) { 676 | const superWrapper = angular.element('') // this allows people not have to have a single root in wrappers 677 | superWrapper.append(wrapper) 678 | let transcludeEl = superWrapper.find('formly-transclude') 679 | if (!transcludeEl.length) { 680 | // try it using our custom find function 681 | transcludeEl = formlyUtil.findByNodeName(superWrapper, 'formly-transclude') 682 | } 683 | transcludeEl.replaceWith(template) 684 | return superWrapper.html() 685 | } 686 | 687 | function getWrapperOption(options, formOptions) { 688 | /* eslint complexity:[2, 6] */ 689 | let wrapper = options.wrapper 690 | // explicit null means no wrapper 691 | if (wrapper === null) { 692 | return [] 693 | } 694 | 695 | // nothing specified means use the default wrapper for the type 696 | if (!wrapper) { 697 | // get all wrappers that specify they apply to this type 698 | wrapper = arrayify(formlyConfig.getWrapperByType(options.type)) 699 | } else { 700 | wrapper = arrayify(wrapper).map(formlyConfig.getWrapper) 701 | } 702 | 703 | // get all wrappers for that the type specified that it uses. 704 | const type = formlyConfig.getType(options.type, true, options) 705 | if (type && type.wrapper) { 706 | const typeWrappers = arrayify(type.wrapper).map(formlyConfig.getWrapper) 707 | wrapper = wrapper.concat(typeWrappers) 708 | } 709 | 710 | // add form wrappers 711 | if (formOptions.wrapper) { 712 | const formWrappers = arrayify(formOptions.wrapper).map(formlyConfig.getWrapper) 713 | wrapper = wrapper.concat(formWrappers) 714 | } 715 | 716 | // add the default wrapper last 717 | const defaultWrapper = formlyConfig.getWrapper() 718 | if (defaultWrapper) { 719 | wrapper.push(defaultWrapper) 720 | } 721 | return wrapper 722 | } 723 | 724 | function checkApi(options) { 725 | formlyApiCheck.throw(formlyApiCheck.formlyFieldOptions, options, { 726 | prefix: 'formly-field directive', 727 | url: 'formly-field-directive-validation-failed', 728 | }) 729 | // validate with the type 730 | const type = options.type && formlyConfig.getType(options.type) 731 | if (type) { 732 | runApiCheck(type, options, true) 733 | } 734 | if (options.expressionProperties && options.expressionProperties.hide) { 735 | formlyWarn( 736 | 'dont-use-expressionproperties.hide-use-hideexpression-instead', 737 | 'You have specified `hide` in `expressionProperties`. Use `hideExpression` instead', 738 | options 739 | ) 740 | } 741 | } 742 | 743 | function checkFieldGroupApi(options) { 744 | formlyApiCheck.throw(formlyApiCheck.fieldGroup, options, { 745 | prefix: 'formly-field directive', 746 | url: 'formly-field-directive-validation-failed', 747 | }) 748 | } 749 | 750 | function runApiCheck({apiCheck, apiCheckInstance, apiCheckFunction, apiCheckOptions}, options, forType) { 751 | runApiCheckForType(apiCheck, apiCheckInstance, apiCheckFunction, apiCheckOptions, options) 752 | if (forType && options.type) { 753 | angular.forEach(formlyConfig.getTypeHeritage(options.type), function(type) { 754 | runApiCheckForType(type.apiCheck, type.apiCheckInstance, type.apiCheckFunction, type.apiCheckOptions, options) 755 | }) 756 | } 757 | } 758 | 759 | function runApiCheckForType(apiCheck, apiCheckInstance, apiCheckFunction, apiCheckOptions, options) { 760 | /* eslint complexity:[2, 9] */ 761 | if (!apiCheck) { 762 | return 763 | } 764 | const instance = apiCheckInstance || formlyConfig.extras.apiCheckInstance || formlyApiCheck 765 | if (instance.config.disabled || apiCheckFactory.globalConfig.disabled) { 766 | return 767 | } 768 | const fn = apiCheckFunction || 'warn' 769 | // this is the new API 770 | const checkerObjects = apiCheck(instance) 771 | angular.forEach(checkerObjects, (shape, name) => { 772 | const checker = instance.shape(shape) 773 | const checkOptions = angular.extend({ 774 | prefix: `formly-field type ${options.type} for property ${name}`, 775 | url: formlyApiCheck.config.output.docsBaseUrl + 'formly-field-type-apicheck-failed', 776 | }, apiCheckOptions) 777 | instance[fn](checker, options[name], checkOptions) 778 | }) 779 | } 780 | 781 | 782 | } 783 | 784 | 785 | // Stateless util functions 786 | function getDefaultOptionsOptionsTypes(type) { 787 | return getDefaultOptionsProperty(type, 'optionsTypes', []) 788 | } 789 | 790 | function getDefaultOptionsProperty(type, prop, defaultValue) { 791 | return type.defaultOptions && type.defaultOptions[prop] || defaultValue 792 | } 793 | -------------------------------------------------------------------------------- /src/directives/formly-focus.js: -------------------------------------------------------------------------------- 1 | export default formlyFocus 2 | 3 | // @ngInject 4 | function formlyFocus($timeout, $document) { 5 | return { 6 | restrict: 'A', 7 | link: function formlyFocusLink(scope, element, attrs) { 8 | let previousEl = null 9 | const el = element[0] 10 | const doc = $document[0] 11 | attrs.$observe('formlyFocus', function respondToFocusExpressionChange(value) { 12 | /* eslint no-bitwise:0 */ // I know what I'm doing. I promise... 13 | if (value === 'true') { 14 | $timeout(function setElementFocus() { 15 | previousEl = doc.activeElement 16 | el.focus() 17 | }, ~~attrs.focusWait) 18 | } else if (value === 'false') { 19 | if (doc.activeElement === el) { 20 | el.blur() 21 | if (attrs.hasOwnProperty('refocus') && previousEl) { 22 | previousEl.focus() 23 | } 24 | } 25 | } 26 | }) 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/directives/formly-focus.test.js: -------------------------------------------------------------------------------- 1 | /* eslint max-len:[2,200] */ 2 | import _ from 'lodash' 3 | 4 | describe('formly-form', () => { 5 | let $compile, scope, el, node, input, textarea, $timeout 6 | 7 | const basicTemplate = '
' 8 | beforeEach(window.module('formly')) 9 | 10 | beforeEach(inject((_$compile_, _$timeout_, $rootScope) => { 11 | $compile = _$compile_ 12 | $timeout = _$timeout_ 13 | scope = $rootScope.$new() 14 | })) 15 | 16 | it(`focus the element when focus is set to "true" and then blurred when it's set to "false"`, () => { 17 | compileAndDigest() 18 | expectFocus(false) 19 | scope.focus = true 20 | scope.$digest() 21 | $timeout.flush() 22 | expectFocus(true) 23 | 24 | scope.focus = false 25 | scope.$digest() 26 | expectFocus(false) 27 | }) 28 | 29 | it(`should not bother unfocusing the element if it doesn't have focus to begin with`, () => { 30 | compileAndDigest() 31 | expectFocus(false) 32 | scope.focus = false 33 | scope.$digest() 34 | expectFocus(false) 35 | }) 36 | 37 | it(`should refocus the previously focused element`, () => { 38 | compileAndDigest() 39 | textarea.focus() 40 | scope.focus = true 41 | scope.$digest() 42 | $timeout.flush() 43 | expectFocus(true) 44 | 45 | scope.focus = false 46 | scope.$digest() 47 | expectFocus(false) 48 | expectFocus(true, textarea) 49 | }) 50 | 51 | it(`should not refocus the previously focused element when the refocus attribute doesn't exist`, () => { 52 | compileAndDigest( 53 | '
' 54 | ) 55 | textarea.focus() 56 | scope.focus = true 57 | scope.$digest() 58 | $timeout.flush() 59 | expectFocus(true) 60 | 61 | scope.focus = false 62 | scope.$digest() 63 | expectFocus(false) 64 | expectFocus(false, textarea) 65 | }) 66 | 67 | it(`should not refocus if the previously active element has not been set`, () => { 68 | compileAndDigest(undefined, {focus: false}) 69 | expectFocus(false) 70 | }) 71 | 72 | afterEach(() => { 73 | if (document.body.contains(node)) { 74 | node.parentNode.removeChild(node) 75 | } 76 | }) 77 | 78 | function expectFocus(focus, focusedNode = input) { 79 | const isFocused = document.activeElement === focusedNode 80 | if (focus) { 81 | expect(isFocused, 'expected focused element to be: ' + focusedNode + ' but it was ' + document.activeElement).to.be.true 82 | } else { 83 | expect(isFocused, 'expected focused element to not be: ' + focusedNode + ' but it was ' + document.activeElement).to.be.false 84 | } 85 | } 86 | 87 | 88 | function compileAndDigest(template = basicTemplate, scopeOverrides = {}) { 89 | _.assign(scope, scopeOverrides) 90 | el = $compile(template)(scope) 91 | scope.$digest() 92 | node = el[0] 93 | document.body.appendChild(node) 94 | input = document.getElementById('main-input') 95 | textarea = document.getElementById('textarea') 96 | return el 97 | } 98 | 99 | }) 100 | -------------------------------------------------------------------------------- /src/directives/formly-form.controller.js: -------------------------------------------------------------------------------- 1 | import angular from 'angular-fix' 2 | 3 | function isFieldGroup(field) { 4 | return field && !!field.fieldGroup 5 | } 6 | 7 | // @ngInject 8 | export default function FormlyFormController( 9 | formlyUsability, formlyWarn, formlyConfig, $parse, $scope, formlyApiCheck, formlyUtil) { 10 | 11 | setupOptions() 12 | $scope.model = $scope.model || {} 13 | setupFields() 14 | 15 | // watch the model and evaluate watch expressions that depend on it. 16 | if (!$scope.options.manualModelWatcher) { 17 | $scope.$watch('model', onModelOrFormStateChange, true) 18 | } else if (angular.isFunction($scope.options.manualModelWatcher)) { 19 | $scope.$watch($scope.options.manualModelWatcher, onModelOrFormStateChange, true) 20 | } 21 | 22 | if ($scope.options.formState) { 23 | $scope.$watch('options.formState', onModelOrFormStateChange, true) 24 | } 25 | 26 | function onModelOrFormStateChange() { 27 | angular.forEach($scope.fields, runFieldExpressionProperties) 28 | } 29 | 30 | function validateFormControl(formControl, promise) { 31 | const validate = formControl.$validate 32 | if (promise) { 33 | promise.then(() => validate.apply(formControl)) 34 | } else { 35 | validate() 36 | } 37 | } 38 | 39 | function runFieldExpressionProperties(field, index) { 40 | const model = field.model || $scope.model 41 | const promise = field.runExpressions && field.runExpressions() 42 | if (field.hideExpression) { // can't use hide with expressionProperties reliably 43 | const val = model[field.key] 44 | field.hide = evalCloseToFormlyExpression(field.hideExpression, val, field, index, {model}) 45 | } 46 | if (field.extras && field.extras.validateOnModelChange && field.formControl) { 47 | if (angular.isArray(field.formControl)) { 48 | angular.forEach(field.formControl, function(formControl) { 49 | validateFormControl(formControl, promise) 50 | }) 51 | } else { 52 | validateFormControl(field.formControl, promise) 53 | } 54 | } 55 | } 56 | 57 | function setupFields() { 58 | $scope.fields = $scope.fields || [] 59 | 60 | checkDeprecatedOptions($scope.options) 61 | 62 | let fieldTransforms = $scope.options.fieldTransform || formlyConfig.extras.fieldTransform 63 | 64 | if (!angular.isArray(fieldTransforms)) { 65 | fieldTransforms = [fieldTransforms] 66 | } 67 | 68 | angular.forEach(fieldTransforms, function transformFields(fieldTransform) { 69 | if (fieldTransform) { 70 | $scope.fields = fieldTransform($scope.fields, $scope.model, $scope.options, $scope.form) 71 | if (!$scope.fields) { 72 | throw formlyUsability.getFormlyError('fieldTransform must return an array of fields') 73 | } 74 | } 75 | }) 76 | 77 | setupModels() 78 | 79 | if ($scope.options.watchAllExpressions) { 80 | angular.forEach($scope.fields, setupHideExpressionWatcher) 81 | } 82 | 83 | angular.forEach($scope.fields, attachKey) // attaches a key based on the index if a key isn't specified 84 | angular.forEach($scope.fields, setupWatchers) // setup watchers for all fields 85 | } 86 | 87 | function checkDeprecatedOptions(options) { 88 | if (formlyConfig.extras.fieldTransform && angular.isFunction(formlyConfig.extras.fieldTransform)) { 89 | formlyWarn( 90 | 'fieldtransform-as-a-function-deprecated', 91 | 'fieldTransform as a function has been deprecated.', 92 | `Attempted for formlyConfig.extras: ${formlyConfig.extras.fieldTransform.name}`, 93 | formlyConfig.extras 94 | ) 95 | } else if (options.fieldTransform && angular.isFunction(options.fieldTransform)) { 96 | formlyWarn( 97 | 'fieldtransform-as-a-function-deprecated', 98 | 'fieldTransform as a function has been deprecated.', 99 | `Attempted for form`, 100 | options 101 | ) 102 | } 103 | } 104 | 105 | function setupOptions() { 106 | formlyApiCheck.throw( 107 | [formlyApiCheck.formOptionsApi.optional], [$scope.options], {prefix: 'formly-form options check'} 108 | ) 109 | $scope.options = $scope.options || {} 110 | $scope.options.formState = $scope.options.formState || {} 111 | 112 | angular.extend($scope.options, { 113 | updateInitialValue, 114 | resetModel, 115 | }) 116 | 117 | } 118 | 119 | function updateInitialValue() { 120 | angular.forEach($scope.fields, field => { 121 | if (isFieldGroup(field) && field.options) { 122 | field.options.updateInitialValue() 123 | } else { 124 | field.updateInitialValue() 125 | } 126 | }) 127 | } 128 | 129 | function resetModel() { 130 | angular.forEach($scope.fields, field => { 131 | if (isFieldGroup(field) && field.options) { 132 | field.options.resetModel() 133 | } else if (field.resetModel) { 134 | field.resetModel() 135 | } 136 | }) 137 | } 138 | 139 | function setupModels() { 140 | // a set of field models that are already watched (the $scope.model will have its own watcher) 141 | const watchedModels = [$scope.model] 142 | // we will not set up automatic model watchers if manual mode is set 143 | const manualModelWatcher = $scope.options.manualModelWatcher 144 | 145 | if ($scope.options.formState) { 146 | // $scope.options.formState will have its own watcher 147 | watchedModels.push($scope.options.formState) 148 | } 149 | 150 | angular.forEach($scope.fields, (field) => { 151 | const isNewModel = initModel(field) 152 | 153 | if (field.model && isNewModel && watchedModels.indexOf(field.model) === -1 && !manualModelWatcher) { 154 | $scope.$watch(() => field.model, onModelOrFormStateChange, true) 155 | watchedModels.push(field.model) 156 | } 157 | }) 158 | } 159 | 160 | function setupHideExpressionWatcher(field, index) { 161 | if (field.hideExpression) { // can't use hide with expressionProperties reliably 162 | const model = field.model || $scope.model 163 | $scope.$watch(function hideExpressionWatcher() { 164 | const val = model[field.key] 165 | return evalCloseToFormlyExpression(field.hideExpression, val, field, index, {model}) 166 | }, (hide) => field.hide = hide, true) 167 | } 168 | } 169 | 170 | function initModel(field) { 171 | let isNewModel = true 172 | 173 | if (angular.isString(field.model)) { 174 | const expression = field.model 175 | 176 | isNewModel = !referencesCurrentlyWatchedModel(expression) 177 | 178 | field.model = resolveStringModel(expression) 179 | 180 | $scope.$watch(() => resolveStringModel(expression), (model) => field.model = model) 181 | } 182 | 183 | return isNewModel 184 | 185 | function resolveStringModel(expression) { 186 | const index = $scope.fields.indexOf(field) 187 | const model = evalCloseToFormlyExpression(expression, undefined, field, index, {model: $scope.model}) 188 | 189 | if (!model) { 190 | throw formlyUsability.getFieldError( 191 | 'field-model-must-be-initialized', 192 | 'Field model must be initialized. When specifying a model as a string for a field, the result of the' + 193 | ' expression must have been initialized ahead of time.', 194 | field) 195 | } 196 | 197 | return model 198 | } 199 | } 200 | 201 | function referencesCurrentlyWatchedModel(expression) { 202 | return ['model', 'formState'].some(item => { 203 | return formlyUtil.startsWith(expression, `${item}.`) || formlyUtil.startsWith(expression, `${item}[`) 204 | }) 205 | } 206 | 207 | function attachKey(field, index) { 208 | if (!isFieldGroup(field)) { 209 | field.key = field.key || index || 0 210 | } 211 | } 212 | 213 | function setupWatchers(field, index) { 214 | if (!angular.isDefined(field.watcher)) { 215 | return 216 | } 217 | let watchers = field.watcher 218 | if (!angular.isArray(watchers)) { 219 | watchers = [watchers] 220 | } 221 | angular.forEach(watchers, function setupWatcher(watcher) { 222 | if (!angular.isDefined(watcher.listener) && !watcher.runFieldExpressions) { 223 | throw formlyUsability.getFieldError( 224 | 'all-field-watchers-must-have-a-listener', 225 | 'All field watchers must have a listener', field 226 | ) 227 | } 228 | const watchExpression = getWatchExpression(watcher, field, index) 229 | const watchListener = getWatchListener(watcher, field, index) 230 | 231 | const type = watcher.type || '$watch' 232 | watcher.stopWatching = $scope[type](watchExpression, watchListener, watcher.watchDeep) 233 | }) 234 | } 235 | 236 | function getWatchExpression(watcher, field, index) { 237 | let watchExpression 238 | if (!angular.isUndefined(watcher.expression)) { 239 | watchExpression = watcher.expression 240 | } else if (field.key) { 241 | watchExpression = 'model[\'' + field.key.toString().split('.').join('\'][\'') + '\']' 242 | } 243 | if (angular.isFunction(watchExpression)) { 244 | // wrap the field's watch expression so we can call it with the field as the first arg 245 | // and the stop function as the last arg as a helper 246 | const originalExpression = watchExpression 247 | watchExpression = function formlyWatchExpression() { 248 | const args = modifyArgs(watcher, index, ...arguments) 249 | return originalExpression(...args) 250 | } 251 | watchExpression.displayName = `Formly Watch Expression for field for ${field.key}` 252 | } else if (field.model) { 253 | watchExpression = $parse(watchExpression).bind(null, $scope, {model: field.model}) 254 | } 255 | return watchExpression 256 | } 257 | 258 | function getWatchListener(watcher, field, index) { 259 | let watchListener = watcher.listener 260 | if (angular.isFunction(watchListener) || watcher.runFieldExpressions) { 261 | // wrap the field's watch listener so we can call it with the field as the first arg 262 | // and the stop function as the last arg as a helper 263 | const originalListener = watchListener 264 | watchListener = function formlyWatchListener() { 265 | let value 266 | if (originalListener) { 267 | const args = modifyArgs(watcher, index, ...arguments) 268 | value = originalListener(...args) 269 | } 270 | if (watcher.runFieldExpressions) { 271 | runFieldExpressionProperties(field, index) 272 | } 273 | return value 274 | } 275 | watchListener.displayName = `Formly Watch Listener for field for ${field.key}` 276 | } 277 | return watchListener 278 | } 279 | 280 | function modifyArgs(watcher, index, ...originalArgs) { 281 | return [$scope.fields[index], ...originalArgs, watcher.stopWatching] 282 | } 283 | 284 | function evalCloseToFormlyExpression(expression, val, field, index, extraLocals = {}) { 285 | extraLocals = angular.extend(getFormlyFieldLikeLocals(field, index), extraLocals) 286 | return formlyUtil.formlyEval($scope, expression, val, val, extraLocals) 287 | } 288 | 289 | function getFormlyFieldLikeLocals(field, index) { 290 | // this makes it closer to what a regular formlyExpression would be 291 | return { 292 | model: field.model, 293 | options: field, 294 | index, 295 | formState: $scope.options.formState, 296 | originalModel: $scope.model, 297 | formOptions: $scope.options, 298 | formId: $scope.formId, 299 | } 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /src/directives/formly-form.controller.test.js: -------------------------------------------------------------------------------- 1 | /* eslint no-shadow:0 */ 2 | /* eslint no-console:0 */ 3 | /* eslint max-len:0 */ 4 | /* eslint max-nested-callbacks:0 */ 5 | import {getNewField, shouldWarnWithLog} from '../test.utils.js' 6 | import _ from 'lodash' 7 | 8 | describe('FormlyFormController', () => { 9 | let $controller, formlyConfig, scope 10 | beforeEach(window.module('formly')) 11 | beforeEach(inject((_formlyConfig_, $rootScope, _$controller_) => { 12 | formlyConfig = _formlyConfig_ 13 | scope = $rootScope.$new() 14 | scope.model = {} 15 | scope.fields = [] 16 | scope.options = {} 17 | $controller = _$controller_ 18 | })) 19 | 20 | describe(`fieldTransform`, () => { 21 | beforeEach(() => { 22 | formlyConfig.extras.fieldTransform = fieldTransform 23 | }) 24 | 25 | it(`should give a deprecation warning when formlyConfig.extras.fieldTransform is a function rather than an array`, inject(($log) => { 26 | 27 | shouldWarnWithLog( 28 | $log, 29 | [ 30 | 'Formly Warning:', 31 | 'fieldTransform as a function has been deprecated.', 32 | /Attempted for formlyConfig.extras/, 33 | ], 34 | () => $controller('FormlyFormController', {$scope: scope}) 35 | ) 36 | })) 37 | 38 | it(`should give a deprecation warning when options.fieldTransform function rather than an array`, inject(($log) => { 39 | formlyConfig.extras.fieldTransform = undefined 40 | scope.fields = [getNewField()] 41 | scope.options.fieldTransform = fields => fields 42 | shouldWarnWithLog( 43 | $log, 44 | [ 45 | 'Formly Warning:', 46 | 'fieldTransform as a function has been deprecated.', 47 | 'Attempted for form', 48 | ], 49 | () => $controller('FormlyFormController', {$scope: scope}) 50 | ) 51 | })) 52 | 53 | it(`should throw an error if something is passed in and nothing is returned`, () => { 54 | scope.fields = [getNewField()] 55 | scope.options.fieldTransform = function() { 56 | // I return nothing... 57 | } 58 | expect(() => $controller('FormlyFormController', {$scope: scope})).to.throw(/^Formly Error: fieldTransform must return an array of fields/) 59 | }) 60 | 61 | it(`should allow you to transform field configuration`, () => { 62 | scope.options.fieldTransform = fieldTransform 63 | const spy = sinon.spy(scope.options, 'fieldTransform') 64 | doExpectations(spy) 65 | }) 66 | 67 | it(`should use formlyConfig.extras.fieldTransform when not specified on options`, () => { 68 | const spy = sinon.spy(formlyConfig.extras, 'fieldTransform') 69 | doExpectations(spy) 70 | }) 71 | 72 | it(`should allow you to use an array of transform functions`, () => { 73 | scope.fields = [getNewField({ 74 | customThing: 'foo', 75 | otherCustomThing: { 76 | whatever: '|-o-|', 77 | }})] 78 | scope.options.fieldTransform = [fieldTransform] 79 | expect(() => $controller('FormlyFormController', {$scope: scope})).to.not.throw() 80 | 81 | const field = scope.fields[0] 82 | expect(field).to.have.deep.property('data.customThing') 83 | expect(field).to.have.deep.property('data.otherCustomThing') 84 | }) 85 | 86 | function doExpectations(spy) { 87 | const originalFields = [{ 88 | key: 'keyProp', 89 | template: '
', 90 | customThing: 'foo', 91 | otherCustomThing: { 92 | whatever: '|-o-|', 93 | }, 94 | }] 95 | scope.fields = originalFields 96 | $controller('FormlyFormController', {$scope: scope}) 97 | expect(spy).to.have.been.calledWith(originalFields, scope.model, scope.options, scope.form) 98 | const field = scope.fields[0] 99 | 100 | expect(field).to.not.have.property('customThing') 101 | expect(field).to.not.have.property('otherCustomThing') 102 | expect(field).to.have.deep.property('data.customThing') 103 | expect(field).to.have.deep.property('data.otherCustomThing') 104 | } 105 | 106 | function fieldTransform(fields) { 107 | const extraKeys = ['customThing', 'otherCustomThing'] 108 | return _.map(fields, field => { 109 | const newField = {data: {}} 110 | _.each(field, (val, name) => { 111 | if (_.includes(extraKeys, name)) { 112 | newField.data[name] = val 113 | } else { 114 | newField[name] = val 115 | } 116 | }) 117 | return newField 118 | }) 119 | } 120 | }) 121 | 122 | }) 123 | -------------------------------------------------------------------------------- /src/directives/formly-form.js: -------------------------------------------------------------------------------- 1 | import angular from 'angular-fix' 2 | 3 | export default formlyForm 4 | 5 | /** 6 | * @ngdoc directive 7 | * @name formlyForm 8 | * @restrict AE 9 | */ 10 | // @ngInject 11 | function formlyForm(formlyUsability, formlyWarn, $parse, formlyConfig, $interpolate) { 12 | let currentFormId = 1 13 | return { 14 | restrict: 'AE', 15 | template: formlyFormGetTemplate, 16 | replace: true, 17 | transclude: true, 18 | scope: { 19 | fields: '=', 20 | model: '=', 21 | form: '=?', 22 | options: '=?', 23 | }, 24 | controller: 'FormlyFormController', 25 | link: formlyFormLink, 26 | } 27 | 28 | function formlyFormGetTemplate(el, attrs) { 29 | const rootEl = getRootEl() 30 | const fieldRootEl = getFieldRootEl() 31 | const formId = `formly_${currentFormId++}` 32 | let parentFormAttributes = '' 33 | if (attrs.hasOwnProperty('isFieldGroup') && el.parent().parent().hasClass('formly')) { 34 | parentFormAttributes = copyAttributes(el.parent().parent()[0].attributes) 35 | } 36 | return ` 37 | <${rootEl} class="formly" 38 | name="${getFormName()}" 39 | role="form" ${parentFormAttributes}> 40 | <${fieldRootEl} formly-field 41 | ng-repeat="field in fields ${getTrackBy()}" 42 | ${getHideDirective()}="!field.hide" 43 | class="formly-field" 44 | options="field" 45 | model="field.model || model" 46 | original-model="model" 47 | fields="fields" 48 | form="theFormlyForm" 49 | form-id="${getFormName()}" 50 | form-state="options.formState" 51 | form-options="options" 52 | index="$index"> 53 | 54 |
55 | 56 | ` 57 | 58 | function getRootEl() { 59 | return attrs.rootEl || 'ng-form' 60 | } 61 | 62 | function getFieldRootEl() { 63 | return attrs.fieldRootEl || 'div' 64 | } 65 | 66 | function getHideDirective() { 67 | return attrs.hideDirective || formlyConfig.extras.defaultHideDirective || 'ng-if' 68 | } 69 | 70 | function getTrackBy() { 71 | if (!attrs.trackBy) { 72 | return '' 73 | } else { 74 | return `track by ${attrs.trackBy}` 75 | } 76 | } 77 | 78 | function getFormName() { 79 | let formName = formId 80 | const bindName = attrs.bindName 81 | if (bindName) { 82 | if (angular.version.minor < 3) { 83 | throw formlyUsability.getFormlyError('bind-name attribute on formly-form not allowed in < angular 1.3') 84 | } 85 | // we can do a one-time binding here because we know we're in 1.3.x territory 86 | formName = `${$interpolate.startSymbol()}::'formly_' + ${bindName}${$interpolate.endSymbol()}` 87 | } 88 | return formName 89 | } 90 | 91 | function getTranscludeClass() { 92 | return attrs.transcludeClass || '' 93 | } 94 | 95 | function copyAttributes(attributes) { 96 | const excluded = ['model', 'form', 'fields', 'options', 'name', 'role', 'class', 97 | 'data-model', 'data-form', 'data-fields', 'data-options', 'data-name'] 98 | const arrayAttrs = [] 99 | angular.forEach(attributes, ({nodeName, value}) => { 100 | if (nodeName !== 'undefined' && excluded.indexOf(nodeName) === -1) { 101 | arrayAttrs.push(`${toKebabCase(nodeName)}="${value}"`) 102 | } 103 | }) 104 | return arrayAttrs.join(' ') 105 | } 106 | } 107 | 108 | function formlyFormLink(scope, el, attrs) { 109 | setFormController() 110 | fixChromeAutocomplete() 111 | 112 | function setFormController() { 113 | const formId = attrs.name 114 | scope.formId = formId 115 | scope.theFormlyForm = scope[formId] 116 | if (attrs.form) { 117 | const getter = $parse(attrs.form) 118 | const setter = getter.assign 119 | const parentForm = getter(scope.$parent) 120 | if (parentForm) { 121 | scope.theFormlyForm = parentForm 122 | if (scope[formId]) { 123 | scope.theFormlyForm.$removeControl(scope[formId]) 124 | } 125 | 126 | // this next line is probably one of the more dangerous things that angular-formly does to improve the 127 | // API for angular-formly forms. It ensures that the NgModelControllers inside of formly-form will be 128 | // attached to the form that is passed to formly-form rather than the one that formly-form creates 129 | // this is necessary because it's confusing to have a step between the form you pass in 130 | // and the fields in that form. It also is because angular doesn't propagate properties like $submitted down 131 | // to children forms :-( This line was added to solve this issue: 132 | // https://github.com/formly-js/angular-formly/issues/287 133 | // luckily, this is how the formController has been accessed by the NgModelController since angular 1.0.0 134 | // so I expect it will remain this way for the life of angular 1.x 135 | el.removeData('$formController') 136 | } else { 137 | setter(scope.$parent, scope[formId]) 138 | } 139 | } 140 | if (!scope.theFormlyForm && !formlyConfig.disableWarnings) { 141 | /* eslint no-console:0 */ 142 | formlyWarn( 143 | 'formly-form-has-no-formcontroller', 144 | 'Your formly-form does not have a `form` property. Many functions of the form (like validation) may not work', 145 | el, 146 | scope 147 | ) 148 | } 149 | } 150 | 151 | /* 152 | * chrome autocomplete lameness 153 | * see https://code.google.com/p/chromium/issues/detail?id=468153#c14 154 | * ლ(ಠ益ಠლ) (╯°□°)╯︵ ┻━┻ (◞‸◟;) 155 | */ 156 | function fixChromeAutocomplete() { 157 | const global = formlyConfig.extras.removeChromeAutoComplete === true 158 | const offInstance = scope.options && scope.options.removeChromeAutoComplete === false 159 | const onInstance = scope.options && scope.options.removeChromeAutoComplete === true 160 | if ((global && !offInstance) || onInstance) { 161 | const input = document.createElement('input') 162 | input.setAttribute('autocomplete', 'address-level4') 163 | input.setAttribute('hidden', 'true') 164 | el[0].appendChild(input) 165 | } 166 | 167 | } 168 | } 169 | 170 | 171 | // stateless util functions 172 | function toKebabCase(string) { 173 | if (string) { 174 | return string.replace(/([A-Z])/g, $1 => '-' + $1.toLowerCase()) 175 | } else { 176 | return '' 177 | } 178 | } 179 | 180 | } 181 | -------------------------------------------------------------------------------- /src/index.common.js: -------------------------------------------------------------------------------- 1 | import angular from 'angular-fix' 2 | 3 | import formlyApiCheck from './providers/formlyApiCheck' 4 | import formlyErrorAndWarningsUrlPrefix from './other/docsBaseUrl' 5 | import formlyUsability from './providers/formlyUsability' 6 | import formlyConfig from './providers/formlyConfig' 7 | import formlyValidationMessages from './providers/formlyValidationMessages' 8 | import formlyUtil from './services/formlyUtil' 9 | import formlyWarn from './services/formlyWarn' 10 | 11 | import formlyCustomValidation from './directives/formly-custom-validation' 12 | import formlyField from './directives/formly-field' 13 | import formlyFocus from './directives/formly-focus' 14 | import formlyForm from './directives/formly-form' 15 | import FormlyFormController from './directives/formly-form.controller' 16 | 17 | import formlyNgModelAttrsManipulator from './run/formlyNgModelAttrsManipulator' 18 | import formlyCustomTags from './run/formlyCustomTags' 19 | 20 | const ngModuleName = 'formly' 21 | 22 | export default ngModuleName 23 | 24 | const ngModule = angular.module(ngModuleName, []) 25 | 26 | ngModule.constant('formlyApiCheck', formlyApiCheck) 27 | ngModule.constant('formlyErrorAndWarningsUrlPrefix', formlyErrorAndWarningsUrlPrefix) 28 | ngModule.constant('formlyVersion', VERSION) // <-- webpack variable 29 | 30 | ngModule.provider('formlyUsability', formlyUsability) 31 | ngModule.provider('formlyConfig', formlyConfig) 32 | 33 | ngModule.factory('formlyValidationMessages', formlyValidationMessages) 34 | ngModule.factory('formlyUtil', formlyUtil) 35 | ngModule.factory('formlyWarn', formlyWarn) 36 | 37 | ngModule.directive('formlyCustomValidation', formlyCustomValidation) 38 | ngModule.directive('formlyField', formlyField) 39 | ngModule.directive('formlyFocus', formlyFocus) 40 | ngModule.directive('formlyForm', formlyForm) 41 | ngModule.controller('FormlyFormController', FormlyFormController) 42 | 43 | ngModule.run(formlyNgModelAttrsManipulator) 44 | ngModule.run(formlyCustomTags) 45 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import index from './index.common' 2 | export default index 3 | -------------------------------------------------------------------------------- /src/index.test.js: -------------------------------------------------------------------------------- 1 | // Load up the angular formly module 2 | import index from './index.common' 3 | 4 | // Bring in the test suites 5 | import './providers/formlyApiCheck.test' 6 | import './providers/formlyConfig.test' 7 | import './services/formlyUtil.test' 8 | import './directives/formly-custom-validation.test' 9 | import './directives/formly-field.test' 10 | import './directives/formly-focus.test' 11 | import './directives/formly-form.test' 12 | import './directives/formly-form.controller.test' 13 | import './run/formlyCustomTags.test' 14 | import './run/formlyNgModelAttrsManipulator.test' 15 | import './other/utils.test' 16 | 17 | export default index 18 | -------------------------------------------------------------------------------- /src/other/docsBaseUrl.js: -------------------------------------------------------------------------------- 1 | export default `https://github.com/formly-js/angular-formly/blob/${VERSION}/other/ERRORS_AND_WARNINGS.md#` 2 | -------------------------------------------------------------------------------- /src/other/utils.js: -------------------------------------------------------------------------------- 1 | import angular from 'angular-fix' 2 | 3 | export default { 4 | containsSelector, containsSpecialChar, formlyEval, getFieldId, reverseDeepMerge, findByNodeName, 5 | arrayify, extendFunction, extendArray, startsWith, contains, 6 | } 7 | 8 | function containsSelector(string) { 9 | return containsSpecialChar(string, '.') || (containsSpecialChar(string, '[') && containsSpecialChar(string, ']')) 10 | } 11 | 12 | function containsSpecialChar(a, b) { 13 | if (!a || !a.indexOf) { 14 | return false 15 | } 16 | return a.indexOf(b) !== -1 17 | } 18 | 19 | 20 | function formlyEval(scope, expression, $modelValue, $viewValue, extraLocals) { 21 | if (angular.isFunction(expression)) { 22 | return expression($viewValue, $modelValue, scope, extraLocals) 23 | } else { 24 | return scope.$eval(expression, angular.extend({$viewValue, $modelValue}, extraLocals)) 25 | } 26 | } 27 | 28 | function getFieldId(formId, options, index) { 29 | if (options.id) { 30 | return options.id 31 | } 32 | let type = options.type 33 | if (!type && options.template) { 34 | type = 'template' 35 | } else if (!type && options.templateUrl) { 36 | type = 'templateUrl' 37 | } 38 | 39 | return [formId, type, options.key, index].join('_') 40 | } 41 | 42 | 43 | function reverseDeepMerge(dest) { 44 | angular.forEach(arguments, (src, index) => { 45 | if (!index) { 46 | return 47 | } 48 | angular.forEach(src, (val, prop) => { 49 | if (!angular.isDefined(dest[prop])) { 50 | dest[prop] = angular.copy(val) 51 | } else if (objAndSameType(dest[prop], val)) { 52 | reverseDeepMerge(dest[prop], val) 53 | } 54 | }) 55 | }) 56 | return dest 57 | } 58 | 59 | function objAndSameType(obj1, obj2) { 60 | return angular.isObject(obj1) && angular.isObject(obj2) && 61 | Object.getPrototypeOf(obj1) === Object.getPrototypeOf(obj2) 62 | } 63 | 64 | // recurse down a node tree to find a node with matching nodeName, for custom tags jQuery.find doesn't work in IE8 65 | function findByNodeName(el, nodeName) { 66 | if (!el.prop) { // not a jQuery or jqLite object -> wrap it 67 | el = angular.element(el) 68 | } 69 | 70 | if (el.prop('nodeName') === nodeName.toUpperCase()) { 71 | return el 72 | } 73 | 74 | const c = el.children() 75 | for (let i = 0; c && i < c.length; i++) { 76 | const node = findByNodeName(c[i], nodeName) 77 | if (node) { 78 | return node 79 | } 80 | } 81 | } 82 | 83 | 84 | function arrayify(obj) { 85 | if (obj && !angular.isArray(obj)) { 86 | obj = [obj] 87 | } else if (!obj) { 88 | obj = [] 89 | } 90 | return obj 91 | } 92 | 93 | 94 | function extendFunction(...fns) { 95 | return function extendedFunction() { 96 | const args = arguments 97 | fns.forEach(fn => fn.apply(null, args)) 98 | } 99 | } 100 | 101 | function extendArray(primary, secondary, property) { 102 | if (property) { 103 | primary = primary[property] 104 | secondary = secondary[property] 105 | } 106 | if (secondary && primary) { 107 | angular.forEach(secondary, function(item) { 108 | if (primary.indexOf(item) === -1) { 109 | primary.push(item) 110 | } 111 | }) 112 | return primary 113 | } else if (secondary) { 114 | return secondary 115 | } else { 116 | return primary 117 | } 118 | } 119 | 120 | function startsWith(str, search) { 121 | if (angular.isString(str) && angular.isString(search)) { 122 | return str.length >= search.length && str.substring(0, search.length) === search 123 | } else { 124 | return false 125 | } 126 | } 127 | 128 | function contains(str, search) { 129 | if (angular.isString(str) && angular.isString(search)) { 130 | return str.length >= search.length && str.indexOf(search) !== -1 131 | } else { 132 | return false 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/other/utils.test.js: -------------------------------------------------------------------------------- 1 | /* eslint no-unused-vars:0 */ 2 | import utils from './utils.js' 3 | 4 | // gotta do this because webstorm/jshint doesn't like destructuring imports :-( 5 | const {extendFunction, startsWith} = utils 6 | 7 | 8 | describe(`utils`, () => { 9 | 10 | describe(`extendFunction`, () => { 11 | let fn1, fn2, fn3 12 | beforeEach(() => { 13 | fn1 = sinon.spy() 14 | fn2 = sinon.spy() 15 | fn3 = sinon.spy() 16 | }) 17 | 18 | it(`should call all functions with the given`, () => { 19 | const extended = extendFunction(fn1, fn2) 20 | extended('foo') 21 | 22 | expect(fn1).to.have.been.calledWith('foo') 23 | }) 24 | }) 25 | 26 | describe(`startsWith`, () => { 27 | it(`should return true if a string has a given prefix`, () => { 28 | expect(startsWith('fooBar', 'foo')).to.be.true 29 | }) 30 | 31 | it(`should return false if a string does not have a given prefix`, () => { 32 | expect(startsWith('fooBar', 'nah')).to.be.false 33 | }) 34 | 35 | it(`should return false if no a string`, () => { 36 | expect(startsWith(undefined, 'foo')).to.be.false 37 | expect(startsWith(5, 'foo')).to.be.false 38 | expect(startsWith('foo', undefined)).to.be.false 39 | expect(startsWith('foo', 5)).to.be.false 40 | expect(startsWith(undefined, undefined)).to.be.false 41 | }) 42 | }) 43 | 44 | }) 45 | -------------------------------------------------------------------------------- /src/providers/formlyApiCheck.js: -------------------------------------------------------------------------------- 1 | import angular from 'angular-fix' 2 | import apiCheckFactory from 'api-check' 3 | 4 | const apiCheck = apiCheckFactory({ 5 | output: { 6 | prefix: 'angular-formly:', 7 | docsBaseUrl: require('../other/docsBaseUrl'), 8 | }, 9 | }) 10 | 11 | function shapeRequiredIfNot(otherProps, propChecker) { 12 | if (!angular.isArray(otherProps)) { 13 | otherProps = [otherProps] 14 | } 15 | const type = `specified if these are not specified: \`${otherProps.join(', ')}\` (otherwise it's optional)` 16 | 17 | function shapeRequiredIfNotDefinition(prop, propName, location, obj) { 18 | const propExists = obj && obj.hasOwnProperty(propName) 19 | const otherPropsExist = otherProps.some(function(otherProp) { 20 | return obj && obj.hasOwnProperty(otherProp) 21 | }) 22 | if (!otherPropsExist && !propExists) { 23 | return apiCheck.utils.getError(propName, location, type) 24 | } else if (propExists) { 25 | return propChecker(prop, propName, location, obj) 26 | } 27 | } 28 | 29 | shapeRequiredIfNotDefinition.type = type 30 | return apiCheck.utils.checkerHelpers.setupChecker(shapeRequiredIfNotDefinition) 31 | } 32 | 33 | const formlyExpression = apiCheck.oneOfType([apiCheck.string, apiCheck.func]) 34 | const specifyWrapperType = apiCheck.typeOrArrayOf(apiCheck.string).nullable 35 | 36 | const apiCheckProperty = apiCheck.func 37 | 38 | const apiCheckInstanceProperty = apiCheck.shape.onlyIf('apiCheck', apiCheck.func.withProperties({ 39 | warn: apiCheck.func, 40 | throw: apiCheck.func, 41 | shape: apiCheck.func, 42 | })) 43 | 44 | const apiCheckFunctionProperty = apiCheck.shape.onlyIf('apiCheck', apiCheck.oneOf(['throw', 'warn'])) 45 | 46 | const formlyWrapperType = apiCheck.shape({ 47 | name: shapeRequiredIfNot('types', apiCheck.string).optional, 48 | template: apiCheck.shape.ifNot('templateUrl', apiCheck.string).optional, 49 | templateUrl: apiCheck.shape.ifNot('template', apiCheck.string).optional, 50 | types: apiCheck.typeOrArrayOf(apiCheck.string).optional, 51 | overwriteOk: apiCheck.bool.optional, 52 | apiCheck: apiCheckProperty.optional, 53 | apiCheckInstance: apiCheckInstanceProperty.optional, 54 | apiCheckFunction: apiCheckFunctionProperty.optional, 55 | apiCheckOptions: apiCheck.object.optional, 56 | }).strict 57 | 58 | const expressionProperties = apiCheck.objectOf(apiCheck.oneOfType([ 59 | formlyExpression, 60 | apiCheck.shape({ 61 | expression: formlyExpression, 62 | message: formlyExpression.optional, 63 | }).strict, 64 | ])) 65 | 66 | const modelChecker = apiCheck.oneOfType([apiCheck.string, apiCheck.object]) 67 | 68 | const templateManipulators = apiCheck.shape({ 69 | preWrapper: apiCheck.arrayOf(apiCheck.func).nullable.optional, 70 | postWrapper: apiCheck.arrayOf(apiCheck.func).nullable.optional, 71 | }).strict.nullable 72 | 73 | const validatorChecker = apiCheck.objectOf(apiCheck.oneOfType([ 74 | formlyExpression, apiCheck.shape({ 75 | expression: formlyExpression, 76 | message: formlyExpression.optional, 77 | }).strict, 78 | ])) 79 | 80 | const watcherChecker = apiCheck.typeOrArrayOf( 81 | apiCheck.shape({ 82 | expression: formlyExpression.optional, 83 | listener: formlyExpression.optional, 84 | runFieldExpressions: apiCheck.bool.optional, 85 | }) 86 | ) 87 | 88 | const fieldOptionsApiShape = { 89 | $$hashKey: apiCheck.any.optional, 90 | type: apiCheck.shape.ifNot(['template', 'templateUrl'], apiCheck.string).optional, 91 | template: apiCheck.shape.ifNot( 92 | ['type', 'templateUrl'], 93 | apiCheck.oneOfType([apiCheck.string, apiCheck.func]) 94 | ).optional, 95 | templateUrl: apiCheck.shape.ifNot( 96 | ['type', 'template'], 97 | apiCheck.oneOfType([apiCheck.string, apiCheck.func]) 98 | ).optional, 99 | key: apiCheck.oneOfType([apiCheck.string, apiCheck.number]).optional, 100 | model: modelChecker.optional, 101 | originalModel: modelChecker.optional, 102 | className: apiCheck.string.optional, 103 | id: apiCheck.string.optional, 104 | name: apiCheck.string.optional, 105 | expressionProperties: expressionProperties.optional, 106 | extras: apiCheck.shape({ 107 | validateOnModelChange: apiCheck.bool.optional, 108 | skipNgModelAttrsManipulator: apiCheck.oneOfType([ 109 | apiCheck.string, apiCheck.bool, 110 | ]).optional, 111 | }).strict.optional, 112 | data: apiCheck.object.optional, 113 | templateOptions: apiCheck.object.optional, 114 | wrapper: specifyWrapperType.optional, 115 | modelOptions: apiCheck.shape({ 116 | updateOn: apiCheck.string.optional, 117 | debounce: apiCheck.oneOfType([ 118 | apiCheck.objectOf(apiCheck.number), apiCheck.number, 119 | ]).optional, 120 | allowInvalid: apiCheck.bool.optional, 121 | getterSetter: apiCheck.bool.optional, 122 | timezone: apiCheck.string.optional, 123 | }).optional, 124 | watcher: watcherChecker.optional, 125 | validators: validatorChecker.optional, 126 | asyncValidators: validatorChecker.optional, 127 | parsers: apiCheck.arrayOf(formlyExpression).optional, 128 | formatters: apiCheck.arrayOf(formlyExpression).optional, 129 | noFormControl: apiCheck.bool.optional, 130 | hide: apiCheck.bool.optional, 131 | hideExpression: formlyExpression.optional, 132 | ngModelElAttrs: apiCheck.objectOf(apiCheck.string).optional, 133 | ngModelAttrs: apiCheck.objectOf(apiCheck.shape({ 134 | statement: apiCheck.shape.ifNot(['value', 'attribute', 'bound', 'boolean'], apiCheck.any).optional, 135 | value: apiCheck.shape.ifNot('statement', apiCheck.any).optional, 136 | attribute: apiCheck.shape.ifNot('statement', apiCheck.any).optional, 137 | bound: apiCheck.shape.ifNot('statement', apiCheck.any).optional, 138 | boolean: apiCheck.shape.ifNot('statement', apiCheck.any).optional, 139 | }).strict).optional, 140 | elementAttributes: apiCheck.objectOf(apiCheck.string).optional, 141 | optionsTypes: apiCheck.typeOrArrayOf(apiCheck.string).optional, 142 | link: apiCheck.func.optional, 143 | controller: apiCheck.oneOfType([ 144 | apiCheck.string, apiCheck.func, apiCheck.array, 145 | ]).optional, 146 | validation: apiCheck.shape({ 147 | show: apiCheck.bool.nullable.optional, 148 | messages: apiCheck.objectOf(formlyExpression).optional, 149 | errorExistsAndShouldBeVisible: apiCheck.bool.optional, 150 | }).optional, 151 | formControl: apiCheck.typeOrArrayOf(apiCheck.object).optional, 152 | value: apiCheck.func.optional, 153 | runExpressions: apiCheck.func.optional, 154 | templateManipulators: templateManipulators.optional, 155 | resetModel: apiCheck.func.optional, 156 | updateInitialValue: apiCheck.func.optional, 157 | initialValue: apiCheck.any.optional, 158 | defaultValue: apiCheck.any.optional, 159 | } 160 | 161 | 162 | const formlyFieldOptions = apiCheck.shape(fieldOptionsApiShape).strict 163 | 164 | const formOptionsApi = apiCheck.shape({ 165 | formState: apiCheck.object.optional, 166 | resetModel: apiCheck.func.optional, 167 | updateInitialValue: apiCheck.func.optional, 168 | removeChromeAutoComplete: apiCheck.bool.optional, 169 | parseKeyArrays: apiCheck.bool.optional, 170 | templateManipulators: templateManipulators.optional, 171 | manualModelWatcher: apiCheck.oneOfType([apiCheck.bool, apiCheck.func]).optional, 172 | watchAllExpressions: apiCheck.bool.optional, 173 | wrapper: specifyWrapperType.optional, 174 | fieldTransform: apiCheck.oneOfType([ 175 | apiCheck.func, apiCheck.array, 176 | ]).optional, 177 | data: apiCheck.object.optional, 178 | }).strict 179 | 180 | 181 | const fieldGroup = apiCheck.shape({ 182 | $$hashKey: apiCheck.any.optional, 183 | key: apiCheck.oneOfType([apiCheck.string, apiCheck.number]).optional, 184 | // danger. Nested field groups wont get api-checked... 185 | fieldGroup: apiCheck.arrayOf(apiCheck.oneOfType([formlyFieldOptions, apiCheck.object])), 186 | className: apiCheck.string.optional, 187 | options: formOptionsApi.optional, 188 | templateOptions: apiCheck.object.optional, 189 | wrapper: specifyWrapperType.optional, 190 | watcher: watcherChecker.optional, 191 | hide: apiCheck.bool.optional, 192 | hideExpression: formlyExpression.optional, 193 | data: apiCheck.object.optional, 194 | model: modelChecker.optional, 195 | form: apiCheck.object.optional, 196 | elementAttributes: apiCheck.objectOf(apiCheck.string).optional, 197 | }).strict 198 | 199 | const typeOptionsDefaultOptions = angular.copy(fieldOptionsApiShape) 200 | typeOptionsDefaultOptions.key = apiCheck.string.optional 201 | 202 | const formlyTypeOptions = apiCheck.shape({ 203 | name: apiCheck.string, 204 | template: apiCheck.shape.ifNot('templateUrl', apiCheck.oneOfType([apiCheck.string, apiCheck.func])).optional, 205 | templateUrl: apiCheck.shape.ifNot('template', apiCheck.oneOfType([apiCheck.string, apiCheck.func])).optional, 206 | controller: apiCheck.oneOfType([ 207 | apiCheck.func, apiCheck.string, apiCheck.array, 208 | ]).optional, 209 | link: apiCheck.func.optional, 210 | defaultOptions: apiCheck.oneOfType([ 211 | apiCheck.func, apiCheck.shape(typeOptionsDefaultOptions), 212 | ]).optional, 213 | extends: apiCheck.string.optional, 214 | wrapper: specifyWrapperType.optional, 215 | data: apiCheck.object.optional, 216 | apiCheck: apiCheckProperty.optional, 217 | apiCheckInstance: apiCheckInstanceProperty.optional, 218 | apiCheckFunction: apiCheckFunctionProperty.optional, 219 | apiCheckOptions: apiCheck.object.optional, 220 | overwriteOk: apiCheck.bool.optional, 221 | }).strict 222 | 223 | angular.extend(apiCheck, { 224 | formlyTypeOptions, formlyFieldOptions, formlyExpression, formlyWrapperType, fieldGroup, formOptionsApi, 225 | }) 226 | 227 | export default apiCheck 228 | -------------------------------------------------------------------------------- /src/providers/formlyApiCheck.test.js: -------------------------------------------------------------------------------- 1 | /* jshint maxlen:false */ 2 | 3 | describe('formlyApiCheck', () => { 4 | beforeEach(window.module('formly')) 5 | 6 | let formlyApiCheck 7 | 8 | beforeEach(inject((_formlyApiCheck_) => { 9 | formlyApiCheck = _formlyApiCheck_ 10 | })) 11 | 12 | describe('formlyFieldOptions', () => { 13 | it(`should pass when validation.messages is an object of functions or strings`, () => { 14 | expectPass({ 15 | key: '♪┏(・o・)┛♪┗ ( ・o・) ┓♪', 16 | template: 'hi', 17 | validation: { 18 | messages: { 19 | thing1() { 20 | }, 21 | thing2: '"Formly Expression"', 22 | }, 23 | }, 24 | }, 'formlyFieldOptions') 25 | }) 26 | 27 | it(`should allow $$hashKey`, () => { 28 | expectPass({ 29 | $$hashKey: 'object:1', 30 | template: 'hello', 31 | key: 'whatevs', 32 | }, 'formlyFieldOptions') 33 | }) 34 | 35 | describe('ngModelAttrs', () => { 36 | it(`should allow property of 'boolean'`, () => { 37 | expectPass({ 38 | template: 'hello', 39 | key: 'whatevs', 40 | templateOptions: { 41 | foo: 'bar', 42 | }, 43 | ngModelAttrs: { 44 | foo: { 45 | boolean: 'foo-bar', 46 | }, 47 | }, 48 | }, 'formlyFieldOptions') 49 | }) 50 | }) 51 | }) 52 | 53 | describe(`fieldGroup`, () => { 54 | it(`should pass when specifying data`, () => { 55 | expectPass({ 56 | fieldGroup: [], 57 | data: {foo: 'bar'}, 58 | }, 'fieldGroup') 59 | }) 60 | }) 61 | 62 | describe(`extras`, () => { 63 | describe(`skipNgModelAttrsManipulator`, () => { 64 | it(`should pass with a boolean`, () => { 65 | expectPass({ 66 | template: 'foo', 67 | extras: {skipNgModelAttrsManipulator: true}, 68 | }, 'formlyFieldOptions') 69 | }) 70 | 71 | it(`should pass with a string`, () => { 72 | expectPass({ 73 | template: 'foo', 74 | extras: {skipNgModelAttrsManipulator: '.selector'}, 75 | }, 'formlyFieldOptions') 76 | }) 77 | 78 | it(`should pass with nothing`, () => { 79 | expectPass({ 80 | template: 'foo', 81 | extras: {skipNgModelAttrsManipulator: '.selector'}, 82 | }, 'formlyFieldOptions') 83 | }) 84 | 85 | it(`should fail with anything else`, () => { 86 | expectFail({ 87 | template: 'foo', 88 | extras: {skipNgModelAttrsManipulator: 32}, 89 | }, 'formlyFieldOptions') 90 | }) 91 | }) 92 | }) 93 | 94 | function expectPass(options, checker) { 95 | const result = formlyApiCheck[checker](options) 96 | expect(result).to.be.undefined 97 | } 98 | 99 | function expectFail(options, checker) { 100 | const result = formlyApiCheck[checker](options) 101 | expect(result).to.be.an.instanceOf(Error) 102 | } 103 | 104 | }) 105 | -------------------------------------------------------------------------------- /src/providers/formlyConfig.js: -------------------------------------------------------------------------------- 1 | import angular from 'angular-fix' 2 | import utils from '../other/utils' 3 | 4 | export default formlyConfig 5 | 6 | // @ngInject 7 | function formlyConfig(formlyUsabilityProvider, formlyErrorAndWarningsUrlPrefix, formlyApiCheck) { 8 | 9 | const typeMap = {} 10 | const templateWrappersMap = {} 11 | const defaultWrapperName = 'default' 12 | const _this = this 13 | const getError = formlyUsabilityProvider.getFormlyError 14 | 15 | angular.extend(this, { 16 | setType, 17 | getType, 18 | getTypes, 19 | getTypeHeritage, 20 | setWrapper, 21 | getWrapper, 22 | getWrapperByType, 23 | removeWrapperByName, 24 | removeWrappersForType, 25 | disableWarnings: false, 26 | extras: { 27 | disableNgModelAttrsManipulator: false, 28 | fieldTransform: [], 29 | ngModelAttrsManipulatorPreferUnbound: false, 30 | removeChromeAutoComplete: false, 31 | parseKeyArrays: false, 32 | defaultHideDirective: 'ng-if', 33 | getFieldId: null, 34 | }, 35 | templateManipulators: { 36 | preWrapper: [], 37 | postWrapper: [], 38 | }, 39 | $get: () => this, 40 | }) 41 | 42 | function setType(options) { 43 | if (angular.isArray(options)) { 44 | const allTypes = [] 45 | angular.forEach(options, item => { 46 | allTypes.push(setType(item)) 47 | }) 48 | return allTypes 49 | } else if (angular.isObject(options)) { 50 | checkType(options) 51 | if (options.extends) { 52 | extendTypeOptions(options) 53 | } 54 | typeMap[options.name] = options 55 | return typeMap[options.name] 56 | } else { 57 | throw getError(`You must provide an object or array for setType. You provided: ${JSON.stringify(arguments)}`) 58 | } 59 | } 60 | 61 | function checkType(options) { 62 | formlyApiCheck.throw(formlyApiCheck.formlyTypeOptions, options, { 63 | prefix: 'formlyConfig.setType', 64 | url: 'settype-validation-failed', 65 | }) 66 | if (!options.overwriteOk) { 67 | checkOverwrite(options.name, typeMap, options, 'types') 68 | } else { 69 | options.overwriteOk = undefined 70 | } 71 | } 72 | 73 | function extendTypeOptions(options) { 74 | const extendsType = getType(options.extends, true, options) 75 | extendTypeControllerFunction(options, extendsType) 76 | extendTypeLinkFunction(options, extendsType) 77 | extendTypeDefaultOptions(options, extendsType) 78 | utils.reverseDeepMerge(options, extendsType) 79 | extendTemplate(options, extendsType) 80 | } 81 | 82 | function extendTemplate(options, extendsType) { 83 | if (options.template && extendsType.templateUrl) { 84 | delete options.templateUrl 85 | } else if (options.templateUrl && extendsType.template) { 86 | delete options.template 87 | } 88 | } 89 | 90 | function extendTypeControllerFunction(options, extendsType) { 91 | const extendsCtrl = extendsType.controller 92 | if (!angular.isDefined(extendsCtrl)) { 93 | return 94 | } 95 | const optionsCtrl = options.controller 96 | if (angular.isDefined(optionsCtrl)) { 97 | options.controller = function($scope, $controller) { 98 | $controller(extendsCtrl, {$scope}) 99 | $controller(optionsCtrl, {$scope}) 100 | } 101 | options.controller.$inject = ['$scope', '$controller'] 102 | } else { 103 | options.controller = extendsCtrl 104 | } 105 | } 106 | 107 | function extendTypeLinkFunction(options, extendsType) { 108 | const extendsFn = extendsType.link 109 | if (!angular.isDefined(extendsFn)) { 110 | return 111 | } 112 | const optionsFn = options.link 113 | if (angular.isDefined(optionsFn)) { 114 | options.link = function() { 115 | extendsFn(...arguments) 116 | optionsFn(...arguments) 117 | } 118 | } else { 119 | options.link = extendsFn 120 | } 121 | } 122 | 123 | function extendTypeDefaultOptions(options, extendsType) { 124 | const extendsDO = extendsType.defaultOptions 125 | if (!angular.isDefined(extendsDO)) { 126 | return 127 | } 128 | const optionsDO = options.defaultOptions || {} 129 | const optionsDOIsFn = angular.isFunction(optionsDO) 130 | const extendsDOIsFn = angular.isFunction(extendsDO) 131 | if (extendsDOIsFn) { 132 | options.defaultOptions = function defaultOptions(opts, scope) { 133 | const extendsDefaultOptions = extendsDO(opts, scope) 134 | const mergedDefaultOptions = {} 135 | utils.reverseDeepMerge(mergedDefaultOptions, opts, extendsDefaultOptions) 136 | let extenderOptionsDefaultOptions = optionsDO 137 | if (optionsDOIsFn) { 138 | extenderOptionsDefaultOptions = extenderOptionsDefaultOptions(mergedDefaultOptions, scope) 139 | } 140 | utils.reverseDeepMerge(extenderOptionsDefaultOptions, extendsDefaultOptions) 141 | return extenderOptionsDefaultOptions 142 | } 143 | } else if (optionsDOIsFn) { 144 | options.defaultOptions = function defaultOptions(opts, scope) { 145 | const newDefaultOptions = {} 146 | utils.reverseDeepMerge(newDefaultOptions, opts, extendsDO) 147 | return optionsDO(newDefaultOptions, scope) 148 | } 149 | } 150 | } 151 | 152 | function getType(name, throwError, errorContext) { 153 | if (!name) { 154 | return undefined 155 | } 156 | const type = typeMap[name] 157 | if (!type && throwError === true) { 158 | throw getError( 159 | `There is no type by the name of "${name}": ${JSON.stringify(errorContext)}` 160 | ) 161 | } else { 162 | return type 163 | } 164 | } 165 | 166 | function getTypes() { 167 | return typeMap 168 | } 169 | 170 | function getTypeHeritage(parent) { 171 | const heritage = [] 172 | let type = parent 173 | if (angular.isString(type)) { 174 | type = getType(parent) 175 | } 176 | parent = type.extends 177 | while (parent) { 178 | type = getType(parent) 179 | heritage.push(type) 180 | parent = type.extends 181 | } 182 | return heritage 183 | } 184 | 185 | 186 | function setWrapper(options, name) { 187 | if (angular.isArray(options)) { 188 | return options.map(wrapperOptions => setWrapper(wrapperOptions)) 189 | } else if (angular.isObject(options)) { 190 | options.types = getOptionsTypes(options) 191 | options.name = getOptionsName(options, name) 192 | checkWrapperAPI(options) 193 | templateWrappersMap[options.name] = options 194 | return options 195 | } else if (angular.isString(options)) { 196 | return setWrapper({ 197 | template: options, 198 | name, 199 | }) 200 | } 201 | } 202 | 203 | function getOptionsTypes(options) { 204 | if (angular.isString(options.types)) { 205 | return [options.types] 206 | } 207 | if (!angular.isDefined(options.types)) { 208 | return [] 209 | } else { 210 | return options.types 211 | } 212 | } 213 | 214 | function getOptionsName(options, name) { 215 | return options.name || name || options.types.join(' ') || defaultWrapperName 216 | } 217 | 218 | function checkWrapperAPI(options) { 219 | formlyUsabilityProvider.checkWrapper(options) 220 | if (options.template) { 221 | formlyUsabilityProvider.checkWrapperTemplate(options.template, options) 222 | } 223 | if (!options.overwriteOk) { 224 | checkOverwrite(options.name, templateWrappersMap, options, 'templateWrappers') 225 | } else { 226 | delete options.overwriteOk 227 | } 228 | checkWrapperTypes(options) 229 | } 230 | 231 | function checkWrapperTypes(options) { 232 | const shouldThrow = !angular.isArray(options.types) || !options.types.every(angular.isString) 233 | if (shouldThrow) { 234 | throw getError(`Attempted to create a template wrapper with types that is not a string or an array of strings`) 235 | } 236 | } 237 | 238 | function checkOverwrite(property, object, newValue, objectName) { 239 | if (object.hasOwnProperty(property)) { 240 | warn('overwriting-types-or-wrappers', [ 241 | `Attempting to overwrite ${property} on ${objectName} which is currently`, 242 | `${JSON.stringify(object[property])} with ${JSON.stringify(newValue)}`, 243 | `To supress this warning, specify the property "overwriteOk: true"`, 244 | ].join(' ')) 245 | } 246 | } 247 | 248 | function getWrapper(name) { 249 | return templateWrappersMap[name || defaultWrapperName] 250 | } 251 | 252 | function getWrapperByType(type) { 253 | /* eslint prefer-const:0 */ 254 | const wrappers = [] 255 | for (let name in templateWrappersMap) { 256 | if (templateWrappersMap.hasOwnProperty(name)) { 257 | if (templateWrappersMap[name].types && templateWrappersMap[name].types.indexOf(type) !== -1) { 258 | wrappers.push(templateWrappersMap[name]) 259 | } 260 | } 261 | } 262 | return wrappers 263 | } 264 | 265 | function removeWrapperByName(name) { 266 | const wrapper = templateWrappersMap[name] 267 | delete templateWrappersMap[name] 268 | return wrapper 269 | } 270 | 271 | function removeWrappersForType(type) { 272 | const wrappers = getWrapperByType(type) 273 | if (!wrappers) { 274 | return undefined 275 | } 276 | if (!angular.isArray(wrappers)) { 277 | return removeWrapperByName(wrappers.name) 278 | } else { 279 | wrappers.forEach((wrapper) => removeWrapperByName(wrapper.name)) 280 | return wrappers 281 | } 282 | } 283 | 284 | 285 | function warn() { 286 | if (!_this.disableWarnings && console.warn) { 287 | /* eslint no-console:0 */ 288 | const args = Array.prototype.slice.call(arguments) 289 | const warnInfoSlug = args.shift() 290 | args.unshift('Formly Warning:') 291 | args.push(`${formlyErrorAndWarningsUrlPrefix}${warnInfoSlug}`) 292 | console.warn(...args) 293 | } 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /src/providers/formlyConfig.test.js: -------------------------------------------------------------------------------- 1 | /* eslint max-len:0 */ 2 | /* eslint max-nested-callbacks:0 */ 3 | /* eslint no-shadow:0 */ 4 | /* eslint no-console:0 */ 5 | /* eslint no-unused-vars:0 */ 6 | import angular from 'angular-fix' 7 | import testUtils from '../test.utils.js' 8 | 9 | const {getNewField, basicForm, shouldWarn, shouldNotWarn} = testUtils 10 | 11 | describe('formlyConfig', () => { 12 | beforeEach(window.module('formly')) 13 | 14 | let formlyConfig 15 | 16 | beforeEach(inject((_formlyConfig_) => { 17 | formlyConfig = _formlyConfig_ 18 | })) 19 | 20 | describe('setWrapper/getWrapper', () => { 21 | let getterFn, setterFn, $log 22 | const template = 'This is my template' 23 | const templateUrl = '/path/to/my/template.html' 24 | const typesString = 'checkbox' 25 | const types = ['text', 'textarea'] 26 | const name = 'hi' 27 | const name2 = 'name2' 28 | const template2 = template + '2' 29 | beforeEach(inject(function(_$log_) { 30 | getterFn = formlyConfig.getWrapper 31 | setterFn = formlyConfig.setWrapper 32 | $log = _$log_ 33 | })) 34 | 35 | describe('\(^O^)/ path', () => { 36 | describe('the default template', () => { 37 | 38 | it('can be a string without a name', () => { 39 | setterFn(template) 40 | expect(getterFn()).to.eql({name: 'default', template, types: []}) 41 | }) 42 | 43 | it('can be a string with a name', () => { 44 | setterFn(template, name) 45 | expect(getterFn(name)).to.eql({name, template, types: []}) 46 | }) 47 | 48 | it('can be an object with a template', () => { 49 | setterFn({template}) 50 | expect(getterFn()).to.eql({name: 'default', template, types: []}) 51 | }) 52 | 53 | it('can be an object with a template and a name', () => { 54 | setterFn({template, name}) 55 | expect(getterFn(name)).to.eql({name, template, types: []}) 56 | }) 57 | 58 | it('can be an object with a templateUrl', () => { 59 | setterFn({templateUrl}) 60 | expect(getterFn()).to.eql({name: 'default', templateUrl, types: []}) 61 | }) 62 | 63 | it('can be an object with a templateUrl and a name', () => { 64 | setterFn({name, templateUrl}) 65 | expect(getterFn(name)).to.eql({name, templateUrl, types: []}) 66 | }) 67 | 68 | it('can be an array of objects with names, urls, and/or templates', () => { 69 | setterFn([ 70 | {templateUrl}, 71 | {name, template}, 72 | {name: name2, template: template2}, 73 | ]) 74 | expect(getterFn()).to.eql({templateUrl, name: 'default', types: []}) 75 | expect(getterFn(name)).to.eql({template, name, types: []}) 76 | expect(getterFn(name2)).to.eql({template: template2, name: name2, types: []}) 77 | }) 78 | 79 | it('can specify types as a string (using types as the name when not specified)', () => { 80 | setterFn({types: typesString, template}) 81 | expect(getterFn(typesString)).to.eql({template, name: typesString, types: [typesString]}) 82 | }) 83 | 84 | it('can specify types as an array (using types as the name when not specified)', () => { 85 | setterFn({types, template}) 86 | expect(getterFn(types.join(' '))).to.eql({template, name: types.join(' '), types}) 87 | }) 88 | }) 89 | }) 90 | 91 | describe('(◞‸◟;) path', () => { 92 | it('should throw an error when providing both a template and templateUrl', () => { 93 | expect(() => setterFn({template, templateUrl}, name)).to.throw(/`template` must be `ifNot\[templateUrl]`/i) 94 | }) 95 | 96 | it('should throw an error when the template does not use formly-transclude', () => { 97 | const error = /templates.*?must.*?<\/formly-transclude>/ 98 | expect(() => setterFn({template: 'no formly-transclude'})).to.throw(error) 99 | }) 100 | 101 | it('should throw an error when specifying an array type where not all items are strings', () => { 102 | const error = /types.*?typeOrArrayOf.*?String.*?/i 103 | expect(() => setterFn({template, types: ['hi', 2, false, 'cool']})).to.throw(error) 104 | }) 105 | 106 | it('should warn when attempting to override a template wrapper', () => { 107 | shouldWarn(/overwrite/, function() { 108 | setterFn({template}) 109 | setterFn({template}) 110 | }) 111 | }) 112 | 113 | it('should not warn when attempting to override a template wrapper if overwriteOk is true', () => { 114 | shouldNotWarn(() => { 115 | setterFn({template}) 116 | setterFn({template, overwriteOk: true}) 117 | }) 118 | }) 119 | }) 120 | 121 | 122 | describe(`apiCheck`, () => { 123 | testApiCheck('setWrapper', 'getWrapper') 124 | }) 125 | 126 | }) 127 | 128 | describe('getWrapperByType', () => { 129 | let getterFn, setterFn 130 | const types = ['input', 'checkbox'] 131 | const types2 = ['input', 'select'] 132 | const templateUrl = '/path/to/file.html' 133 | beforeEach(inject(function(formlyConfig) { 134 | setterFn = formlyConfig.setWrapper 135 | getterFn = formlyConfig.getWrapperByType 136 | })) 137 | 138 | describe('\(^O^)/ path', () => { 139 | it('should return a template wrapper that has the same type', () => { 140 | const option = setterFn({templateUrl, types}) 141 | expect(getterFn(types[0])).to.eql([option]) 142 | }) 143 | 144 | it('should return an array when multiple wrappers have the same time', () => { 145 | setterFn({templateUrl, types}) 146 | setterFn({templateUrl, types: types2}) 147 | const inputWrappers = getterFn('input') 148 | expect(inputWrappers).to.be.instanceOf(Array) 149 | expect(inputWrappers).to.have.length(2) 150 | }) 151 | 152 | }) 153 | }) 154 | 155 | describe('removeWrapper', () => { 156 | let remove, removeForType, setterFn, getterFn, getByTypeFn 157 | const template = '
Something cool
' 158 | const name = 'name' 159 | const types = ['input', 'checkbox'] 160 | const types2 = ['input', 'something else'] 161 | const types3 = ['checkbox', 'something else'] 162 | beforeEach(inject((formlyConfig) => { 163 | remove = formlyConfig.removeWrapperByName 164 | removeForType = formlyConfig.removeWrappersForType 165 | setterFn = formlyConfig.setWrapper 166 | getterFn = formlyConfig.getWrapper 167 | getByTypeFn = formlyConfig.getWrapperByType 168 | })) 169 | 170 | it('should allow you to remove a wrapper', () => { 171 | setterFn(template, name) 172 | remove(name) 173 | expect(getterFn(name)).to.be.empty 174 | }) 175 | 176 | it('should allow you to remove a wrapper for a type', () => { 177 | setterFn({types, template}) 178 | setterFn({types: types2, template}) 179 | const checkboxAndSomethingElseWrapper = setterFn({types: types3, template}) 180 | removeForType('input') 181 | expect(getByTypeFn('input')).to.be.empty 182 | const checkboxWrappers = getByTypeFn('checkbox') 183 | expect(checkboxWrappers).to.eql([checkboxAndSomethingElseWrapper]) 184 | }) 185 | }) 186 | 187 | 188 | describe('setType/getType/getTypes', () => { 189 | let getterFn, setterFn, getTypesFn 190 | const name = 'input' 191 | const template = '' 192 | const templateUrl = '/input.html' 193 | const wrapper = 'input' 194 | const wrapper2 = 'input2' 195 | beforeEach(inject(function(formlyConfig) { 196 | getterFn = formlyConfig.getType 197 | setterFn = formlyConfig.setType 198 | getTypesFn = formlyConfig.getTypes 199 | })) 200 | 201 | describe('\(^O^)/ path', () => { 202 | it('should accept an object with a name and a template', () => { 203 | setterFn({name, template}) 204 | expect(getterFn(name).template).to.equal(template) 205 | }) 206 | 207 | it('should accept an object with a name and a templateUrl', () => { 208 | setterFn({name, templateUrl}) 209 | expect(getterFn(name).templateUrl).to.equal(templateUrl) 210 | }) 211 | 212 | it('should accept an array of objects', () => { 213 | setterFn([ 214 | {name, template}, 215 | {name: 'type2', templateUrl}, 216 | ]) 217 | expect(getterFn(name).template).to.equal(template) 218 | expect(getterFn('type2').templateUrl).to.equal(templateUrl) 219 | }) 220 | 221 | it('should expose the mapping from type name to config', () => { 222 | setterFn([ 223 | {name, template}, 224 | {name: 'type2', templateUrl}, 225 | ]) 226 | expect(getTypesFn()).to.eql({[name]: getterFn(name), type2: getterFn('type2')}) 227 | }) 228 | 229 | it('should allow you to set a wrapper as a string', () => { 230 | setterFn({name, template, wrapper}) 231 | expect(getterFn(name).wrapper).to.equal(wrapper) 232 | }) 233 | 234 | it('should allow you to set a wrapper as an array', () => { 235 | setterFn({name, template, wrapper: [wrapper, wrapper2]}) 236 | expect(getterFn(name).wrapper).to.eql([wrapper, wrapper2]) 237 | }) 238 | 239 | describe(`extends`, () => { 240 | describe(`object case`, () => { 241 | beforeEach(() => { 242 | setterFn([ 243 | { 244 | name, 245 | template, 246 | defaultOptions: { 247 | templateOptions: { 248 | required: true, 249 | min: 3, 250 | }, 251 | }, 252 | }, 253 | { 254 | name: 'type2', 255 | extends: name, 256 | defaultOptions: { 257 | templateOptions: { 258 | required: false, 259 | max: 4, 260 | }, 261 | data: { 262 | extraStuff: [1, 2, 3], 263 | }, 264 | }, 265 | }, 266 | ]) 267 | }) 268 | it(`should inherit all fields that it does not have itself`, () => { 269 | expect(getterFn('type2').template).to.eql(template) 270 | }) 271 | 272 | it(`should merge objects that it shares`, () => { 273 | expect(getterFn('type2').defaultOptions).to.eql({ 274 | templateOptions: { 275 | required: false, 276 | min: 3, 277 | max: 4, 278 | }, 279 | data: { 280 | extraStuff: [1, 2, 3], 281 | }, 282 | }) 283 | }) 284 | 285 | it(`should not error when extends is specified without a template, templateUrl, or defaultOptions`, () => { 286 | expect(() => setterFn({name: 'type3', extends: 'type2'})).to.not.throw() 287 | }) 288 | 289 | }) 290 | 291 | describe(`abstractType function case`, () => { 292 | beforeEach(() => { 293 | setterFn([ 294 | { 295 | name, 296 | template, 297 | defaultOptions: function(options) { 298 | return { 299 | templateOptions: { 300 | required: true, 301 | min: 3, 302 | }, 303 | } 304 | }, 305 | }, 306 | { 307 | name: 'type2', 308 | extends: name, 309 | defaultOptions: function(options) { 310 | return { 311 | templateOptions: { 312 | required: false, 313 | max: 4, 314 | }, 315 | } 316 | }, 317 | }, 318 | { 319 | name: 'type3', 320 | extends: name, 321 | defaultOptions: { 322 | templateOptions: { 323 | required: false, 324 | max: 4, 325 | }, 326 | }, 327 | }, 328 | ]) 329 | }) 330 | 331 | it(`should merge options when extending defaultOptions is a function`, () => { 332 | expect(getterFn('type2').defaultOptions({})).to.eql({ 333 | templateOptions: { 334 | required: false, 335 | min: 3, 336 | max: 4, 337 | }, 338 | }) 339 | }) 340 | 341 | it(`should merge options when extending defaultOptions is an object`, () => { 342 | expect(getterFn('type3').defaultOptions({})).to.eql({ 343 | templateOptions: { 344 | required: false, 345 | min: 3, 346 | max: 4, 347 | }, 348 | }) 349 | }) 350 | 351 | }) 352 | 353 | describe(`template/templateUrl Cases`, () => { 354 | it('should use templateUrl if type defines it and its parent has template defined', function() { 355 | setterFn([ 356 | { 357 | name, 358 | template, 359 | }, 360 | { 361 | name: 'type2', 362 | extends: name, 363 | templateUrl, 364 | }, 365 | ]) 366 | 367 | expect(getterFn('type2').templateUrl).not.to.be.undefined 368 | expect(getterFn('type2').template).to.be.undefined 369 | }) 370 | 371 | it('should use template if type defines it and its parent had templateUrl defined', function() { 372 | setterFn([ 373 | { 374 | name, 375 | templateUrl, 376 | }, 377 | { 378 | name: 'type2', 379 | extends: name, 380 | template, 381 | }, 382 | ]) 383 | 384 | expect(getterFn('type2').template).not.to.be.undefined 385 | expect(getterFn('type2').templateUrl).to.be.undefined 386 | }) 387 | }) 388 | 389 | describe(`function cases`, () => { 390 | let args, fakeScope, parentFn, childFn, parentDefaultOptions, childDefaultOptions, argsAndParent 391 | beforeEach(() => { 392 | args = {data: {someData: true}} 393 | fakeScope = {} 394 | parentDefaultOptions = { 395 | data: {extraOptions: true}, 396 | templateOptions: {placeholder: 'hi'}, 397 | } 398 | childDefaultOptions = { 399 | templateOptions: {placeholder: 'hey', required: true}, 400 | } 401 | parentFn = sinon.stub().returns(parentDefaultOptions) 402 | childFn = sinon.stub().returns(childDefaultOptions) 403 | argsAndParent = { 404 | data: {someData: true, extraOptions: true}, 405 | templateOptions: {placeholder: 'hi'}, 406 | } 407 | }) 408 | 409 | it(`should call the extended parent's defaultOptions function and its own defaultOptions function`, () => { 410 | setterFn([ 411 | {name, defaultOptions: parentFn}, 412 | {name: 'type2', extends: name, defaultOptions: childFn}, 413 | ]) 414 | getterFn('type2').defaultOptions(args, fakeScope) 415 | expect(parentFn).to.have.been.calledWith(args, fakeScope) 416 | expect(childFn).to.have.been.calledWith(argsAndParent, fakeScope) 417 | }) 418 | 419 | it(`should call the extended parent's defaultOptions function when it doesn't have one of its own`, () => { 420 | setterFn([ 421 | {name, defaultOptions: parentFn}, 422 | {name: 'type2', extends: name}, 423 | ]) 424 | getterFn('type2').defaultOptions(args, fakeScope) 425 | expect(parentFn).to.have.been.calledWith(args, fakeScope) 426 | }) 427 | 428 | it(`should call its own defaultOptions function when the parent doesn't have one`, () => { 429 | setterFn([ 430 | {name, template}, 431 | {name: 'type2', extends: name, defaultOptions: childFn}, 432 | ]) 433 | getterFn('type2').defaultOptions(args, fakeScope) 434 | expect(childFn).to.have.been.calledWith(args, fakeScope) 435 | }) 436 | 437 | it(`should extend its defaultOptions object with the parent's defaultOptions object`, () => { 438 | const objectMergedDefaultOptions = { 439 | data: {extraOptions: true}, 440 | templateOptions: {placeholder: 'hey', required: true}, 441 | } 442 | setterFn([ 443 | {name, defaultOptions: parentDefaultOptions}, 444 | {name: 'type2', extends: name, defaultOptions: childDefaultOptions}, 445 | ]) 446 | expect(getterFn('type2').defaultOptions).to.eql(objectMergedDefaultOptions) 447 | }) 448 | 449 | it(`should call its defaultOptions with the parent's defaultOptions object merged with the given args`, () => { 450 | setterFn([ 451 | {name, defaultOptions: parentDefaultOptions}, 452 | {name: 'type2', extends: name, defaultOptions: childFn}, 453 | ]) 454 | const returned = getterFn('type2').defaultOptions(args, fakeScope) 455 | expect(childFn).to.have.been.calledWith(argsAndParent, fakeScope) 456 | expect(returned).to.eql(childDefaultOptions) 457 | }) 458 | }) 459 | 460 | describe(`link functions`, () => { 461 | let linkArgs, parentFn, childFn 462 | beforeEach(inject(($rootScope) => { 463 | linkArgs = [$rootScope.$new(), angular.element('
'), {}] 464 | parentFn = sinon.spy() 465 | childFn = sinon.spy() 466 | })) 467 | 468 | it(`should call the parent link function when there is no child function`, () => { 469 | setterFn([ 470 | {name, template, link: parentFn}, 471 | {name: 'type2', extends: name}, 472 | ]) 473 | getterFn('type2').link(...linkArgs) 474 | expect(parentFn).to.have.been.calledWith(...linkArgs) 475 | }) 476 | 477 | it(`should call the child link function when there is no parent function`, () => { 478 | setterFn([ 479 | {name, template}, 480 | {name: 'type2', extends: name, link: childFn}, 481 | ]) 482 | getterFn('type2').link(...linkArgs) 483 | expect(childFn).to.have.been.calledWith(...linkArgs) 484 | }) 485 | 486 | it(`should call the child link function and the parent link function when they are both present`, () => { 487 | setterFn([ 488 | {name, template, link: parentFn}, 489 | {name: 'type2', extends: name, link: childFn}, 490 | ]) 491 | getterFn('type2').link(...linkArgs) 492 | expect(parentFn).to.have.been.calledWith(...linkArgs) 493 | expect(childFn).to.have.been.calledWith(...linkArgs) 494 | }) 495 | 496 | }) 497 | 498 | describe(`controller functions`, () => { 499 | let parentFn, childFn, $controller, $scope 500 | beforeEach(inject(($rootScope, _$controller_) => { 501 | $scope = $rootScope.$new() 502 | $controller = _$controller_ 503 | parentFn = sinon.spy() 504 | parentFn.$inject = ['$log'] 505 | childFn = sinon.spy() 506 | childFn.$inject = ['$http'] 507 | })) 508 | 509 | it(`should call the parent controller function when there is no child controller function`, inject(($log) => { 510 | setterFn([ 511 | {name, template, controller: parentFn}, 512 | {name: 'type2', extends: name}, 513 | ]) 514 | $controller(getterFn('type2').controller, {$scope}) 515 | expect(parentFn).to.have.been.calledWith($log) 516 | })) 517 | 518 | it(`should call the parent controller function and the child's when there is a child controller function`, inject(($log, $http) => { 519 | setterFn([ 520 | {name, template, controller: parentFn}, 521 | {name: 'type2', extends: name, controller: childFn}, 522 | ]) 523 | $controller(getterFn('type2').controller, {$scope}) 524 | expect(parentFn).to.have.been.calledWith($log) 525 | expect(childFn).to.have.been.calledWith($http) 526 | })) 527 | 528 | it(`should call the child controller function when there's no parent controller`, inject(($http) => { 529 | setterFn([ 530 | {name, template}, 531 | {name: 'type2', extends: name, controller: childFn}, 532 | ]) 533 | $controller(getterFn('type2').controller, {$scope}) 534 | expect(childFn).to.have.been.calledWith($http) 535 | })) 536 | 537 | }) 538 | }) 539 | }) 540 | 541 | describe('(◞‸◟;) path', () => { 542 | it('should throw an error when the first argument is not an object or an array', () => { 543 | expect(() => setterFn('string')).to.throw(/must.*provide.*object.*array/) 544 | expect(() => setterFn(324)).to.throw(/must.*provide.*object.*array/) 545 | expect(() => setterFn(false)).to.throw(/must.*provide.*object.*array/) 546 | }) 547 | 548 | it('should throw an error when a name is not provided', () => { 549 | expect(() => setterFn({templateUrl})).to.throw(/formlyConfig\.setType/) 550 | }) 551 | 552 | it(`should throw an error when specifying both a template and a templateUrl`, () => { 553 | expect(() => setterFn({name, template, templateUrl})).to.throw(/formlyConfig\.setType/) 554 | }) 555 | 556 | it(`should throw an error when an extra property is provided`, () => { 557 | expect(() => setterFn({name, templateUrl, extra: true})).to.throw(/formlyConfig\.setType/) 558 | }) 559 | 560 | it('should warn when attempting to override a type', () => { 561 | shouldWarn(/overwrite/, function() { 562 | setterFn({name, template}) 563 | setterFn({name, template}) 564 | }) 565 | }) 566 | }) 567 | 568 | describe(`apiCheck`, () => { 569 | testApiCheck('setType', 'getType') 570 | }) 571 | }) 572 | 573 | function testApiCheck(setterName, getterName) { 574 | const template = 'something with ' 575 | const name = 'input' 576 | let setterFn, getterFn, formlyApiCheck 577 | beforeEach(inject((_formlyApiCheck_, formlyConfig) => { 578 | formlyApiCheck = _formlyApiCheck_ 579 | setterFn = formlyConfig[setterName] 580 | getterFn = formlyConfig[getterName] 581 | })) 582 | 583 | it(`should allow you to specify an apiCheck function that will be used to validate your options`, () => { 584 | expect(() => { 585 | setterFn({ 586 | name, 587 | apiCheck, 588 | template, 589 | }) 590 | }).to.not.throw() 591 | 592 | expect(getterFn(name).apiCheck).to.equal(apiCheck) 593 | 594 | function apiCheck() { 595 | return { 596 | templateOptions: {}, 597 | data: {}, 598 | } 599 | } 600 | }) 601 | 602 | describe(`apiCheckInstance`, () => { 603 | let apiCheckInstance 604 | beforeEach(() => { 605 | apiCheckInstance = require('api-check')() 606 | }) 607 | it(`should allow you to specify an instance of your own apiCheck so messaging will be custom`, () => { 608 | expect(() => { 609 | setterFn({name, apiCheck, apiCheckInstance, template}) 610 | }).to.not.throw() 611 | expect(getterFn(name).apiCheckInstance).to.equal(apiCheckInstance) 612 | }) 613 | it(`should throw an error if you specify an instance without specifying an apiCheck`, () => { 614 | expect(() => { 615 | setterFn({name, apiCheckInstance, template}) 616 | }).to.throw() 617 | }) 618 | 619 | function apiCheck() { 620 | return { 621 | templateOptions: {}, 622 | data: {}, 623 | } 624 | } 625 | }) 626 | 627 | describe(`apiCheckFunction`, () => { 628 | it(`should allow you to specify warn or throw as the `, () => { 629 | expect(() => { 630 | setterFn({name, apiCheck, apiCheckFunction: 'warn', template}) 631 | }).to.not.throw() 632 | expect(getterFn(name).apiCheckFunction).to.equal('warn') 633 | expect(() => { 634 | setterFn({name: 'name2', apiCheck, apiCheckFunction: 'throw', template}) 635 | }).to.not.throw() 636 | expect(getterFn('name2').apiCheckFunction).to.equal('throw') 637 | }) 638 | 639 | it(`should throw an error if you specify anything other than warn or throw`, () => { 640 | expect(() => { 641 | setterFn({name, apiCheckFunction: 'other', template}) 642 | }).to.throw() 643 | }) 644 | 645 | function apiCheck() { 646 | return { 647 | templateOptions: {}, 648 | data: {}, 649 | } 650 | } 651 | }) 652 | } 653 | 654 | 655 | describe(`extras`, () => { 656 | 657 | describe(`that impact field rendering`, () => { 658 | 659 | let scope, $compile, el, field 660 | 661 | beforeEach(inject(($rootScope, _$compile_) => { 662 | scope = $rootScope.$new() 663 | $compile = _$compile_ 664 | scope.fields = [{template: ''}] 665 | })) 666 | 667 | describe(`defaultHideDirective`, () => { 668 | 669 | it(`should default formly-form to use ng-if when not specified`, () => { 670 | compileAndDigest(` 671 | 672 | `) 673 | const fieldNode = getFieldNode() 674 | expect(fieldNode.getAttribute('ng-if')).to.exist 675 | }) 676 | 677 | it(`should default formly-form to use the specified directive for hiding and showing`, () => { 678 | formlyConfig.extras.defaultHideDirective = 'ng-show' 679 | compileAndDigest(` 680 | 681 | `) 682 | const fieldNode = getFieldNode() 683 | expect(fieldNode.getAttribute('ng-show')).to.exist 684 | }) 685 | 686 | it(`should be overrideable on a per-form basis`, () => { 687 | formlyConfig.extras.defaultHideDirective = '(╯°□°)╯︵ ┻━┻' 688 | compileAndDigest(` 689 | 690 | `) 691 | const fieldNode = getFieldNode() 692 | expect(fieldNode.getAttribute('ng-show')).to.exist 693 | expect(fieldNode.getAttribute('(╯°□°)╯︵ ┻━┻')).to.not.exist 694 | }) 695 | 696 | }) 697 | 698 | describe(`getFieldId`, () => { 699 | it(`should allow you to specify your own function for generating the IDs for a field`, () => { 700 | scope.fields = [ 701 | getNewField({id: 'custom'}), 702 | getNewField({model: {foo: 'bar', id: '1234'}, key: 'foo'}), 703 | getNewField({key: 'bar'}), 704 | ] 705 | formlyConfig.extras.getFieldId = function(options, model, scope) { 706 | if (options.id) { 707 | return options.id 708 | } 709 | return [scope.index, (model && model.id) || 'new-model', options.key].join('_') 710 | } 711 | compileAndDigest() 712 | 713 | const field0 = getFieldNgModelNode(0) 714 | const field1 = getFieldNgModelNode(1) 715 | const field2 = getFieldNgModelNode(2) 716 | 717 | expect(field0.id).to.eq('custom') 718 | expect(field1.id).to.eq('1_1234_foo') 719 | expect(field2.id).to.eq('2_new-model_bar') 720 | }) 721 | }) 722 | 723 | 724 | function compileAndDigest(template) { 725 | el = $compile(template || basicForm)(scope) 726 | scope.$digest() 727 | field = scope.fields[0] 728 | return el 729 | } 730 | 731 | function getFieldNode(index = 0) { 732 | return el[0].querySelectorAll('.formly-field')[index] 733 | } 734 | 735 | function getFieldNgModelNode(index = 0) { 736 | return getFieldNode(index).querySelector('[ng-model]') 737 | } 738 | 739 | }) 740 | 741 | }) 742 | 743 | describe(`getTypeHeritage`, () => { 744 | it(`should get the heritage of all type extensions`, () => { 745 | formlyConfig.setType([ 746 | {name: 'grandparent'}, 747 | {name: 'parent', extends: 'grandparent'}, 748 | {name: 'child', extends: 'parent'}, 749 | {name: 'extra', extends: 'grandparent'}, 750 | {name: 'extra2'}, 751 | ]) 752 | expect(formlyConfig.getTypeHeritage('child')).to.eql([ 753 | formlyConfig.getType('parent'), formlyConfig.getType('grandparent'), 754 | ]) 755 | }) 756 | }) 757 | }) 758 | -------------------------------------------------------------------------------- /src/providers/formlyUsability.js: -------------------------------------------------------------------------------- 1 | import angular from 'angular-fix' 2 | 3 | export default formlyUsability 4 | 5 | // @ngInject 6 | function formlyUsability(formlyApiCheck, formlyErrorAndWarningsUrlPrefix) { 7 | angular.extend(this, { 8 | getFormlyError, 9 | getFieldError, 10 | checkWrapper, 11 | checkWrapperTemplate, 12 | getErrorMessage, 13 | $get: () => this, 14 | }) 15 | 16 | function getFieldError(errorInfoSlug, message, field) { 17 | if (arguments.length < 3) { 18 | field = message 19 | message = errorInfoSlug 20 | errorInfoSlug = null 21 | } 22 | return new Error(getErrorMessage(errorInfoSlug, message) + ` Field definition: ${angular.toJson(field)}`) 23 | } 24 | 25 | function getFormlyError(errorInfoSlug, message) { 26 | if (!message) { 27 | message = errorInfoSlug 28 | errorInfoSlug = null 29 | } 30 | return new Error(getErrorMessage(errorInfoSlug, message)) 31 | } 32 | 33 | function getErrorMessage(errorInfoSlug, message) { 34 | let url = '' 35 | if (errorInfoSlug !== null) { 36 | url = `${formlyErrorAndWarningsUrlPrefix}${errorInfoSlug}` 37 | } 38 | return `Formly Error: ${message}. ${url}` 39 | } 40 | 41 | function checkWrapper(wrapper) { 42 | formlyApiCheck.throw(formlyApiCheck.formlyWrapperType, wrapper, { 43 | prefix: 'formlyConfig.setWrapper', 44 | urlSuffix: 'setwrapper-validation-failed', 45 | }) 46 | } 47 | 48 | function checkWrapperTemplate(template, additionalInfo) { 49 | const formlyTransclude = '' 50 | if (template.indexOf(formlyTransclude) === -1) { 51 | throw getFormlyError( 52 | `Template wrapper templates must use "${formlyTransclude}" somewhere in them. ` + 53 | `This one does not have "" in it: ${template}` + '\n' + 54 | `Additional information: ${JSON.stringify(additionalInfo)}` 55 | ) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/providers/formlyValidationMessages.js: -------------------------------------------------------------------------------- 1 | export default formlyValidationMessages 2 | 3 | 4 | // @ngInject 5 | function formlyValidationMessages() { 6 | 7 | const validationMessages = { 8 | addTemplateOptionValueMessage, 9 | addStringMessage, 10 | messages: {}, 11 | } 12 | 13 | return validationMessages 14 | 15 | function addTemplateOptionValueMessage(name, prop, prefix, suffix, alternate) { 16 | validationMessages.messages[name] = templateOptionValue(prop, prefix, suffix, alternate) 17 | } 18 | 19 | function addStringMessage(name, string) { 20 | validationMessages.messages[name] = () => string 21 | } 22 | 23 | 24 | function templateOptionValue(prop, prefix, suffix, alternate) { 25 | return function getValidationMessage(viewValue, modelValue, scope) { 26 | if (typeof scope.options.templateOptions[prop] !== 'undefined') { 27 | return `${prefix} ${scope.options.templateOptions[prop]} ${suffix}` 28 | } else { 29 | return alternate 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/run/formlyCustomTags.js: -------------------------------------------------------------------------------- 1 | import angular from 'angular-fix' 2 | export default addCustomTags 3 | 4 | // @ngInject 5 | function addCustomTags($document) { 6 | // IE8 check -> 7 | // https://msdn.microsoft.com/en-us/library/cc196988(v=vs.85).aspx 8 | if ($document && $document.documentMode < 9) { 9 | const document = $document.get(0) 10 | // add the custom elements that we need for formly 11 | const customElements = [ 12 | 'formly-field', 'formly-form', 13 | ] 14 | angular.forEach(customElements, el => { 15 | document.createElement(el) 16 | }) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/run/formlyCustomTags.test.js: -------------------------------------------------------------------------------- 1 | import angular from 'angular' 2 | 3 | describe(`formlyCustomTags`, () => { 4 | 5 | beforeEach(window.module(`formly`, $provide => { 6 | $provide.value(`$document`, { 7 | documentMode: 8, 8 | get: sinon.stub().withArgs(0).returnsThis(), 9 | createElement: sinon.spy(), 10 | }) 11 | })) 12 | 13 | let $document 14 | 15 | beforeEach(inject((_$document_) => { 16 | $document = _$document_ 17 | })) 18 | 19 | describe(`addCustomTags`, () => { 20 | it(`should create custom formly tags`, () => { 21 | const customElements = [ 22 | `formly-field`, `formly-form`, 23 | ] 24 | 25 | expect($document.get).to.have.been.calledOnce 26 | 27 | angular.forEach(customElements, el => { 28 | expect($document.createElement).to.have.been.calledWith(el) 29 | }) 30 | }) 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /src/run/formlyNgModelAttrsManipulator.js: -------------------------------------------------------------------------------- 1 | import angular from 'angular-fix' 2 | import {contains} from '../other/utils' 3 | 4 | export default addFormlyNgModelAttrsManipulator 5 | 6 | // @ngInject 7 | function addFormlyNgModelAttrsManipulator(formlyConfig, $interpolate) { 8 | if (formlyConfig.extras.disableNgModelAttrsManipulator) { 9 | return 10 | } 11 | formlyConfig.templateManipulators.preWrapper.push(ngModelAttrsManipulator) 12 | 13 | 14 | function ngModelAttrsManipulator(template, options, scope) { 15 | const node = document.createElement('div') 16 | const skip = options.extras && options.extras.skipNgModelAttrsManipulator 17 | if (skip === true) { 18 | return template 19 | } 20 | node.innerHTML = template 21 | 22 | const modelNodes = getNgModelNodes(node, skip) 23 | if (!modelNodes || !modelNodes.length) { 24 | return template 25 | } 26 | 27 | addIfNotPresent(modelNodes, 'id', scope.id) 28 | addIfNotPresent(modelNodes, 'name', scope.name || scope.id) 29 | 30 | addValidation() 31 | alterNgModelAttr() 32 | addModelOptions() 33 | addTemplateOptionsAttrs() 34 | addNgModelElAttrs() 35 | 36 | 37 | return node.innerHTML 38 | 39 | 40 | function addValidation() { 41 | if (angular.isDefined(options.validators) || angular.isDefined(options.validation.messages)) { 42 | addIfNotPresent(modelNodes, 'formly-custom-validation', '') 43 | } 44 | } 45 | 46 | function alterNgModelAttr() { 47 | if (isPropertyAccessor(options.key)) { 48 | addRegardlessOfPresence(modelNodes, 'ng-model', 'model.' + options.key) 49 | } 50 | } 51 | 52 | function addModelOptions() { 53 | if (angular.isDefined(options.modelOptions)) { 54 | addIfNotPresent(modelNodes, 'ng-model-options', 'options.modelOptions') 55 | if (options.modelOptions.getterSetter) { 56 | addRegardlessOfPresence(modelNodes, 'ng-model', 'options.value') 57 | } 58 | } 59 | } 60 | 61 | function addTemplateOptionsAttrs() { 62 | if (!options.templateOptions && !options.expressionProperties) { 63 | // no need to run these if there are no templateOptions or expressionProperties 64 | return 65 | } 66 | const to = options.templateOptions || {} 67 | const ep = options.expressionProperties || {} 68 | 69 | const ngModelAttributes = getBuiltInAttributes() 70 | 71 | // extend with the user's specifications winning 72 | angular.extend(ngModelAttributes, options.ngModelAttrs) 73 | 74 | // Feel free to make this more simple :-) 75 | angular.forEach(ngModelAttributes, (val, name) => { 76 | /* eslint complexity:[2, 14] */ 77 | let attrVal, attrName 78 | const ref = `options.templateOptions['${name}']` 79 | const toVal = to[name] 80 | const epVal = getEpValue(ep, name) 81 | 82 | const inTo = angular.isDefined(toVal) 83 | const inEp = angular.isDefined(epVal) 84 | if (val.value) { 85 | // I realize this looks backwards, but it's right, trust me... 86 | attrName = val.value 87 | attrVal = name 88 | } else if (val.statement && inTo) { 89 | attrName = val.statement 90 | if (angular.isString(to[name])) { 91 | attrVal = `$eval(${ref})` 92 | } else if (angular.isFunction(to[name])) { 93 | attrVal = `${ref}(model[options.key], options, this, $event)` 94 | } else { 95 | throw new Error( 96 | `options.templateOptions.${name} must be a string or function: ${JSON.stringify(options)}` 97 | ) 98 | } 99 | } else if (val.bound && inEp) { 100 | attrName = val.bound 101 | attrVal = ref 102 | } else if ((val.attribute || val.boolean) && inEp) { 103 | attrName = val.attribute || val.boolean 104 | attrVal = `${$interpolate.startSymbol()}${ref}${$interpolate.endSymbol()}` 105 | } else if (val.attribute && inTo) { 106 | attrName = val.attribute 107 | attrVal = toVal 108 | } else if (val.boolean) { 109 | if (inTo && !inEp && toVal) { 110 | attrName = val.boolean 111 | attrVal = true 112 | } else { 113 | /* eslint no-empty:0 */ 114 | // empty to illustrate that a boolean will not be added via val.bound 115 | // if you want it added via val.bound, then put it in expressionProperties 116 | } 117 | } else if (val.bound && inTo) { 118 | attrName = val.bound 119 | attrVal = ref 120 | } 121 | 122 | if (angular.isDefined(attrName) && angular.isDefined(attrVal)) { 123 | addIfNotPresent(modelNodes, attrName, attrVal) 124 | } 125 | }) 126 | } 127 | 128 | function addNgModelElAttrs() { 129 | angular.forEach(options.ngModelElAttrs, (val, name) => { 130 | addRegardlessOfPresence(modelNodes, name, val) 131 | }) 132 | } 133 | } 134 | 135 | // Utility functions 136 | function getNgModelNodes(node, skip) { 137 | const selectorNot = angular.isString(skip) ? `:not(${skip})` : '' 138 | const skipNot = ':not([formly-skip-ng-model-attrs-manipulator])' 139 | const query = `[ng-model]${selectorNot}${skipNot}, [data-ng-model]${selectorNot}${skipNot}` 140 | try { 141 | return node.querySelectorAll(query) 142 | } catch (e) { 143 | //this code is needed for IE8, as it does not support the CSS3 ':not' selector 144 | //it should be removed when IE8 support is dropped 145 | return getNgModelNodesFallback(node, skip) 146 | } 147 | } 148 | 149 | function getNgModelNodesFallback(node, skip) { 150 | const allNgModelNodes = node.querySelectorAll('[ng-model], [data-ng-model]') 151 | const matchingNgModelNodes = [] 152 | 153 | //make sure this array is compatible with NodeList type by adding an 'item' function 154 | matchingNgModelNodes.item = function(i) { 155 | return this[i] 156 | } 157 | 158 | for (let i = 0; i < allNgModelNodes.length; i++) { 159 | const ngModelNode = allNgModelNodes[i] 160 | if (!ngModelNode.hasAttribute('formly-skip-ng-model-attrs-manipulator') && 161 | !(angular.isString(skip) && nodeMatches(ngModelNode, skip))) { 162 | matchingNgModelNodes.push(ngModelNode) 163 | } 164 | } 165 | 166 | return matchingNgModelNodes 167 | } 168 | 169 | function nodeMatches(node, selector) { 170 | const div = document.createElement('div') 171 | div.innerHTML = node.outerHTML 172 | return div.querySelector(selector) 173 | } 174 | 175 | function getBuiltInAttributes() { 176 | const ngModelAttributes = { 177 | focus: { 178 | attribute: 'formly-focus', 179 | }, 180 | } 181 | const boundOnly = [] 182 | const bothBooleanAndBound = ['required', 'disabled'] 183 | const bothAttributeAndBound = ['pattern', 'minlength'] 184 | const statementOnly = ['change', 'keydown', 'keyup', 'keypress', 'click', 'focus', 'blur'] 185 | const attributeOnly = ['placeholder', 'min', 'max', 'step', 'tabindex', 'type'] 186 | if (formlyConfig.extras.ngModelAttrsManipulatorPreferUnbound) { 187 | bothAttributeAndBound.push('maxlength') 188 | } else { 189 | boundOnly.push('maxlength') 190 | } 191 | 192 | angular.forEach(boundOnly, item => { 193 | ngModelAttributes[item] = {bound: 'ng-' + item} 194 | }) 195 | 196 | angular.forEach(bothBooleanAndBound, item => { 197 | ngModelAttributes[item] = {boolean: item, bound: 'ng-' + item} 198 | }) 199 | 200 | angular.forEach(bothAttributeAndBound, item => { 201 | ngModelAttributes[item] = {attribute: item, bound: 'ng-' + item} 202 | }) 203 | 204 | angular.forEach(statementOnly, item => { 205 | const propName = 'on' + item.substr(0, 1).toUpperCase() + item.substr(1) 206 | ngModelAttributes[propName] = {statement: 'ng-' + item} 207 | }) 208 | 209 | angular.forEach(attributeOnly, item => { 210 | ngModelAttributes[item] = {attribute: item} 211 | }) 212 | return ngModelAttributes 213 | } 214 | 215 | function getEpValue(ep, name) { 216 | return ep['templateOptions.' + name] || 217 | ep[`templateOptions['${name}']`] || 218 | ep[`templateOptions["${name}"]`] 219 | } 220 | 221 | function addIfNotPresent(nodes, attr, val) { 222 | angular.forEach(nodes, node => { 223 | if (!node.getAttribute(attr)) { 224 | node.setAttribute(attr, val) 225 | } 226 | }) 227 | } 228 | 229 | function addRegardlessOfPresence(nodes, attr, val) { 230 | angular.forEach(nodes, node => { 231 | node.setAttribute(attr, val) 232 | }) 233 | } 234 | 235 | function isPropertyAccessor(key) { 236 | return contains(key, '.') || (contains(key, '[') && contains(key, ']')) 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /src/run/formlyNgModelAttrsManipulator.test.js: -------------------------------------------------------------------------------- 1 | /* eslint max-len:0 */ 2 | import angular from 'angular' 3 | import _ from 'lodash' 4 | 5 | describe('formlyNgModelAttrsManipulator', () => { 6 | beforeEach(window.module('formly')) 7 | 8 | let formlyConfig, manipulator, scope, field, result, resultEl, resultNode 9 | const template = '' 10 | 11 | beforeEach(inject((_formlyConfig_, $rootScope) => { 12 | formlyConfig = _formlyConfig_ 13 | manipulator = formlyConfig.templateManipulators.preWrapper[0] 14 | scope = $rootScope.$new() 15 | scope.id = 'id' 16 | field = { 17 | extras: {}, 18 | data: {}, 19 | validation: {}, 20 | templateOptions: {}, 21 | } 22 | })) 23 | 24 | describe(`skipping`, () => { 25 | 26 | it(`should allow you to skip the manipulator wholesale for the field`, () => { 27 | field.extras.skipNgModelAttrsManipulator = true 28 | manipulate() 29 | expect(result).to.equal(template) 30 | }) 31 | 32 | 33 | const skipWithSelectorTitle = `should allow you to specify a selector for specific elements to skip` 34 | function skipWithSelector() { 35 | const className = 'ignored-thing' + _.random(0, 10) 36 | field.templateOptions.required = true 37 | field.extras.skipNgModelAttrsManipulator = `.${className}` 38 | manipulate(` 39 |
40 | 41 | 42 |
43 | `) 44 | const firstInput = angular.element(resultNode.querySelector('.first-thing')) 45 | const secondInput = angular.element(resultNode.querySelector(`.${className}`)) 46 | expect(firstInput.attr('required')).to.exist 47 | expect(secondInput.attr('required')).to.not.exist 48 | } 49 | it(skipWithSelectorTitle, skipWithSelector) 50 | 51 | const skipWithAttributeTitle = `should allow you to place the attribute formly-skip-ng-model-attrs-manipulator on an ng-model to have it skip` 52 | function skipWithAttribute() { 53 | field.templateOptions.required = true 54 | manipulate(` 55 |
56 | 57 | 58 |
59 | `) 60 | const firstInput = angular.element(resultNode.querySelector('.first-thing')) 61 | const secondInput = angular.element(resultNode.querySelector('[formly-skip-ng-model-attrs-manipulator]')) 62 | expect(firstInput.attr('required')).to.exist 63 | expect(secondInput.attr('required')).to.not.exist 64 | } 65 | it(skipWithAttributeTitle, skipWithAttribute) 66 | 67 | 68 | const dontSkipWithBooleanTitle = `should not skip by selector if skipNgModelAttrsManipulator is a boolean value` 69 | function dontSkipWithBoolean() { 70 | field.templateOptions.required = true 71 | field.extras.skipNgModelAttrsManipulator = false 72 | manipulate(` 73 |
74 | 75 | 76 |
77 | `) 78 | const firstInput = angular.element(resultNode.querySelector('.first-thing')) 79 | const secondInput = angular.element(resultNode.querySelector('.second-thing')) 80 | expect(firstInput.attr('required')).to.exist 81 | expect(secondInput.attr('required')).to.exist 82 | } 83 | it(dontSkipWithBooleanTitle, dontSkipWithBoolean) 84 | 85 | const skipWithAttributeAndSelectorTitle = `should allow you to skip using both the special attribute and the custom selector` 86 | function skipWithAttributeAndSelector() { 87 | const className = 'ignored-thing' + _.random(0, 10) 88 | field.templateOptions.required = true 89 | field.extras.skipNgModelAttrsManipulator = `.${className}` 90 | manipulate(` 91 |
92 | 93 | 94 | 95 |
96 | `) 97 | const firstInput = angular.element(resultNode.querySelector('.first-thing')) 98 | const secondInput = angular.element(resultNode.querySelector(`.${className}`)) 99 | const thirdInput = angular.element(resultNode.querySelector('[formly-skip-ng-model-attrs-manipulator]')) 100 | expect(firstInput.attr('required')).to.exist 101 | expect(secondInput.attr('required')).to.not.exist 102 | expect(thirdInput.attr('required')).to.not.exist 103 | } 104 | it(skipWithAttributeAndSelectorTitle, skipWithAttributeAndSelector) 105 | 106 | //repeat a few skipping tests with a broken Element.querySelectorAll function 107 | describe('node search fallback', () => { 108 | let origQuerySelectorAll 109 | 110 | //deliberately break querySelectorAll to mimic IE8's behaviour 111 | beforeEach(() => { 112 | origQuerySelectorAll = Element.prototype.querySelectorAll 113 | Element.prototype.querySelectorAll = function brokenQuerySelectorAll(selector) { 114 | if (selector && selector.indexOf(':not') >= 0) { 115 | throw new Error(':not selector not supported') 116 | } 117 | return origQuerySelectorAll.apply(this, arguments) 118 | } 119 | }) 120 | 121 | afterEach(() => { 122 | Element.prototype.querySelectorAll = origQuerySelectorAll 123 | }) 124 | 125 | it(skipWithSelectorTitle, skipWithSelector) 126 | it(skipWithAttributeTitle, skipWithAttribute) 127 | it(dontSkipWithBooleanTitle, dontSkipWithBoolean) 128 | it(skipWithAttributeAndSelectorTitle, skipWithAttributeAndSelector) 129 | }) 130 | 131 | }) 132 | 133 | 134 | it(`should have a limited number of automatically added attributes without any specific options`, () => { 135 | manipulate() 136 | // because different browsers place attributes in different places... 137 | const spaces = ''.split(' ').length 138 | expect(result.split(' ').length).to.equal(spaces) 139 | attrExists('ng-model') 140 | attrExists('id') 141 | attrExists('name') 142 | }) 143 | 144 | it(`should automatically add an id and name`, () => { 145 | manipulate() 146 | expect(resultEl.attr('name')).to.eq('id') 147 | expect(resultEl.attr('id')).to.eq('id') 148 | }) 149 | 150 | describe(`name`, () => { 151 | it(`should automatically be added when id is specified`, () => { 152 | scope.id = 'some_random_id' 153 | manipulate() 154 | expect(resultEl.attr('name')).to.eq('some_random_id') 155 | expect(resultEl.attr('id')).to.eq('some_random_id') 156 | }) 157 | 158 | it(`should allow to be set in scope`, () => { 159 | scope.id = 'some_random_id' 160 | scope.name = 'some_random_name' 161 | manipulate() 162 | expect(resultEl.attr('name')).to.eq('some_random_name') 163 | expect(resultEl.attr('id')).to.eq('some_random_id') 164 | }) 165 | }) 166 | 167 | describe(`ng-model-options`, () => { 168 | it(`should be added if modelOptions is specified`, () => { 169 | field.modelOptions = {} 170 | manipulate() 171 | attrExists('ng-model-options') 172 | }) 173 | 174 | it(`should change the value of ng-model if getterSetter is specified`, () => { 175 | field.modelOptions = {getterSetter: true} 176 | manipulate() 177 | expect(resultEl.attr('ng-model')).to.equal('options.value') 178 | }) 179 | }) 180 | 181 | describe(`selector key notation`, () => { 182 | it(`should change the ng-model when the key is a dot property accessor`, () => { 183 | field.key = 'bar.foo' 184 | manipulate() 185 | expect(resultEl.attr('ng-model')).to.equal('model.' + field.key) 186 | }) 187 | 188 | it(`should change the ng-model when the key is a bracket property accessor`, () => { 189 | field.key = 'bar["foo-bar"]' 190 | manipulate() 191 | expect(resultEl.attr('ng-model')).to.equal('model.' + field.key) 192 | }) 193 | }) 194 | 195 | describe(`formly-custom-validation`, () => { 196 | it(`shouldn't be added if there aren't validators or messages`, () => { 197 | formlyCustomValidationPresence(false) 198 | }) 199 | 200 | it(`should be added if there are validators`, () => { 201 | field.validators = {foo: 'bar'} 202 | formlyCustomValidationPresence(true) 203 | }) 204 | 205 | it(`should be added if there are messages`, () => { 206 | field.validators = {foo: 'bar'} 207 | field.validation.messages = {foo: '"bar"'} 208 | formlyCustomValidationPresence(true) 209 | }) 210 | 211 | it(`should be added if there are validators and messages`, () => { 212 | field.validators = {foo: 'bar'} 213 | field.validation.messages = {foo: '"bar"'} 214 | formlyCustomValidationPresence(true) 215 | }) 216 | 217 | function formlyCustomValidationPresence(present) { 218 | manipulate() 219 | attrExists('formly-custom-validation', !present) 220 | } 221 | }) 222 | 223 | describe(`templateOptions attributes`, () => { 224 | describe(`boolean attributes`, () => { 225 | 226 | testAttribute('required') 227 | testAttribute('disabled') 228 | 229 | function testAttribute(name) { 230 | it(`should allow you to specify 'true' for ${name}`, () => { 231 | field.templateOptions = { 232 | [name]: true, 233 | } 234 | manipulate() 235 | attrExists(name) 236 | }) 237 | 238 | it(`should allow you to specify 'false' for ${name}`, () => { 239 | field.templateOptions = { 240 | [name]: false, 241 | } 242 | manipulate() 243 | attrExists(name, false) 244 | attrExists(`ng-${name}`, false) 245 | }) 246 | 247 | it(`should allow you to specify expressionProperties for ${name}`, () => { 248 | field.expressionProperties = { 249 | [`templateOptions.${name}`]: 'someExpression', 250 | } 251 | manipulate() 252 | attrExists(name, false) 253 | attrExists(`ng-${name}`) 254 | expect(resultEl.attr(`ng-${name}`)).to.eq(`options.templateOptions['${name}']`) 255 | }) 256 | } 257 | }) 258 | 259 | describe(`attributeOnly`, () => { 260 | 261 | ['placeholder', 'min', 'max', 'step', 'tabindex', 'type'].forEach(testAttribute) 262 | 263 | function testAttribute(name) { 264 | it(`should be placed as an attribute if it is present in the templateOptions`, () => { 265 | field.templateOptions = { 266 | [name]: 'Ammon', 267 | } 268 | manipulate() 269 | expect(resultEl.attr(name)).to.eq('Ammon') 270 | }) 271 | 272 | it(`should be placed as an attribute with {{expression}} if it is present in the expressionProperties`, () => { 273 | field.expressionProperties = { 274 | ['templateOptions.' + name]: 'Ammon', 275 | } 276 | manipulate() 277 | expect(resultEl.attr(name)).to.eq(`{{options.templateOptions['${name}']}}`) 278 | }) 279 | } 280 | }) 281 | 282 | describe(`preferUnbound`, () => { 283 | it(`should prefer to specify maxlength as ng-maxlegnth even when it's not in expressionProperties`, () => { 284 | field.templateOptions = { 285 | maxlength: 3, 286 | } 287 | manipulate() 288 | expect(resultEl.attr('ng-maxlength')).to.eq(`options.templateOptions['maxlength']`) 289 | attrExists('maxlength', false) 290 | }) 291 | 292 | it(`should allow you to specify maxlength that gets set to maxlength if it's not in expressionProperties`, () => { 293 | formlyConfig.extras.ngModelAttrsManipulatorPreferUnbound = true 294 | field.templateOptions = { 295 | maxlength: 3, 296 | } 297 | manipulate() 298 | attrExists('ng-maxlength', false) 299 | expect(resultEl.attr('maxlength')).to.eq('3') 300 | formlyConfig.extras.ngModelAttrsManipulatorPreferUnbound = false 301 | }) 302 | 303 | it(`should still allow maxlength work with expressionProperties`, () => { 304 | field.expressionProperties = { 305 | 'templateOptions.maxlength': '3', 306 | } 307 | manipulate() 308 | expect(resultEl.attr('ng-maxlength')).to.eq(`options.templateOptions['maxlength']`) 309 | attrExists('maxlength', false) 310 | }) 311 | 312 | }) 313 | }) 314 | 315 | 316 | describe(`ngModelElAttrs`, () => { 317 | 318 | it(`should place the attributes you specify on the ng-model element`, () => { 319 | _.assign(field, { 320 | ngModelElAttrs: {foo: '{{::to.bar}}'}, 321 | }) 322 | manipulate() 323 | expect( 324 | resultNode.getAttribute('foo'), 325 | 'foo attribute should equal the value of foo in ngModelElAttrs' 326 | ).to.equal('{{::to.bar}}') 327 | }) 328 | 329 | it(`should work with multiple ng-models`, () => { 330 | _.assign(field, { 331 | ngModelElAttrs: {foo: '{{::to.bar}}'}, 332 | }) 333 | manipulate(` 334 |
335 | 336 | 337 |
338 | `) 339 | expect( 340 | resultNode.querySelector('.first').getAttribute('foo'), 341 | 'foo attribute should equal the value of foo in ngModelElAttrs' 342 | ).to.equal('{{::to.bar}}') 343 | expect( 344 | resultNode.querySelector('.second').getAttribute('foo'), 345 | 'foo attribute should equal the value of foo in ngModelElAttrs' 346 | ).to.equal('{{::to.bar}}') 347 | 348 | }) 349 | 350 | it(`should override existing attributes`, () => { 351 | field.ngModelElAttrs = { 352 | 'ng-model': 'formState.foo.bar', 353 | } 354 | manipulate() 355 | expect(resultEl.attr('ng-model')).to.equal('formState.foo.bar') 356 | }) 357 | }) 358 | 359 | 360 | 361 | function manipulate(theTemplate = template) { 362 | result = manipulator(theTemplate, field, scope) 363 | resultEl = angular.element(result) 364 | resultNode = resultEl[0] 365 | } 366 | 367 | function attrExists(name, notExists) { 368 | const attr = resultNode.getAttribute(name) 369 | if (notExists) { 370 | expect(attr).to.be.null 371 | } else { 372 | expect(attr).to.be.defined 373 | } 374 | } 375 | }) 376 | -------------------------------------------------------------------------------- /src/services/formlyUtil.js: -------------------------------------------------------------------------------- 1 | import utils from '../other/utils' 2 | 3 | export default formlyUtil 4 | 5 | // @ngInject 6 | function formlyUtil() { 7 | return utils 8 | } 9 | -------------------------------------------------------------------------------- /src/services/formlyUtil.test.js: -------------------------------------------------------------------------------- 1 | 2 | describe('formlyUtil', () => { 3 | beforeEach(window.module('formly')) 4 | 5 | describe('reverseDeepMerge', () => { 6 | let merge 7 | beforeEach(inject(function(formlyUtil) { 8 | merge = formlyUtil.reverseDeepMerge 9 | })) 10 | 11 | it(`should modify and prefer the first object`, () => { 12 | const firstObj = { 13 | obj1a: { 14 | obj2a: { 15 | string3a: 'Hello world', 16 | number3a: 4, 17 | bool3a: false, 18 | }, 19 | }, 20 | arry1a: [ 21 | 1, 2, 3, 4, 22 | ], 23 | } 24 | const secondObj = { 25 | obj1a: { 26 | obj2a: { 27 | string3a: 'Should not win', 28 | string3b: 'Should exist', 29 | number3a: 5, 30 | bool3a: true, 31 | bool3b: false, 32 | }, 33 | }, 34 | } 35 | 36 | const thirdObj = { 37 | obj1a: 'false', 38 | arry1a: [ 39 | 4, 3, 2, 1, 0, 40 | ], 41 | } 42 | 43 | const result = { 44 | obj1a: { 45 | obj2a: { 46 | string3a: 'Hello world', 47 | string3b: 'Should exist', 48 | number3a: 4, 49 | bool3a: false, 50 | bool3b: false, 51 | }, 52 | }, 53 | arry1a: [ 54 | 1, 2, 3, 4, 0, 55 | ], 56 | } 57 | 58 | merge(firstObj, secondObj, thirdObj) 59 | expect(firstObj).to.eql(result) 60 | }) 61 | 62 | it(`should allow for adding of empty objects`, () => { 63 | const firstObj = { 64 | a: 'a', 65 | b: 'b', 66 | } 67 | 68 | const secondObj = { 69 | data: {}, 70 | templateOptions: {}, 71 | validation: {}, 72 | } 73 | 74 | const result = { 75 | a: 'a', 76 | b: 'b', 77 | data: {}, 78 | templateOptions: {}, 79 | validation: {}, 80 | } 81 | 82 | merge(firstObj, secondObj) 83 | expect(firstObj).to.eql(result) 84 | }) 85 | }) 86 | 87 | describe('findByNodeName', () => { 88 | let find, $compile, scope 89 | beforeEach(inject(function(_$compile_, $rootScope, formlyUtil) { 90 | $compile = _$compile_ 91 | scope = $rootScope 92 | find = formlyUtil.findByNodeName 93 | })) 94 | 95 | it('should find an element by nodeName from a single root', () => { 96 | const template = 97 | '
' 98 | const el = $compile(template)(scope) 99 | const found = find(el, 'input') 100 | expect(found.length).to.equal(1) 101 | expect(found.prop('nodeName')).to.equal('INPUT') 102 | }) 103 | 104 | it('should find an element by nodeName from multiple root', () => { 105 | const template = 106 | '
' + 107 | '' 108 | const el = $compile(template)(scope) 109 | const found = find(el, 'i') 110 | expect(found.length).to.equal(1) 111 | expect(found.prop('nodeName')).to.equal('I') 112 | }) 113 | 114 | it('should return undefined when a node can\'t be found', () => { 115 | const template = 116 | '
' 117 | const el = $compile(template)(scope) 118 | const found = find(el, 'bla') 119 | expect(found).to.be.undefined 120 | }) 121 | 122 | }) 123 | }) 124 | -------------------------------------------------------------------------------- /src/services/formlyWarn.js: -------------------------------------------------------------------------------- 1 | export default formlyWarn 2 | 3 | // @ngInject 4 | function formlyWarn(formlyConfig, formlyErrorAndWarningsUrlPrefix, $log) { 5 | return function warn() { 6 | if (!formlyConfig.disableWarnings) { 7 | const args = Array.prototype.slice.call(arguments) 8 | const warnInfoSlug = args.shift() 9 | args.unshift('Formly Warning:') 10 | args.push(`${formlyErrorAndWarningsUrlPrefix}${warnInfoSlug}`) 11 | $log.warn(...args) 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/test.utils.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console:0 */ 2 | import _ from 'lodash' 3 | 4 | let key = 0 5 | 6 | const input = '' 7 | const multiNgModelField = ` 8 | 9 | 10 | ` 11 | const basicForm = '' 12 | 13 | export default { 14 | getNewField, input, multiNgModelField, basicForm, shouldWarn, shouldNotWarn, shouldWarnWithLog, 15 | } 16 | 17 | function getNewField(options) { 18 | return _.merge({template: input, key: key++}, options) 19 | } 20 | 21 | function shouldWarn(match, test) { 22 | const originalWarn = console.warn 23 | let calledArgs 24 | console.warn = function() { 25 | calledArgs = arguments 26 | } 27 | test() 28 | expect(calledArgs, 'expected warning and there was none').to.exist 29 | expect(Array.prototype.join.call(calledArgs, ' ')).to.match(match) 30 | console.warn = originalWarn 31 | } 32 | 33 | 34 | function shouldNotWarn(test) { 35 | const originalWarn = console.warn 36 | let calledArgs 37 | console.warn = function() { 38 | calledArgs = arguments 39 | } 40 | test() 41 | if (calledArgs) { 42 | console.log(calledArgs) 43 | throw new Error('Expected no warning, but there was one', calledArgs) 44 | } 45 | console.warn = originalWarn 46 | } 47 | 48 | function shouldWarnWithLog($log, logArgs, test) { 49 | /* eslint no-console:0 */ 50 | test() 51 | expect($log.warn.logs, '$log should have only been called once').to.have.length(1) 52 | const log = $log.warn.logs[0] 53 | _.each(logArgs, (arg, index) => { 54 | if (_.isRegExp(arg)) { 55 | expect(log[index]).to.match(arg) 56 | } else { 57 | expect(log[index]).to.equal(arg) 58 | } 59 | }) 60 | } 61 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | require('babel/register'); 2 | module.exports = require('./other/webpack.config.es6'); 3 | --------------------------------------------------------------------------------