├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── comment-issue.yml │ └── testsuite.yml ├── .gitignore ├── .meteorignore ├── .prettierignore ├── .prettierrc.json ├── .versions ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── MIGRATION.md ├── README.md ├── api.md ├── autoform-api.js ├── autoform-arrays.js ├── autoform-events.js ├── autoform-formdata.js ├── autoform-helpers.js ├── autoform-hooks.js ├── autoform-inputs.js ├── autoform-validation.js ├── autoform.js ├── common.js ├── components ├── afArrayField │ ├── afArrayField.html │ └── afArrayField.js ├── afEachArrayItem │ ├── afEachArrayItem.html │ └── afEachArrayItem.js ├── afFieldInput │ ├── afFieldInput.html │ └── afFieldInput.js ├── afFormGroup │ ├── afFormGroup.html │ └── afFormGroup.js ├── afObjectField │ ├── afObjectField.html │ └── afObjectField.js ├── afQuickField │ ├── afQuickField.html │ └── afQuickField.js ├── afQuickFields │ ├── afQuickFields.html │ └── afQuickFields.js ├── autoForm │ ├── autoForm.html │ └── autoForm.js └── quickForm │ ├── quickForm.html │ ├── quickForm.js │ └── quickFormUtils.js ├── dynamic.js ├── form-preserve.js ├── formTypes ├── disabled.js ├── insert.js ├── method-update.js ├── method.js ├── normal.js ├── readonly.js ├── update-pushArray.js └── update.js ├── getMoment.js ├── inputTypes ├── boolean-checkbox │ ├── boolean-checkbox.html │ └── boolean-checkbox.js ├── boolean-radios │ ├── boolean-radios.html │ └── boolean-radios.js ├── boolean-select │ ├── boolean-select.html │ └── boolean-select.js ├── button │ ├── button.html │ └── button.js ├── color │ ├── color.html │ └── color.js ├── contenteditable │ ├── contenteditable.html │ └── contenteditable.js ├── date │ ├── date.html │ └── date.js ├── datetime-local │ ├── datetime-local.html │ └── datetime-local.js ├── datetime │ ├── datetime.html │ └── datetime.js ├── email │ ├── email.html │ └── email.js ├── file │ ├── file.html │ └── file.js ├── hidden │ ├── hidden.html │ └── hidden.js ├── image │ ├── image.html │ └── image.js ├── month │ ├── month.html │ └── month.js ├── number │ ├── number.html │ └── number.js ├── password │ ├── password.html │ └── password.js ├── radio │ ├── radio.html │ └── radio.js ├── range │ ├── range.html │ └── range.js ├── reset │ ├── reset.html │ └── reset.js ├── search │ ├── search.html │ └── search.js ├── select-checkbox-inline │ ├── select-checkbox-inline.html │ └── select-checkbox-inline.js ├── select-checkbox │ ├── select-checkbox.html │ └── select-checkbox.js ├── select-multiple │ ├── select-multiple.html │ └── select-multiple.js ├── select-radio-inline │ ├── select-radio-inline.html │ └── select-radio-inline.js ├── select-radio │ ├── select-radio.html │ └── select-radio.js ├── select │ ├── select.html │ └── select.js ├── submit │ ├── submit.html │ └── submit.js ├── tel │ ├── tel.html │ └── tel.js ├── text │ ├── text.html │ └── text.js ├── textarea │ ├── textarea.html │ └── textarea.js ├── time │ ├── time.html │ └── time.js ├── url │ ├── url.html │ └── url.js ├── value-converters.js └── week │ ├── week.html │ └── week.js ├── internal.api.md ├── internal.js ├── main.js ├── package.js ├── static.js ├── testapp ├── .gitignore ├── .meteor │ ├── .finished-upgraders │ ├── .gitignore │ ├── .id │ ├── packages │ ├── platforms │ ├── release │ └── versions ├── client │ ├── main.html │ ├── main.js │ └── setup.js ├── package-lock.json └── package.json ├── tests ├── ArrayTracker.tests.js ├── FormData.tests.js ├── FormPreserve.tests.js ├── Hooks.tests.js ├── autoform-api.tests.js ├── autoform-helpers.tests.js ├── autoform-inputs.tests.js ├── autoform-validation.tests.js ├── common.tests.js ├── components │ └── quickForm │ │ └── quickFormUtils.tests.js ├── inputTypes │ └── value-converters.tests.js ├── setup.tests.js ├── test-utils.tests.js ├── testSuite.tests.js └── utility.tests.js ├── utility.js └── versions.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | indent_style = space 7 | indent_size = 2 8 | trim_trailing_whitespace = true 9 | charset = utf-8 10 | 11 | [*.js] 12 | max_line_length = 80 13 | indent_brace_style = 1TBS 14 | spaces_around_operators = true 15 | quote_type = auto -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | 12 | A clear and concise description of what the bug is. 13 | 14 | **To Reproduce** 15 | 16 | Steps to reproduce the behavior: 17 | 1. Go to '...' 18 | 2. Click on '....' 19 | 3. Scroll down to '....' 20 | 4. See error 21 | 22 | **Expected behavior** 23 | 24 | A clear and concise description of what you expected to happen. 25 | 26 | **Screenshots** 27 | 28 | If applicable, add screenshots to help explain your problem. Please avoid 29 | screenshots of code or error outputs and use [formatted code via markdown](https://guides.github.com/features/mastering-markdown/) 30 | instead. 31 | 32 | **Versions (please complete the following information):** 33 | - Meteor version: [e.g. 1.8.2] 34 | - Browser: [e.g. firefox, chrome, safari] 35 | - Package version: [e.g. 1.0.0] 36 | 37 | 38 | **Additional context** 39 | 40 | Add any other context about the problem here. 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | 12 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 13 | 14 | **Describe the solution you'd like** 15 | 16 | A clear and concise description of what you want to happen. 17 | 18 | **Describe alternatives you've considered** 19 | 20 | A clear and concise description of any alternative solutions or features you've considered. 21 | 22 | **Additional context** 23 | 24 | Add any other context or screenshots about the feature request here. 25 | -------------------------------------------------------------------------------- /.github/workflows/comment-issue.yml: -------------------------------------------------------------------------------- 1 | name: Add immediate comment on new issues 2 | 3 | on: 4 | issues: 5 | types: [opened] 6 | 7 | jobs: 8 | createComment: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Create Comment 12 | uses: peter-evans/create-or-update-comment@v1.4.2 13 | with: 14 | issue-number: ${{ github.event.issue.number }} 15 | body: | 16 | Thank you for submitting this issue! 17 | 18 | We, the Members of Meteor Community Packages take every issue seriously. 19 | Our goal is to provide long-term lifecycles for packages and keep up 20 | with the newest changes in Meteor and the overall NodeJs/JavaScript ecosystem. 21 | 22 | However, we contribute to these packages mostly in our free time. 23 | Therefore, we can't guarantee you issues to be solved within certain time. 24 | 25 | If you think this issue is trivial to solve, don't hesitate to submit 26 | a pull request, too! We will accompany you in the process with reviews and hints 27 | on how to get development set up. 28 | 29 | Please also consider sponsoring the maintainers of the package. 30 | If you don't know who is currently maintaining this package, just leave a comment 31 | and we'll let you know 32 | -------------------------------------------------------------------------------- /.github/workflows/testsuite.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Test suite 5 | 6 | on: 7 | push: 8 | branches: 9 | - master 10 | - devel 11 | pull_request: 12 | 13 | jobs: 14 | tests: 15 | name: tests 16 | runs-on: ubuntu-latest 17 | # needs: [lintcode,lintstyle,lintdocs] # we could add prior jobs for linting, if desired 18 | steps: 19 | - name: checkout 20 | uses: actions/checkout@v4 21 | 22 | - name: Setup meteor 23 | uses: meteorengineer/setup-meteor@v1 24 | with: 25 | meteor-release: '3.1' 26 | 27 | - name: cache dependencies 28 | uses: actions/cache@v4 29 | with: 30 | path: ~/.npm 31 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 32 | restore-keys: | 33 | ${{ runner.os }}-node- 34 | 35 | - run: cd testapp && meteor npm ci 36 | - run: cd testapp && meteor npm run lint 37 | - run: cd testapp && meteor npm run test 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .build* 3 | .npm* 4 | smart.lock 5 | nbproject* 6 | /packages/ 7 | .idea 8 | .vscode 9 | node_modules/ 10 | packages/ 11 | -------------------------------------------------------------------------------- /.meteorignore: -------------------------------------------------------------------------------- 1 | /testapp* 2 | /tests 3 | **/*tests.js 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | coverage 3 | testdummy 4 | *.md 5 | *.json 6 | .github 7 | .jshintrc 8 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.versions: -------------------------------------------------------------------------------- 1 | aldeed:autoform@8.0.0 2 | aldeed:moment-timezone@0.4.0 3 | aldeed:simple-schema@2.0.0 4 | allow-deny@2.0.0 5 | babel-compiler@7.11.1 6 | babel-runtime@1.5.2 7 | base64@1.0.13 8 | binary-heap@1.0.12 9 | blaze@3.0.0 10 | blaze-tools@2.0.0 11 | boilerplate-generator@2.0.0 12 | caching-compiler@2.0.1 13 | caching-html-compiler@2.0.0 14 | callback-hook@1.6.0 15 | check@1.4.4 16 | core-runtime@1.0.0 17 | ddp@1.4.2 18 | ddp-client@3.0.2 19 | ddp-common@1.4.4 20 | ddp-server@3.0.2 21 | diff-sequence@1.1.3 22 | dynamic-import@0.7.4 23 | ecmascript@0.16.9 24 | ecmascript-runtime@0.8.3 25 | ecmascript-runtime-client@0.12.2 26 | ecmascript-runtime-server@0.11.1 27 | ejson@1.1.4 28 | facts-base@1.0.2 29 | fetch@0.1.5 30 | geojson-utils@1.0.12 31 | html-tools@2.0.0 32 | htmljs@2.0.1 33 | id-map@1.2.0 34 | inter-process-messaging@0.1.2 35 | jquery@3.0.0 36 | local-test:aldeed:autoform@8.0.0 37 | logging@1.3.5 38 | meteor@2.0.1 39 | meteortesting:browser-tests@1.7.0 40 | meteortesting:mocha@3.2.0 41 | meteortesting:mocha-core@8.3.1-rc300.1 42 | minimongo@2.0.1 43 | modern-browsers@0.1.11 44 | modules@0.20.2 45 | modules-runtime@0.13.2 46 | momentjs:moment@2.30.1 47 | mongo@2.0.2 48 | mongo-decimal@0.1.4-beta300.7 49 | mongo-dev-server@1.1.1 50 | mongo-id@1.0.9 51 | npm-mongo@4.17.4 52 | observe-sequence@2.0.0 53 | ordered-dict@1.2.0 54 | promise@1.0.0 55 | random@1.2.2 56 | react-fast-refresh@0.2.9 57 | reactive-dict@1.3.2 58 | reactive-var@1.0.13 59 | reload@1.3.2 60 | retry@1.1.1 61 | routepolicy@1.1.2 62 | socket-stream-client@0.5.3 63 | spacebars@2.0.0 64 | spacebars-compiler@2.0.0 65 | templating@1.4.4 66 | templating-compiler@2.0.0 67 | templating-runtime@2.0.0 68 | templating-tools@2.0.0 69 | tracker@1.3.4 70 | typescript@5.4.3 71 | underscore@1.6.4 72 | webapp@2.0.3 73 | webapp-hashing@1.1.2 74 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Meteor Community Code of Conduct 2 | 3 | ### Community 4 | 5 | We want to build a productive, happy and agile community that welcomes new ideas, constantly looks for areas to improve, and fosters collaboration. 6 | 7 | The project gains strength from a diversity of backgrounds and perspectives in our contributor community, and we actively seek participation from those who enhance it. This code of conduct exists to lay some ground rules that ensure we can collaborate and communicate effectively, despite our diversity. The code applies equally to founders, team members and those seeking help and guidance. 8 | 9 | ### Using This Code 10 | 11 | This isn’t an exhaustive list of things that you can’t do. Rather, it’s a guide for participation in the community that outlines how each of us can work to keep Meteor a positive, successful, and growing project. 12 | 13 | This code of conduct applies to all spaces managed by the Meteor Community project. This includes GitHub issues, and any other forums created by the team which the community uses for communication. We expect it to be honored by everyone who represents or participates in the project, whether officially or informally. 14 | 15 | #### When Something Happens 16 | 17 | If you see a Code of Conduct violation, follow these steps: 18 | 19 | * Let the person know that what they did is not appropriate and ask them to stop and/or edit their message(s). 20 | * That person should immediately stop the behavior and correct the issue. 21 | * If this doesn’t happen, or if you’re uncomfortable speaking up, contact admins. 22 | * As soon as available, an admin will join, identify themselves, and take further action (see below), starting with a warning, then temporary deactivation, then long-term deactivation. 23 | 24 | When reporting, please include any relevant details, links, screenshots, context, or other information that may be used to better understand and resolve the situation. 25 | 26 | The Admin team will prioritize the well-being and comfort of the recipients of the violation over the comfort of the violator. 27 | 28 | If you believe someone is violating the code of conduct, please report it to any member of the governing team. 29 | 30 | ### We Strive To: 31 | 32 | - **Be open, patient, and welcoming** 33 | 34 | Members of this community are open to collaboration, whether it's on PRs, issues, or problems. We're receptive to constructive comment and criticism, as we value what the experiences and skill sets of contributors bring to the project. We're accepting of all who wish to get involved, and find ways for anyone to participate in a way that best matches their strengths. 35 | 36 | - **Be considerate** 37 | 38 | We are considerate of our peers: other Meteor users and contributors. We’re thoughtful when addressing others’ efforts, keeping in mind that work is often undertaken for the benefit of the community. We also value others’ time and appreciate that not every issue or comment will be responded to immediately. We strive to be mindful in our communications, whether in person or online, and we're tactful when approaching views that are different from our own. 39 | 40 | - **Be respectful** 41 | 42 | As a community of professionals, we are professional in our handling of disagreements, and don’t allow frustration to turn into a personal attack. We work together to resolve conflict, assume good intentions and do our best to act in an empathic fashion. 43 | 44 | We do not tolerate harassment or exclusionary behavior. This includes, but is not limited to: 45 | - Violent threats or language directed against another person. 46 | - Discriminatory jokes and language. 47 | - Posting sexually explicit or sexualized content. 48 | - Posting content depicting or encouraging violence. 49 | - Posting (or threatening to post) other people's personally identifying information ("doxing"). 50 | - Personal insults, especially those using racist or sexist terms. 51 | - Unwelcome sexual attention. 52 | - Advocating for, or encouraging, any of the above behavior. 53 | - Repeated harassment of others. In general, if someone asks you to stop, then stop. 54 | 55 | - **Take responsibility for our words and our actions** 56 | 57 | We can all make mistakes; when we do, we take responsibility for them. If someone has been harmed or offended, we listen carefully and respectfully. We are also considerate of others’ attempts to amend their mistakes. 58 | 59 | - **Be collaborative** 60 | 61 | The work we produce is (and is part of) an ecosystem containing several parallel efforts working towards a similar goal. Collaboration between teams and individuals that each have their own goal and vision is essential to reduce redundancy and improve the quality of our work. 62 | 63 | Internally and externally, we celebrate good collaboration. Wherever possible, we work closely with upstream projects and others in the free software community to coordinate our efforts. We prefer to work transparently and involve interested parties as early as possible. 64 | 65 | - **Ask for help when in doubt** 66 | 67 | Nobody is expected to be perfect in this community. Asking questions early avoids many problems later, so questions are encouraged, though they may be directed to the appropriate forum. Those who are asked should be responsive and helpful. 68 | 69 | - **Take initiative** 70 | 71 | We encourage new participants to feel empowered to lead, to take action, and to experiment when they feel innovation could improve the project. If we have an idea for a new tool, or how an existing tool can be improved, we speak up and take ownership of that work when possible. 72 | 73 | ### Attribution 74 | 75 | This Code of Conduct was inspired by [Meteor CoC](https://github.com/meteor/meteor/blob/devel/CODE_OF_CONDUCT.md). 76 | 77 | Sections of this Code of Conduct were inspired in by the following Codes from other open source projects and resources we admire: 78 | 79 | - [The Contributor Covenant](http://contributor-covenant.org/version/1/4/) 80 | - [Python](https://www.python.org/psf/codeofconduct/) 81 | - [Ubuntu](http://www.ubuntu.com/about/about-ubuntu/conduct) 82 | - [Django](https://www.djangoproject.com/conduct/) 83 | 84 | *This Code of Conduct is licensed under the [Creative Commons Attribution-ShareAlike 4.0 International](https://creativecommons.org/licenses/by-sa/4.0/) license. This Code was last updated on March 12, 2019.* 85 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribute 2 | 3 | 4 | 5 | **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* 6 | 7 | - [Introduction](#introduction) 8 | - [Your First Contribution](#your-first-contribution) 9 | - [Submitting code](#submitting-code) 10 | - [Code review process](#code-review-process) 11 | - [Questions](#questions) 12 | 13 | 14 | 15 | ## Introduction 16 | 17 | First, thank you for considering contributing to AutoForm! It's people like you that make the open source community such a great community! 😊 18 | 19 | We welcome any type of contribution, not only code. You can help with 20 | - **QA**: file bug reports, the more details you can give the better (e.g. screenshots with the console open) 21 | - **Marketing**: writing blog posts, howto's, printing stickers, ... 22 | - **Community**: presenting the project at meetups, organizing a dedicated meetup for the local community, ... 23 | - **Code**: take a look at the [open issues](issues). Even if you can't write code, commenting on them, showing that you care about a given issue matters. It helps us triage them. 24 | - **Money**: we welcome financial contributions in full transparency on our [open collective](https://opencollective.com/autoform). 25 | 26 | ## Your First Contribution 27 | 28 | 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). 29 | 30 | ## Submitting code 31 | 32 | Any code change should be submitted as a pull request. The description should explain what the code does and give steps to execute it. The pull request should also contain tests. 33 | 34 | ## Code review process 35 | 36 | The bigger the pull request, the longer it will take to review and merge. Try to break down large pull requests in smaller chunks that are easier to review and merge. 37 | It is also always helpful to have some context for your pull request. What was the purpose? Why does it matter to you? 38 | 39 | 40 | ## Questions 41 | 42 | If you have any questions, create an [issue](issue) (protip: do a quick search first to see if someone else didn't ask the same question before!). 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013-2020 Eric Dobbertin and Meteor Community Packages 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /MIGRATION.md: -------------------------------------------------------------------------------- 1 | # Meteor 3.0 Migration 2 | 3 | Until we have a stable release, this document will server as reference for migrations. 4 | In order to successfully Migrate this package you will need a few updates. 5 | 6 | -------------------------------------------------------------------------------- /autoform-arrays.js: -------------------------------------------------------------------------------- 1 | import { Tracker } from 'meteor/tracker' 2 | import { Mongo } from 'meteor/mongo' 3 | import { Utility } from './utility' 4 | 5 | /** 6 | * Track arrays; this allows us to add/remove fields or groups of fields for an array 7 | * but still easily respect minCount and maxCount, and properly add/remove the same 8 | * items from the database once the form is submitted. 9 | */ 10 | 11 | export class ArrayTracker { 12 | constructor () { 13 | const self = this 14 | self.info = {} 15 | } 16 | 17 | getMinMax ( 18 | ss, 19 | field, 20 | overrideMinCount, 21 | overrideMaxCount 22 | ) { 23 | const defs = Utility.getFieldDefinition(ss, field) 24 | 25 | // minCount is set by the schema, but can be set higher on the field attribute 26 | overrideMinCount = overrideMinCount || 0 27 | let minCount = defs.minCount || 0 28 | minCount = Math.max(overrideMinCount, minCount) 29 | 30 | // maxCount is set by the schema, but can be set lower on the field attribute 31 | overrideMaxCount = overrideMaxCount || Infinity 32 | let maxCount = defs.maxCount || Infinity 33 | maxCount = Math.min(overrideMaxCount, maxCount) 34 | 35 | return { minCount: minCount, maxCount: maxCount } 36 | } 37 | 38 | initForm (formId) { 39 | const self = this 40 | if (self.info[formId]) return 41 | self.info[formId] = {} 42 | } 43 | 44 | getForm (formId) { 45 | const self = this 46 | self.initForm(formId) 47 | return self.info[formId] 48 | } 49 | 50 | ensureField (formId, field) { 51 | const self = this 52 | self.initForm(formId) 53 | 54 | if (!self.info[formId][field]) self.resetField(formId, field) 55 | } 56 | 57 | initField ( 58 | formId, 59 | field, 60 | ss, 61 | docCount, 62 | overrideMinCount, 63 | overrideMaxCount 64 | ) { 65 | const self = this 66 | self.ensureField(formId, field) 67 | 68 | if (self.info[formId][field].array != null) return 69 | 70 | // If we have a doc: The count should be the maximum of docCount or schema minCount or field minCount or 1. 71 | // If we don't have a doc: The count should be the maximum of schema minCount or field minCount or 1. 72 | const range = self.getMinMax(ss, field, overrideMinCount, overrideMaxCount) 73 | const arrayCount = Math.max(range.minCount, docCount == null ? 1 : docCount) 74 | 75 | // If this is an array of objects, collect names of object props 76 | let childKeys = [] 77 | if (Utility.getFieldDefinition(ss, `${field}.$`).type === Object) { 78 | const genericKey = Utility.makeKeyGeneric(field) 79 | childKeys = ss.objectKeys(`${genericKey}.$`) 80 | } 81 | 82 | const collection = new Mongo.Collection(null) 83 | 84 | const loopArray = [] 85 | for (let i = 0; i < arrayCount; i++) { 86 | const loopCtx = createLoopCtx( 87 | formId, 88 | field, 89 | i, 90 | childKeys, 91 | overrideMinCount, 92 | overrideMaxCount 93 | ) 94 | loopArray.push(loopCtx) 95 | collection.insert(loopCtx) 96 | } 97 | 98 | self.info[formId][field].collection = collection 99 | self.info[formId][field].array = loopArray 100 | 101 | const count = loopArray.length 102 | self.info[formId][field].count = count 103 | self.info[formId][field].visibleCount = count 104 | self.info[formId][field].deps.changed() 105 | } 106 | 107 | resetField (formId, field) { 108 | const self = this 109 | self.initForm(formId) 110 | 111 | if (!self.info[formId][field]) { 112 | self.info[formId][field] = { 113 | deps: new Tracker.Dependency() 114 | } 115 | } 116 | 117 | if (self.info[formId][field].collection) { 118 | self.info[formId][field].collection.remove({}) 119 | } 120 | 121 | self.info[formId][field].array = null 122 | self.info[formId][field].count = 0 123 | self.info[formId][field].visibleCount = 0 124 | self.info[formId][field].deps.changed() 125 | } 126 | 127 | resetForm (formId) { 128 | const self = this 129 | Object.keys(self.info[formId] || {}).forEach(function (field) { 130 | self.resetField(formId, field) 131 | }) 132 | } 133 | 134 | untrackForm (formId) { 135 | const self = this 136 | if (self.info[formId]) { 137 | Object.keys(self.info[formId]).forEach((field) => { 138 | if (self.info[formId][field].collection) { 139 | self.info[formId][field].collection.remove({}) 140 | } 141 | }) 142 | } 143 | self.info[formId] = {} 144 | } 145 | 146 | tracksField (formId, field) { 147 | const self = this 148 | self.ensureField(formId, field) 149 | self.info[formId][field].deps.depend() 150 | return !!self.info[formId][field].array 151 | } 152 | 153 | getField (formId, field) { 154 | const self = this 155 | self.ensureField(formId, field) 156 | self.info[formId][field].deps.depend() 157 | return self.info[formId][field].collection.find({}) 158 | } 159 | 160 | getCount (formId, field) { 161 | const self = this 162 | self.ensureField(formId, field) 163 | self.info[formId][field].deps.depend() 164 | return self.info[formId][field].count 165 | } 166 | 167 | getVisibleCount (formId, field) { 168 | const self = this 169 | self.ensureField(formId, field) 170 | self.info[formId][field].deps.depend() 171 | return self.info[formId][field].visibleCount 172 | } 173 | 174 | isFirstFieldlVisible (formId, field, currentIndex) { 175 | const self = this 176 | self.ensureField(formId, field) 177 | self.info[formId][field].deps.depend() 178 | const firstVisibleField = self.info[formId][field].array.find(function ( 179 | currentField 180 | ) { 181 | return !currentField.removed 182 | }) 183 | return firstVisibleField && firstVisibleField.index === currentIndex 184 | } 185 | 186 | isLastFieldlVisible (formId, field, currentIndex) { 187 | const self = this 188 | self.ensureField(formId, field) 189 | self.info[formId][field].deps.depend() 190 | const lastVisibleField = self.info[formId][field].array 191 | .filter(function (currentField) { 192 | return !currentField.removed 193 | }) 194 | .pop() 195 | return lastVisibleField && lastVisibleField.index === currentIndex 196 | } 197 | 198 | addOneToField ( 199 | formId, 200 | field, 201 | ss, 202 | overrideMinCount, 203 | overrideMaxCount 204 | ) { 205 | const self = this 206 | self.ensureField(formId, field) 207 | 208 | if (!self.info[formId][field].array) return 209 | 210 | const currentCount = self.info[formId][field].visibleCount 211 | const maxCount = self.getMinMax(ss, field, overrideMinCount, overrideMaxCount) 212 | .maxCount 213 | 214 | if (currentCount < maxCount) { 215 | const i = self.info[formId][field].array.length 216 | 217 | // If this is an array of objects, collect names of object props 218 | let childKeys = [] 219 | if (Utility.getFieldDefinition(ss, `${field}.$`).type === Object) { 220 | const genericKey = Utility.makeKeyGeneric(field) 221 | childKeys = ss.objectKeys(`${genericKey}.$`) 222 | } 223 | 224 | const loopCtx = createLoopCtx( 225 | formId, 226 | field, 227 | i, 228 | childKeys, 229 | overrideMinCount, 230 | overrideMaxCount 231 | ) 232 | 233 | self.info[formId][field].collection.insert(loopCtx) 234 | self.info[formId][field].array.push(loopCtx) 235 | self.info[formId][field].count++ 236 | self.info[formId][field].visibleCount++ 237 | self.info[formId][field].deps.changed() 238 | 239 | AutoForm.resetValueCache(formId, field) 240 | } 241 | } 242 | 243 | removeFromFieldAtIndex ( 244 | formId, 245 | field, 246 | index, 247 | ss, 248 | overrideMinCount, 249 | overrideMaxCount 250 | ) { 251 | const self = this 252 | self.ensureField(formId, field) 253 | 254 | if (!self.info[formId][field].array) return 255 | 256 | const currentCount = self.info[formId][field].visibleCount 257 | const minCount = self.getMinMax(ss, field, overrideMinCount, overrideMaxCount) 258 | .minCount 259 | 260 | if (currentCount > minCount) { 261 | self.info[formId][field].collection.update( 262 | { index: index }, 263 | { $set: { removed: true } } 264 | ) 265 | self.info[formId][field].array[index].removed = true 266 | self.info[formId][field].count-- 267 | self.info[formId][field].visibleCount-- 268 | self.info[formId][field].deps.changed() 269 | 270 | AutoForm.resetValueCache(formId, field) 271 | } 272 | } 273 | } 274 | 275 | /* ---------------------------------------------------------------------------- 276 | * PRIVATE 277 | * -------------------------------------------------------------------------- */ 278 | const createLoopCtx = function ( 279 | formId, 280 | field, 281 | index, 282 | childKeys, 283 | overrideMinCount, 284 | overrideMaxCount 285 | ) { 286 | const loopCtx = { 287 | formId: formId, 288 | arrayFieldName: field, 289 | name: field + '.' + index, 290 | index: index, 291 | minCount: overrideMinCount, 292 | maxCount: overrideMaxCount 293 | } 294 | 295 | // If this is an array of objects, add child key names under loopCtx.current[childName] = fullKeyName 296 | if (childKeys.length) { 297 | loopCtx.current = {} 298 | childKeys.forEach(function (k) { 299 | loopCtx.current[k] = field + '.' + index + '.' + k 300 | }) 301 | } 302 | 303 | return loopCtx 304 | } 305 | 306 | export const arrayTracker = new ArrayTracker() 307 | -------------------------------------------------------------------------------- /autoform-formdata.js: -------------------------------------------------------------------------------- 1 | import { Tracker } from 'meteor/tracker' 2 | /* 3 | * Tracks form data with reactivity. This is similar to 4 | * ReactiveDict, but we need to store typed objects and 5 | * keep their type upon retrieval. 6 | */ 7 | 8 | export class FormData { 9 | constructor () { 10 | const self = this 11 | self.forms = {} 12 | } 13 | 14 | /** 15 | * Initializes tracking for a given form, if not already done. 16 | * @param {String} formId The form's `id` attribute 17 | */ 18 | initForm (formId) { 19 | const self = this 20 | 21 | if (self.forms[formId]) { 22 | return 23 | } 24 | 25 | self.forms[formId] = { 26 | sourceDoc: null, 27 | deps: { 28 | sourceDoc: new Tracker.Dependency() 29 | } 30 | } 31 | } 32 | 33 | /** 34 | * Gets or sets a source doc for the given form. Reactive. 35 | * @param {String} formId The form's `id` attribute 36 | * @param {MongoObject|null} sourceDoc The mDoc for the form or `null` if no doc. 37 | * @returns {MongoObject|undefined} Returns the form's MongoObject if getting. 38 | */ 39 | sourceDoc (formId, sourceDoc) { 40 | const self = this 41 | self.initForm(formId) 42 | 43 | if (sourceDoc || sourceDoc === null) { 44 | // setter 45 | self.forms[formId].sourceDoc = sourceDoc 46 | self.forms[formId].deps.sourceDoc.changed() 47 | } 48 | else { 49 | // getter 50 | self.forms[formId].deps.sourceDoc.depend() 51 | return self.forms[formId].sourceDoc 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /autoform-hooks.js: -------------------------------------------------------------------------------- 1 | // Manages all hooks, supporting append/replace, get 2 | 3 | export const Hooks = { 4 | form: {} 5 | } 6 | 7 | // The names of all supported hooks, excluding "before" and "after". 8 | const hookNames = [ 9 | 'formToDoc', 10 | 'formToModifier', 11 | 'docToForm', 12 | 'onSubmit', 13 | 'onSuccess', 14 | 'onError', 15 | 'beginSubmit', 16 | 'endSubmit' 17 | ] 18 | 19 | Hooks.getDefault = function () { 20 | const hooks = { 21 | before: {}, 22 | after: {} 23 | } 24 | hookNames.forEach(function (hookName) { 25 | hooks[hookName] = [] 26 | }) 27 | return hooks 28 | } 29 | 30 | Hooks.global = Hooks.getDefault() 31 | 32 | Hooks.addHooksToList = function addHooksToList (hooksList, hooks, replace) { 33 | // Add before hooks 34 | hooks.before && 35 | Object.entries(hooks.before).forEach(function autoFormBeforeHooksEach ([ 36 | type, 37 | func 38 | ]) { 39 | if (typeof func !== 'function') { 40 | throw new Error( 41 | 'AutoForm before hook must be a function, not ' + typeof func 42 | ) 43 | } 44 | hooksList.before[type] = 45 | !replace && hooksList.before[type] ? hooksList.before[type] : [] 46 | hooksList.before[type].push(func) 47 | }) 48 | 49 | // Add after hooks 50 | hooks.after && 51 | Object.entries(hooks.after).forEach(function autoFormAfterHooksEach ([ 52 | type, 53 | func 54 | ]) { 55 | if (typeof func !== 'function') { 56 | throw new Error( 57 | 'AutoForm after hook must be a function, not ' + typeof func 58 | ) 59 | } 60 | hooksList.after[type] = 61 | !replace && hooksList.after[type] ? hooksList.after[type] : [] 62 | hooksList.after[type].push(func) 63 | }) 64 | 65 | // Add all other hooks 66 | hookNames.forEach(function autoFormHooksEach (name) { 67 | if (hooks[name]) { 68 | if (typeof hooks[name] !== 'function') { 69 | throw new Error( 70 | 'AutoForm ' + 71 | name + 72 | ' hook must be a function, not ' + 73 | typeof hooks[name] 74 | ) 75 | } 76 | 77 | if (replace) { 78 | hooksList[name] = [] 79 | } 80 | 81 | hooksList[name].push(hooks[name]) 82 | } 83 | }) 84 | } 85 | 86 | Hooks.getHooks = function getHooks (formId, type, subtype) { 87 | let f, g 88 | if (subtype) { 89 | f = 90 | (Hooks.form[formId] && 91 | Hooks.form[formId][type] && 92 | Hooks.form[formId][type][subtype]) || 93 | [] 94 | g = (Hooks.global[type] && Hooks.global[type][subtype]) || [] 95 | } 96 | else { 97 | f = (Hooks.form[formId] && Hooks.form[formId][type]) || [] 98 | g = Hooks.global[type] || [] 99 | } 100 | return f.concat(g) 101 | } 102 | -------------------------------------------------------------------------------- /autoform-inputs.js: -------------------------------------------------------------------------------- 1 | /* global AutoForm $ */ 2 | import { Tracker } from 'meteor/tracker' 3 | import { Utility } from './utility' 4 | 5 | /** 6 | * Creates a flat document that contains all field values as key/value pair, where key = fieldname and value = the 7 | * field's current input value. 8 | * @param fields {jQueryObjectList} A current jQuery-Object list, that allows to iterate over each element. 9 | * @param ss {SimpleSchema} The current SimpleSchema instance for the form, related to the fields. 10 | * @returns {Object} The document Object with key/value-paired fields. 11 | */ 12 | export const getFlatDocOfFieldValues = function getFlatDocOfFieldValues ( 13 | fields, 14 | ss 15 | ) { 16 | const doc = {} 17 | fields.each(function () { 18 | let fieldName 19 | const val = AutoForm.getInputValue(this, ss) 20 | if (val !== undefined) { 21 | // Get the field/schema key name 22 | fieldName = $(this).attr('data-schema-key') 23 | doc[fieldName] = val 24 | } 25 | }) 26 | return doc 27 | } 28 | 29 | /* 30 | * package scope functions 31 | */ 32 | 33 | /** 34 | * Gets the value that should be shown/selected in the input. Returns 35 | * a string, a boolean, or an array of strings. The value used, 36 | * in order of preference, is one of: 37 | * - The `value` attribute provided 38 | * - The value that is set in the `doc` provided on the containing autoForm 39 | * - The `defaultValue` from the schema 40 | * @param atts {Object} The current field attributes 41 | * @param value {*} The current value of the field, can be anything 42 | * @param mDoc {Object} The current doc, wrapped by MongoObject 43 | * @param schemaDefaultValue {*} The defaultValue as defined in the schema 44 | * @param fieldDefaultValue {*} The defaultValue as defined on the field level 45 | * @param typeDefs {Object} The type definitions that are used when an input is registered (valueIn, valueIsArray etc.) 46 | * @returns {*} The (maybe transformed) input value. 47 | */ 48 | export const getInputValue = function getInputValue ( 49 | atts, 50 | value, 51 | mDoc, 52 | schemaDefaultValue, 53 | fieldDefaultValue, 54 | typeDefs 55 | ) { 56 | if (typeof value === 'undefined') { 57 | // Get the value for this key in the current document 58 | if (mDoc) { 59 | const valueInfo = mDoc.getInfoForKey(atts.name) 60 | if (valueInfo) { 61 | value = valueInfo.value 62 | } 63 | else { 64 | value = fieldDefaultValue 65 | } 66 | } 67 | 68 | // Only if there is no current document, use the schema defaultValue 69 | else { 70 | // Use the field default value if provided 71 | if (typeof fieldDefaultValue !== 'undefined') { 72 | value = fieldDefaultValue 73 | } 74 | // Or use the defaultValue in the schema 75 | else { 76 | value = schemaDefaultValue 77 | } 78 | } 79 | } 80 | 81 | // Change null or undefined to an empty string 82 | value = value === null || value === undefined ? '' : value 83 | 84 | // If the component expects the value to be an array, and it's not, make it one 85 | if (typeDefs.valueIsArray && !Array.isArray(value)) { 86 | if (typeof value === 'string') { 87 | value = value.split(',') 88 | } 89 | else { 90 | value = [value] 91 | } 92 | } 93 | 94 | // At this point we have a value or an array of values. 95 | // Run through the components valueIn function if we have one. 96 | // It should then be in whatever format the component expects. 97 | if (typeof typeDefs.valueIn === 'function') { 98 | value = typeDefs.valueIn(value, atts) 99 | } 100 | 101 | return value 102 | } 103 | 104 | /** 105 | * Builds the data context that the input component will have. Not reactive. 106 | * @param defs {Object} The field definitions 107 | * @param hash {Object} The field attributes 108 | * @param value {*} The value of the input, can be many types 109 | * @param label {String} The label to be displayed 110 | * @param formType {String} the type of the form (insert, update, normal, method etc.) 111 | * @example 112 | * const iData = getInputData(defs, atts, value, ss.label(c.atts.name), form.type); 113 | */ 114 | export const getInputData = function getInputData ( 115 | defs, 116 | hash, 117 | value, 118 | label, 119 | formType 120 | ) { 121 | /* 122 | * Get HTML attributes 123 | */ 124 | 125 | // We don't want to alter the original hash, so we clone it and 126 | // remove some stuff that should not be HTML attributes. 127 | const { 128 | type, 129 | value: hashValue, 130 | noselect, 131 | options, 132 | template, 133 | defaultValue, 134 | data, 135 | ...inputAtts 136 | } = hash 137 | 138 | // Add required if required 139 | if (typeof inputAtts.required === 'undefined' && !defs.optional) { 140 | inputAtts.required = '' 141 | } 142 | 143 | // Add data-schema-key to every type of element 144 | inputAtts['data-schema-key'] = inputAtts.name 145 | 146 | // Set placeholder to label from schema if requested. 147 | // We check hash.placeholder instead of inputAtts.placeholder because 148 | // we're setting inputAtts.placeholder, so it wouldn't be the same on 149 | // subsequent reactive runs of this function. 150 | if (hash.placeholder === 'schemaLabel') { 151 | inputAtts.placeholder = label 152 | } 153 | 154 | // To enable reactively toggling boolean attributes 155 | // in a simple way, we add the attributes to the HTML 156 | // only if their value is `true`. That is, unlike in 157 | // HTML, their mere presence does not matter. 158 | ['disabled', 'readonly', 'checked', 'required', 'autofocus'].forEach( 159 | function (booleanProp) { 160 | if (!(booleanProp in hash)) { 161 | return 162 | } 163 | 164 | // For historical reasons, we treat the string "true" and an empty string as `true`, too. 165 | // But an empty string value results in the cleanest rendered output for boolean props, 166 | // so we standardize as that. 167 | if ( 168 | hash[booleanProp] === true || 169 | hash[booleanProp] === 'true' || 170 | hash[booleanProp] === '' 171 | ) { 172 | inputAtts[booleanProp] = '' 173 | } 174 | else { 175 | // If the value is anything else, we don't render it 176 | delete inputAtts[booleanProp] 177 | } 178 | } 179 | ) 180 | 181 | /* 182 | * Set up the context. This is the object that becomes `this` in the 183 | * input type template. 184 | */ 185 | 186 | const inputTypeContext = { 187 | name: inputAtts.name, 188 | schemaType: defs.type, 189 | min: defs.min, 190 | max: defs.max, 191 | value: value, 192 | atts: inputAtts, 193 | selectOptions: AutoForm.Utility.getSelectOptions(defs, hash) 194 | } 195 | 196 | /* 197 | * Merge data property from the field schema with the context. 198 | * We do not want these turned into HTML attributes. 199 | */ 200 | if (hash.data) Object.assign(inputTypeContext, hash.data) 201 | 202 | // Before returning the context, we allow the registered form type to 203 | // adjust it if necessary. 204 | const ftd = Utility.getFormTypeDef(formType) 205 | if (typeof ftd.adjustInputContext === 'function') { 206 | return ftd.adjustInputContext(inputTypeContext) 207 | } 208 | 209 | return inputTypeContext 210 | } 211 | 212 | /** 213 | * @private Throttle factory-function - specific to markChanged. Timeouts are related to the respective fieldName. 214 | * @param fn {Function} The markChanged function to be passed 215 | * @param limit {Number} The throttle limit in ms 216 | * @return {Function} The throttled markChanged function 217 | */ 218 | function markChangedThrottle (fn, limit) { 219 | const timeouts = {} 220 | return function (template, fieldName, fieldValue) { 221 | clearTimeout(timeouts[fieldName]) 222 | timeouts[fieldName] = setTimeout(function () { 223 | fn(template, fieldName, fieldValue) 224 | }, limit) 225 | } 226 | } 227 | 228 | /** 229 | * @private If the given field is a subfield within an array (fieldName = something.$) then this 230 | * ensures, that the ancestor (something) is marked changed, too. 231 | * @param {Template} template 232 | * @param {String} fieldName 233 | */ 234 | const markChangedAncestors = (template, fieldName) => { 235 | // To properly handle array fields, we'll mark the ancestors as changed, too 236 | // FIX THIS 237 | // XXX Might be a more elegant way to handle this 238 | 239 | const dotPos = fieldName.lastIndexOf('.') 240 | if (dotPos === -1) return 241 | const ancestorFieldName = fieldName.slice(0, dotPos) 242 | doMarkChanged(template, ancestorFieldName) 243 | } 244 | 245 | /** 246 | * @private Checks, whether a Template can be considered as rendered. 247 | * @param {Template} template 248 | * @return {*|{}|boolean} truthy/falsy value, based on all checked properties 249 | */ 250 | const isRendered = (template) => 251 | template && 252 | template.view && 253 | template.view._domrange && 254 | !template.view.isDestroyed 255 | 256 | /** 257 | * @private Applies the change marking, creates a new Tracker Dependency if there is none for the field. 258 | * @param {Template} template 259 | * @param {String} fieldName 260 | */ 261 | const doMarkChanged = (template, fieldName) => { 262 | if (!template.formValues[fieldName]) { 263 | template.formValues[fieldName] = new Tracker.Dependency() 264 | } 265 | if (isRendered(template)) { 266 | template.formValues[fieldName].isMarkedChanged = true 267 | template.formValues[fieldName].changed() 268 | } 269 | markChangedAncestors(template, fieldName) 270 | } 271 | 272 | /** 273 | * Marks a field as changed and updates the Treacker.Dependdency as changed. Reactivity compatible. 274 | * @param template {Template} The current form template 275 | * @param fieldName {String} The name of the current field 276 | * @param fieldValue {*} The current field value 277 | */ 278 | export const markChanged = markChangedThrottle(function _markChanged ( 279 | template, 280 | fieldName, 281 | fieldValue 282 | ) { 283 | // is it really changed? 284 | const { cachedValue } = template.formValues[fieldName] || {} 285 | if (fieldValue === cachedValue) return 286 | // is there really a value?? 287 | if (fieldValue === undefined) return 288 | // is the form rendered??? 289 | 290 | if (!isRendered(template)) { 291 | return markChanged(template, fieldName, fieldValue) 292 | } 293 | doMarkChanged(template, fieldName) 294 | }, 295 | 150) 296 | 297 | /** 298 | * Creates a formValues entry on the template, in case it does not exist yet and updates the given 299 | * field by fieldName as changed (if ok for update). Reactivity compatible. 300 | * @see {markChanged} 301 | * @param template {Template} The current form template 302 | * @param fieldName {String} The name of the current field 303 | * @param fieldValue {*} The current field value 304 | */ 305 | export const updateTrackedFieldValue = function updateTrackedFieldValue ( 306 | template, 307 | fieldName, 308 | fieldValue 309 | ) { 310 | if (!template) return 311 | 312 | template.formValues = template.formValues || {} 313 | if (!template.formValues[fieldName]) { 314 | template.formValues[fieldName] = new Tracker.Dependency() 315 | } 316 | 317 | markChanged(template, fieldName, fieldValue) 318 | } 319 | 320 | /** 321 | * Calls {updateTrackedFieldValue} on all fields it can find in template.formValues. Reactivity compatible. 322 | * @see {updateTrackedFieldValue} 323 | * @param template {Template} The current form template 324 | */ 325 | export const updateAllTrackedFieldValues = function updateAllTrackedFieldValues ( 326 | template 327 | ) { 328 | if (template && template.formValues) { 329 | Object.keys(template.formValues).forEach(function (fieldName) { 330 | // XXX - if we would not pass a fieldValue here, then there would be none of the fields marked as 331 | // XXX - changed when the 'reset form' event is running. We use a random number in order to prevent 332 | // XXX - the chance of collision with the cachedValue. 333 | updateTrackedFieldValue(template, fieldName, Math.random()) 334 | }) 335 | } 336 | } 337 | 338 | export const getAllFieldsInForm = function getAllFieldsInForm ( 339 | template, 340 | disabled = false 341 | ) { 342 | // Get all elements with `data-schema-key` attribute, unless disabled 343 | const formId = template.data.id 344 | const allFields = template.$('[data-schema-key]').filter(function () { 345 | const fieldForm = $(this).closest('form').attr('id') 346 | return fieldForm === formId 347 | }) 348 | return disabled ? allFields : allFields.not('[disabled]') 349 | // Exclude fields in sub-forms, since they will belong to a different AutoForm and schema. 350 | // TODO need some selector/filter that actually works correctly for excluding subforms 351 | // return template.$('[data-schema-key]').not("[disabled]").not(template.$('form form [data-schema-key]')); 352 | } 353 | -------------------------------------------------------------------------------- /autoform-validation.js: -------------------------------------------------------------------------------- 1 | /* global AutoForm */ 2 | import { Meteor } from 'meteor/meteor' 3 | import { throttle } from './common' 4 | import { Utility } from './utility' 5 | 6 | /** 7 | * Validates a field on a given form by id. 8 | * @param key {String} a specific schema key to validate 9 | * @param formId {String} the id the form the key belongs to 10 | * @param skipEmpty {Boolean} allows to skip validation if the key has no value 11 | * @param {Boolean} onlyIfAlreadyInvalid 12 | * @return {*} 13 | * @private 14 | */ 15 | const _validateField = function _validateField ( 16 | key, 17 | formId, 18 | skipEmpty, 19 | onlyIfAlreadyInvalid 20 | ) { 21 | // Due to throttling, this can be called after the autoForm template is destroyed. 22 | // If that happens, we exit without error. 23 | const template = AutoForm.templateInstanceForForm(formId) 24 | 25 | // If form is not currently rendered, return true 26 | if (!Utility.checkTemplate(template)) return true 27 | 28 | const form = AutoForm.getCurrentDataForForm(formId) 29 | const ss = AutoForm.getFormSchema(formId, form) 30 | 31 | if (!ss) return true 32 | 33 | // Skip validation if onlyIfAlreadyInvalid is true and the form is 34 | // currently valid. 35 | if (onlyIfAlreadyInvalid && ss.namedContext(formId).isValid()) { 36 | return true // skip validation 37 | } 38 | 39 | // Create a document based on all the values of all the inputs on the form 40 | // Get the form type definition 41 | const ftd = Utility.getFormTypeDef(form.type) 42 | 43 | // Clean and validate doc 44 | const docToValidate = AutoForm.getFormValues( 45 | formId, 46 | template, 47 | ss, 48 | !!ftd.usesModifier 49 | ) 50 | 51 | // If form is not currently rendered, return true 52 | if (!docToValidate) { 53 | return true 54 | } 55 | 56 | // Skip validation if skipEmpty is true and the field we're validating 57 | // has no value. 58 | if (skipEmpty && !AutoForm.Utility.objAffectsKey(docToValidate, key)) { 59 | return true // skip validation 60 | } 61 | 62 | return AutoForm._validateFormDoc( 63 | docToValidate, 64 | !!ftd.usesModifier, 65 | formId, 66 | ss, 67 | form, 68 | key 69 | ) 70 | } 71 | 72 | // Throttle field validation to occur at most every 300ms, 73 | // with leading and trailing calls. 74 | export const validateField = throttle(_validateField, 300) 75 | 76 | // make the original function available to tests 77 | if (Meteor.isPackageTest) { 78 | module.exports = { _validateField } 79 | } 80 | -------------------------------------------------------------------------------- /autoform.js: -------------------------------------------------------------------------------- 1 | /* global AutoForm */ 2 | import './utility.js' 3 | import './autoform-validation.js' 4 | import './autoform-hooks.js' 5 | import './autoform-inputs.js' 6 | import './autoform-api.js' 7 | import { FormPreserve } from './form-preserve' 8 | import { FormData } from './autoform-formdata' 9 | 10 | AutoForm = AutoForm || {} // eslint-disable-line no-global-assign 11 | 12 | // formPreserve is used to keep current form data across hot code 13 | // reloads for any forms that are currently rendered 14 | AutoForm.formPreserve = new FormPreserve('autoforms') 15 | 16 | AutoForm.reactiveFormData = new FormData() 17 | 18 | AutoForm._inputTypeDefinitions = {} // for storing input type definitions added by AutoForm.addInputType 19 | AutoForm._formTypeDefinitions = {} // for storing submit type definitions added by AutoForm.addFormType 20 | 21 | // Used by AutoForm._forceResetFormValues; temporary hack 22 | AutoForm._destroyForm = {} 23 | 24 | module.exports.AutoForm = AutoForm 25 | -------------------------------------------------------------------------------- /common.js: -------------------------------------------------------------------------------- 1 | export const isObject = (obj) => 2 | Object.prototype.toString.call(obj) === '[object Object]' 3 | export const isFunction = (obj) => { 4 | const type = Object.prototype.toString.call(obj) 5 | return type === '[object Function]' || type === '[object AsyncFunction]' 6 | } 7 | 8 | export const throttle = (fn, limit) => { 9 | let timeout 10 | return (...args) => { 11 | clearTimeout(timeout) 12 | timeout = setTimeout(() => fn(...args), limit) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /components/afArrayField/afArrayField.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /components/afArrayField/afArrayField.js: -------------------------------------------------------------------------------- 1 | /* global AutoForm */ 2 | import { Template } from 'meteor/templating' 3 | import { arrayTracker } from '../../autoform-arrays' 4 | 5 | Template.afArrayField.helpers({ 6 | getTemplateName: function () { 7 | return AutoForm.getTemplateName('afArrayField', this.template, this.name) 8 | }, 9 | innerContext: function afArrayFieldContext () { 10 | const ctx = AutoForm.Utility.getComponentContext(this, 'afArrayField') 11 | const name = ctx.atts.name 12 | const fieldMinCount = ctx.atts.minCount || 0 13 | const fieldMaxCount = ctx.atts.maxCount || Infinity 14 | const ss = AutoForm.getFormSchema() 15 | const formId = AutoForm.getFormId() 16 | 17 | // Init the array tracking for this field 18 | let docCount = AutoForm.getArrayCountFromDocForField(formId, name) 19 | if (docCount === undefined) { 20 | docCount = ctx.atts.initialCount 21 | } 22 | arrayTracker.initField(formId, name, ss, docCount, fieldMinCount, fieldMaxCount) 23 | 24 | return { atts: ctx.atts } 25 | } 26 | }) 27 | -------------------------------------------------------------------------------- /components/afEachArrayItem/afEachArrayItem.html: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /components/afEachArrayItem/afEachArrayItem.js: -------------------------------------------------------------------------------- 1 | /* global AutoForm */ 2 | import { Template } from 'meteor/templating' 3 | import { arrayTracker } from '../../autoform-arrays' 4 | 5 | Template.afEachArrayItem.helpers({ 6 | innerContext: function afEachArrayItemContext () { 7 | const ctx = AutoForm.Utility.getComponentContext(this, 'afEachArrayItem') 8 | const formId = AutoForm.getFormId() 9 | const formSchema = AutoForm.getFormSchema() 10 | const name = ctx.atts.name 11 | 12 | let docCount = AutoForm.getArrayCountFromDocForField(formId, name) 13 | if (docCount === undefined) { 14 | docCount = ctx.atts.initialCount 15 | } 16 | 17 | const minCount = typeof ctx.atts.minCount === 'number' ? ctx.atts.minCount : ctx.defs.minCount 18 | const maxCount = typeof ctx.atts.maxCount === 'number' ? ctx.atts.maxCount : ctx.defs.maxCount 19 | 20 | arrayTracker.initField(formId, name, formSchema, docCount, minCount, maxCount) 21 | return arrayTracker.getField(formId, name) 22 | } 23 | }) 24 | -------------------------------------------------------------------------------- /components/afFieldInput/afFieldInput.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /components/afFieldInput/afFieldInput.js: -------------------------------------------------------------------------------- 1 | /* global AutoForm */ 2 | import { Template } from 'meteor/templating' 3 | import { getInputData, getInputValue, updateTrackedFieldValue } from '../../autoform-inputs' 4 | 5 | Template.afFieldInput.onRendered(() => { 6 | const template = AutoForm.templateInstanceForForm() 7 | const instance = Template.instance() 8 | updateTrackedFieldValue( 9 | template, 10 | instance.afFieldName, 11 | instance.afFieldValue 12 | ) 13 | }) 14 | 15 | export function afFieldInputContext () { 16 | const ctx = AutoForm.Utility.getComponentContext(this, 'afFieldInput') 17 | const form = AutoForm.getCurrentDataForForm() 18 | const formId = form.id 19 | const formSchema = AutoForm.getFormSchema() 20 | let defs = ctx.defs 21 | const instance = Template.instance() 22 | 23 | // Get schema default value. 24 | // We must do this before adjusting defs for arrays. 25 | const schemaDefaultValue = defs.defaultValue 26 | 27 | // Adjust for array fields if necessary 28 | if (defs.type === Array) { 29 | defs = AutoForm.Utility.getFieldDefinition(formSchema, `${ctx.atts.name}.$`) 30 | } 31 | 32 | // Determine what `type` attribute should be if not set 33 | const inputType = AutoForm.getInputType(this) 34 | const componentDef = AutoForm._inputTypeDefinitions[inputType] 35 | if (!componentDef) { 36 | throw new Error(`AutoForm: No component found for rendering input with type "${inputType}"`) 37 | } 38 | 39 | // Get reactive mDoc 40 | const mDoc = AutoForm.reactiveFormData.sourceDoc(formId, undefined) 41 | 42 | // Get input value 43 | const value = getInputValue( 44 | ctx.atts, 45 | ctx.atts.value, 46 | mDoc, 47 | schemaDefaultValue, 48 | ctx.atts.defaultValue, 49 | componentDef 50 | ) 51 | 52 | // Build input data context 53 | const iData = getInputData( 54 | defs, 55 | ctx.atts, 56 | value, 57 | formSchema.label(ctx.atts.name), 58 | form.type 59 | ) 60 | 61 | // These are needed for onRendered 62 | 63 | instance.afFieldName = ctx.atts.name 64 | instance.afFieldValue = value 65 | 66 | // Adjust and return context 67 | return typeof componentDef.contextAdjust === 'function' 68 | ? componentDef.contextAdjust(iData) 69 | : iData 70 | } 71 | 72 | Template.afFieldInput.helpers({ 73 | // similar to AutoForm.getTemplateName, but we have fewer layers of fallback, and we fall back 74 | // lastly to a template without an _ piece at the end 75 | getTemplateName: function getTemplateName () { 76 | const self = this 77 | // Determine what `type` attribute should be if not set 78 | const inputType = AutoForm.getInputType(this) 79 | const componentDef = AutoForm._inputTypeDefinitions[inputType] 80 | if (!componentDef) { 81 | throw new Error(`AutoForm: No component found for rendering input with type "${inputType}"`) 82 | } 83 | 84 | const inputTemplateName = componentDef.template 85 | const styleTemplateName = this.template 86 | 87 | // on first attempt we try to get the template without skipping non-existent 88 | // templates in order to circumvent false-positives that may occur due to 89 | // custom data context, set in content blocks or forms in forms 90 | let templateName = AutoForm.getTemplateName( 91 | inputTemplateName, 92 | styleTemplateName, 93 | self.name, 94 | false 95 | ) 96 | 97 | if (!templateName) { 98 | // In case we found nothing, we skip the check for existence here so that 99 | // we can get the `_plain` string even though they don"t exist. 100 | templateName = AutoForm.getTemplateName( 101 | inputTemplateName, 102 | styleTemplateName, 103 | self.name, 104 | true 105 | ) 106 | } 107 | 108 | // Special case: the built-in "plain" template uses the basic input templates for 109 | // everything, so if we found _plain, we use inputTemplateName instead 110 | if (templateName.indexOf('_plain') !== -1) { 111 | templateName = null 112 | } 113 | 114 | // If no override templateName found, use the exact name from the input type definition 115 | if (!templateName || !Template[templateName]) { 116 | templateName = inputTemplateName 117 | } 118 | 119 | return templateName 120 | }, 121 | innerContext: afFieldInputContext 122 | }) 123 | -------------------------------------------------------------------------------- /components/afFormGroup/afFormGroup.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/afFormGroup/afFormGroup.js: -------------------------------------------------------------------------------- 1 | /* global AutoForm */ 2 | import { Template } from 'meteor/templating' 3 | import { Random } from 'meteor/random' 4 | 5 | Template.afFormGroup.helpers({ 6 | getTemplateName: function () { 7 | return AutoForm.getTemplateName('afFormGroup', this.template, this.name) 8 | }, 9 | innerContext: function afFormGroupContext () { 10 | const ctx = AutoForm.Utility.getComponentContext(this, 'afFormGroup') 11 | const afFormGroupAtts = formGroupAtts(ctx.atts) 12 | const afFieldLabelAtts = formGroupLabelAtts(ctx.atts) 13 | const afFieldInputAtts = formGroupInputAtts(ctx.atts) 14 | 15 | // Construct an `id` attribute for the input, optionally 16 | // adding a user-provided prefix. Since id attribute is 17 | // supposed to be unique in the DOM and templates can be 18 | // included multiple times, it's best not to provide an `id` 19 | // and generate a random one here for accessibility reasons. 20 | const instance = Template.instance() 21 | instance.fieldIds = instance.fieldIds || {} 22 | 23 | let id 24 | 25 | if (typeof ctx.atts.id !== 'undefined') { 26 | id = ctx.atts.id 27 | } 28 | else { 29 | const name = ctx.atts.name 30 | id = instance.fieldIds[name] 31 | if (!id) { 32 | id = Random.id() 33 | const idPrefix = ctx.atts['id-prefix'] 34 | if (idPrefix && idPrefix.length > 0) { 35 | id = `${idPrefix}-${id}` 36 | } 37 | instance.fieldIds[name] = id 38 | } 39 | } 40 | 41 | // Set the input's `id` attribute and the label's `for` attribute to 42 | // the same ID. 43 | // NOTE: `afFieldLabelAtts.for` causes exception in IE8 44 | afFieldLabelAtts.for = afFieldInputAtts.id = id 45 | 46 | // Get the field's schema definition 47 | const fieldSchema = AutoForm.getSchemaForField(ctx.atts.name) 48 | 49 | return { 50 | skipLabel: ctx.atts.label === false, 51 | afFormGroupClass: ctx.atts['formgroup-class'], 52 | afFormGroupAtts: afFormGroupAtts, 53 | afFieldLabelAtts: afFieldLabelAtts, 54 | afFieldInputAtts: afFieldInputAtts, 55 | name: ctx.atts.name, 56 | required: fieldSchema ? !fieldSchema.optional : false, 57 | labelText: typeof ctx.atts.label === 'string' ? ctx.atts.label : null 58 | } 59 | } 60 | }) 61 | 62 | /* 63 | * Private 64 | */ 65 | 66 | function formGroupAtts (atts) { 67 | // Separate formgroup options from input options; formgroup items begin with 'formgroup-' 68 | const labelAtts = {} 69 | Object.entries(atts).forEach(function autoFormLabelAttsEach ([key, val]) { 70 | if (key.indexOf('formgroup-') === 0 && key !== 'formgroup-class') { 71 | labelAtts[key.substring(10)] = val 72 | } 73 | }) 74 | return labelAtts 75 | } 76 | 77 | function formGroupLabelAtts (atts) { 78 | // Separate label options from input options; label items begin with 'label-' 79 | const labelAtts = {} 80 | Object.entries(atts).forEach(function autoFormLabelAttsEach ([key, val]) { 81 | if (key.indexOf('label-') === 0) { 82 | labelAtts[key.substring(6)] = val 83 | } 84 | }) 85 | return labelAtts 86 | } 87 | 88 | function formGroupInputAtts (atts) { 89 | // Separate input options from label and formgroup options 90 | // We also don't want the 'label' option 91 | const inputAtts = {} 92 | Object.entries(atts).forEach(function autoFormLabelAttsEach ([key, val]) { 93 | if ( 94 | ['id-prefix', 'id', 'label'].indexOf(key) === -1 && 95 | key.indexOf('label-') !== 0 && 96 | key.indexOf('formgroup-') !== 0 97 | ) { 98 | inputAtts[key] = val 99 | } 100 | }) 101 | return inputAtts 102 | } 103 | -------------------------------------------------------------------------------- /components/afObjectField/afObjectField.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /components/afObjectField/afObjectField.js: -------------------------------------------------------------------------------- 1 | /* global AutoForm */ 2 | import { Template } from 'meteor/templating' 3 | 4 | Template.afObjectField.helpers({ 5 | getTemplateName: function () { 6 | return AutoForm.getTemplateName('afObjectField', this.template, this.name) 7 | }, 8 | innerContext: function () { 9 | const ctx = AutoForm.Utility.getComponentContext(this, 'afObjectField') 10 | return { ...this, ...ctx.atts } 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /components/afQuickField/afQuickField.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/afQuickField/afQuickField.js: -------------------------------------------------------------------------------- 1 | /* global AutoForm */ 2 | import { Template } from 'meteor/templating' 3 | 4 | Template.afQuickField.helpers({ 5 | isReady: function afIsComponentContextReady () { 6 | const context = AutoForm.Utility.getComponentContext(this, 'afQuickField') || {} 7 | return Object.keys(context).length > 0 8 | }, 9 | isGroup: function afQuickFieldIsGroup () { 10 | const ctx = AutoForm.Utility.getComponentContext(this, 'afQuickField') 11 | // Render a group of fields if we expect an Object and we don"t have options 12 | // and we have not overridden the type 13 | const isSubschema = typeof ctx.defs.type === 'object' && ctx.defs.type._schema 14 | return ((ctx.defs.type === Object || isSubschema) && !ctx.atts.options && !ctx.atts.type) 15 | }, 16 | isFieldArray: function afQuickFieldIsFieldArray () { 17 | const ctx = AutoForm.Utility.getComponentContext(this, 'afQuickField') 18 | // Render an array of fields if we expect an Array and we don"t have options 19 | // and we have not overridden the type 20 | return (ctx.defs.type === Array && !ctx.atts.options && !ctx.atts.type) 21 | }, 22 | groupAtts: function afQuickFieldGroupAtts () { 23 | // afQuickField passes `fields` and `omitFields` on to `afObjectField` 24 | // and `afArrayField`, but not to `afFormGroup` 25 | const { fields, omitFields, ...rest } = this 26 | return rest 27 | }, 28 | isHiddenInput: function afQuickFieldIsHiddenInput () { 29 | const ctx = AutoForm.Utility.getComponentContext(this, 'afQuickField') 30 | const inputType = ctx.atts.type 31 | if (inputType) { 32 | const componentDef = AutoForm._inputTypeDefinitions[inputType] 33 | if (!componentDef) { 34 | throw new Error(`AutoForm: No component found for rendering input with type "${inputType}"`) 35 | } 36 | return componentDef.isHidden 37 | } 38 | 39 | return false 40 | } 41 | }) 42 | -------------------------------------------------------------------------------- /components/afQuickFields/afQuickFields.html: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /components/afQuickFields/afQuickFields.js: -------------------------------------------------------------------------------- 1 | /* global AutoForm */ 2 | import { Template } from 'meteor/templating' 3 | 4 | Template.afQuickFields.helpers({ 5 | quickFieldAtts: function afQuickFieldsQuickFieldAtts () { 6 | let afQuickFieldsComponentAtts 7 | const atts = {} 8 | 9 | // Get the attributes that were on the afQuickFields component 10 | afQuickFieldsComponentAtts = Template.parentData(1) 11 | 12 | // It's possible to call {{> afQuickFields}} with no attributes, in which case we 13 | // don't want the 'attributes' because they're really just the parent context. 14 | if (!afQuickFieldsComponentAtts || afQuickFieldsComponentAtts.atts) { 15 | afQuickFieldsComponentAtts = {} 16 | } 17 | 18 | // Add default options from schema/allowed 19 | const defaultOptions = AutoForm._getOptionsForField(this.name) 20 | if (defaultOptions) { 21 | atts.options = defaultOptions 22 | } 23 | 24 | return { ...atts, ...afQuickFieldsComponentAtts, ...this } 25 | } 26 | }) 27 | -------------------------------------------------------------------------------- /components/autoForm/autoForm.html: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /components/autoForm/autoForm.js: -------------------------------------------------------------------------------- 1 | /* global AutoForm, ReactiveVar, setDefaults */ 2 | import { Template } from 'meteor/templating' 3 | import { EJSON } from 'meteor/ejson' 4 | import MongoObject from 'mongo-object' 5 | import { isObject, isFunction } from '../../common' 6 | import { Hooks } from '../../autoform-hooks' 7 | import { Utility } from '../../utility' 8 | import { arrayTracker } from '../../autoform-arrays' 9 | 10 | Template.autoForm.helpers({ 11 | atts: function autoFormTplAtts () { 12 | // After removing all of the props we know about, everything else should 13 | // become a form attribute unless it's an array or object. 14 | let val 15 | const htmlAttributes = {} 16 | const context = this 17 | const removeProps = [ 18 | 'schema', 19 | 'collection', 20 | 'validation', 21 | 'doc', 22 | 'resetOnSuccess', 23 | 'type', 24 | 'template', 25 | 'autosave', 26 | 'autosaveOnKeyup', 27 | 'meteormethod', 28 | 'methodargs', 29 | 'filter', 30 | 'autoConvert', 31 | 'removeEmptyStrings', 32 | 'trimStrings' 33 | ] 34 | 35 | // Filter out any attributes that have a component prefix 36 | function hasComponentPrefix (prop) { 37 | return Utility.componentTypeList.some(function (componentType) { 38 | return prop.indexOf(componentType + '-') === 0 39 | }) 40 | } 41 | 42 | // Filter out arrays and objects and functions, which are obviously not meant to be 43 | // HTML attributes. 44 | for (const prop in context) { 45 | if ( 46 | Object.prototype.hasOwnProperty.call(context, prop) && 47 | !removeProps.includes(prop) && 48 | !hasComponentPrefix(prop) 49 | ) { 50 | val = context[prop] 51 | if (!Array.isArray(val) && !isObject(val) && !isFunction(val)) { 52 | htmlAttributes[prop] = val 53 | } 54 | } 55 | } 56 | 57 | // By default, we add the `novalidate="novalidate"` attribute to our form, 58 | // unless the user passes `validation="browser"`. 59 | if (this.validation !== 'browser' && !htmlAttributes.novalidate) { 60 | htmlAttributes.novalidate = 'novalidate' 61 | } 62 | 63 | return htmlAttributes 64 | }, 65 | afDestroyUpdateForm: function (formId) { 66 | AutoForm._destroyForm[formId] = 67 | AutoForm._destroyForm[formId] || new ReactiveVar(false) 68 | return AutoForm._destroyForm[formId].get() 69 | } 70 | }) 71 | 72 | Template.autoForm.created = function autoFormCreated () { 73 | const template = this 74 | 75 | // We'll add tracker dependencies for reactive field values 76 | // to this object as necessary 77 | template.formValues = template.formValues || {} 78 | 79 | // We'll store "sticky" errors here. These are errors added 80 | // manually based on server validation, which we don't want to 81 | // be wiped out by further client validation. 82 | template._stickyErrors = {} 83 | 84 | template.autorun(function (c) { 85 | let data = Template.currentData() // rerun when current data changes 86 | const formId = data.id 87 | 88 | if (!formId) { 89 | throw new Error( 90 | 'Every autoForm and quickForm must have an "id" attribute set to a unique string.' 91 | ) 92 | } 93 | 94 | // When we change the form, loading a different doc, reloading the current doc, etc., 95 | // we also want to reset the array counts for the form 96 | 97 | arrayTracker.resetForm(formId) 98 | // and the stored last key value for the form 99 | delete AutoForm._lastKeyVals[formId] 100 | 101 | data = setDefaults(data) 102 | 103 | // Clone the doc so that docToForm and other modifications do not change 104 | // the original referenced object. 105 | let doc = data.doc ? EJSON.clone(data.doc) : null 106 | 107 | // Update cached form values for hot code reload persistence 108 | if (data.preserveForm === false) { 109 | AutoForm.formPreserve.unregisterForm(formId) 110 | } 111 | else { 112 | // Even if we have already registered, we reregister to ensure that the 113 | // closure values of template, formId, and ss remain correct after each 114 | // reaction 115 | AutoForm.formPreserve.registerForm( 116 | formId, 117 | function autoFormRegFormCallback () { 118 | return AutoForm.getFormValues( 119 | formId, 120 | template, 121 | data._resolvedSchema, 122 | false 123 | ) 124 | } 125 | ) 126 | } 127 | 128 | // Retain doc values after a "hot code push", if possible 129 | if (c.firstRun) { 130 | const retrievedDoc = AutoForm.formPreserve.getDocument(formId) 131 | if (retrievedDoc !== false) { 132 | // Ensure we keep the _id property which may not be present in retrievedDoc. 133 | doc = { ...doc, ...retrievedDoc } 134 | } 135 | } 136 | 137 | let mDoc 138 | if (doc && Object.keys(doc).length) { 139 | const hookCtx = { formId: formId } 140 | // Pass doc through docToForm hooks 141 | Hooks.getHooks(formId, 'docToForm').forEach( 142 | function autoFormEachDocToForm (hook) { 143 | doc = hook.call(hookCtx, doc, data._resolvedSchema) 144 | if (!doc) { 145 | throw new Error( 146 | 'Oops! Did you forget to return the modified document from your docToForm hook for the ' + 147 | formId + 148 | ' form?' 149 | ) 150 | } 151 | } 152 | ) 153 | 154 | // Create a "flat doc" that can be used to easily get values for corresponding 155 | // form fields. 156 | mDoc = new MongoObject(doc) 157 | AutoForm.reactiveFormData.sourceDoc(formId, mDoc) 158 | } 159 | else { 160 | AutoForm.reactiveFormData.sourceDoc(formId, undefined) 161 | } 162 | }) 163 | } 164 | 165 | Template.autoForm.rendered = function autoFormRendered () { 166 | let lastId 167 | this.autorun(function () { 168 | const data = Template.currentData() // rerun when current data changes 169 | 170 | if (data.id === lastId) return 171 | lastId = data.id 172 | 173 | AutoForm.triggerFormRenderedDestroyedReruns(data.id) 174 | }) 175 | } 176 | 177 | Template.autoForm.destroyed = function autoFormDestroyed () { 178 | const self = this 179 | const formId = self.data.id 180 | 181 | // TODO if formId was changing reactively during life of instance, 182 | // some data won't be removed by the calls below. 183 | 184 | // Remove from array fields list 185 | arrayTracker.untrackForm(formId) 186 | 187 | // Unregister form preservation 188 | AutoForm.formPreserve.unregisterForm(formId) 189 | 190 | // Trigger value reruns 191 | AutoForm.triggerFormRenderedDestroyedReruns(formId) 192 | } 193 | -------------------------------------------------------------------------------- /components/quickForm/quickForm.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/quickForm/quickForm.js: -------------------------------------------------------------------------------- 1 | /* global AutoForm */ 2 | import { Template } from 'meteor/templating' 3 | import { 4 | getSortedFieldGroupNames, 5 | getFieldsForGroup, 6 | getFieldsWithNoGroup 7 | } from './quickFormUtils' 8 | 9 | Template.quickForm.helpers({ 10 | getTemplateName: function () { 11 | return AutoForm.getTemplateName('quickForm', this.template) 12 | }, 13 | innerContext: function quickFormContext () { 14 | const atts = this 15 | const adjustedData = AutoForm.parseData({ ...this }) 16 | const simpleSchema = adjustedData._resolvedSchema 17 | const sortedSchema = {} 18 | const fieldGroups = [] 19 | let grouplessFieldContext 20 | 21 | // --------------- A. Schema --------------- // 22 | 23 | let fieldList = atts.fields 24 | if (fieldList) { 25 | fieldList = AutoForm.Utility.stringToArray(fieldList, 'AutoForm: fields attribute must be an array or a string containing a comma-delimited list of fields') 26 | } 27 | else { 28 | const fullSchema = simpleSchema.mergedSchema() 29 | fieldList = Object.keys(fullSchema) 30 | } 31 | 32 | // get the schema object, but sorted into the same order as the field list 33 | fieldList.forEach(fieldName => { 34 | sortedSchema[fieldName] = AutoForm.Utility.getFieldDefinition(simpleSchema, fieldName) 35 | }) 36 | 37 | // --------------- B. Field With No Groups --------------- // 38 | 39 | const grouplessFields = getFieldsWithNoGroup(sortedSchema) 40 | if (grouplessFields.length > 0) { 41 | grouplessFieldContext = { 42 | atts: { ...atts, fields: grouplessFields }, 43 | fields: grouplessFields 44 | } 45 | } 46 | 47 | // --------------- C. Field With Groups --------------- // 48 | 49 | // get sorted list of field groups 50 | const fieldGroupNames = getSortedFieldGroupNames(sortedSchema) 51 | 52 | // Loop through the list and make a field group context for each 53 | fieldGroupNames.forEach(function (fieldGroupName) { 54 | const fieldsForGroup = getFieldsForGroup(fieldGroupName, sortedSchema) 55 | 56 | if (fieldsForGroup.length > 0) { 57 | fieldGroups.push({ 58 | name: fieldGroupName, 59 | atts: { ...atts, fields: fieldsForGroup }, 60 | fields: fieldsForGroup 61 | }) 62 | } 63 | }) 64 | 65 | // --------------- D. Context --------------- // 66 | 67 | // Pass along quickForm context to autoForm context, minus a few 68 | // properties that are specific to quickForms. 69 | const { 70 | buttonContent, 71 | buttonClasses, 72 | fields, 73 | omitFields, 74 | 'id-prefix': idPrefix, 75 | ...qfAutoFormContext 76 | } = atts 77 | 78 | // Determine whether we want to render a submit button 79 | const qfShouldRenderButton = ( 80 | atts.buttonContent !== false && 81 | atts.type !== 'readonly' && 82 | atts.type !== 'disabled' 83 | ) 84 | 85 | return { 86 | qfAutoFormContext: qfAutoFormContext, 87 | atts: atts, 88 | qfShouldRenderButton: qfShouldRenderButton, 89 | fieldGroups: fieldGroups, 90 | grouplessFields: grouplessFieldContext 91 | } 92 | } 93 | }) 94 | -------------------------------------------------------------------------------- /components/quickForm/quickFormUtils.js: -------------------------------------------------------------------------------- 1 | const falsyValues = [null, undefined, '', false] 2 | const byFalsyValues = f => !falsyValues.includes(f) 3 | 4 | /** 5 | * Takes a schema object and returns a sorted array of field group names for it 6 | * 7 | * @param {Object} schemaObj Like from mySimpleSchema.schema() 8 | * @returns {String[]} Array of field group names 9 | */ 10 | export const getSortedFieldGroupNames = function getSortedFieldGroupNames (schemaObj) { 11 | const names = Object 12 | .values(schemaObj) 13 | .map(field => field.autoform && field.autoform.group) 14 | .filter(byFalsyValues) 15 | 16 | // Remove duplicate names and sort 17 | return [...new Set(names)].sort() 18 | } 19 | 20 | /** 21 | * Returns the schema field names that belong in the group. 22 | * 23 | * @param {String} groupName The group name 24 | * @param {Object} schemaObj Like from mySimpleSchema.schema() 25 | * @returns {String[]} Array of field names (schema keys) 26 | */ 27 | export const getFieldsForGroup = function getFieldsForGroup (groupName, schemaObj) { 28 | return Object 29 | .entries(schemaObj) 30 | .map(([fieldName, field]) => { 31 | return (fieldName.slice(-2) !== '.$') && 32 | field.autoform && 33 | field.autoform.group === groupName && 34 | fieldName 35 | }) 36 | .filter(byFalsyValues) 37 | } 38 | 39 | /** 40 | * Returns the schema field names that don't belong to a group 41 | * 42 | * @param {Object} schemaObj Like from mySimpleSchema.schema() 43 | * @returns {String[]} Array of field names (schema keys) 44 | */ 45 | export const getFieldsWithNoGroup = function getFieldsWithNoGroup (schemaObj) { 46 | return Object 47 | .entries(schemaObj) 48 | .map(function ([fieldName, field]) { 49 | return (fieldName.slice(-2) !== '.$') && 50 | (!field.autoform || !field.autoform.group) && 51 | fieldName 52 | }) 53 | .filter(byFalsyValues) 54 | } 55 | -------------------------------------------------------------------------------- /dynamic.js: -------------------------------------------------------------------------------- 1 | let initialized = false 2 | 3 | export const AutoForm = global.AutoForm 4 | 5 | AutoForm.load = async function load () { 6 | if (!initialized) { 7 | await init() 8 | initialized = true 9 | } 10 | 11 | return initialized 12 | } 13 | 14 | function init () { 15 | return Promise.all([ 16 | import('./autoform-helpers.js'), 17 | // form types 18 | import('./formTypes/insert.js'), 19 | import('./formTypes/update.js'), 20 | import('./formTypes/update-pushArray.js'), 21 | import('./formTypes/method.js'), 22 | import('./formTypes/method-update.js'), 23 | import('./formTypes/normal.js'), 24 | import('./formTypes/readonly.js'), 25 | import('./formTypes/disabled.js'), 26 | // input types 27 | import('./inputTypes/value-converters.js'), 28 | import('./inputTypes/boolean-checkbox/boolean-checkbox.html'), 29 | import('./inputTypes/boolean-checkbox/boolean-checkbox.js'), 30 | import('./inputTypes/boolean-radios/boolean-radios.html'), 31 | import('./inputTypes/boolean-radios/boolean-radios.js'), 32 | import('./inputTypes/boolean-select/boolean-select.html'), 33 | import('./inputTypes/boolean-select/boolean-select.js'), 34 | import('./inputTypes/button/button.html'), 35 | import('./inputTypes/button/button.js'), 36 | import('./inputTypes/color/color.html'), 37 | import('./inputTypes/color/color.js'), 38 | import('./inputTypes/contenteditable/contenteditable.html'), 39 | import('./inputTypes/contenteditable/contenteditable.js'), 40 | import('./inputTypes/date/date.html'), 41 | import('./inputTypes/date/date.js'), 42 | import('./inputTypes/datetime/datetime.html'), 43 | import('./inputTypes/datetime/datetime.js'), 44 | import('./inputTypes/datetime-local/datetime-local.html'), 45 | import('./inputTypes/datetime-local/datetime-local.js'), 46 | import('./inputTypes/email/email.html'), 47 | import('./inputTypes/email/email.js'), 48 | import('./inputTypes/file/file.html'), 49 | import('./inputTypes/file/file.js'), 50 | import('./inputTypes/hidden/hidden.html'), 51 | import('./inputTypes/hidden/hidden.js'), 52 | import('./inputTypes/image/image.html'), 53 | import('./inputTypes/image/image.js'), 54 | import('./inputTypes/month/month.html'), 55 | import('./inputTypes/month/month.js'), 56 | import('./inputTypes/number/number.html'), 57 | import('./inputTypes/number/number.js'), 58 | import('./inputTypes/password/password.html'), 59 | import('./inputTypes/password/password.js'), 60 | import('./inputTypes/radio/radio.html'), 61 | import('./inputTypes/radio/radio.js'), 62 | import('./inputTypes/range/range.html'), 63 | import('./inputTypes/range/range.js'), 64 | import('./inputTypes/reset/reset.html'), 65 | import('./inputTypes/reset/reset.js'), 66 | import('./inputTypes/search/search.html'), 67 | import('./inputTypes/search/search.js'), 68 | import('./inputTypes/select/select.html'), 69 | import('./inputTypes/select/select.js'), 70 | import('./inputTypes/select-checkbox/select-checkbox.html'), 71 | import('./inputTypes/select-checkbox/select-checkbox.js'), 72 | import('./inputTypes/select-checkbox-inline/select-checkbox-inline.html'), 73 | import('./inputTypes/select-checkbox-inline/select-checkbox-inline.js'), 74 | import('./inputTypes/select-multiple/select-multiple.html'), 75 | import('./inputTypes/select-multiple/select-multiple.js'), 76 | import('./inputTypes/select-radio/select-radio.html'), 77 | import('./inputTypes/select-radio/select-radio.js'), 78 | import('./inputTypes/select-radio-inline/select-radio-inline.html'), 79 | import('./inputTypes/select-radio-inline/select-radio-inline.js'), 80 | import('./inputTypes/submit/submit.html'), 81 | import('./inputTypes/submit/submit.js'), 82 | import('./inputTypes/tel/tel.html'), 83 | import('./inputTypes/tel/tel.js'), 84 | import('./inputTypes/text/text.html'), 85 | import('./inputTypes/text/text.js'), 86 | import('./inputTypes/textarea/textarea.html'), 87 | import('./inputTypes/textarea/textarea.js'), 88 | import('./inputTypes/time/time.html'), 89 | import('./inputTypes/time/time.js'), 90 | import('./inputTypes/url/url.html'), 91 | import('./inputTypes/url/url.js'), 92 | import('./inputTypes/week/week.html'), 93 | import('./inputTypes/week/week.js'), 94 | // components that render a form 95 | import('./components/autoForm/autoForm.html'), 96 | import('./components/autoForm/autoForm.js'), 97 | import('./components/quickForm/quickForm.html'), 98 | import('./components/quickForm/quickForm.js'), 99 | // components that render controls within a form 100 | import('./components/afArrayField/afArrayField.html'), 101 | import('./components/afArrayField/afArrayField.js'), 102 | import('./components/afEachArrayItem/afEachArrayItem.html'), 103 | import('./components/afEachArrayItem/afEachArrayItem.js'), 104 | import('./components/afFieldInput/afFieldInput.html'), 105 | import('./components/afFieldInput/afFieldInput.js'), 106 | import('./components/afFormGroup/afFormGroup.html'), 107 | import('./components/afFormGroup/afFormGroup.js'), 108 | import('./components/afObjectField/afObjectField.html'), 109 | import('./components/afObjectField/afObjectField.js'), 110 | import('./components/afQuickField/afQuickField.html'), 111 | import('./components/afQuickField/afQuickField.js'), 112 | import('./components/afQuickFields/afQuickFields.html'), 113 | import('./components/afQuickFields/afQuickFields.js'), 114 | // event handling 115 | import('./autoform-events.js') 116 | ]) 117 | } 118 | -------------------------------------------------------------------------------- /form-preserve.js: -------------------------------------------------------------------------------- 1 | /* global Package */ 2 | import { EJSON } from 'meteor/ejson' 3 | 4 | /** 5 | * Internal helper object to preserve form inputs across Hot Code Push 6 | * and across "pages" navigation if the option is enabled. 7 | */ 8 | export class FormPreserve { 9 | /** 10 | * @constructor 11 | * @param {String} migrationName 12 | */ 13 | constructor (migrationName) { 14 | const self = this 15 | if (typeof migrationName !== 'string') { 16 | throw Error('You must define an unique migration name of type String') 17 | } 18 | self.registeredForms = {} 19 | self.retrievedDocuments = {} 20 | if (Package.reload) { 21 | const Reload = Package.reload.Reload 22 | self.retrievedDocuments = Reload._migrationData(migrationName) || '{}' 23 | 24 | // Currently migration does not seem to support proper storage 25 | // of Date type. It comes back as a string, so we need to store 26 | // EJSON instead. 27 | if (typeof self.retrievedDocuments === 'string') { 28 | self.retrievedDocuments = EJSON.parse(self.retrievedDocuments) 29 | } 30 | 31 | Reload._onMigrate(migrationName, function () { 32 | const doc = self._retrieveRegisteredDocuments() 33 | return [true, EJSON.stringify(doc)] 34 | }) 35 | } 36 | } 37 | 38 | getDocument (formId) { 39 | const self = this 40 | if (!(formId in self.retrievedDocuments)) { 41 | return false 42 | } 43 | 44 | return self.retrievedDocuments[formId] 45 | } 46 | 47 | clearDocument (formId) { 48 | delete this.retrievedDocuments[formId] 49 | } 50 | 51 | registerForm (formId, retrieveFunc) { 52 | this.registeredForms[formId] = retrieveFunc 53 | } 54 | 55 | formIsRegistered (formId) { 56 | return !!this.registeredForms[formId] 57 | } 58 | 59 | unregisterForm (formId) { 60 | delete this.registeredForms[formId] 61 | delete this.retrievedDocuments[formId] 62 | } 63 | 64 | unregisterAllForms () { 65 | const self = this 66 | self.registeredForms = {} 67 | self.retrievedDocuments = {} 68 | } 69 | 70 | _retrieveRegisteredDocuments () { 71 | const self = this 72 | const res = {} 73 | Object 74 | .entries(self.registeredForms) 75 | .forEach(function ([formId, retrieveFunc]) { 76 | res[formId] = retrieveFunc() 77 | }) 78 | return res 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /formTypes/disabled.js: -------------------------------------------------------------------------------- 1 | /* global AutoForm */ 2 | 3 | AutoForm.addFormType('disabled', { 4 | onSubmit: function () { 5 | // Prevent browser form submission 6 | this.event.preventDefault() 7 | // Nothing else 8 | }, 9 | validateForm: function () { 10 | // Always valid 11 | return true 12 | }, 13 | adjustInputContext: function (ctx) { 14 | ctx.atts.disabled = '' 15 | return ctx 16 | }, 17 | hideArrayItemButtons: true 18 | }) 19 | -------------------------------------------------------------------------------- /formTypes/insert.js: -------------------------------------------------------------------------------- 1 | /* global AutoForm */ 2 | 3 | AutoForm.addFormType('insert', { 4 | onSubmit: function () { 5 | const ctx = this 6 | 7 | // Prevent browser form submission 8 | this.event.preventDefault() 9 | 10 | // Make sure we have a collection 11 | const collection = this.collection 12 | if (!collection) { 13 | throw new Error('AutoForm: You must specify a collection when form type is insert.') 14 | } 15 | 16 | // See if the collection has a schema attached 17 | const collectionHasSchema = (typeof collection.simpleSchema === 'function' && 18 | collection.simpleSchema(this.insertDoc) != null) 19 | 20 | // Run "before.insert" hooks 21 | this.runBeforeHooks(this.insertDoc, function (doc) { 22 | // Perform insert 23 | if (collectionHasSchema) { 24 | // If the collection2 pkg is used and a schema is attached, we pass a validationContext 25 | collection.insert(doc, ctx.validationOptions, ctx.result) 26 | } 27 | else { 28 | // If the collection2 pkg is not used or no schema is attached, we don't pass options 29 | // because core Meteor's `insert` function does not accept 30 | // an options argument. 31 | collection.insert(doc, ctx.result) 32 | } 33 | }) 34 | }, 35 | validateForm: function () { 36 | // Get SimpleSchema 37 | const formSchema = AutoForm.getFormSchema(this.form.id) 38 | // Validate 39 | return AutoForm._validateFormDoc(this.formDoc, false, this.form.id, formSchema, this.form) 40 | }, 41 | shouldPrevalidate: function () { 42 | // Prevalidate only if there is both a `schema` attribute and a `collection` attribute 43 | return !!this.formAttributes.collection && !!this.formAttributes.schema 44 | } 45 | }) 46 | -------------------------------------------------------------------------------- /formTypes/method-update.js: -------------------------------------------------------------------------------- 1 | /* global AutoForm */ 2 | import { Meteor } from 'meteor/meteor' 3 | 4 | AutoForm.addFormType('method-update', { 5 | onSubmit: function () { 6 | const ctx = this 7 | 8 | // Prevent browser form submission 9 | this.event.preventDefault() 10 | 11 | if (!this.formAttributes.meteormethod) { 12 | throw new Error( 13 | 'When form type is "method-update", you must also provide a "meteormethod" attribute' 14 | ) 15 | } 16 | 17 | // Run "before.method" hooks 18 | this.runBeforeHooks(this.updateDoc, function (updateDoc) { 19 | // Validate. If both schema and collection were provided, then we validate 20 | // against the collection schema here. Otherwise we validate against whichever 21 | // one was passed. 22 | const valid = 23 | ctx.formAttributes.validation === 'none' || 24 | ctx.formTypeDefinition.validateForm.call({ 25 | form: ctx.formAttributes, 26 | formDoc: updateDoc, 27 | useCollectionSchema: ctx.ssIsOverride 28 | }) 29 | 30 | if (valid === false) { 31 | ctx.failedValidation() 32 | } 33 | else { 34 | const { methodargs } = ctx.formAttributes 35 | const args = methodargs 36 | ? typeof methodargs === 'function' 37 | ? methodargs() 38 | : methodargs 39 | : [] 40 | // Call the method. If a ddp connection was provided, use 41 | // that instead of the default Meteor connection 42 | let ddp = ctx.formAttributes.ddp 43 | ddp = ddp && typeof ddp.call === 'function' ? ddp : Meteor 44 | ddp.call( 45 | ctx.formAttributes.meteormethod, 46 | { 47 | _id: ctx.docId, 48 | modifier: updateDoc 49 | }, 50 | ...args, 51 | ctx.result 52 | ) 53 | } 54 | }) 55 | }, 56 | usesModifier: true, 57 | validateForm: function () { 58 | // Get SimpleSchema 59 | let formSchema = AutoForm.getFormSchema(this.form.id) 60 | 61 | const collection = AutoForm.getFormCollection(this.form.id) 62 | // If there is a `schema` attribute but you want to force validation against the 63 | // collection's schema instead, pass useCollectionSchema=true 64 | formSchema = this.useCollectionSchema && collection 65 | ? collection.simpleSchema() 66 | : formSchema 67 | 68 | // We validate the modifier. We don't want to throw errors about missing required fields, etc. 69 | return AutoForm._validateFormDoc( 70 | this.formDoc, 71 | true, 72 | this.form.id, 73 | formSchema, 74 | this.form 75 | ) 76 | }, 77 | shouldPrevalidate: function () { 78 | // Prevalidate only if there is both a `schema` attribute and a `collection` attribute 79 | return !!this.formAttributes.collection && !!this.formAttributes.schema 80 | } 81 | }) 82 | -------------------------------------------------------------------------------- /formTypes/method.js: -------------------------------------------------------------------------------- 1 | /* global AutoForm */ 2 | import { Meteor } from 'meteor/meteor' 3 | 4 | AutoForm.addFormType('method', { 5 | onSubmit: function () { 6 | const ctx = this 7 | 8 | // Prevent browser form submission 9 | this.event.preventDefault() 10 | 11 | if (!this.formAttributes.meteormethod) { 12 | throw new Error( 13 | 'When form type is "method", you must also provide a "meteormethod" attribute' 14 | ) 15 | } 16 | 17 | // Run "before.method" hooks 18 | this.runBeforeHooks(this.insertDoc, function (doc) { 19 | // Validate. If both schema and collection were provided, then we validate 20 | // against the collection schema here. Otherwise we validate against whichever 21 | // one was passed. 22 | const valid = 23 | ctx.formAttributes.validation === 'none' || 24 | ctx.formTypeDefinition.validateForm.call({ 25 | form: ctx.formAttributes, 26 | formDoc: doc, 27 | useCollectionSchema: ctx.ssIsOverride 28 | }) 29 | 30 | if (valid === false) { 31 | ctx.failedValidation() 32 | } 33 | else { 34 | const { methodargs } = ctx.formAttributes 35 | const args = methodargs 36 | ? typeof methodargs === 'function' 37 | ? methodargs() 38 | : methodargs 39 | : [] 40 | // Call the method. If a ddp connection was provided, use 41 | // that instead of the default Meteor connection 42 | let ddp = ctx.formAttributes.ddp 43 | ddp = ddp && typeof ddp.call === 'function' ? ddp : Meteor 44 | ddp.call(ctx.formAttributes.meteormethod, doc, ...args, ctx.result) 45 | } 46 | }) 47 | }, 48 | validateForm: function () { 49 | // Get SimpleSchema 50 | let formSchema = AutoForm.getFormSchema(this.form.id) 51 | 52 | const collection = AutoForm.getFormCollection(this.form.id) 53 | // If there is a `schema` attribute but you want to force validation against the 54 | // collection's schema instead, pass useCollectionSchema=true 55 | formSchema = this.useCollectionSchema && collection 56 | ? collection.simpleSchema() 57 | : formSchema 58 | 59 | // Validate 60 | return AutoForm._validateFormDoc( 61 | this.formDoc, 62 | false, 63 | this.form.id, 64 | formSchema, 65 | this.form 66 | ) 67 | }, 68 | shouldPrevalidate: function () { 69 | // Prevalidate only if there is both a `schema` attribute and a `collection` attribute 70 | return !!this.formAttributes.collection && !!this.formAttributes.schema 71 | } 72 | }) 73 | -------------------------------------------------------------------------------- /formTypes/normal.js: -------------------------------------------------------------------------------- 1 | import { Hooks } from '../autoform-hooks' 2 | /* global AutoForm */ 3 | 4 | AutoForm.addFormType('normal', { 5 | onSubmit: function () { 6 | const ctx = this 7 | 8 | // Get onSubmit hooks 9 | // These are called differently from the before hooks because 10 | // they run async, but they can run in parallel and we need the 11 | // result of all of them immediately because they can return 12 | // false to stop normal form submission. 13 | const hooks = Hooks.getHooks(this.formId, 'onSubmit') 14 | 15 | const hookCount = hooks.length 16 | let doneCount = 0 17 | let submitError 18 | let submitResult 19 | 20 | if (hookCount === 0) { 21 | // we haven't called preventDefault, so normal browser 22 | // submission will now happen 23 | this.endSubmission() 24 | return 25 | } 26 | 27 | // Set up onSubmit hook context 28 | const onSubmitCtx = { 29 | done: function (error, result) { 30 | doneCount++ 31 | if (!submitError && error) { 32 | submitError = error 33 | } 34 | if (!submitResult && result) { 35 | submitResult = result 36 | } 37 | if (doneCount === hookCount) { 38 | // run onError, onSuccess, endSubmit 39 | ctx.result(submitError, submitResult) 40 | } 41 | }, 42 | ...this.hookContext 43 | } 44 | 45 | // Call all hooks at once. 46 | // Pass both types of doc plus the doc attached to the form. 47 | // If any return false, we stop normal submission, but we don't 48 | // run onError, onSuccess, endSubmit hooks until they all call this.done(). 49 | let shouldStop = false 50 | hooks.forEach(function eachOnSubmit (hook) { 51 | const result = hook.call(onSubmitCtx, ctx.insertDoc, ctx.updateDoc, ctx.currentDoc) 52 | if (shouldStop === false && result === false) { 53 | shouldStop = true 54 | } 55 | }) 56 | if (shouldStop) { 57 | this.event.preventDefault() 58 | this.event.stopPropagation() 59 | } 60 | }, 61 | needsModifierAndDoc: true, 62 | validateForm: function () { 63 | // Get SimpleSchema 64 | const formSchema = AutoForm.getFormSchema(this.form.id) 65 | // Validate 66 | return AutoForm._validateFormDoc(this.formDoc.insertDoc, false, this.form.id, formSchema, this.form) 67 | } 68 | }) 69 | -------------------------------------------------------------------------------- /formTypes/readonly.js: -------------------------------------------------------------------------------- 1 | /* global AutoForm */ 2 | 3 | AutoForm.addFormType('readonly', { 4 | onSubmit: function () { 5 | // Prevent browser form submission 6 | this.event.preventDefault() 7 | // Nothing else 8 | }, 9 | validateForm: function () { 10 | // Always valid 11 | return true 12 | }, 13 | adjustInputContext: function (ctx) { 14 | ctx.atts.readonly = '' 15 | return ctx 16 | }, 17 | hideArrayItemButtons: true 18 | }) 19 | -------------------------------------------------------------------------------- /formTypes/update-pushArray.js: -------------------------------------------------------------------------------- 1 | /* global AutoForm */ 2 | 3 | AutoForm.addFormType('update-pushArray', { 4 | onSubmit: function () { 5 | const ctx = this 6 | 7 | // Prevent browser form submission 8 | this.event.preventDefault() 9 | 10 | // Make sure we have a collection 11 | const collection = this.collection 12 | if (!collection) { 13 | throw new Error('AutoForm: You must specify a collection when form type is update-pushArray.') 14 | } 15 | 16 | // Make sure we have a scope 17 | const scope = ctx.formAttributes.scope 18 | if (!scope) { 19 | throw new Error('AutoForm: You must specify a scope when form type is update-pushArray.') 20 | } 21 | 22 | // Run "before.update" hooks 23 | this.runBeforeHooks(this.insertDoc, function (doc) { 24 | if (!Object.keys(doc).length) { // make sure this check stays after the before hooks 25 | // Nothing to update. Just treat it as a successful update. 26 | ctx.result(null, 0) 27 | } 28 | else { 29 | const modifer = { $push: {} } 30 | modifer.$push[scope] = doc 31 | // Perform update 32 | collection.update({ _id: ctx.docId }, modifer, ctx.validationOptions, ctx.result) 33 | } 34 | }) 35 | }, 36 | validateForm: function () { 37 | // Get SimpleSchema 38 | const formSchema = AutoForm.getFormSchema(this.form.id) 39 | // We validate as if it's an insert form 40 | return AutoForm._validateFormDoc(this.formDoc, false, this.form.id, formSchema, this.form) 41 | }, 42 | adjustSchema: function (formSchema) { 43 | return formSchema.getObjectSchema(`${this.form.scope}.$`) 44 | }, 45 | shouldPrevalidate: function () { 46 | // Prevalidate because the form is generated with a schema 47 | // that has keys different from the collection schema 48 | return true 49 | } 50 | }) 51 | -------------------------------------------------------------------------------- /formTypes/update.js: -------------------------------------------------------------------------------- 1 | /* global AutoForm */ 2 | 3 | AutoForm.addFormType('update', { 4 | onSubmit: function () { 5 | const ctx = this 6 | 7 | // Prevent browser form submission 8 | this.event.preventDefault() 9 | 10 | // Make sure we have a collection 11 | const collection = this.collection 12 | if (!collection) { 13 | throw new Error('AutoForm: You must specify a collection when form type is update.') 14 | } 15 | 16 | // Run "before.update" hooks 17 | this.runBeforeHooks(this.updateDoc, function (modifier) { 18 | if (!Object.keys(modifier).length) { // make sure this check stays after the before hooks 19 | // Nothing to update. Just treat it as a successful update. 20 | ctx.result(null, 0) 21 | } 22 | else { 23 | // Perform update 24 | collection.update({ _id: ctx.docId }, modifier, ctx.validationOptions, ctx.result) 25 | } 26 | }) 27 | }, 28 | usesModifier: true, 29 | validateForm: function () { 30 | // Get SimpleSchema 31 | const formSchema = AutoForm.getFormSchema(this.form.id) 32 | // We validate the modifier. We don't want to throw errors about missing required fields, etc. 33 | return AutoForm._validateFormDoc(this.formDoc, true, this.form.id, formSchema, this.form) 34 | }, 35 | shouldPrevalidate: function () { 36 | // Prevalidate only if there is both a `schema` attribute and a `collection` attribute 37 | return !!this.formAttributes.collection && !!this.formAttributes.schema 38 | } 39 | }) 40 | -------------------------------------------------------------------------------- /getMoment.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Resolves optional/weak momentjs dependency 3 | * @return {function|undefined} 4 | */ 5 | export const getMoment = (throwIfNotFound) => { 6 | if (!moment) { 7 | const message = 'aldeed:autoform requires momentjs:moment to handle date/time.' 8 | if (throwIfNotFound) { 9 | throw new TypeError(message) 10 | } 11 | else { 12 | console.warn(message) 13 | } 14 | } 15 | return moment 16 | } 17 | 18 | const name = 'momentjs:moment' 19 | const moment = ((packageDef) => { 20 | if (typeof packageDef !== 'undefined' && packageDef[name]) { 21 | return packageDef[name].moment 22 | } 23 | })(window.Package) 24 | -------------------------------------------------------------------------------- /inputTypes/boolean-checkbox/boolean-checkbox.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /inputTypes/boolean-checkbox/boolean-checkbox.js: -------------------------------------------------------------------------------- 1 | AutoForm.addInputType('boolean-checkbox', { 2 | template: 'afCheckbox', 3 | valueOut: function () { 4 | return !!this.is(':checked') 5 | }, 6 | valueConverters: { 7 | string: AutoForm.valueConverters.booleanToString, 8 | stringArray: AutoForm.valueConverters.booleanToStringArray, 9 | number: AutoForm.valueConverters.booleanToNumber, 10 | numberArray: AutoForm.valueConverters.booleanToNumberArray 11 | }, 12 | contextAdjust: function (context) { 13 | if (context.value === true) { 14 | context.atts.checked = '' 15 | } 16 | // don't add required attribute to checkboxes because some browsers assume that to mean that it must be checked, which is not what we mean by "required" 17 | delete context.atts.required 18 | return context 19 | } 20 | }) 21 | -------------------------------------------------------------------------------- /inputTypes/boolean-radios/boolean-radios.html: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /inputTypes/boolean-radios/boolean-radios.js: -------------------------------------------------------------------------------- 1 | import { Template } from 'meteor/templating' 2 | 3 | AutoForm.addInputType('boolean-radios', { 4 | template: 'afBooleanRadioGroup', 5 | valueOut: function () { 6 | if (this.find('input[value=false]').is(':checked')) { 7 | return false 8 | } 9 | else if (this.find('input[value=true]').is(':checked')) { 10 | return true 11 | } 12 | else if (this.find('input[value=null]').is(':checked')) { 13 | return null 14 | } 15 | }, 16 | valueConverters: { 17 | string: AutoForm.valueConverters.booleanToString, 18 | stringArray: AutoForm.valueConverters.booleanToStringArray, 19 | number: AutoForm.valueConverters.booleanToNumber, 20 | numberArray: AutoForm.valueConverters.booleanToNumberArray 21 | } 22 | }) 23 | 24 | Template.afBooleanRadioGroup.helpers({ 25 | falseAtts: function falseAtts () { 26 | const { trueLabel, falseLabel, nullLabel, 'data-schema-key': dataSchemaKey, ...atts } = this.atts 27 | if (this.value === false) { 28 | atts.checked = '' 29 | } 30 | return atts 31 | }, 32 | trueAtts: function trueAtts () { 33 | const { trueLabel, falseLabel, nullLabel, 'data-schema-key': dataSchemaKey, ...atts } = this.atts 34 | if (this.value === true) { 35 | atts.checked = '' 36 | } 37 | return atts 38 | }, 39 | nullAtts: function nullAtts () { 40 | const { trueLabel, falseLabel, nullLabel, 'data-schema-key': dataSchemaKey, ...atts } = this.atts 41 | if (this.value !== true && this.value !== false) { 42 | atts.checked = '' 43 | } 44 | return atts 45 | }, 46 | dsk: function () { 47 | return { 'data-schema-key': this.atts['data-schema-key'] } 48 | } 49 | }) 50 | -------------------------------------------------------------------------------- /inputTypes/boolean-select/boolean-select.html: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /inputTypes/boolean-select/boolean-select.js: -------------------------------------------------------------------------------- 1 | AutoForm.addInputType('boolean-select', { 2 | template: 'afBooleanSelect', 3 | valueOut: function () { 4 | const val = this.val() 5 | if (val === 'true') { 6 | return true 7 | } 8 | else if (val === 'false') { 9 | return false 10 | } 11 | }, 12 | valueConverters: { 13 | string: AutoForm.valueConverters.booleanToString, 14 | stringArray: AutoForm.valueConverters.booleanToStringArray, 15 | number: AutoForm.valueConverters.booleanToNumber, 16 | numberArray: AutoForm.valueConverters.booleanToNumberArray 17 | }, 18 | contextAdjust: function (context) { 19 | const { trueLabel, falseLabel, nullLabel, firstOption, ...atts } = context.atts 20 | 21 | // build items list 22 | context.items = [ 23 | { 24 | name: context.name, 25 | value: '', 26 | // _id must be included because it is a special property that 27 | // #each uses to track unique list items when adding and removing them 28 | // See https://github.com/meteor/meteor/issues/2174 29 | _id: '', 30 | selected: (context.value !== false && context.value !== true), 31 | label: context.atts.nullLabel || context.atts.firstOption || '(Select One)', 32 | atts: atts 33 | }, 34 | { 35 | name: context.name, 36 | value: 'false', 37 | // _id must be included because it is a special property that 38 | // #each uses to track unique list items when adding and removing them 39 | // See https://github.com/meteor/meteor/issues/2174 40 | _id: 'false', 41 | selected: (context.value === false), 42 | label: context.atts.falseLabel || 'False', 43 | atts: atts 44 | }, 45 | { 46 | name: context.name, 47 | value: 'true', 48 | // _id must be included because it is a special property that 49 | // #each uses to track unique list items when adding and removing them 50 | // See https://github.com/meteor/meteor/issues/2174 51 | _id: 'true', 52 | selected: (context.value === true), 53 | label: context.atts.trueLabel || 'True', 54 | atts: atts 55 | } 56 | ] 57 | 58 | return context 59 | } 60 | }) 61 | -------------------------------------------------------------------------------- /inputTypes/button/button.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /inputTypes/button/button.js: -------------------------------------------------------------------------------- 1 | AutoForm.addInputType('button', { 2 | template: 'afInputButton' 3 | }) 4 | -------------------------------------------------------------------------------- /inputTypes/color/color.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /inputTypes/color/color.js: -------------------------------------------------------------------------------- 1 | AutoForm.addInputType('color', { 2 | template: 'afInputColor' 3 | }) 4 | -------------------------------------------------------------------------------- /inputTypes/contenteditable/contenteditable.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /inputTypes/contenteditable/contenteditable.js: -------------------------------------------------------------------------------- 1 | import { Template } from 'meteor/templating' 2 | 3 | AutoForm.addInputType('contenteditable', { 4 | template: 'afContenteditable', 5 | valueOut: function () { 6 | return this.html() 7 | }, 8 | contextAdjust: function (context) { 9 | if (typeof context.atts['data-maxlength'] === 'undefined' && typeof context.max === 'number') { 10 | context.atts['data-maxlength'] = context.max 11 | } 12 | return context 13 | } 14 | }) 15 | 16 | Template.afContenteditable.events({ 17 | 'blur div[contenteditable=true]': function (event, template) { 18 | template.$(event.target).change() 19 | } 20 | }) 21 | 22 | Template.afContenteditable.helpers({ 23 | getValue: function (value) { 24 | if (Template.instance().view.isRendered) { 25 | Template.instance().$('[contenteditable]').html(value) 26 | } 27 | } 28 | }) 29 | 30 | Template.afContenteditable.onRendered(function () { 31 | const template = this 32 | 33 | template.autorun(function () { 34 | const data = Template.currentData() 35 | template.$('[contenteditable]').html(data.value) 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /inputTypes/date/date.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /inputTypes/date/date.js: -------------------------------------------------------------------------------- 1 | AutoForm.addInputType('date', { 2 | template: 'afInputDate', 3 | valueIn: function (val) { 4 | // convert Date to string value 5 | return AutoForm.valueConverters.dateToDateStringUTC(val) 6 | }, 7 | valueOut: function () { 8 | const val = this.val() 9 | if (AutoForm.Utility.isValidDateString(val)) { 10 | // Date constructor will interpret val as UTC and create 11 | // date at mignight in the morning of val date in UTC time zone 12 | return new Date(val) 13 | } 14 | else { 15 | return null 16 | } 17 | }, 18 | valueConverters: { 19 | string: AutoForm.valueConverters.dateToDateStringUTC, 20 | stringArray: AutoForm.valueConverters.dateToDateStringUTCArray, 21 | number: AutoForm.valueConverters.dateToNumber, 22 | numberArray: AutoForm.valueConverters.dateToNumberArray, 23 | dateArray: AutoForm.valueConverters.dateToDateArray 24 | }, 25 | contextAdjust: function (context) { 26 | if (typeof context.atts.max === 'undefined' && context.max instanceof Date) { 27 | context.atts.max = AutoForm.valueConverters.dateToDateStringUTC(context.max) 28 | } 29 | if (typeof context.atts.min === 'undefined' && context.min instanceof Date) { 30 | context.atts.min = AutoForm.valueConverters.dateToDateStringUTC(context.min) 31 | } 32 | return context 33 | } 34 | }) 35 | -------------------------------------------------------------------------------- /inputTypes/datetime-local/datetime-local.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /inputTypes/datetime-local/datetime-local.js: -------------------------------------------------------------------------------- 1 | import { getMoment } from '../../getMoment' 2 | 3 | AutoForm.addInputType('datetime-local', { 4 | template: 'afInputDateTimeLocal', 5 | valueIn: function (val, atts) { 6 | // convert Date to string value 7 | return (val instanceof Date) ? AutoForm.valueConverters.dateToNormalizedLocalDateAndTimeString(val, atts.timezoneId) : val 8 | }, 9 | valueOut: function () { 10 | const moment = getMoment(true) 11 | let val = this.val() 12 | val = (typeof val === 'string') ? val.replace(/ /g, 'T') : val 13 | if (AutoForm.Utility.isValidNormalizedLocalDateAndTimeString(val)) { 14 | const timezoneId = this.attr('data-timezone-id') 15 | // default is local, but if there's a timezoneId, we use that 16 | if (typeof timezoneId === 'string') { 17 | if (typeof moment.tz !== 'function') { 18 | throw new Error("If you specify a timezoneId, make sure that you've added a moment-timezone package to your app") 19 | } 20 | return moment.tz(val, timezoneId).toDate() 21 | } 22 | else { 23 | return moment(val).toDate() 24 | } 25 | } 26 | else { 27 | return this.val() 28 | } 29 | }, 30 | valueConverters: { 31 | string: function dateToNormalizedLocalDateAndTimeString (val) { 32 | return (val instanceof Date) ? AutoForm.valueConverters.dateToNormalizedLocalDateAndTimeString(val, this.attr('data-timezone-id')) : val 33 | }, 34 | stringArray: function dateToNormalizedLocalDateAndTimeStringArray (val) { 35 | if (val instanceof Date) { 36 | return [AutoForm.valueConverters.dateToNormalizedLocalDateAndTimeString(val, this.attr('data-timezone-id'))] 37 | } 38 | return val 39 | }, 40 | number: AutoForm.valueConverters.dateToNumber, 41 | numberArray: AutoForm.valueConverters.dateToNumberArray, 42 | dateArray: AutoForm.valueConverters.dateToDateArray 43 | }, 44 | contextAdjust: function (context) { 45 | if (typeof context.atts.max === 'undefined' && context.max instanceof Date) { 46 | context.atts.max = AutoForm.valueConverters.dateToNormalizedLocalDateAndTimeString(context.max, context.atts.timezoneId) 47 | } 48 | if (typeof context.atts.min === 'undefined' && context.min instanceof Date) { 49 | context.atts.min = AutoForm.valueConverters.dateToNormalizedLocalDateAndTimeString(context.min, context.atts.timezoneId) 50 | } 51 | if (context.atts.timezoneId) { 52 | context.atts['data-timezone-id'] = context.atts.timezoneId 53 | } 54 | delete context.atts.timezoneId 55 | return context 56 | } 57 | }) 58 | -------------------------------------------------------------------------------- /inputTypes/datetime/datetime.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /inputTypes/datetime/datetime.js: -------------------------------------------------------------------------------- 1 | AutoForm.addInputType('datetime', { 2 | template: 'afInputDateTime', 3 | valueIn: function (val) { 4 | // convert Date to string value 5 | return AutoForm.valueConverters.dateToNormalizedForcedUtcGlobalDateAndTimeString(val) 6 | }, 7 | valueOut: function () { 8 | let val = this.val() 9 | val = (typeof val === 'string') ? val.replace(/ /g, 'T') : val 10 | if (AutoForm.Utility.isValidNormalizedForcedUtcGlobalDateAndTimeString(val)) { 11 | // Date constructor will interpret val as UTC due to ending "Z" 12 | return new Date(val) 13 | } 14 | else { 15 | return null 16 | } 17 | }, 18 | valueConverters: { 19 | string: AutoForm.valueConverters.dateToNormalizedForcedUtcGlobalDateAndTimeString, 20 | stringArray: AutoForm.valueConverters.dateToNormalizedForcedUtcGlobalDateAndTimeStringArray, 21 | number: AutoForm.valueConverters.dateToNumber, 22 | numberArray: AutoForm.valueConverters.dateToNumberArray, 23 | dateArray: AutoForm.valueConverters.dateToDateArray 24 | }, 25 | contextAdjust: function (context) { 26 | if (typeof context.atts.max === 'undefined' && context.max instanceof Date) { 27 | context.atts.max = AutoForm.valueConverters.dateToNormalizedForcedUtcGlobalDateAndTimeString(context.max) 28 | } 29 | if (typeof context.atts.min === 'undefined' && context.min instanceof Date) { 30 | context.atts.min = AutoForm.valueConverters.dateToNormalizedForcedUtcGlobalDateAndTimeString(context.min) 31 | } 32 | return context 33 | } 34 | }) 35 | -------------------------------------------------------------------------------- /inputTypes/email/email.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /inputTypes/email/email.js: -------------------------------------------------------------------------------- 1 | AutoForm.addInputType('email', { 2 | template: 'afInputEmail', 3 | contextAdjust: function (context) { 4 | if (typeof context.atts.maxlength === 'undefined' && typeof context.max === 'number') { 5 | context.atts.maxlength = context.max 6 | } 7 | return context 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /inputTypes/file/file.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /inputTypes/file/file.js: -------------------------------------------------------------------------------- 1 | AutoForm.addInputType('file', { 2 | template: 'afInputFile' 3 | }) 4 | -------------------------------------------------------------------------------- /inputTypes/hidden/hidden.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /inputTypes/hidden/hidden.js: -------------------------------------------------------------------------------- 1 | AutoForm.addInputType('hidden', { 2 | template: 'afInputHidden', 3 | isHidden: true, 4 | valueOut: function () { 5 | return this.val() 6 | }, 7 | valueConverters: { 8 | stringArray: AutoForm.valueConverters.stringToStringArray, 9 | number: AutoForm.valueConverters.stringToNumber, 10 | numberArray: AutoForm.valueConverters.stringToNumberArray, 11 | boolean: AutoForm.valueConverters.stringToBoolean, 12 | booleanArray: AutoForm.valueConverters.stringToBooleanArray, 13 | date: AutoForm.valueConverters.stringToDate, 14 | dateArray: AutoForm.valueConverters.stringToDateArray 15 | } 16 | }) 17 | -------------------------------------------------------------------------------- /inputTypes/image/image.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /inputTypes/image/image.js: -------------------------------------------------------------------------------- 1 | AutoForm.addInputType('image', { 2 | template: 'afInputImage' 3 | }) 4 | -------------------------------------------------------------------------------- /inputTypes/month/month.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /inputTypes/month/month.js: -------------------------------------------------------------------------------- 1 | AutoForm.addInputType('month', { 2 | template: 'afInputMonth', 3 | valueConverters: { 4 | stringArray: AutoForm.valueConverters.stringToStringArray 5 | } 6 | }) 7 | -------------------------------------------------------------------------------- /inputTypes/number/number.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /inputTypes/number/number.js: -------------------------------------------------------------------------------- 1 | AutoForm.addInputType('number', { 2 | template: 'afInputNumber', 3 | valueOut: function () { 4 | return AutoForm.valueConverters.stringToNumber(this.val()) 5 | }, 6 | valueConverters: { 7 | string: AutoForm.valueConverters.numberToString, 8 | stringArray: AutoForm.valueConverters.numberToStringArray, 9 | numberArray: AutoForm.valueConverters.numberToNumberArray, 10 | boolean: AutoForm.valueConverters.numberToBoolean, 11 | booleanArray: AutoForm.valueConverters.numberToBooleanArray 12 | }, 13 | contextAdjust: function (context) { 14 | if (typeof context.atts.max === 'undefined' && typeof context.max === 'number') { 15 | context.atts.max = context.max 16 | } 17 | if (typeof context.atts.min === 'undefined' && typeof context.min === 'number') { 18 | context.atts.min = context.min 19 | } 20 | return context 21 | } 22 | }) 23 | -------------------------------------------------------------------------------- /inputTypes/password/password.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /inputTypes/password/password.js: -------------------------------------------------------------------------------- 1 | AutoForm.addInputType('password', { 2 | template: 'afInputPassword', 3 | valueConverters: { 4 | stringArray: AutoForm.valueConverters.stringToStringArray 5 | }, 6 | contextAdjust: function (context) { 7 | if (typeof context.atts.maxlength === 'undefined' && typeof context.max === 'number') { 8 | context.atts.maxlength = context.max 9 | } 10 | return context 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /inputTypes/radio/radio.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /inputTypes/radio/radio.js: -------------------------------------------------------------------------------- 1 | import { Template } from 'meteor/templating' 2 | 3 | AutoForm.addInputType('radio', { 4 | template: 'afRadio', 5 | valueOut: function () { 6 | if (this.is(':checked')) { 7 | return this.val() 8 | } 9 | }, 10 | valueConverters: { 11 | stringArray: AutoForm.valueConverters.stringToStringArray 12 | } 13 | }) 14 | 15 | Template.afRadio.helpers({ 16 | atts: function selectedAttsAdjust () { 17 | const atts = { ...this.atts } 18 | if (this.selected) { 19 | atts.checked = '' 20 | } 21 | return atts 22 | } 23 | }) 24 | -------------------------------------------------------------------------------- /inputTypes/range/range.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /inputTypes/range/range.js: -------------------------------------------------------------------------------- 1 | AutoForm.addInputType('range', { 2 | template: 'afInputRange', 3 | valueOut: function () { 4 | return AutoForm.valueConverters.stringToNumber(this.val()) 5 | }, 6 | valueConverters: { 7 | string: AutoForm.valueConverters.numberToString, 8 | stringArray: AutoForm.valueConverters.numberToStringArray, 9 | numberArray: AutoForm.valueConverters.numberToNumberArray, 10 | boolean: AutoForm.valueConverters.numberToBoolean, 11 | booleanArray: AutoForm.valueConverters.numberToBooleanArray 12 | } 13 | }) 14 | -------------------------------------------------------------------------------- /inputTypes/reset/reset.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /inputTypes/reset/reset.js: -------------------------------------------------------------------------------- 1 | AutoForm.addInputType('reset', { 2 | template: 'afInputReset' 3 | }) 4 | -------------------------------------------------------------------------------- /inputTypes/search/search.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /inputTypes/search/search.js: -------------------------------------------------------------------------------- 1 | AutoForm.addInputType('search', { 2 | template: 'afInputSearch', 3 | valueConverters: { 4 | stringArray: AutoForm.valueConverters.stringToStringArray 5 | }, 6 | contextAdjust: function (context) { 7 | if (typeof context.atts.maxlength === 'undefined' && typeof context.max === 'number') { 8 | context.atts.maxlength = context.max 9 | } 10 | return context 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /inputTypes/select-checkbox-inline/select-checkbox-inline.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /inputTypes/select-checkbox-inline/select-checkbox-inline.js: -------------------------------------------------------------------------------- 1 | /* global $ */ 2 | import { Template } from 'meteor/templating' 3 | 4 | AutoForm.addInputType('select-checkbox-inline', { 5 | template: 'afCheckboxGroupInline', 6 | valueIsArray: true, 7 | valueOut: function () { 8 | const val = [] 9 | this.find('input[type=checkbox]').each(function () { 10 | if ($(this).is(':checked')) { 11 | val.push($(this).val()) 12 | } 13 | }) 14 | return val 15 | }, 16 | contextAdjust: function (context) { 17 | const itemAtts = { ...context.atts } 18 | 19 | // build items list 20 | context.items = [] 21 | 22 | // Add all defined options 23 | context.selectOptions.forEach(function (opt) { 24 | context.items.push({ 25 | name: context.name, 26 | label: opt.label, 27 | value: opt.value, 28 | // _id must be included because it is a special property that 29 | // #each uses to track unique list items when adding and removing them 30 | // See https://github.com/meteor/meteor/issues/2174 31 | _id: opt.value, 32 | selected: (context.value.includes(opt.value)), 33 | atts: itemAtts 34 | }) 35 | }) 36 | 37 | return context 38 | } 39 | }) 40 | 41 | Template.afCheckboxGroupInline.helpers({ 42 | atts: function selectedAttsAdjust () { 43 | const atts = { ...this.atts } 44 | if (this.selected) { 45 | atts.checked = '' 46 | } 47 | // remove data-schema-key attribute because we put it 48 | // on the entire group 49 | delete atts['data-schema-key'] 50 | return atts 51 | }, 52 | dsk: function dsk () { 53 | return { 54 | 'data-schema-key': this.atts['data-schema-key'] 55 | } 56 | } 57 | }) 58 | -------------------------------------------------------------------------------- /inputTypes/select-checkbox/select-checkbox.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /inputTypes/select-checkbox/select-checkbox.js: -------------------------------------------------------------------------------- 1 | /* global $ */ 2 | import { Template } from 'meteor/templating' 3 | 4 | AutoForm.addInputType('select-checkbox', { 5 | template: 'afCheckboxGroup', 6 | valueIsArray: true, 7 | valueOut: function () { 8 | const val = [] 9 | this.find('input[type=checkbox]').each(function () { 10 | if ($(this).is(':checked')) { 11 | val.push($(this).val()) 12 | } 13 | }) 14 | return val 15 | }, 16 | contextAdjust: function (context) { 17 | const itemAtts = { ...context.atts } 18 | 19 | // build items list 20 | context.items = [] 21 | 22 | // Add all defined options 23 | context.selectOptions.forEach(function (opt) { 24 | context.items.push({ 25 | name: context.name, 26 | label: opt.label, 27 | value: opt.value, 28 | // _id must be included because it is a special property that 29 | // #each uses to track unique list items when adding and removing them 30 | // See https://github.com/meteor/meteor/issues/2174 31 | _id: opt.value, 32 | selected: (context.value.includes(opt.value)), 33 | atts: itemAtts 34 | }) 35 | }) 36 | 37 | return context 38 | } 39 | }) 40 | 41 | Template.afCheckboxGroup.helpers({ 42 | atts: function selectedAttsAdjust () { 43 | const atts = { ...this.atts } 44 | if (this.selected) { 45 | atts.checked = '' 46 | } 47 | // remove data-schema-key attribute because we put it 48 | // on the entire group 49 | delete atts['data-schema-key'] 50 | return atts 51 | }, 52 | dsk: function dsk () { 53 | return { 54 | 'data-schema-key': this.atts['data-schema-key'] 55 | } 56 | } 57 | }) 58 | -------------------------------------------------------------------------------- /inputTypes/select-multiple/select-multiple.html: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /inputTypes/select-multiple/select-multiple.js: -------------------------------------------------------------------------------- 1 | AutoForm.addInputType('select-multiple', { 2 | template: 'afSelectMultiple', 3 | valueIsArray: true, 4 | valueOut: function () { 5 | return AutoForm.Utility.getSelectValues(this[0]) 6 | }, 7 | contextAdjust: function (context) { 8 | // build items list 9 | context.items = context.selectOptions.map(function (opt) { 10 | if (opt.optgroup) { 11 | const subItems = opt.options.map(function (subOpt) { 12 | const { label, value, ...htmlAtts } = subOpt 13 | return { 14 | name: context.name, 15 | label, 16 | value, 17 | htmlAtts, 18 | // _id must be included because it is a special property that 19 | // #each uses to track unique list items when adding and removing them 20 | // See https://github.com/meteor/meteor/issues/2174 21 | _id: subOpt.value, 22 | selected: context.value.includes(subOpt.value), 23 | disabled: !!opt.disabled, 24 | atts: context.atts 25 | } 26 | }) 27 | return { 28 | optgroup: opt.optgroup, 29 | items: subItems 30 | } 31 | } 32 | else { 33 | const { label, value, ...htmlAtts } = opt 34 | return { 35 | name: context.name, 36 | label, 37 | value, 38 | htmlAtts, 39 | // _id must be included because it is a special property that 40 | // #each uses to track unique list items when adding and removing them 41 | // See https://github.com/meteor/meteor/issues/2174 42 | _id: opt.value, 43 | selected: context.value.includes(opt.value), 44 | disabled: !!opt.disabled, 45 | atts: context.atts 46 | } 47 | } 48 | }) 49 | 50 | return context 51 | } 52 | }) 53 | -------------------------------------------------------------------------------- /inputTypes/select-radio-inline/select-radio-inline.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /inputTypes/select-radio-inline/select-radio-inline.js: -------------------------------------------------------------------------------- 1 | import { Template } from 'meteor/templating' 2 | 3 | AutoForm.addInputType('select-radio-inline', { 4 | template: 'afRadioGroupInline', 5 | valueOut: function () { 6 | return this.find('input[type=radio]:checked').val() 7 | }, 8 | contextAdjust: function (context) { 9 | const itemAtts = { ...context.atts } 10 | 11 | // build items list 12 | context.items = [] 13 | 14 | // Add all defined options 15 | context.selectOptions.forEach(function (opt) { 16 | context.items.push({ 17 | name: context.name, 18 | label: opt.label, 19 | value: opt.value, 20 | // _id must be included because it is a special property that 21 | // #each uses to track unique list items when adding and removing them 22 | // See https://github.com/meteor/meteor/issues/2174 23 | _id: opt.value, 24 | selected: (opt.value === context.value), 25 | atts: itemAtts 26 | }) 27 | }) 28 | 29 | return context 30 | } 31 | }) 32 | 33 | Template.afRadioGroupInline.helpers({ 34 | atts: function selectedAttsAdjust () { 35 | const atts = { ...this.atts } 36 | if (this.selected) { 37 | atts.checked = '' 38 | } 39 | // remove data-schema-key attribute because we put it 40 | // on the entire group 41 | delete atts['data-schema-key'] 42 | return atts 43 | }, 44 | dsk: function dsk () { 45 | return { 46 | 'data-schema-key': this.atts['data-schema-key'] 47 | } 48 | } 49 | }) 50 | -------------------------------------------------------------------------------- /inputTypes/select-radio/select-radio.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /inputTypes/select-radio/select-radio.js: -------------------------------------------------------------------------------- 1 | import { Template } from 'meteor/templating' 2 | 3 | AutoForm.addInputType('select-radio', { 4 | template: 'afRadioGroup', 5 | valueOut: function () { 6 | return this.find('input[type=radio]:checked').val() 7 | }, 8 | contextAdjust: function (context) { 9 | const itemAtts = { ...context.atts } 10 | 11 | // build items list 12 | context.items = [] 13 | 14 | // Add all defined options 15 | context.selectOptions.forEach(function (opt) { 16 | context.items.push({ 17 | name: context.name, 18 | label: opt.label, 19 | value: opt.value, 20 | // _id must be included because it is a special property that 21 | // #each uses to track unique list items when adding and removing them 22 | // See https://github.com/meteor/meteor/issues/2174 23 | _id: opt.value, 24 | selected: (opt.value === context.value), 25 | atts: itemAtts 26 | }) 27 | }) 28 | 29 | return context 30 | } 31 | }) 32 | 33 | Template.afRadioGroup.helpers({ 34 | atts: function selectedAttsAdjust () { 35 | const atts = { ...this.atts } 36 | if (this.selected) { 37 | atts.checked = '' 38 | } 39 | // remove data-schema-key attribute because we put it 40 | // on the entire group 41 | delete atts['data-schema-key'] 42 | return atts 43 | }, 44 | dsk: function dsk () { 45 | return { 46 | 'data-schema-key': this.atts['data-schema-key'] 47 | } 48 | } 49 | }) 50 | -------------------------------------------------------------------------------- /inputTypes/select/select.html: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /inputTypes/select/select.js: -------------------------------------------------------------------------------- 1 | AutoForm.addInputType('select', { 2 | template: 'afSelect', 3 | valueOut: function () { 4 | return this.val() 5 | }, 6 | valueConverters: { 7 | stringArray: AutoForm.valueConverters.stringToStringArray, 8 | number: AutoForm.valueConverters.stringToNumber, 9 | numberArray: AutoForm.valueConverters.stringToNumberArray, 10 | boolean: AutoForm.valueConverters.stringToBoolean, 11 | booleanArray: AutoForm.valueConverters.stringToBooleanArray, 12 | date: AutoForm.valueConverters.stringToDate, 13 | dateArray: AutoForm.valueConverters.stringToDateArray 14 | }, 15 | contextAdjust: function (context) { 16 | // can fix issues with some browsers selecting the firstOption instead of the selected option 17 | context.atts.autocomplete = 'off' 18 | 19 | let { firstOption, ...itemAtts } = context.atts 20 | 21 | // build items list 22 | context.items = [] 23 | 24 | // If a firstOption was provided, add that to the items list first 25 | if (firstOption !== false) { 26 | if (typeof firstOption === 'function') { 27 | firstOption = firstOption() 28 | } 29 | context.items.push({ 30 | name: context.name, 31 | label: (typeof firstOption === 'string' ? firstOption : '(Select One)'), 32 | value: '', 33 | // _id must be included because it is a special property that 34 | // #each uses to track unique list items when adding and removing them 35 | // See https://github.com/meteor/meteor/issues/2174 36 | // 37 | // Setting this to an empty string caused problems if option with value 38 | // 1 was in the options list because Spacebars evaluates "" to 1 and 39 | // considers that a duplicate. 40 | // See https://github.com/aldeed/meteor-autoform/issues/656 41 | _id: 'AUTOFORM_EMPTY_FIRST_OPTION', 42 | selected: false, 43 | atts: itemAtts 44 | }) 45 | } 46 | 47 | // Add all defined options 48 | context.selectOptions.forEach(function (opt) { 49 | if (opt.optgroup) { 50 | const subItems = opt.options.map(function (subOpt) { 51 | const { label, value, ...htmlAtts } = subOpt 52 | return { 53 | name: context.name, 54 | label, 55 | value, 56 | htmlAtts, 57 | // _id must be included because it is a special property that 58 | // #each uses to track unique list items when adding and removing them 59 | // See https://github.com/meteor/meteor/issues/2174 60 | // 61 | // The toString() is necessary because otherwise Spacebars evaluates 62 | // any string to 1 if the other values are numbers, and then considers 63 | // that a duplicate. 64 | // See https://github.com/aldeed/meteor-autoform/issues/656 65 | _id: subOpt.value.toString(), 66 | selected: (subOpt.value === context.value), 67 | atts: itemAtts 68 | } 69 | }) 70 | context.items.push({ 71 | optgroup: opt.optgroup, 72 | items: subItems 73 | }) 74 | } 75 | else { 76 | const { label, value, ...htmlAtts } = opt 77 | context.items.push({ 78 | name: context.name, 79 | label, 80 | value, 81 | htmlAtts, 82 | // _id must be included because it is a special property that 83 | // #each uses to track unique list items when adding and removing them 84 | // See https://github.com/meteor/meteor/issues/2174 85 | // 86 | // The toString() is necessary because otherwise Spacebars evaluates 87 | // any string to 1 if the other values are numbers, and then considers 88 | // that a duplicate. 89 | // See https://github.com/aldeed/meteor-autoform/issues/656 90 | _id: opt.value.toString(), 91 | selected: (opt.value === context.value), 92 | atts: itemAtts 93 | }) 94 | } 95 | }) 96 | 97 | return context 98 | } 99 | }) 100 | -------------------------------------------------------------------------------- /inputTypes/submit/submit.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /inputTypes/submit/submit.js: -------------------------------------------------------------------------------- 1 | AutoForm.addInputType('submit', { 2 | template: 'afInputSubmit' 3 | }) 4 | -------------------------------------------------------------------------------- /inputTypes/tel/tel.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /inputTypes/tel/tel.js: -------------------------------------------------------------------------------- 1 | AutoForm.addInputType('tel', { 2 | template: 'afInputTel', 3 | valueConverters: { 4 | stringArray: AutoForm.valueConverters.stringToStringArray 5 | }, 6 | contextAdjust: function (context) { 7 | if (typeof context.atts.maxlength === 'undefined' && typeof context.max === 'number') { 8 | context.atts.maxlength = context.max 9 | } 10 | return context 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /inputTypes/text/text.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /inputTypes/text/text.js: -------------------------------------------------------------------------------- 1 | AutoForm.addInputType('text', { 2 | template: 'afInputText', 3 | valueOut: function () { 4 | return this.val() 5 | }, 6 | valueConverters: { 7 | stringArray: AutoForm.valueConverters.stringToStringArray, 8 | number: AutoForm.valueConverters.stringToNumber, 9 | numberArray: AutoForm.valueConverters.stringToNumberArray, 10 | boolean: AutoForm.valueConverters.stringToBoolean, 11 | booleanArray: AutoForm.valueConverters.stringToBooleanArray, 12 | date: AutoForm.valueConverters.stringToDate, 13 | dateArray: AutoForm.valueConverters.stringToDateArray 14 | }, 15 | contextAdjust: function (context) { 16 | if (typeof context.atts.maxlength === 'undefined' && typeof context.max === 'number') { 17 | context.atts.maxlength = context.max 18 | } 19 | return context 20 | } 21 | }) 22 | -------------------------------------------------------------------------------- /inputTypes/textarea/textarea.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /inputTypes/textarea/textarea.js: -------------------------------------------------------------------------------- 1 | AutoForm.addInputType('textarea', { 2 | template: 'afTextarea', 3 | valueConverters: { 4 | stringArray: function (val) { 5 | if (typeof val === 'string' && val.length > 0) { 6 | return linesToArray(val) 7 | } 8 | return val 9 | }, 10 | number: AutoForm.valueConverters.stringToNumber, 11 | numberArray: AutoForm.valueConverters.stringToNumberArray, 12 | boolean: AutoForm.valueConverters.stringToBoolean, 13 | booleanArray: function (val) { 14 | if (typeof val === 'string' && val.length > 0) { 15 | const arr = linesToArray(val) 16 | return arr.map(function (item) { 17 | return AutoForm.valueConverters.stringToBoolean(item) 18 | }) 19 | } 20 | return val 21 | }, 22 | date: AutoForm.valueConverters.stringToDate, 23 | dateArray: function (val) { 24 | if (typeof val === 'string' && val.length > 0) { 25 | const arr = linesToArray(val) 26 | return arr.map(function (item) { 27 | return AutoForm.valueConverters.stringToDate(item) 28 | }) 29 | } 30 | return val 31 | } 32 | }, 33 | contextAdjust: function (context) { 34 | if (typeof context.atts.maxlength === 'undefined' && typeof context.max === 'number') { 35 | context.atts.maxlength = context.max 36 | } 37 | return context 38 | } 39 | }) 40 | 41 | function linesToArray (text) { 42 | text = text.split('\n') 43 | const lines = [] 44 | text.forEach(function (line) { 45 | line = line.trim() 46 | if (line.length) { 47 | lines.push(line) 48 | } 49 | }) 50 | return lines 51 | } 52 | -------------------------------------------------------------------------------- /inputTypes/time/time.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /inputTypes/time/time.js: -------------------------------------------------------------------------------- 1 | AutoForm.addInputType('time', { 2 | template: 'afInputTime', 3 | valueConverters: { 4 | stringArray: AutoForm.valueConverters.stringToStringArray 5 | } 6 | }) 7 | -------------------------------------------------------------------------------- /inputTypes/url/url.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /inputTypes/url/url.js: -------------------------------------------------------------------------------- 1 | AutoForm.addInputType('url', { 2 | template: 'afInputUrl', 3 | valueConverters: { 4 | stringArray: AutoForm.valueConverters.stringToStringArray 5 | }, 6 | contextAdjust: function (context) { 7 | if (typeof context.atts.maxlength === 'undefined' && typeof context.max === 'number') { 8 | context.atts.maxlength = context.max 9 | } 10 | return context 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /inputTypes/value-converters.js: -------------------------------------------------------------------------------- 1 | /* global AutoForm */ 2 | import { Utility } from '../utility' 3 | import { getMoment } from '../getMoment' 4 | 5 | const isDate = d => Object.prototype.toString.call(d) === '[object Date]' 6 | const toTrimmedString = s => s.trim() 7 | 8 | /** 9 | * The conversion functions in this file can be used by input types to convert 10 | * heir outgoing values into the data type expected by the schema. 11 | */ 12 | AutoForm.valueConverters = { 13 | 14 | /** 15 | * Converts a boolean to a string 16 | * @param val 17 | * @return {String} a String value representing a boolean value 18 | */ 19 | booleanToString: function booleanToString (val) { 20 | if (val === true) { 21 | return 'TRUE' 22 | } 23 | if (val === false) { 24 | return 'FALSE' 25 | } 26 | return val 27 | }, 28 | booleanToStringArray: function booleanToStringArray (val) { 29 | if (val === true) { 30 | return ['TRUE'] 31 | } 32 | if (val === false) { 33 | return ['FALSE'] 34 | } 35 | return val 36 | }, 37 | booleanToNumber: function booleanToNumber (val) { 38 | if (val === true) { 39 | return 1 40 | } 41 | if (val === false) { 42 | return 0 43 | } 44 | return val 45 | }, 46 | booleanToNumberArray: function booleanToNumberArray (val) { 47 | if (val === true) { 48 | return [1] 49 | } 50 | if (val === false) { 51 | return [0] 52 | } 53 | return val 54 | }, 55 | /** 56 | * @method AutoForm.valueConverters.dateToDateString 57 | * @private 58 | * @param {Date} val 59 | * @return {String} 60 | * 61 | * Returns a "valid date string" representing the local date in format 62 | * YYYY-MM-DD 63 | */ 64 | dateToDateString: function dateToDateString (val) { 65 | if (!isDate(val)) return val 66 | 67 | const fyr = (val.getFullYear()).toString().padStart(4, '0') 68 | const mon = (val.getMonth() + 1).toString().padStart(2, '0') 69 | const day = (val.getDate()).toString().padStart(2, '0') 70 | 71 | return `${fyr}-${mon}-${day}` 72 | }, 73 | /** 74 | * @method AutoForm.valueConverters.dateToDateStringUTC 75 | * @private 76 | * @param {Date} val 77 | * @return {String} 78 | * 79 | * Returns a "valid date string" representing the date converted to the UTC time zone. 80 | */ 81 | dateToDateStringUTC: function dateToDateStringUTC (val) { 82 | if (!isDate(val)) return val 83 | 84 | const fyr = (val.getUTCFullYear()).toString().padStart(4, '0') 85 | const mon = (val.getUTCMonth() + 1).toString().padStart(2, '0') 86 | const day = (val.getUTCDate()).toString().padStart(2, '0') 87 | 88 | return `${fyr}-${mon}-${day}` 89 | }, 90 | /** 91 | * 92 | * @param val 93 | * @return {*} 94 | */ 95 | dateToDateStringUTCArray: function dateToDateStringUTCArray (val) { 96 | if (!isDate(val)) return val 97 | 98 | return [AutoForm.valueConverters.dateToDateStringUTC(val)] 99 | }, 100 | /** 101 | * @method AutoForm.valueConverters.dateToNormalizedForcedUtcGlobalDateAndTimeString 102 | * @private 103 | * @param {Date} val 104 | * @return {String} 105 | * 106 | * Returns a "valid normalized forced-UTC global date and time string" representing the time 107 | * converted to the UTC time zone and expressed as the shortest possible string for the given 108 | * time (e.g. omitting the seconds component entirely if the given time is zero seconds past the minute). 109 | * 110 | * http://www.whatwg.org/specs/web-apps/current-work/multipage/states-of-the-type-attribute.html#date-and-time-state-(type=datetime) 111 | * http://www.whatwg.org/specs/web-apps/current-work/multipage/common-microsyntaxes.html#valid-normalized-forced-utc-global-date-and-time-string 112 | */ 113 | dateToNormalizedForcedUtcGlobalDateAndTimeString: function dateToNormalizedForcedUtcGlobalDateAndTimeString (val) { 114 | if (!isDate(val)) return val 115 | 116 | return val.toISOString() 117 | }, 118 | dateToNormalizedForcedUtcGlobalDateAndTimeStringArray: function dateToNormalizedForcedUtcGlobalDateAndTimeStringArray (val) { 119 | if (!isDate(val)) return val 120 | 121 | return [AutoForm.valueConverters.dateToNormalizedForcedUtcGlobalDateAndTimeString(val)] 122 | }, 123 | /** 124 | * @method AutoForm.valueConverters.dateToNormalizedLocalDateAndTimeString 125 | * @private 126 | * @param {Date} date The Date object 127 | * @param {String} [timezoneId] A valid timezoneId that moment-timezone understands, e.g., "America/Los_Angeles" 128 | * @return {String} 129 | * 130 | * Returns a "valid normalized local date and time string". 131 | */ 132 | dateToNormalizedLocalDateAndTimeString: function dateToNormalizedLocalDateAndTimeString (date, timezoneId) { 133 | const moment = getMoment() 134 | if (!isDate(date) || !moment) return date 135 | 136 | const m = moment(date) 137 | // by default, we assume local timezone; add moment-timezone to app and pass timezoneId 138 | // to use a different timezone 139 | if (typeof timezoneId === 'string') { 140 | if (typeof m.tz !== 'function') { 141 | throw new Error("If you specify a timezoneId, make sure that you've added a moment-timezone package to your app") 142 | } 143 | m.tz(timezoneId) 144 | } 145 | return m.format('YYYY-MM-DD[T]HH:mm:ss.SSS') 146 | }, 147 | /** 148 | * Returns the timestamp number of a date 149 | * @param {Date} val 150 | * @return {number} the unix timestamp of the date 151 | */ 152 | dateToNumber: function dateToNumber (val) { 153 | return isDate(val) 154 | ? val.getTime() 155 | : val 156 | }, 157 | dateToNumberArray: function dateToNumberArray (val) { 158 | return isDate(val) 159 | ? [val.getTime()] 160 | : val 161 | }, 162 | dateToDateArray: function dateToDateArray (val) { 163 | return isDate(val) 164 | ? [val] 165 | : val 166 | }, 167 | /** 168 | * Returns an array of (trimmed) strings from a comma-separated string. 169 | * @example 'hello, world' => ['hello', 'world'] 170 | * @param {string] val 171 | * @return {[String]} 172 | */ 173 | stringToStringArray: function stringToStringArray (val) { 174 | if (typeof val === 'string') { 175 | return val.split(',').map(toTrimmedString) 176 | } 177 | return val 178 | }, 179 | /** 180 | * @method AutoForm.valueConverters.stringToNumber 181 | * @public 182 | * @param {String} val A string or null or undefined. 183 | * @return {Number|String} The string converted to a Number or the original value. 184 | * 185 | * For strings, returns Number(val) unless the result is NaN. Otherwise returns val. 186 | */ 187 | stringToNumber: function stringToNumber (val) { 188 | if (typeof val === 'string' && val.length > 0) { 189 | const numVal = Number(val) 190 | if (!isNaN(numVal)) { 191 | return numVal 192 | } 193 | } 194 | return val 195 | }, 196 | stringToNumberArray: function stringToNumberArray (val) { 197 | if (typeof val === 'string') { 198 | return val.split(',').map(item => AutoForm.valueConverters.stringToNumber(item.trim())) 199 | } 200 | return val 201 | }, 202 | /** 203 | * @method AutoForm.valueConverters.stringToBoolean 204 | * @private 205 | * @param {String} val A string or null or undefined. 206 | * @return {Boolean|String} The string converted to a Boolean. 207 | * 208 | * If the string is "true" or "1", returns `true`. If the string is "false" or "0", returns `false`. Otherwise returns the original string. 209 | */ 210 | stringToBoolean: function stringToBoolean (val) { 211 | if (typeof val === 'string' && val.length > 0) { 212 | const lowerCaseVal = val.toLowerCase() 213 | if (lowerCaseVal === 'true' || lowerCaseVal === '1') { 214 | return true 215 | } 216 | if (lowerCaseVal === 'false' || lowerCaseVal === '0') { 217 | return false 218 | } 219 | } 220 | return val 221 | }, 222 | stringToBooleanArray: function stringToBooleanArray (val) { 223 | if (typeof val === 'string') { 224 | return val.split(',').map(item => AutoForm.valueConverters.stringToBoolean(item.trim())) 225 | } 226 | return val 227 | }, 228 | /** 229 | * @method AutoForm.valueConverters.stringToDate 230 | * @private 231 | * @param {String} val A string or null or undefined. 232 | * @return {Date|String} The string converted to a Date instance. 233 | * 234 | * Returns new Date(val) as long as val is a string with at least one character. 235 | * If the resulting date is an 'Invalid Date' the original string is returned. 236 | * Otherwise returns the original string. 237 | */ 238 | stringToDate: function stringToDate (val) { 239 | if (typeof val === 'string' && val.length > 0) { 240 | let d 241 | 242 | // try number-strings first 243 | const num = Number(val) 244 | d = !Number.isNaN(num) && new Date(num) 245 | 246 | if (d && d.toString() !== 'Invalid Date') { 247 | return d 248 | } 249 | 250 | // fallback to direct value input 251 | d = new Date(val) 252 | 253 | if (d.toString() !== 'Invalid Date') { 254 | return d 255 | } 256 | } 257 | return val 258 | }, 259 | stringToDateArray: function stringToDateArray (val) { 260 | if (typeof val === 'string') { 261 | return val.split(',').map(item => AutoForm.valueConverters.stringToDate(item.trim())) 262 | } 263 | return val 264 | }, 265 | numberToString: function numberToString (val) { 266 | if (typeof val === 'number') { 267 | return val.toString() 268 | } 269 | return val 270 | }, 271 | numberToStringArray: function numberToStringArray (val) { 272 | if (typeof val === 'number') { 273 | return [val.toString()] 274 | } 275 | return val 276 | }, 277 | numberToNumberArray: function numberToNumberArray (val) { 278 | if (typeof val === 'number') { 279 | return [val] 280 | } 281 | return val 282 | }, 283 | numberToBoolean: function numberToBoolean (val) { 284 | if (val === 0) { 285 | return false 286 | } 287 | if (val === 1) { 288 | return true 289 | } 290 | return val 291 | }, 292 | numberToBooleanArray: function numberToBooleanArray (val) { 293 | if (val === 0) { 294 | return [false] 295 | } 296 | if (val === 1) { 297 | return [true] 298 | } 299 | return val 300 | } 301 | } 302 | 303 | // BACKWARDS COMPATIBILITY - some of these were formerly on the Utility object 304 | Utility.dateToDateString = AutoForm.valueConverters.dateToDateString 305 | Utility.dateToDateStringUTC = AutoForm.valueConverters.dateToDateStringUTC 306 | Utility.dateToNormalizedForcedUtcGlobalDateAndTimeString = AutoForm.valueConverters.dateToNormalizedForcedUtcGlobalDateAndTimeString 307 | Utility.dateToNormalizedLocalDateAndTimeString = AutoForm.valueConverters.dateToNormalizedLocalDateAndTimeString 308 | Utility.stringToBool = AutoForm.valueConverters.stringToBoolean 309 | Utility.stringToNumber = AutoForm.valueConverters.stringToNumber 310 | Utility.stringToDate = AutoForm.valueConverters.stringToDate 311 | -------------------------------------------------------------------------------- /inputTypes/week/week.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /inputTypes/week/week.js: -------------------------------------------------------------------------------- 1 | AutoForm.addInputType('week', { 2 | template: 'afInputWeek', 3 | valueConverters: { 4 | stringArray: AutoForm.valueConverters.stringToStringArray 5 | } 6 | }) 7 | -------------------------------------------------------------------------------- /internal.js: -------------------------------------------------------------------------------- 1 | import { Tracker } from 'meteor/tracker' 2 | 3 | export const Internal = {} 4 | 5 | Internal.globalDefaultTemplate = 'bootstrap3' 6 | 7 | Internal.defaultTypeTemplates = {} 8 | 9 | Internal.deps = { 10 | defaultTemplate: new Tracker.Dependency(), 11 | defaultTypeTemplates: {} 12 | } 13 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | /* eslint-env meteor */ 2 | Package.describe({ 3 | name: 'aldeed:autoform', 4 | summary: 5 | 'Easily create forms with automatic insert and update, and automatic reactive validation.', 6 | git: 'https://github.com/aldeed/meteor-autoform.git', 7 | version: '8.0.0' 8 | }) 9 | 10 | Npm.depends({ 11 | 'mongo-object': '3.0.1' 12 | }) 13 | 14 | Package.onUse(function (api) { 15 | api.versionsFrom(['3.0.1']) 16 | 17 | // Dependencies 18 | api.use([ 19 | 'ejson', 20 | 'reactive-var', 21 | 'reactive-dict', 22 | 'random', 23 | 'ecmascript', 24 | 'mongo', 25 | 'blaze@3.0.0', 26 | 'templating@1.4.4', 27 | 'jquery@3.0.0' 28 | ]) 29 | 30 | api.use( 31 | [ 32 | 'momentjs:moment@2.30.1', 33 | 'mrt:moment-timezone@0.2.1', 34 | 'aldeed:collection2@4.0.4', 35 | 'aldeed:simple-schema@2.0.0', 36 | 'aldeed:moment-timezone@0.4.0', 37 | 'reload' 38 | ], 39 | 'client', 40 | { weak: true } 41 | ) 42 | 43 | // Exports 44 | api.export('AutoForm', 'client') 45 | 46 | // adding the core files in order to keep it backwards-compatible with 47 | // extensions and themes 48 | api.addFiles([ 49 | './utility.js', 50 | './form-preserve.js', 51 | './autoform-hooks.js', 52 | './autoform-formdata.js', 53 | './autoform-arrays.js', 54 | './autoform.js', 55 | './autoform-validation.js', 56 | './autoform-inputs.js', 57 | './autoform-api.js' 58 | ], 'client') 59 | 60 | // api.mainModule('main.js', 'client') 61 | }) 62 | 63 | Package.onTest(function (api) { 64 | api.versionsFrom(['2.8.0', '3.0.1']) 65 | // Running the tests requires a dummy project in order to 66 | // resolve npm dependencies and the test env dependencies. 67 | api.use([ 68 | 'ecmascript', 69 | 'random', 70 | 'tracker', 71 | 'mongo', 72 | 'blaze@3.0.0', 73 | 'templating@1.4.4', 74 | 'meteortesting:mocha@3.2.0' 75 | ]) 76 | api.use([ 77 | 'aldeed:collection2@4.0.4', 78 | 'momentjs:moment@2.30.1' 79 | ], 'client', { weak: true }) 80 | api.use([ 81 | 'aldeed:autoform@8.0.0', 82 | 'aldeed:moment-timezone', 83 | 'aldeed:simple-schema@2.0.0' 84 | ], 'client') 85 | 86 | api.addFiles([ 87 | 'tests/setup.tests.js', 88 | 'tests/utility.tests.js', 89 | 'tests/common.tests.js', 90 | 'tests/FormPreserve.tests.js', 91 | 'tests/FormData.tests.js', 92 | 'tests/Hooks.tests.js', 93 | 'tests/ArrayTracker.tests.js', 94 | 'tests/autoform-inputs.tests.js', 95 | 'tests/autoform-helpers.tests.js', 96 | 'tests/autoform-validation.tests.js', 97 | 'tests/autoform-api.tests.js', 98 | // component specific 99 | 'tests/components/quickForm/quickFormUtils.tests.js', 100 | // input types 101 | 'tests/inputTypes/value-converters.tests.js' 102 | ], 'client') 103 | }) 104 | -------------------------------------------------------------------------------- /static.js: -------------------------------------------------------------------------------- 1 | import './autoform-helpers.js' 2 | // form types 3 | import './formTypes/insert.js' 4 | import './formTypes/update.js' 5 | import './formTypes/update-pushArray.js' 6 | import './formTypes/method.js' 7 | import './formTypes/method-update.js' 8 | import './formTypes/normal.js' 9 | import './formTypes/readonly.js' 10 | import './formTypes/disabled.js' 11 | // input types 12 | import './inputTypes/value-converters.js' 13 | import './inputTypes/boolean-checkbox/boolean-checkbox.html' 14 | import './inputTypes/boolean-checkbox/boolean-checkbox.js' 15 | import './inputTypes/boolean-radios/boolean-radios.html' 16 | import './inputTypes/boolean-radios/boolean-radios.js' 17 | import './inputTypes/boolean-select/boolean-select.html' 18 | import './inputTypes/boolean-select/boolean-select.js' 19 | import './inputTypes/button/button.html' 20 | import './inputTypes/button/button.js' 21 | import './inputTypes/color/color.html' 22 | import './inputTypes/color/color.js' 23 | import './inputTypes/contenteditable/contenteditable.html' 24 | import './inputTypes/contenteditable/contenteditable.js' 25 | import './inputTypes/date/date.html' 26 | import './inputTypes/date/date.js' 27 | import './inputTypes/datetime/datetime.html' 28 | import './inputTypes/datetime/datetime.js' 29 | import './inputTypes/datetime-local/datetime-local.html' 30 | import './inputTypes/datetime-local/datetime-local.js' 31 | import './inputTypes/email/email.html' 32 | import './inputTypes/email/email.js' 33 | import './inputTypes/file/file.html' 34 | import './inputTypes/file/file.js' 35 | import './inputTypes/hidden/hidden.html' 36 | import './inputTypes/hidden/hidden.js' 37 | import './inputTypes/image/image.html' 38 | import './inputTypes/image/image.js' 39 | import './inputTypes/month/month.html' 40 | import './inputTypes/month/month.js' 41 | import './inputTypes/number/number.html' 42 | import './inputTypes/number/number.js' 43 | import './inputTypes/password/password.html' 44 | import './inputTypes/password/password.js' 45 | import './inputTypes/radio/radio.html' 46 | import './inputTypes/radio/radio.js' 47 | import './inputTypes/range/range.html' 48 | import './inputTypes/range/range.js' 49 | import './inputTypes/reset/reset.html' 50 | import './inputTypes/reset/reset.js' 51 | import './inputTypes/search/search.html' 52 | import './inputTypes/search/search.js' 53 | import './inputTypes/select/select.html' 54 | import './inputTypes/select/select.js' 55 | import './inputTypes/select-checkbox/select-checkbox.html' 56 | import './inputTypes/select-checkbox/select-checkbox.js' 57 | import './inputTypes/select-checkbox-inline/select-checkbox-inline.html' 58 | import './inputTypes/select-checkbox-inline/select-checkbox-inline.js' 59 | import './inputTypes/select-multiple/select-multiple.html' 60 | import './inputTypes/select-multiple/select-multiple.js' 61 | import './inputTypes/select-radio/select-radio.html' 62 | import './inputTypes/select-radio/select-radio.js' 63 | import './inputTypes/select-radio-inline/select-radio-inline.html' 64 | import './inputTypes/select-radio-inline/select-radio-inline.js' 65 | import './inputTypes/submit/submit.html' 66 | import './inputTypes/submit/submit.js' 67 | import './inputTypes/tel/tel.html' 68 | import './inputTypes/tel/tel.js' 69 | import './inputTypes/text/text.html' 70 | import './inputTypes/text/text.js' 71 | import './inputTypes/textarea/textarea.html' 72 | import './inputTypes/textarea/textarea.js' 73 | import './inputTypes/time/time.html' 74 | import './inputTypes/time/time.js' 75 | import './inputTypes/url/url.html' 76 | import './inputTypes/url/url.js' 77 | import './inputTypes/week/week.html' 78 | import './inputTypes/week/week.js' 79 | // components that render a form 80 | import './components/autoForm/autoForm.html' 81 | import './components/autoForm/autoForm.js' 82 | import './components/quickForm/quickForm.html' 83 | import './components/quickForm/quickForm.js' 84 | // components that render controls within a form 85 | import './components/afArrayField/afArrayField.html' 86 | import './components/afArrayField/afArrayField.js' 87 | import './components/afEachArrayItem/afEachArrayItem.html' 88 | import './components/afEachArrayItem/afEachArrayItem.js' 89 | import './components/afFieldInput/afFieldInput.html' 90 | import './components/afFieldInput/afFieldInput.js' 91 | import './components/afFormGroup/afFormGroup.html' 92 | import './components/afFormGroup/afFormGroup.js' 93 | import './components/afObjectField/afObjectField.html' 94 | import './components/afObjectField/afObjectField.js' 95 | import './components/afQuickField/afQuickField.html' 96 | import './components/afQuickField/afQuickField.js' 97 | import './components/afQuickFields/afQuickFields.html' 98 | import './components/afQuickFields/afQuickFields.js' 99 | // event handling 100 | import './autoform-events.js' 101 | 102 | AutoForm.load = () => {} // keep isomorph with dynamic version 103 | 104 | export { AutoForm } from './autoform' 105 | -------------------------------------------------------------------------------- /testapp/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /testapp/.meteor/.finished-upgraders: -------------------------------------------------------------------------------- 1 | # This file contains information which helps Meteor properly upgrade your 2 | # app when you run 'meteor update'. You should check it into version control 3 | # with your project. 4 | 5 | notices-for-0.9.0 6 | notices-for-0.9.1 7 | 0.9.4-platform-file 8 | notices-for-facebook-graph-api-2 9 | 1.2.0-standard-minifiers-package 10 | 1.2.0-meteor-platform-split 11 | 1.2.0-cordova-changes 12 | 1.2.0-breaking-changes 13 | 1.3.0-split-minifiers-package 14 | 1.4.0-remove-old-dev-bundle-link 15 | 1.4.1-add-shell-server-package 16 | 1.4.3-split-account-service-packages 17 | 1.5-add-dynamic-import-package 18 | 1.7-split-underscore-from-meteor-base 19 | 1.8.3-split-jquery-from-blaze 20 | -------------------------------------------------------------------------------- /testapp/.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /testapp/.meteor/.id: -------------------------------------------------------------------------------- 1 | # This file contains a token that is unique to your project. 2 | # Check it into your repository along with the rest of this directory. 3 | # It can be used for purposes such as: 4 | # - ensuring you don't accidentally deploy one app on top of another 5 | # - providing package authors with aggregated statistics 6 | 7 | w9709x9e21kn.t88l59w1jw34 8 | -------------------------------------------------------------------------------- /testapp/.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # Check this file (and the other files in this directory) into your repository. 3 | # 4 | # 'meteor add' and 'meteor remove' will edit this file for you, 5 | # but you can also edit it by hand. 6 | 7 | meteor-base@1.5.2 # Packages every Meteor app needs to have 8 | mobile-experience@1.1.2 # Packages for a great mobile UX 9 | mongo@2.0.3 # The database Meteor supports right now 10 | reactive-var@1.0.13 # Reactive variable for tracker 11 | tracker@1.3.4 # Meteor's client-side reactive programming library 12 | 13 | standard-minifier-css@1.9.3 # CSS minifier run for production mode 14 | standard-minifier-js@3.0.0 # JS minifier run for production mode 15 | es5-shim@4.8.1 # ECMAScript 5 compatibility for older browsers 16 | ecmascript@0.16.10 # Enable ECMAScript2015+ syntax in app code 17 | typescript@5.6.3 # Enable TypeScript syntax in .ts and .tsx modules 18 | shell-server@0.6.1 # Server-side component of the `meteor shell` command 19 | 20 | blaze-html-templates 21 | aldeed:autoform@8.0.0 22 | communitypackages:autoform-bootstrap5@2.0.0 23 | aldeed:simple-schema@2.0.0 24 | momentjs:moment 25 | -------------------------------------------------------------------------------- /testapp/.meteor/platforms: -------------------------------------------------------------------------------- 1 | server 2 | browser 3 | -------------------------------------------------------------------------------- /testapp/.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@3.1 2 | -------------------------------------------------------------------------------- /testapp/.meteor/versions: -------------------------------------------------------------------------------- 1 | aldeed:autoform@8.0.0 2 | aldeed:simple-schema@2.0.0 3 | allow-deny@2.0.0 4 | autoupdate@2.0.0 5 | babel-compiler@7.11.2 6 | babel-runtime@1.5.2 7 | base64@1.0.13 8 | binary-heap@1.0.12 9 | blaze@3.0.0 10 | blaze-html-templates@3.0.0-alpha300.17 11 | blaze-tools@2.0.0 12 | boilerplate-generator@2.0.0 13 | caching-compiler@2.0.1 14 | caching-html-compiler@2.0.0 15 | callback-hook@1.6.0 16 | check@1.4.4 17 | communitypackages:autoform-bootstrap5@2.0.0 18 | core-runtime@1.0.0 19 | ddp@1.4.2 20 | ddp-client@3.0.3 21 | ddp-common@1.4.4 22 | ddp-server@3.0.3 23 | diff-sequence@1.1.3 24 | dynamic-import@0.7.4 25 | ecmascript@0.16.10 26 | ecmascript-runtime@0.8.3 27 | ecmascript-runtime-client@0.12.2 28 | ecmascript-runtime-server@0.11.1 29 | ejson@1.1.4 30 | es5-shim@4.8.1 31 | facts-base@1.0.2 32 | fetch@0.1.5 33 | geojson-utils@1.0.12 34 | hot-code-push@1.0.5 35 | html-tools@2.0.0 36 | htmljs@2.0.1 37 | id-map@1.2.0 38 | inter-process-messaging@0.1.2 39 | jquery@3.0.2 40 | launch-screen@2.0.1 41 | logging@1.3.5 42 | meteor@2.0.2 43 | meteor-base@1.5.2 44 | minifier-css@2.0.0 45 | minifier-js@3.0.1 46 | minimongo@2.0.2 47 | mobile-experience@1.1.2 48 | mobile-status-bar@1.1.1 49 | modern-browsers@0.1.11 50 | modules@0.20.3 51 | modules-runtime@0.13.2 52 | momentjs:moment@2.30.1 53 | mongo@2.0.3 54 | mongo-decimal@0.2.0 55 | mongo-dev-server@1.1.1 56 | mongo-id@1.0.9 57 | npm-mongo@6.10.0 58 | observe-sequence@2.0.0 59 | ordered-dict@1.2.0 60 | promise@1.0.0 61 | random@1.2.2 62 | react-fast-refresh@0.2.9 63 | reactive-dict@1.3.2 64 | reactive-var@1.0.13 65 | reload@1.3.2 66 | retry@1.1.1 67 | routepolicy@1.1.2 68 | shell-server@0.6.1 69 | socket-stream-client@0.5.3 70 | spacebars@2.0.0 71 | spacebars-compiler@2.0.0 72 | standard-minifier-css@1.9.3 73 | standard-minifier-js@3.0.0 74 | templating@1.4.4 75 | templating-compiler@2.0.0 76 | templating-runtime@2.0.0 77 | templating-tools@2.0.0 78 | tracker@1.3.4 79 | typescript@5.6.3 80 | webapp@2.0.4 81 | webapp-hashing@1.1.2 82 | -------------------------------------------------------------------------------- /testapp/client/main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | AutoForm DEMO 4 | 5 | 6 |
7 |

AutoForm DEMO

8 | 9 | {{> quickForm id="userForm" type="normal" schema=schema}} 10 | 11 | {{#let userDoc=userDoc}} 12 | Name: {{userDoc.name}} 13 | Age: {{userDoc.age}} 14 | {{/let}} 15 |
16 | 17 | -------------------------------------------------------------------------------- /testapp/client/main.js: -------------------------------------------------------------------------------- 1 | import { Template } from 'meteor/templating' 2 | import { ReactiveVar } from 'meteor/reactive-var' 3 | import { Tracker } from 'meteor/tracker' 4 | import SimpleSchema from 'meteor/aldeed:simple-schema' 5 | import './setup' 6 | import './main.html' 7 | 8 | 9 | SimpleSchema.extendOptions(['autoform']) 10 | const schema = new SimpleSchema({ 11 | name: { 12 | type: String, 13 | min: 2, 14 | max: 20 15 | }, 16 | age: { 17 | type: Number, 18 | min: 18, 19 | max: () => { 20 | const name = AutoForm.getFieldValue('name') 21 | return name === 'Jan' ? 999 : 100 22 | }, 23 | autoform: { 24 | defaultValue: 18, 25 | } 26 | } 27 | }, { tracker: Tracker }) 28 | 29 | Template.body.onCreated(function () { 30 | const instance = this 31 | instance.user = new ReactiveVar() 32 | }) 33 | 34 | Template.body.helpers({ 35 | schema () { 36 | return schema 37 | }, 38 | userDoc () { 39 | return Template.instance().user.get() 40 | } 41 | }) 42 | 43 | Template.body.events({ 44 | 'submit #userForm' (event, instance) { 45 | event.preventDefault() 46 | const { insertDoc } = AutoForm.getFormValues('userForm') 47 | instance.user.set(insertDoc) 48 | } 49 | }) 50 | -------------------------------------------------------------------------------- /testapp/client/setup.js: -------------------------------------------------------------------------------- 1 | import 'bootstrap' 2 | import 'bootstrap/dist/css/bootstrap.css' // optional, default theme 3 | import popper from '@popperjs/core' 4 | import { AutoFormThemeBootstrap5 } from 'meteor/communitypackages:autoform-bootstrap5/static' 5 | import { AutoForm } from 'meteor/aldeed:autoform/static' 6 | 7 | AutoForm.load() 8 | AutoFormThemeBootstrap5.load() 9 | AutoForm.setDefaultTemplate('bootstrap5') 10 | window.Popper = popper 11 | -------------------------------------------------------------------------------- /testapp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "testapp", 3 | "private": true, 4 | "scripts": { 5 | "start": "meteor run", 6 | "setup": "mkdir -p packages && ln -sfn ../../ ./packages/meteor-autoform", 7 | "lint": "npm run setup && standardx -v ./packages/meteor-autoform/ | snazzy", 8 | "lint:fix": "npm run setup && standardx --fix ./packages/meteor-autoform/ | snazzy", 9 | "test": "METEOR_PACKAGE_DIRS='../' TEST_BROWSER_DRIVER=puppeteer TEST_SERVER=0 meteor test-packages --once --raw-logs --driver-package meteortesting:mocha ../", 10 | "test:watch": "METEOR_PACKAGE_DIRS='../' TEST_BROWSER_DRIVER=puppeteer TEST_SERVER=0 TEST_WATCH=1 meteor test-packages --raw-logs --driver-package meteortesting:mocha ../", 11 | "test:browser": "METEOR_PACKAGE_DIRS='../' TEST_SERVER=0 TEST_WATCH=1 meteor test-packages --raw-logs --driver-package meteortesting:mocha ../" 12 | }, 13 | "dependencies": { 14 | "@babel/runtime": "^7.24.4", 15 | "@popperjs/core": "^2.11.8", 16 | "bootstrap": "^5.3.3", 17 | "jquery": "^3.7.1", 18 | "meteor-node-stubs": "^1.2.9", 19 | "puppeteer": "^18.2.1" 20 | }, 21 | "devDependencies": { 22 | "@babel/core": "^7.12.7", 23 | "@babel/eslint-parser": "^7.12.1", 24 | "chai": "^4.2.0", 25 | "eslint-config-standard": "^16.0.2", 26 | "sinon": "^9.2.1", 27 | "snazzy": "^9.0.0", 28 | "standardx": "^7.0.0" 29 | }, 30 | "babel": {}, 31 | "standardx": { 32 | "globals": [ 33 | "AutoForm", 34 | "arrayTracker", 35 | "globalDefaultTemplate", 36 | "defaultTypeTemplates", 37 | "deps" 38 | ], 39 | "ignore": [ 40 | "**/testapp/" 41 | ] 42 | }, 43 | "eslintConfig": { 44 | "parser": "@babel/eslint-parser", 45 | "parserOptions": { 46 | "sourceType": "module", 47 | "allowImportExportEverywhere": false 48 | }, 49 | "rules": { 50 | "brace-style": [ 51 | "error", 52 | "stroustrup", 53 | { 54 | "allowSingleLine": true 55 | } 56 | ] 57 | } 58 | }, 59 | "meteor": { 60 | "mainModule": { 61 | "client": "client/main.js" 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tests/FormData.tests.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | import { Tracker } from 'meteor/tracker' 3 | import { Random } from 'meteor/random' 4 | import { expect } from 'chai' 5 | import { FormData } from '../autoform-formdata' 6 | 7 | describe(FormData.name, function () { 8 | describe('constructor ', function () { 9 | it('creates a new FormData', function () { 10 | const fd = new FormData() 11 | expect(fd instanceof FormData).to.equal(true) 12 | expect(fd.forms).to.deep.equal({}) 13 | }) 14 | }) 15 | describe('initForm ', function () { 16 | it('Initializes tracking for a given form, if not already done', function () { 17 | const fd = new FormData() 18 | const formId = Random.id() 19 | fd.initForm(formId) 20 | expect(fd.forms[formId]).to.deep.equal({ 21 | sourceDoc: null, 22 | deps: { 23 | sourceDoc: new Tracker.Dependency() 24 | } 25 | }) 26 | }) 27 | }) 28 | describe('sourceDoc ', function () { 29 | it('sets a source doc for the given form', function () { 30 | const fd = new FormData() 31 | const formId = Random.id() 32 | fd.initForm(formId) 33 | fd.sourceDoc(formId, { foo: formId }) 34 | 35 | expect(fd.forms[formId]).to.deep.equal({ sourceDoc: { foo: formId }, deps: { sourceDoc: { _dependentsById: {} } } }) 36 | }) 37 | it('gets a source doc for the given form', function (done) { 38 | const fd = new FormData() 39 | const formId = Random.id() 40 | fd.initForm(formId) 41 | 42 | Tracker.autorun(() => { 43 | const sourceDoc = fd.sourceDoc(formId) 44 | if (!sourceDoc) return 45 | 46 | expect(sourceDoc.foo).to.equal(formId) 47 | done() 48 | }) 49 | 50 | setTimeout(() => fd.sourceDoc(formId, { foo: formId }), 100) 51 | }) 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /tests/FormPreserve.tests.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | /* global Package */ 3 | import { Random } from 'meteor/random' 4 | import { expect } from 'chai' 5 | import { FormPreserve } from '../form-preserve' 6 | 7 | const preserve = (name) => new FormPreserve(name) 8 | 9 | describe('FormPreserve', function () { 10 | describe('constructor', function () { 11 | it('throws on invalid migrationName', function () { 12 | console.log(FormPreserve) 13 | expect(() => preserve()).to.throw('You must define an unique migration name of type String') 14 | expect(() => preserve(1)).to.throw('You must define an unique migration name of type String') 15 | expect(() => preserve(false)).to.throw('You must define an unique migration name of type String') 16 | expect(() => preserve({})).to.throw('You must define an unique migration name of type String') 17 | expect(() => preserve([])).to.throw('You must define an unique migration name of type String') 18 | expect(() => preserve(() => {})).to.throw('You must define an unique migration name of type String') 19 | 20 | const fp = preserve(Random.id()) 21 | expect(fp instanceof FormPreserve).to.equal(true) 22 | expect(fp.retrievedDocuments).to.deep.equal({}) 23 | expect(fp.registeredForms).to.deep.equal({}) 24 | }) 25 | 26 | it('loads migrationData if the Reload package is installed', function () { 27 | // let's simple-stub the reload package 28 | const fakeMigratioData = { [Random.id()]: { foo: Random.id() } } 29 | const Reload = { 30 | _migrationData: function (name) { return fakeMigratioData }, 31 | _onMigrate: function (name, fn) {} 32 | } 33 | Package.reload = { Reload } 34 | 35 | const fp = preserve(Random.id()) 36 | expect(fp.retrievedDocuments).to.deep.equal(fakeMigratioData) 37 | delete Package.reload 38 | }) 39 | }) 40 | describe('getDocument', function () { 41 | it('returns false if a doc is not in retrievedDocuments', function () { 42 | const fp = preserve(Random.id()) 43 | expect(fp.getDocument()).to.equal(false) 44 | expect(fp.getDocument(Random.id())).to.equal(false) 45 | }) 46 | it('returns the doc if it is in retrievedDocuments', function () { 47 | const fp = preserve(Random.id()) 48 | const formId = Random.id() 49 | const expectDoc = { foo: Random.id() } 50 | fp.retrievedDocuments[formId] = expectDoc 51 | expect(fp.getDocument(formId)).to.deep.equal(expectDoc) 52 | }) 53 | }) 54 | describe('clearDocument', function () { 55 | it('removes a registered doc', function () { 56 | const fp = preserve(Random.id()) 57 | const formId = Random.id() 58 | const expectDoc = { foo: Random.id() } 59 | fp.retrievedDocuments[formId] = expectDoc 60 | fp.clearDocument(formId) 61 | expect(fp.getDocument(formId)).to.equal(false) 62 | }) 63 | }) 64 | describe('registerForm', function () { 65 | it('adds a form to the resgiteredforms', function () { 66 | const fp = preserve(Random.id()) 67 | const formId = Random.id() 68 | fp.registerForm(formId, () => formId) 69 | 70 | expect(fp.registeredForms[formId]()).to.equal(formId) 71 | }) 72 | }) 73 | describe('formIsRegistered', function () { 74 | it('returns true on registered forms, otherwise false', function () { 75 | const fp = preserve(Random.id()) 76 | const formId = Random.id() 77 | fp.registeredForms[formId] = () => formId 78 | expect(fp.formIsRegistered(formId)).to.equal(true) 79 | expect(fp.formIsRegistered()).to.equal(false) 80 | expect(fp.formIsRegistered(Random.id())).to.equal(false) 81 | }) 82 | }) 83 | describe('unregisterForm', function () { 84 | it('removes a form from registered forms', function () { 85 | const fp = preserve(Random.id()) 86 | const formId = Random.id() 87 | fp.registeredForms[formId] = () => formId 88 | 89 | fp.unregisterForm(formId) 90 | expect(fp.registeredForms[formId]).to.equal(undefined) 91 | }) 92 | }) 93 | describe('unregisterAllForms', function () { 94 | it('removes all forms', function () { 95 | const fp = preserve(Random.id()) 96 | fp.registeredForms[Random.id()] = () => {} 97 | fp.registeredForms[Random.id()] = () => {} 98 | fp.registeredForms[Random.id()] = () => {} 99 | 100 | fp.unregisterAllForms() 101 | expect(fp.registeredForms).to.deep.equal({}) 102 | }) 103 | }) 104 | describe('_retrieveRegisteredDocuments', function () { 105 | it('retrieves from all forms', function () { 106 | const fp = preserve(Random.id()) 107 | const source = {} 108 | source.one = () => ({ index: 0 }) 109 | source.two = () => ({ index: 1 }) 110 | source.three = () => ({ index: 2 }) 111 | Object.assign(fp.registeredForms, source) 112 | 113 | const result = fp._retrieveRegisteredDocuments() 114 | expect(result).to.deep.equal({ 115 | one: { index: 0 }, 116 | two: { index: 1 }, 117 | three: { index: 2 } 118 | }) 119 | }) 120 | }) 121 | }) 122 | -------------------------------------------------------------------------------- /tests/Hooks.tests.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | import { Random } from 'meteor/random' 3 | import { expect } from 'chai' 4 | import { Hooks } from '../autoform-hooks' 5 | 6 | describe('Hooks', function () { 7 | beforeEach(function () { 8 | Hooks.global = Hooks.getDefault() 9 | Hooks.form = {} 10 | }) 11 | 12 | describe('getDefault', function () { 13 | it('creates an empty default object with all hooknames as keys', function () { 14 | const defaultHooks = Hooks.getDefault() 15 | expect(defaultHooks).to.deep.equal({ 16 | before: {}, 17 | after: {}, 18 | formToDoc: [], 19 | formToModifier: [], 20 | docToForm: [], 21 | onSubmit: [], 22 | onSuccess: [], 23 | onError: [], 24 | beginSubmit: [], 25 | endSubmit: [] 26 | }) 27 | }) 28 | }) 29 | describe('addHooksToList', function () { 30 | it('throws if a before hook is not a function', function () { 31 | const hooksList = { 32 | before: {} 33 | }; 34 | 35 | [ 36 | { before: { 0: undefined } }, 37 | { before: { 0: 1 } }, 38 | { before: { 0: {} } }, 39 | { before: { 0: [] } }, 40 | { before: { 0: true } }, 41 | { before: { 0: 'foo' } }, 42 | { before: { 0: new Date() } } 43 | ].forEach(hooks => { 44 | expect(() => Hooks.addHooksToList(hooksList, hooks)).to.throw('AutoForm before hook must be a function') 45 | }) 46 | }) 47 | it('throws if a after hook is not a function', function () { 48 | const hooksList = { 49 | after: {} 50 | }; 51 | 52 | [ 53 | { after: { 0: undefined } }, 54 | { after: { 0: 1 } }, 55 | { after: { 0: {} } }, 56 | { after: { 0: [] } }, 57 | { after: { 0: true } }, 58 | { after: { 0: 'foo' } }, 59 | { after: { 0: new Date() } } 60 | ].forEach(hooks => { 61 | expect(() => Hooks.addHooksToList(hooksList, hooks)).to.throw('AutoForm after hook must be a function') 62 | }) 63 | }) 64 | it('throws if any other hook is not a function', function () { 65 | const hooksList = { 66 | formToDoc: [], 67 | formToModifier: [], 68 | docToForm: [], 69 | onSubmit: [], 70 | onSuccess: [], 71 | onError: [], 72 | beginSubmit: [], 73 | endSubmit: [] 74 | } 75 | 76 | Object.keys(hooksList).forEach(key => { 77 | [ 78 | { [key]: 1 }, 79 | { [key]: true }, 80 | { [key]: {} }, 81 | { [key]: [] }, 82 | { [key]: 'foo' }, 83 | { [key]: new Date() } 84 | ].forEach(hooks => { 85 | expect(() => Hooks.addHooksToList(hooksList, hooks)).to.throw(`AutoForm ${key} hook must be a function`) 86 | }) 87 | }) 88 | }) 89 | it('adds before hooks', function () { 90 | const hookId = Random.id() 91 | const hookFunction = function () { 92 | return hookId 93 | } 94 | const hooksList = { 95 | before: {} 96 | } 97 | const hooks = { 98 | before: { 99 | [hookId]: hookFunction 100 | } 101 | } 102 | Hooks.addHooksToList(hooksList, hooks) 103 | expect(hooksList).to.deep.equal({ 104 | before: { 105 | [hookId]: [hookFunction] 106 | } 107 | }) 108 | }) 109 | it('replaces before hooks, if desired', function () { 110 | const hookId = Random.id() 111 | const hookFunction = function () { 112 | return hookId 113 | } 114 | const hooksList = { 115 | before: { 116 | [hookId]: [ 117 | hookFunction, 118 | hookFunction, 119 | hookFunction, 120 | hookFunction 121 | ] 122 | } 123 | } 124 | const hooks = { 125 | before: { 126 | [hookId]: hookFunction 127 | } 128 | } 129 | Hooks.addHooksToList(hooksList, hooks, true) 130 | expect(hooksList).to.deep.equal({ 131 | before: { 132 | [hookId]: [hookFunction] 133 | } 134 | }) 135 | }) 136 | it('adds after hooks', function () { 137 | const hookId = Random.id() 138 | const hookFunction = function () { 139 | return hookId 140 | } 141 | const hooksList = { 142 | after: {} 143 | } 144 | const hooks = { 145 | after: { 146 | [hookId]: hookFunction 147 | } 148 | } 149 | Hooks.addHooksToList(hooksList, hooks) 150 | expect(hooksList).to.deep.equal({ 151 | after: { 152 | [hookId]: [hookFunction] 153 | } 154 | }) 155 | }) 156 | it('replaces after hooks, if desired', function () { 157 | const hookId = Random.id() 158 | const hookFunction = function () { 159 | return hookId 160 | } 161 | const hooksList = { 162 | after: { 163 | [hookId]: [ 164 | hookFunction, 165 | hookFunction, 166 | hookFunction, 167 | hookFunction 168 | ] 169 | } 170 | } 171 | const hooks = { 172 | after: { 173 | [hookId]: hookFunction 174 | } 175 | } 176 | Hooks.addHooksToList(hooksList, hooks, true) 177 | expect(hooksList).to.deep.equal({ 178 | after: { 179 | [hookId]: [hookFunction] 180 | } 181 | }) 182 | }) 183 | it('adds all other hooks', function () { 184 | const hookId = Random.id() 185 | const hookFunction = function () { 186 | return hookId 187 | } 188 | 189 | const hooksList = { 190 | formToDoc: [hookFunction], 191 | formToModifier: [hookFunction], 192 | docToForm: [hookFunction], 193 | onSubmit: [hookFunction], 194 | onSuccess: [hookFunction], 195 | onError: [hookFunction], 196 | beginSubmit: [hookFunction], 197 | endSubmit: [hookFunction] 198 | } 199 | 200 | Object.keys(hooksList).forEach(key => { 201 | Hooks.addHooksToList(hooksList, { [key]: hookFunction }) 202 | expect(hooksList[key]).to.deep.equal([ 203 | hookFunction, 204 | hookFunction 205 | ]) 206 | }) 207 | }) 208 | it('replaces all other hooks, if desired', function () { 209 | const hookId = Random.id() 210 | const hookFunction = function () { 211 | return hookId 212 | } 213 | 214 | const hooksList = { 215 | formToDoc: [hookFunction], 216 | formToModifier: [hookFunction], 217 | docToForm: [hookFunction], 218 | onSubmit: [hookFunction], 219 | onSuccess: [hookFunction], 220 | onError: [hookFunction], 221 | beginSubmit: [hookFunction], 222 | endSubmit: [hookFunction] 223 | } 224 | 225 | Object.keys(hooksList).forEach(key => { 226 | Hooks.addHooksToList(hooksList, { [key]: hookFunction }, true) 227 | expect(hooksList[key]).to.deep.equal([hookFunction]) 228 | }) 229 | }) 230 | }) 231 | describe('getHooks', function () { 232 | it('gets a hook for a given formId and type', function () { 233 | const formId = Random.id() 234 | const type = Random.id() 235 | const hooksFunction = function () { 236 | return formId 237 | } 238 | 239 | Hooks.form = { 240 | [formId]: { 241 | [type]: [hooksFunction] 242 | } 243 | } 244 | 245 | Hooks.global[type] = [hooksFunction] 246 | expect(Hooks.getHooks(Random.id(), Random.id())).to.deep.equal([]) 247 | expect(Hooks.getHooks(formId, Random.id())).to.deep.equal([]) 248 | 249 | expect(Hooks.getHooks(formId, type)).to.deep.equal([ 250 | hooksFunction, 251 | hooksFunction 252 | ]) 253 | }) 254 | it('gets a hook for a given formId, type and subtype', function () { 255 | const formId = Random.id() 256 | const subType = Random.id() 257 | const hooksFunction = function () { 258 | return formId 259 | } 260 | 261 | Hooks.form = { 262 | [formId]: { 263 | before: { 264 | [subType]: [hooksFunction] 265 | } 266 | } 267 | } 268 | 269 | Hooks.global.before[subType] = [hooksFunction] 270 | expect(Hooks.getHooks(formId, 'after', subType)).to.deep.equal([]) 271 | 272 | expect(Hooks.getHooks(formId, 'before', subType)).to.deep.equal([ 273 | hooksFunction, 274 | hooksFunction 275 | ]) 276 | }) 277 | }) 278 | }) 279 | -------------------------------------------------------------------------------- /tests/autoform-helpers.tests.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | import { Random } from 'meteor/random' 3 | import { expect } from 'chai' 4 | import { overrideStub, restoreAll, stub } from './test-utils.tests' 5 | import { 6 | afSelectOptionAtts, 7 | autoFormArrayFieldHasLessThanMaximum, 8 | autoFormArrayFieldHasMoreThanMinimum, autoFormArrayFieldIsFirstVisible, autoFormArrayFieldIsLastVisible, 9 | autoFormFieldIsInvalid, autoFormFieldLabelText, 10 | autoFormFieldMessage, autoFormFieldNames, autoFormFieldValue, autoFormFieldValueContains, autoFormFieldValueIs 11 | } from '../autoform-helpers' 12 | import { ArrayTracker } from '../autoform-arrays' 13 | import { Utility } from '../utility' 14 | 15 | describe('helpers', function () { 16 | let schema, options 17 | 18 | beforeEach(function () { 19 | schema = {} 20 | options = {} 21 | }) 22 | 23 | afterEach(function () { 24 | restoreAll() 25 | }) 26 | 27 | describe('afFieldMessage', function () { 28 | it('gets the current error messsage from schema', function () { 29 | // pseudo stubbing SimpleSchema functionality 30 | const message = {} 31 | const messageObj = { message: Random.id() } 32 | message.keyErrorMessage = () => messageObj 33 | schema.namedContext = () => message 34 | 35 | stub(AutoForm, 'getFormSchema', () => schema) 36 | stub(AutoForm, 'getFormId', () => Random.id()) 37 | const msg = autoFormFieldMessage(options) 38 | expect(msg).to.deep.equal(messageObj) 39 | }) 40 | }) 41 | describe('afFieldIsInvalid', function () { 42 | it('returns, whether the current field is invalid', function () { 43 | // pseudo stubbing SimpleSchema functionality 44 | const invalid = Random.id() 45 | const context = {} 46 | context.keyIsInvalid = () => invalid 47 | schema.namedContext = () => context 48 | 49 | stub(AutoForm, 'getFormSchema', () => schema) 50 | stub(AutoForm, 'getFormId', () => Random.id()) 51 | const msg = autoFormFieldIsInvalid(options) 52 | expect(msg).to.equal(invalid) 53 | }) 54 | }) 55 | describe('afArrayFieldHasMoreThanMinimum', function () { 56 | it('returns false is the current formType has hideArrayItemButtons as a truthy value', function () { 57 | const def = { 58 | formTypeDef: { 59 | hideArrayItemButtons: true 60 | } 61 | } 62 | 63 | stub(AutoForm, 'getFormSchema', () => ({})) 64 | stub(AutoForm, 'getCurrentDataPlusExtrasForForm', () => def) 65 | 66 | const msg = autoFormArrayFieldHasMoreThanMinimum(options) 67 | expect(msg).to.equal(false) 68 | }) 69 | it('returns the relation between minCount and visibleCount', function () { 70 | // pseudo stubbing SimpleSchema functionality 71 | const def = { formTypeDef: {} } 72 | 73 | stub(AutoForm, 'getFormSchema', () => ({})) 74 | stub(AutoForm, 'getCurrentDataPlusExtrasForForm', () => def) 75 | stub(ArrayTracker.prototype, 'getMinMax', () => ({ minCount: 2 })) 76 | 77 | stub(ArrayTracker.prototype, 'getVisibleCount', () => 1) 78 | expect(autoFormArrayFieldHasMoreThanMinimum(options)).to.equal(false) 79 | 80 | overrideStub(ArrayTracker.prototype, 'getVisibleCount', () => 2) 81 | expect(autoFormArrayFieldHasMoreThanMinimum(options)).to.equal(false) 82 | 83 | overrideStub(ArrayTracker.prototype, 'getVisibleCount', () => 3) 84 | expect(autoFormArrayFieldHasMoreThanMinimum(options)).to.equal(true) 85 | }) 86 | }) 87 | describe('afArrayFieldHasLessThanMaximum', function () { 88 | it('returns false is the current formType has hideArrayItemButtons as a truthy value', function () { 89 | const def = { 90 | formTypeDef: { 91 | hideArrayItemButtons: true 92 | } 93 | } 94 | 95 | stub(AutoForm, 'getFormSchema', () => ({})) 96 | stub(AutoForm, 'getCurrentDataPlusExtrasForForm', () => def) 97 | 98 | const msg = autoFormArrayFieldHasLessThanMaximum(options) 99 | expect(msg).to.equal(false) 100 | }) 101 | it('returns the relation between maxCount and visibleCount', function () { 102 | // pseudo stubbing SimpleSchema functionality 103 | const def = { formTypeDef: {} } 104 | 105 | stub(AutoForm, 'getFormSchema', () => ({})) 106 | stub(AutoForm, 'getCurrentDataPlusExtrasForForm', () => def) 107 | stub(ArrayTracker.prototype, 'getMinMax', () => ({ maxCount: 2 })) 108 | 109 | stub(ArrayTracker.prototype, 'getVisibleCount', () => 3) 110 | expect(autoFormArrayFieldHasLessThanMaximum(options)).to.equal(false) 111 | 112 | overrideStub(ArrayTracker.prototype, 'getVisibleCount', () => 2) 113 | expect(autoFormArrayFieldHasLessThanMaximum(options)).to.equal(false) 114 | 115 | overrideStub(ArrayTracker.prototype, 'getVisibleCount', () => 1) 116 | expect(autoFormArrayFieldHasLessThanMaximum(options)).to.equal(true) 117 | }) 118 | }) 119 | describe('afFieldValueIs', function () { 120 | it('returns whether the current value is explicitly the same as the field value', function () { 121 | const value = Random.id() 122 | 123 | stub(AutoForm, 'getFormSchema', () => ({})) 124 | stub(AutoForm, 'getFieldValue', () => value) 125 | 126 | expect(autoFormFieldValueIs({})).to.equal(false) 127 | expect(autoFormFieldValueIs({ hash: { value: Random.id() } })).to.equal(false) 128 | expect(autoFormFieldValueIs({ hash: { value } })).to.equal(true) 129 | }) 130 | }) 131 | describe('afFieldValue', function () { 132 | it('returns the current field value', function () { 133 | const value = Random.id() 134 | 135 | stub(AutoForm, 'getFormSchema', () => ({})) 136 | stub(AutoForm, 'getFieldValue', () => value) 137 | stub(AutoForm, 'getFormId', () => {}) 138 | 139 | expect(autoFormFieldValue({})).to.equal(value) 140 | }) 141 | }) 142 | describe('afArrayFieldIsFirstVisible', function () { 143 | it('returns the result of ArrayTracker', function () { 144 | const value = Random.id() 145 | stub(ArrayTracker.prototype, 'isFirstFieldlVisible', () => value) 146 | expect(autoFormArrayFieldIsFirstVisible.call({})).to.equal(value) 147 | }) 148 | }) 149 | describe('afArrayFieldIsLastVisible', function () { 150 | it('returns the result of ArrayTracker', function () { 151 | const value = Random.id() 152 | stub(ArrayTracker.prototype, 'isLastFieldlVisible', () => value) 153 | expect(autoFormArrayFieldIsLastVisible.call({})).to.equal(value) 154 | }) 155 | }) 156 | describe('afFieldValueContains', function () { 157 | it('returns false if the current value is not an Array', function () { 158 | const value = Random.id() 159 | 160 | stub(AutoForm, 'getFormSchema', () => ({})) 161 | stub(AutoForm, 'getFieldValue', () => value) 162 | stub(AutoForm, 'getFormId', () => {}) 163 | 164 | expect(autoFormFieldValueContains({})).to.equal(false) 165 | expect(autoFormFieldValueContains({ hash: { value } })).to.equal(false) 166 | }) 167 | it('returns whether the current value is an array and the value is in it', function () { 168 | const value = Random.id() 169 | 170 | stub(AutoForm, 'getFormSchema', () => ({})) 171 | stub(AutoForm, 'getFieldValue', () => []) 172 | stub(AutoForm, 'getFormId', () => {}) 173 | 174 | // expect(autoFormFieldValueContains({})).to.equal(false); 175 | expect(autoFormFieldValueContains({ hash: { value } })).to.equal(false) 176 | 177 | overrideStub(AutoForm, 'getFieldValue', () => [value]) 178 | expect(autoFormFieldValueContains({ hash: { value } })).to.equal(true) 179 | }) 180 | it('returns whether the current value is an array and one of values is in it', function () { 181 | const value = Random.id() 182 | 183 | stub(AutoForm, 'getFormSchema', () => ({})) 184 | stub(AutoForm, 'getFieldValue', () => [value]) 185 | stub(AutoForm, 'getFormId', () => {}) 186 | 187 | // expect(autoFormFieldValueContains({})).to.equal(false); 188 | expect(autoFormFieldValueContains({ hash: { values: `${Random.id()},${Random.id()}` } })).to.deep.equal([]) 189 | expect(autoFormFieldValueContains({ hash: { values: `${Random.id()},${value},${Random.id()}` } })).to.deep.equal([value]) 190 | }) 191 | }) 192 | describe('afFieldLabelText', function () { 193 | it('returns the current label for the field', function () { 194 | const label = Random.id() 195 | 196 | stub(AutoForm, 'getFormSchema', () => ({})) 197 | stub(AutoForm, 'getLabelForField', () => label) 198 | 199 | expect(autoFormFieldLabelText({})).to.equal(label) 200 | }) 201 | }) 202 | describe('afFieldNames', function () { 203 | // TODO needs more test coverage for all the options and branchings 204 | it('returns all field names for a given form, defined by the schema', function () { 205 | const atts = { 206 | hash: { 207 | fields: 'foo,bar,baz.$' 208 | } 209 | } 210 | const form = {} 211 | const schema = {} 212 | const def = { type: String } 213 | 214 | stub(AutoForm, 'getFormSchema', () => schema) 215 | stub(AutoForm, 'getCurrentDataForForm', () => form) 216 | stub(Utility, 'makeKeyGeneric', name => name) 217 | stub(AutoForm, 'findAttribute', () => false) 218 | stub(Utility, 'getFieldDefinition', () => def) 219 | 220 | expect(autoFormFieldNames(atts)).to.deep.equal([{ name: 'foo' }, { name: 'bar' }]) 221 | }) 222 | }) 223 | describe('afSelectOptionAtts', function () { 224 | it('return the current select option HTML attributes', function () { 225 | expect(afSelectOptionAtts.call({})).to.deep.equal({}) 226 | expect(afSelectOptionAtts.call({ value: 'foo' })).to.deep.equal({ value: 'foo' }) 227 | expect(afSelectOptionAtts.call({ value: false })).to.deep.equal({ value: 'false' }) 228 | expect(afSelectOptionAtts.call({ selected: true })).to.deep.equal({ selected: '' }) 229 | expect(afSelectOptionAtts.call({ htmlAtts: { foo: 'bar', bar: 'baz' } })).to.deep.equal({ foo: 'bar', bar: 'baz' }) 230 | }) 231 | }) 232 | }) 233 | -------------------------------------------------------------------------------- /tests/autoform-validation.tests.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | import { Random } from 'meteor/random' 3 | import { expect } from 'chai' 4 | import { _validateField } from '../autoform-validation' 5 | import { restoreAll, stub } from './test-utils.tests' 6 | import { Utility } from '../utility' 7 | 8 | describe('validation', function () { 9 | afterEach(function () { 10 | restoreAll() 11 | }) 12 | 13 | describe('validateField', function () { 14 | it('returns true if form is not currently rendered', function () { 15 | stub(AutoForm, 'templateInstanceForForm', () => {}) 16 | stub(Utility, 'checkTemplate', () => false) 17 | expect(_validateField()).to.equal(true) 18 | }) 19 | it('returns true if no schema is found', function () { 20 | stub(AutoForm, 'templateInstanceForForm', () => {}) 21 | stub(Utility, 'checkTemplate', () => true) 22 | stub(AutoForm, 'getCurrentDataForForm', () => {}) 23 | stub(AutoForm, 'getFormSchema', () => null) 24 | expect(_validateField()).to.equal(true) 25 | }) 26 | it('returns true if onlyIfAlreadyInvalid is true and schema is valid', function () { 27 | let expectedCall = false 28 | const schema = { 29 | namedContext: () => { 30 | return { 31 | isValid () { 32 | expectedCall = true 33 | return true 34 | } 35 | } 36 | } 37 | } 38 | stub(AutoForm, 'templateInstanceForForm', () => {}) 39 | stub(Utility, 'checkTemplate', () => true) 40 | stub(AutoForm, 'getCurrentDataForForm', () => {}) 41 | stub(AutoForm, 'getFormSchema', () => schema) 42 | expect(_validateField(undefined, undefined, undefined, true)).to.equal(true) 43 | expect(expectedCall).to.equal(true) 44 | }) 45 | it('returns true if there is no doc to validate', function () { 46 | let expectedCall = false 47 | stub(AutoForm, 'templateInstanceForForm', () => {}) 48 | stub(Utility, 'checkTemplate', () => true) 49 | stub(AutoForm, 'getCurrentDataForForm', () => ({})) 50 | stub(AutoForm, 'getFormSchema', () => ({})) 51 | stub(Utility, 'getFormTypeDef', () => ({})) 52 | stub(AutoForm, 'getFormValues', () => { 53 | expectedCall = true 54 | return undefined 55 | }) 56 | 57 | expect(_validateField(undefined, undefined, undefined, undefined)).to.equal(true) 58 | expect(expectedCall).to.equal(true) 59 | }) 60 | it("returns true if skipEmpty is true and the field we're validating has no value", function () { 61 | let expectedCall = false 62 | 63 | stub(AutoForm, 'templateInstanceForForm', () => {}) 64 | stub(Utility, 'checkTemplate', () => true) 65 | stub(AutoForm, 'getCurrentDataForForm', () => ({})) 66 | stub(AutoForm, 'getFormSchema', () => ({})) 67 | stub(Utility, 'getFormTypeDef', () => ({})) 68 | stub(AutoForm, 'getFormValues', () => ({})) 69 | stub(Utility, 'objAffectsKey', () => { 70 | expectedCall = true 71 | return false 72 | }) 73 | 74 | expect(_validateField(undefined, undefined, true, undefined)).to.equal(true) 75 | expect(expectedCall).to.equal(true) 76 | }) 77 | it('returns, whether the form is valid or not', function () { 78 | const valid = Random.id() 79 | 80 | stub(AutoForm, 'templateInstanceForForm', () => {}) 81 | stub(Utility, 'checkTemplate', () => true) 82 | stub(AutoForm, 'getCurrentDataForForm', () => ({})) 83 | stub(AutoForm, 'getFormSchema', () => ({})) 84 | stub(Utility, 'getFormTypeDef', () => ({})) 85 | stub(AutoForm, 'getFormValues', () => ({})) 86 | stub(Utility, 'objAffectsKey', () => true) 87 | stub(AutoForm, '_validateFormDoc', () => valid) 88 | 89 | expect(_validateField()).to.equal(valid) 90 | }) 91 | }) 92 | }) 93 | -------------------------------------------------------------------------------- /tests/common.tests.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | import { expect } from 'chai' 3 | import { isObject, isFunction, throttle } from '../common' 4 | 5 | describe('common', function () { 6 | describe(isObject.name, function () { 7 | it('determines, if something is an object', function () { 8 | [{}, { foo: {} }, Object.create(null)].forEach(obj => { 9 | expect(isObject(obj)).to.equal(true) 10 | }); 11 | 12 | [[], () => {}, false, 1, 'foo', new Date()].forEach(obj => { 13 | expect(isObject(obj)).to.equal(false) 14 | }) 15 | }) 16 | }) 17 | describe(isFunction.name, function () { 18 | it('determines, if something is a function', function () { 19 | [new Date(), 1, false, 'foo', [], {}].forEach(obj => { 20 | expect(isFunction(obj)).to.equal(false) 21 | }); 22 | 23 | [function () {}, () => {}, async function () {}, async () => {}].forEach(obj => { 24 | expect(isFunction(obj)).to.equal(true) 25 | }) 26 | }) 27 | }) 28 | describe(throttle.name, function () { 29 | it('it throttles by a certain timeout', function (done) { 30 | let count = 0 31 | const fn = () => { count++ } 32 | const throttled = throttle(fn, 10) 33 | for (let i = 0; i < 1000; i++) { 34 | throttled() 35 | } 36 | 37 | setTimeout(() => { 38 | expect(count).to.equal(1) 39 | done() 40 | }, 300) 41 | }) 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /tests/components/quickForm/quickFormUtils.tests.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | import { expect } from 'chai' 3 | import { restoreAll } from '../../test-utils.tests' 4 | import { 5 | getSortedFieldGroupNames, 6 | getFieldsWithNoGroup, 7 | getFieldsForGroup 8 | } from '../../../components/quickForm/quickFormUtils' 9 | 10 | const schemaObject = { 11 | // ungrouped 12 | name: String, 13 | list: Array, 14 | 'list.$': Object, 15 | 'list.$.entry1': String, 16 | 'list.$.entry2': Number, 17 | complex: Object, 18 | 'complex.foo': String, 19 | 'complex.bar': Number, 20 | 21 | // grouped 22 | gname: { 23 | type: String, 24 | autoform: { 25 | group: 'foos' 26 | } 27 | }, 28 | glist: { 29 | type: Array, 30 | autoform: { 31 | group: 'foos' 32 | } 33 | }, 34 | 'glist.$': { 35 | type: Object, 36 | autoform: { 37 | group: 'foos' 38 | } 39 | }, 40 | 'glist.$.entry1': { 41 | type: String, 42 | autoform: { 43 | group: 'foos' 44 | } 45 | }, 46 | 'glist.$.entry2': { 47 | type: Number, 48 | autoform: { 49 | group: 'foos' 50 | } 51 | }, 52 | gcomplex: { 53 | type: Object, 54 | autoform: { 55 | group: 'bars' 56 | } 57 | }, 58 | 'gcomplex.foo': { 59 | type: String, 60 | autoform: { 61 | group: 'bars' 62 | } 63 | }, 64 | 'gcomplex.bar': { 65 | type: Number, 66 | autoform: { 67 | group: 'bars' 68 | } 69 | }, 70 | gextra: { 71 | type: Number, 72 | autoform: { 73 | group: 'bars' 74 | } 75 | } 76 | } 77 | 78 | describe('quickForm - utils', function () { 79 | afterEach(function () { 80 | restoreAll() 81 | }) 82 | 83 | describe(getSortedFieldGroupNames.name, function () { 84 | it('Takes a schema object and returns a sorted array of field group names for it', function () { 85 | const groupNames = getSortedFieldGroupNames(schemaObject) 86 | expect(groupNames).to.deep.equal(['bars', 'foos']) 87 | }) 88 | }) 89 | 90 | describe(getFieldsWithNoGroup.name, function () { 91 | it("Returns the schema field names that don't belong to a group", function () { 92 | const fieldNames = getFieldsWithNoGroup(schemaObject) 93 | expect(fieldNames).to.deep.equal([ 94 | 'name', 95 | 'list', 96 | 'list.$.entry1', 97 | 'list.$.entry2', 98 | 'complex', 99 | 'complex.foo', 100 | 'complex.bar' 101 | ]) 102 | }) 103 | }) 104 | 105 | describe(getFieldsForGroup.name, function () { 106 | it('Returns the schema field names that belong in the group.', function () { 107 | expect(getFieldsForGroup('noname', schemaObject)).to.deep.equal([]) 108 | expect(getFieldsForGroup('foos', schemaObject)).to.deep.equal([ 109 | 'gname', 110 | 'glist', 111 | 'glist.$.entry1', 112 | 'glist.$.entry2' 113 | ]) 114 | expect(getFieldsForGroup('bars', schemaObject)).to.deep.equal([ 115 | 'gcomplex', 116 | 'gcomplex.foo', 117 | 'gcomplex.bar', 118 | 'gextra' 119 | ]) 120 | }) 121 | }) 122 | }) 123 | -------------------------------------------------------------------------------- /tests/setup.tests.js: -------------------------------------------------------------------------------- 1 | import 'meteor/aldeed:autoform/static' 2 | -------------------------------------------------------------------------------- /tests/test-utils.tests.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | import { Template } from 'meteor/templating' 3 | import { Blaze } from 'meteor/blaze' 4 | import { Tracker } from 'meteor/tracker' 5 | import sinon from 'sinon' 6 | 7 | const withDiv = function withDiv (callback) { 8 | const el = document.createElement('div') 9 | document.body.appendChild(el) 10 | try { 11 | callback(el) 12 | } 13 | finally { 14 | document.body.removeChild(el) 15 | } 16 | } 17 | 18 | export const withRenderedTemplate = function withRenderedTemplate (template, data, callback) { 19 | withDiv((el) => { 20 | const ourTemplate = (typeof template === 'string') ? Template[template] : template 21 | Blaze.renderWithData(ourTemplate, data, el) 22 | Tracker.flush() 23 | callback(el) 24 | }) 25 | } 26 | 27 | /* 28 | * Stubbing, the easy way 29 | */ 30 | 31 | const stubs = new Map() 32 | 33 | export const stub = (target, name, handler) => { 34 | if (stubs.get(target)) { 35 | throw new Error(`already stubbed: ${name}`) 36 | } 37 | const stubbedTarget = sinon.stub(target, name) 38 | if (typeof handler === 'function') { 39 | stubbedTarget.callsFake(handler) 40 | } 41 | else { 42 | stubbedTarget.value(handler) 43 | } 44 | stubs.set(stubbedTarget, name) 45 | } 46 | 47 | export const restore = (target, name) => { 48 | if (!target[name] || !target[name].restore) { 49 | throw new Error(`not stubbed: ${name}`) 50 | } 51 | target[name].restore() 52 | stubs.delete(target) 53 | } 54 | 55 | export const overrideStub = (target, name, handler) => { 56 | restore(target, name) 57 | stub(target, name, handler) 58 | } 59 | 60 | export const restoreAll = () => { 61 | stubs.forEach((name, target) => { 62 | target.restore() 63 | stubs.delete(target) 64 | }) 65 | } 66 | 67 | export class UnexpectedCallError extends Error { 68 | constructor () { 69 | super('Expected not to be called') 70 | } 71 | } 72 | 73 | export const getVoid = () => (void 0) // eslint-disable-line no-void 74 | -------------------------------------------------------------------------------- /tests/testSuite.tests.js: -------------------------------------------------------------------------------- 1 | import 'meteor/aldeed:autoform/static' 2 | import './utility-tests' 3 | import './common.tests' 4 | import './FormPreserve.tests' 5 | import './FormData.tests' 6 | import './Hooks.tests' 7 | import './ArrayTracker.tests' 8 | import './autoform-inputs.tests' 9 | import './autoform-helpers.tests' 10 | import './autoform-validation.tests' 11 | import './autoform-api.tests' 12 | // component specific 13 | import './components/quickForm/quickFormUtils.tests' 14 | // input types 15 | import './inputTypes/value-converters.tests' 16 | -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": [ 3 | [ 4 | "aldeed:simple-schema", 5 | "1.1.0" 6 | ], 7 | [ 8 | "base64", 9 | "1.0.1" 10 | ], 11 | [ 12 | "blaze", 13 | "2.0.3" 14 | ], 15 | [ 16 | "callback-hook", 17 | "1.0.1" 18 | ], 19 | [ 20 | "check", 21 | "1.0.2" 22 | ], 23 | [ 24 | "ddp", 25 | "1.0.11" 26 | ], 27 | [ 28 | "deps", 29 | "1.0.5" 30 | ], 31 | [ 32 | "ejson", 33 | "1.0.4" 34 | ], 35 | [ 36 | "geojson-utils", 37 | "1.0.1" 38 | ], 39 | [ 40 | "htmljs", 41 | "1.0.2" 42 | ], 43 | [ 44 | "id-map", 45 | "1.0.1" 46 | ], 47 | [ 48 | "jquery", 49 | "1.0.1" 50 | ], 51 | [ 52 | "json", 53 | "1.0.1" 54 | ], 55 | [ 56 | "livedata", 57 | "1.0.11" 58 | ], 59 | [ 60 | "logging", 61 | "1.0.5" 62 | ], 63 | [ 64 | "meteor", 65 | "1.1.3" 66 | ], 67 | [ 68 | "minimongo", 69 | "1.0.5" 70 | ], 71 | [ 72 | "momentjs:moment", 73 | "2.8.4" 74 | ], 75 | [ 76 | "observe-sequence", 77 | "1.0.3" 78 | ], 79 | [ 80 | "ordered-dict", 81 | "1.0.1" 82 | ], 83 | [ 84 | "random", 85 | "1.0.1" 86 | ], 87 | [ 88 | "reactive-dict", 89 | "1.0.4" 90 | ], 91 | [ 92 | "reactive-var", 93 | "1.0.3" 94 | ], 95 | [ 96 | "retry", 97 | "1.0.1" 98 | ], 99 | [ 100 | "templating", 101 | "1.0.9" 102 | ], 103 | [ 104 | "tracker", 105 | "1.0.3" 106 | ], 107 | [ 108 | "ui", 109 | "1.0.4" 110 | ] 111 | ], 112 | "pluginDependencies": [], 113 | "toolVersion": "meteor-tool@1.0.35", 114 | "format": "1.0" 115 | } --------------------------------------------------------------------------------