├── .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 |
2 | {{> Template.dynamic template=getTemplateName data=innerContext}}
3 |
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 |
2 | {{! This is a block component and doesn't render anything visible, so no customizable template is needed for this}}
3 | {{#each ctx in innerContext}}
4 | {{#if ctx.removed}}
5 |
6 | {{else}}
7 | {{> Template.contentBlock ctx}}
8 | {{/if}}
9 | {{/each}}
10 |
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 |
2 | {{> Template.dynamic template=getTemplateName data=innerContext}}
3 |
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 |
2 | {{> Template.dynamic template=getTemplateName data=innerContext}}
3 |
--------------------------------------------------------------------------------
/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 |
2 | {{> Template.dynamic template=getTemplateName data=innerContext}}
3 |
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 |
2 | {{#if isReady}}
3 | {{#if isGroup}}
4 | {{> afObjectField}}
5 | {{else}}
6 | {{#if isFieldArray}}
7 | {{> afArrayField}}
8 | {{else}}
9 | {{#if isHiddenInput}}
10 | {{! if input type is defined as hidden, we don't render a form group}}
11 | {{> afFieldInput groupAtts}}
12 | {{else}}
13 | {{> afFormGroup groupAtts}}
14 | {{/if}}
15 | {{/if}}
16 | {{/if}}
17 | {{/if}}
18 |
--------------------------------------------------------------------------------
/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 |
2 | {{#each afFieldNames name=this.name}}
3 | {{> afQuickField quickFieldAtts}}
4 | {{/each}}
5 |
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 |
2 | {{#unless afDestroyUpdateForm this.id}}
3 | {{! afDestroyUpdateForm is a workaround for sticky input attributes}}
4 | {{! See https://github.com/meteor/meteor/issues/2431 }}
5 |
8 | {{/unless}}
9 |
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 |
2 | {{> Template.dynamic template=getTemplateName data=innerContext}}
3 |
--------------------------------------------------------------------------------
/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 |
2 |
3 |
--------------------------------------------------------------------------------
/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 |
2 |