Useful Forms extends your templates to make it easy to build reactive forms.
Write forms the way you want to:
- Your own html
- Your own custom components
- Automatic access to the doc on submit and via the api
- Events that make sense for forms
- Reactive template helpers that make sense for forms
- An awesome api you can use anywhere in your javascript
Read getting started (below) then checkout the api docs.
Step 1: Add Forms
package to you app:
meteor add useful:forms
Step 2: Create your form template. Note that there is no need to add any special css or template inclusions. Forms
simply detects html <form>
tags. This means you are free to use whatever css framework or html structure you wish, and change it however/whenever you need to.
<template name="AddContactForm">
<form>
<label for="fullName">Name</label>
<input type="text" name="fullName">
<label for="telephone">Telephone</label>
<input type="text" name="telephone">
<button type="submit">Add Contact</button>
</form>
</template>
Step 3: Add Forms
functionality to your template using the mixin
method in your client code.
if (Meteor.isClient) {
Forms.mixin(Template.AddContactForm);
}
Step 4: Use the Forms documentSubmit
event to easily get the doc when the user submits the form.
if (Meteor.isClient) {
Forms.mixin(Template.AddContactForm);
Template.AddContactForm.events({
'documentSubmit': function (e, tmpl, doc) {
Contacts.insert(doc);
}
});
}
NOTE: throughout this document
tmpl
keyword refers to the template instance object, even if not explicitly stated.
###Global API
Forms.mixin(template, options)
- This method is available globally, use it to extend your template with the forms behavior.
var options = {
// set helpers to false to disable extending your template with
// the forms helpers
helpers: true,
// set events to false to disable extending your template with
// the forms events (so that no event handlers will be added to
// your template)
events: true,
// Set the initial doc for your form
doc: {},
// Set the schema for your form
schema: {}
}
Forms.mixin(Template.AddContactForm, options);
Forms.instance()
This method is available globally and is an alias forTemplate.instance().form
but will returnnull
ifTemplate.instance()
isnull
.
###Template helpers
Forms adds helpers to your template to make it easy to display document properties and errors directly in your form.
-
{{doc}}
Returns the doc, you can use this to access any property on the doc.<input name="first" type="text" value="{{doc.name}}">
-
{{doc 'fieldName'}}
Returns an individual field, once we implement deep object access, this helper is equivalent to using{{doc.fieldName}}
, but since it's a method you can pass a variable as the argument.<input name={{field}} type="text" value={{doc field}}>
-
{{original}}
Gets the unmodified original doc, without any changes which have taken place since.<p>Change: {{original.first}} to {{doc.first}}?</p>
-
{{original 'fieldName'}}
Gets an individual field from the original doc<p>Change: {{original 'first'}} to {{doc 'first'}}?</p>
-
{{schema}}
Gets the schema<p>{{#if schema}}This document must conform to a schema{{/if}}</p>
-
{{schema 'fieldName'}}
Gets an individual field from the schema<p>{{#if schema 'firstName'}}First name has a schema{{/if}}</p>
Note: we expect to change the way validation works in the future and may deprecate this helper
-
{{errors}}
Returns all errors which exist in the form{{#each errors}} <p class="error">{{message}}</p> {{/each}}
-
{{errors 'fieldName'}}
Returns all errors related to a given field{{#each errors 'firstName'}} <p class="error">First name is invalid: {{message}}</p> {{/each}}
-
{{error}}
Returns the first error which exists in the form{{#with error}} <p class="error">Form is invalid: {{message}}</p> {{/with}}
-
{{error 'fieldName'}}
Returns the first error related to a given field{{#with error 'firstName'}} <p class="error">First name is invalid: {{message}}</p> {{/with}}
-
{{errorMessage}}
Returns the message of the first error which exists in the form<p class="error">{{errorMessage}}</p>
-
{{errorMessage 'fieldName'}}
Returns the message of the first error related to a given field<p class="error">{{errorMessage 'firstName'}}</p>
-
{{isValid}}
Returns true if the form is valid (there are 0 errors) and false otherwise.{{#if isValid}} <p>Everything is good, submit at will!</p> {{/if}}
-
{{isValid 'fieldName'}}
Returns true if the field is valid (there are 0 errors) and false otherwise.{{#if isValid 'username'}} <p>All clear! You can use this username :)</p> {{/if}}
-
{{isInvalid}}
Returns true if the form is invalid (there are errors) and false otherwise.{{#if isInvalid}} <p class="error">Please fix the validation errors and try again!</p> {{/if}}
-
{{isInvalid 'fieldName'}}
Returns true if the field is invalid (there are errors) and false otherwise.{{#if isInvalid 'username'}} <p class="error"> Uh-oh! Looks like there's a problem with the username you entered! </p> {{/if}}
-
{{form}}
Returns an object with all of the Forms helpers bound to the current forms instance, useful for passing to components.{{> datePicker field="reservationDate" form=form}}
###Template form instance
Template instances are extended with a form instance which provides full access to the underlying state of the form.
You can get access to the form instance either by calling Forms.instance()
or if you already have access to the template instance, you can access it via tmpl.form
where tmpl
is the template instance.
NOTE: in this section we'll use
form
to mean a form instance, what you would get by callingForms.instance()
from within your template javascript.
####Access the document
form.doc()
- Use this method to reactively get the doc with the user's input. Should return a javascript object, or null/undefined.
form.doc() // -> { firstName: 'joe', lastName: 'smith' }
form.doc(field)
- Pass a single string argument to get only a single field from the doc.
form.doc('firstName') // -> 'joe'
form.doc(doc)
- Pass an object or null to set the whole doc
form.doc({ _id: 'new' })
form.doc() // -> { _id: 'new' }
form.doc(field, value)
- Pass two arguments to set a single field, if the document does not exist, this method will create an empty doc first.
form.doc('firstName', 'sam')
form.doc() // -> { _id: 'new', firstName: 'sam' }
form.get()
- Use this method to reactively get the doc with the user's input. Should return a javascript object, or null/undefined.
form.get() // -> { firstName: 'joe', lastName: 'smith' }
form.get(field)
- Pass a single string argument to get only a single field from the doc.
form.get('firstName') // -> 'joe'
form.set(doc)
- Pass an object or null to set the whole doc
form.set({ _id: 'new' })
form.get() // -> { _id: 'new' }
form.set(field, value)
- Pass two arguments to set a single field
form.set('firstName', 'sam')
form.get() // -> { _id: 'new', firstName: 'sam' }
####Access the schema
You can get and set the schema the same way you can get and set the doc.
The schema is just an object, and the Forms package does not check it for any particular structure, currently we provide some default validation which expects fields in the schema to match fields in the doc, e.g. if you want to validate doc.firstName
you would set schema.firstName
as appropriate, however we have plans to change validation to be more pluggable, at which point we will probably deprecate getting/setting individual fields on the schema.
form.schema()
- Use this method to reactively get the schema. Should return a javascript object, or null/undefined.
form.schema() // -> { firstName: 'joe', lastName: 'smith' }
form.schema(field)
- Pass a single string argument to get only a single field from the schema.
form.schema('firstName') // -> 'joe'
form.schema(schema)
- Pass an object or null to set the whole schema
form.schema({ _id: 'new' })
form.schema() // -> { _id: 'new' }
form.schema(field, value)
- Pass two arguments to set a single field
form.schema('firstName', 'sam')
form.schema() // -> { _id: 'new', firstName: 'sam' }
####Access errors
The Forms package stores errors in a null-backed collection, you can get and set errors using the forms api.
NOTE: The
errors
,error
,isValid
, andisInvalid
methods do not validate the form, instead they return results based on the current state of the errors collection, which is normally updated via theform.validate
method, so in effect they return results based on the last time the form was validated. This allows you to control when validation happens without giving up the ability to reactively display validation errors.
form.errors()
- Get all errors in the errors collection.
form.errors() // -> [{name: 'firstName', error: new Error(), message: 'must be a string'}]
form.errors(fieldName)
- Get all errors related to a particular field
form.errors('firstName') // -> [{name: 'firstName', error: new Error(), message: 'must be a string'}]
form.errors(errors)
- Remove all existing errors and insert all the specified errors
form.errors([{
name: 'firstName'
, error: new Error()
, message: "I don't like your name"
}])
form.errors() // -> [{name: 'firstName', error: new Error(), message: "I don't like your name"}]
form.errors(field, errors)
- Remove all existing errors related to a given field and insert all the specified errors. This method will set the name property of each error you insert to match the field you specified.
form.errors('firstName', [{
error: new Error()
, message: "I don't like your name"
}])
form.errors() // -> [{name: 'firstName', error: new Error(), message: "I don't like your name"}]
form.error()
- Get the first error in the errors collection.
form.error() // -> {name: 'firstName', error: new Error(), message: 'must be a string'}
form.error(field)
- Get the first error in the errors collection related to a given field.
form.error('firstName') // -> {name: 'firstName', error: new Error(), message: 'must be a string'}
form.isValid()
- Return a boolean indicating the validity of the whole form.
form.errors([]) // remove all errors
form.isValid() // -> true, because there are no errors
form.isValid(field)
- Return a boolean indicating the validity of a given field.
form.errors('firstName', [{}]) // insert an error
form.isValid('firstName') // -> false, because we just inserted an error
form.isInvalid()
- Return a boolean indicating the non-validity of the whole form.
form.errors([]) // remove all errors
form.isInvalid() // -> false, because there are no errors
form.isInvalid(field)
- Return a boolean indicating the non-validity of a given field.
form.errors('firstName', [{}]) // insert an error
form.isInvalid('firstName') // -> true, because we just inserted an error
####Access Validation
The Forms package will validate your document on submit, but you can also validate the form at any time by calling form.validate()
form.validate()
- Validate the form and return a boolean indicating the validity of the form.
form.validate() // -> true, if there are no errors
form.validate(field)
- Validate a given field and return a boolean indicating the validity of that field.
form.validate('firstName') // -> true, if there are no errors
####Triggering Events
The Forms package provides a rich set of events which make it easy for you to react to user input and state changes. (State changes which are triggered by a user action will result in emited events, state changes which are not triggered by a user action are still reactive, but generally do not result in emitted events.)
The Forms package uses it's own API to emit events, which mean all the event-emitting functionality is available to you as the app developer for customizing the way your forms behave.
Each api method which emits events takes two optional arguments as the last two arguments:
eventTarget
- The element which should emit the event, for example ondocumentSubmit
theform
element should be the eventTarget.originalEvent
- The originating event, for example thechange
event which triggers apropertyChange
event. If you pass this argument, we will checkisDefaultPrevented
and callpreventDefault
to ensure that you don't handle the same event multiple times.
If you don't pass eventTarget
then none of the following methods will actually trigger an event, instead they act as aliases for other methods on the api.
form.validate(eventTarget, [originalEvent])
- Validates the document and triggersdocumentInvalid
if the form is not valid.
Template.myForm.events({
'click .validate': function (e, template) {
template.form.validate(e.currentTarget, e);
template.form.isValid(); // -> false if there are errors
}
});
form.validate(fieldName, eventTarget, [originalEvent])
- Validates a field and triggerspropertyInvalid
if the field is not valid. Note that thepropertyInvalid
event won't get triggered unless you validate a field directly, callingform.validate(eventTarget)
won't trigger anypropertyInvalid
events.
Template.myForm.events({
'click .validate': function (e, template) {
template.form.validate('name', e.currentTarget, e);
template.form.isValid('name'); // -> false if there are errors
}
});
form.invalidate(errors, eventTarget, [originalEvent])
- Marks the doc as invalid by inserting the specified errors and triggersdocumentInvalid
. This method is equivilent to callingform.errors
if you don't pass aneventTarget
.
Template.myForm.events({
'change': function (e, template) {
template.form.invalidate([{
message: 'This form is readonly!'
}], e.currentTarget, e);
template.form.isValid(); // -> false because you just inserted an error
}
});
form.invalidate(fieldName, errors, eventTarget, [originalEvent])
- Marks the field as invalid by inserting the specified errors and triggerspropertyInvalid
.
Template.myForm.events({
'change': function (e, template) {
template.form.invalidate('name', [{
message: 'This form is readonly!'
}], e.currentTarget, e);
template.form.isValid('name'); // -> false because you just added an error
}
});
form.change(fieldName, fieldValue, eventTarget, [originalEvent])
- Updates the value of a field on the doc, by settingdoc[fieldName] = fieldValue
, then triggersdocumentChange
. This method is equivilent to callingform.set
if you don't pass aneventTarget
.
Template.myForm.events({
'click .toggle': function (e, template) {
var name = e.currentTarget.name;
var value = template.form.doc(name);
template.form.change(name, !value, e.currentTarget, e);
// updates the doc and triggers `documentChange`
}
});
form.change(changes, eventTarget, [originalEvent])
- Updates the value of multiple fields in the doc, by settingdoc[key] = value
for each key inchanges
. This method only callsdocumentChange
once.
Template.myForm.events({
'click .toggle': function (e, template) {
var name = e.currentTarget.name;
var value = template.form.doc(name);
template.form.change(name, !value, e.currentTarget, e);
// updates the doc and triggers `documentChange`
}
});
form.submit(eventTarget, [originalEvent])
- Validates the document and triggers eitherdocumentSubmit
ordocumentInvalid
as a result
Template.myForm.events({
'click .done': function (e, template) {
template.form.submit(e.currentTarget, e);
// triggers `documentSubmit` or `documentInvalid`
}
});
###Forms Events
The Forms package emits events so you can take action when things change. Events are emitted by the Forms package either as a result of some user action which we catch (via submit
or change
events), or as a result of some custom event which you trigger, either via the propertyChange
event, or by using the event emitting api methods above.
Template.myForm.events({
'propertyChange': function (e, tmpl, changes) {
// This event is the only event which is not triggered using our api
// instead we trigger this event in response to `change` events so that
// we can handle both user initiated `change` events and developer
// initiated `propertyChange` events using a single unified handler and api
// See the source code for more info: lib/forms.js:444
// If you want to be notified of property changes, you should use the
// documentChange event below.
}
, 'documentChange': function (e, tmpl, doc, changes) {
// This event is triggered whenever the doc is updated via `form.change`
// doc contains the updated doc, including any changes made
// changes contains only the properties which were updated
}
, 'propertyInvalid': function (e, tmpl, doc, errors) {
// This event is triggered when the developer calls
// `form.validate(fieldName)` or `form.invalidate(fieldName)`
// errors is an array of relevant errors
}
, 'documentInvalid': function (e, tmpl, doc, errors) {
// This event is triggered when the document is invalidated via an event
// There are quite a few ways this could be triggered:
// - `submit` event
// - `form.invalidate`
// - `form.validate`
// - `form.submit`
}
, 'documentSubmit': function (e, tmpl, doc) {
// This event is triggered either via the `submit` event or by calling
// `form.submit`, but in both cases only if the document is valid
}
})
Each of the events we list above (with the exception of propertyChange
) is emitted via our own API, which means when you call any of the methods in Triggering Events your code will emit events in a manner that is consistent with the way that native Forms code emits events.
Important Note about propertyChange
: In general we recommend interacting with the Forms package through the API, however you might find yourself building a custom component where it's tricky to get the form instance, (e.g. when writing a custom component), in these cases the propertyChange
event makes it easy for you to trigger changes to the underlying state of the form just use $(myDomElement).trigger('propertyChange', { key: value })
to let the form instance know that things have changed.
The Forms package tries to be as minimally-invasive as possible, so we don't handle a whole crowd of events, but to make it really easy to handle most use cases, we do handle 3 events by default.
// Read the source code, it's only 10 lines longer than this explanation :)
Forms.events({
'change input, change textarea, change select': function (e, tmpl) {
// Creates an object `changes` and triggers 'propertyChange' with
// `changes` as the first argument
}
}
, 'propertyChange': function (e, tmpl, changes) {
// calls form.change passing in `changes` as the first argument
// notice that this event is triggered by the `change` event
}
, 'submit': function (e, tmpl) {
// calls form.submit
}
});
Note: If you're doing something custom and don't want these events, you can pass events: false
like this: Forms.mixin(Template.customForm, { events: false })