Note: While this project is generally in working order, it is no longer being actively maintained. If you would like to maintain this project, please create an issue.
Build production-ready, reactive forms in minutes. Even complex workflows can be achieved with just a few lines of code.
This package supports two types of reusable form components:
While Elements represent single form fields, Form Blocks are containers that control workflow and handle submission. Each type has its own set of reactive states, used to control the experience, workflow, and functionality of a given form through template helpers.
Any compatible template can be transformed into one of the above components using the provided API--and either type of component be use used standalone. But, as you'll see, the real power comes from using the two types of components together.
Create a component by registering a normal Meteor template.
ReactiveForms.createElement({
template: 'basicInput',
validationEvent: 'keyup'
});
Reuse the component anywhere--each instance is self-contained.
{{> basicInput schema=schema field='firstName'}}
{{> basicInput schema=schema field='lastName'}}
{{> basicInput schema=schema field='email'}}
Built with Bootstrap 3 and the sacha:spin
package, it demonstrates how flexible and extensible this package is.
One of the package users, Darek Miskiewicz, has put together a great example in the form of a GitHub repo that you might prefer over the live example.
meteor add templates:forms
This package works on the client-side only.
Define these in a parent template, or in global helpers.
Template['testForm'].helpers({
schema: function () {
return new SimpleSchema({
testField: {
type: String,
max: 3,
instructions: "Enter a value!"
}
});
},
action: function () {
return function (els, callbacks, changed) {
console.log("[forms] Action running!");
console.log("[forms] Form data!", this);
console.log("[forms] HTML elements with `.reactive-element` class!", els);
console.log("[forms] Callbacks!", callbacks);
console.log("[forms] Changed fields!", changed);
callbacks.success(); // Display success message.
callbacks.reset(); // Run each Element's custom `reset` function to clear the form.
};
}
});
The action function runs when the form is submitted. It takes three params, as shown above:
els
.reactive-element
.callbacks
callbacks.success()
sets success
.callbacks.failed()
sets failed
.callbacks.reset()
runs the custom reset function for each Element in the Form Block
and clears Form Block state except for success
and failed
states and related messages.
This allows users to see any available feedback even if the form is reset.callbacks.reset(true)
.{{loading}}
state (see below) will run from the time you submit to the time you call one of these.changed
undefined
.this
All validated form values are available with no extra work:
// Inside the action function...
console.log(this); // Returns {testField: "xxx"}
Data from Elements passed into the action function is guaranteed to be valid, considering:
basicInput
in the next example).Hopefully, this satisfies your needs.
The basicFormBlock
and basicInput
templates are included with this package.
Connect Elements to the schema in a surrounding Form Block using the field
property.
<!-- Wrapped input with form-wide schema -->
<template name="testForm">
{{#basicFormBlock schema=schema action=action}}
{{> basicInput field='testField'}}
{{/basicFormBlock}}
</template>
See the templates
folder to view the code.
This is where you configure the components.
ReactiveForms.createFormBlock({
template: 'basicFormBlock',
submitType: 'normal'
});
ReactiveForms.createElement({
template: 'basicInput',
validationEvent: 'keyup',
reset: function (el) {
$(el).val('');
}
});
You only need to register a given component once.
Each time a component is rendered, it will have a unique context. Elements inside a Form Block will always be connected to the instance of the Form Block that contains them.
ReactiveForms has only two API endpoints.
Add any custom template that satisfies the basic requirements (outlined below), and you're ready to go!
Create a ReactiveForms Element from a compatible template.
ReactiveForms.createElement({
template: 'basicInput',
validationEvent: 'keyup', // Can also be an array of events as of 1.13.0!
validationValue: function (el, clean, template) {
// This is an optional method that lets you hook into the validation event
// and return a custom value to validate with.
// Shown below is the ReactiveForms default. Clearly, this won't work in the case
// of a multi-select form, but you could get those values and put them in an array.
// The `clean` argument comes from SimpleSchema, but has been wrapped--
// it now takes and returns just a value, not an object.
console.log('Specifying my own validation value!');
value = $(el).val();
return clean(value);
},
reset: function (el) {
$(el).val('');
}
});
Other available options for createElement
:
validationSelector
allows specifying a custom selector for the element instead of .reactive-element
.passThroughData
relates to how the element handles reactive initial data. If this is set to true
,
changes in the underlying data will be accepted automatically without informing the user.
created
, rendered
, and destroyed
callbacks--these are safe equivalents to the normal Meteor
template callbacks.<input>
.reactive-element
class (or a custom selector you specify using validationSelector
).validationEvent
type(s) you specify in createElement
options.You can also put the
reactive-element
class on a container in the Element to delegate the event.
Here's an example of a ReactiveForms Element template.
<template name="basicInput">
<strong>{{label}}</strong>
<br>
<input placeholder={{instructions}} class="reactive-element" value={{value}}>
{{#if submitted}}
{{#if errorMessage}}<p class="error-message">{{errorMessage}}</p>{{/if}}
{{/if}}
</template>
Elements can be used standalone, with a SimpleSchema specified, like this:
{{> basicInput schema=schema field='testField'}}
However, Elements are usually used within a Form Block helper, where they transparently integrate with the parent form component.
{{#basicFormBlock schema=schema action=action}}
{{> basicInput field='testField'}}
{{/basicFormBlock}}
Here's what changes when this happens:
field
property on the Element specifies which field in the form's schema to use.{{loading}}
.Element templates have access to the following local helpers:
{{value}}
data
object on the form or Element, this will initially hold the value associated with the relevant field in that object.{{originalValue}}
(when initial data)
{{uniqueValue}}
(when initial data)
{{valid}}
rendered
.{{changed}}
(inverse {{unchanged}}
)
changed
.{{isChild}}
These helpers are available when a SimpleSchema is being used:
{{label}}
field
.{{instructions}}
{{errorMessage}}
While inside a Form Block, these form-level helpers will be available:
{{submitted}}
(inverse {{unsubmitted}}
)
{{errorMessage}}
to delay showing Element invalidations until submit.{{loading}}
{{success}}
All the form-level helpers will be false
when an Element is running standalone.
However, you can override specific properties on an Element when you invoke it:
<!-- The `basicInput` example will now show its error messages when standalone -->
{{> basicInput schema=schema field='testField' submitted=true}}
As you build out your elements, you may start to feel like abstracting some common code.
Well, it only takes two steps:
Create a partial Element template, but without the usual .reactive-element
HTML element inside. You can use all the usual Element template helpers.
Register the template using ReactiveForms.createElement()
, but don't include the usual
validationEvent
field.
Here are examples of two types of possible nested elements:
Separate templates for labels and error messages.
<template name="bootstrapLabel">
<label class="control-label">
{{#if schema.label}}
{{schema.label}}
{{else}}
{{field}}
{{/if}}
</label>
</template>
<template name="bootstrapErrorMsg">
<p class="help-block">
{{#if ../valid}}
{{instructions}}
{{else}}
{{errorMessage}}
{{/if}}
</p>
</template>
<template name="bootstrapInput">
<div class="form-group {{#unless valid}}has-error{{/unless}}">
{{> bootstrapLabel}}
<input name="{{field}}" class="form-control reactive-element" value="{{value}}">
{{> bootstrapErrorMsg}}
</div>
</template>
Wrapper template for Elements (use as a block helper).
<!-- Block helper to wrap any Elements -->
<template name="myElementContainer">
<div>
<label>{{label}}</label>
<br>
{{> UI.contentBlock}}
<!-- Show error if submitted but not successful -->
{{#if submitted}}
{{#if errorMessage}}<p class="error-message">{{errorMessage}}</p>{{/if}}
{{/if}}
</div>
</template>
<!-- Element template -->
<template name="myInputElement">
<input placeholder={{schema.instructions}} class="reactive-element" value={{value}}>
</template>
<!-- Here's a form using all the above components -->
<template name="myLeadGenForm">
{{#defaultFormBlock action=action schema=schema data=data}}
{{#if success}}
<p>Success! Form submitted.</p>
{{else}}
{{#myElementContainer field='firstName'}}
{{> myInputElement field='firstName'}}
{{/myElementContainer}}
{{#myElementContainer field='lastName'}}
{{> myInputElement field='lastName'}}
{{/myElementContainer}}
{{#myElementContainer field='phoneNumber'}}
{{> myInputElement field='phoneNumber'}}
{{/myElementContainer}}
{{/if}}
<hr>
<button type="submit">Submit</button>
{{/defaultFormBlock}}
</template>
Of course the above component templates need to be registered with ReactiveForms
to work.
When running standalone (without being wrapped in a Form Block) you'll put the schema on the Element's template invocation. You can also override the other form-level helpers on Elements this way.
To force an element to run in standalone mode, you can specify
standalone=true
in the template's invocation.Be sure to add the reactive-element class to your Element so that it's selected when the form action is run.
Partial element templates can be used to abstract out common code. You can even create element block templates to wrap your elements.
Create a ReactiveForms Form Block from a compatible template.
ReactiveForms.createFormBlock({
template: 'basicFormBlock',
submitType: 'normal' // or 'enterKey', which captures that event in the form
});
Here's an example of a ReactiveForms Form Block template.
<template name="basicFormBlock">
<form>
<!--
Note:
Use this `UI.contentBlock` exactly as it is here, in every Form Block template.
There are two fields.
1. `data`: This allows you to pass default values into the form. If you'll
never use the form for updating existing data, you can leave it
out and nothing will break.
2. `context`: This field is required. ReactiveForms takes care of the value
automatically.
-->
{{> UI.contentBlock data=data context=context}}
<!-- The below helpers represent exclusive states,
meaning they never appear at the same time -->
<p>
<button type="submit">Submit</button>
<span>
{{#if loading}}
Loading...
{{/if}}
{{#if invalid}}
Can't submit! There are {{invalidCount}} invalid fields!
{{/if}}
{{#if failed}}
<strong>{{#if failedMessage}}{{failedMessage}}{{else}}Unable to submit the form.{{/if}}</strong>
{{/if}}
{{#if success}}
<strong>{{#if successMessage}}{{successMessage}}{{else}}Saved!{{/if}}</strong>
{{/if}}
</span>
</p>
</form>
</template>
Form Blocks can technically be used standalone, with normal, non-reactive form elements like
inputs and check boxes. The form's action function, which runs on submit, always receives
an array containing the HTML elements inside the form with the .reactive-element
class.
However, we strongly recommend using ReactiveForms Elements inside a Form Block, which are reactively validated with SimpleSchema:
{{#basicFormBlock schema=schema action=action}}
{{> basicInput field='firstName'}}
{{> basicInput field='lastName'}}
{{> basicInput field='email'}}
{{/basicFormBlock}}
If you do this, you can trust that the data passed to your action function is already valid. All you'll need to do then is get the data from the form elements and save it somewhere!
Form Block templates have access to the following helpers:
{{invalid}}
{{invalidCount}}
{{changed}}
(inverse {{unchanged}}
)
changed
, and neither do duplicate values.changed
is triggered after success
, it resets submitted
and success
to false
.{{submitted}}
(inverse {{unsubmitted}}
)
{{loading}}
{{failed}}
{{failedMessage}}
callbacks.failed('1 item failed!')
.{{success}}
{{successMessage}}
callbacks.success('Thank you!')
.A Form Block's failed, success, invalid, and loading states are mutually exclusive.
When a Form Block's success state is
true
, setting its changed state totrue
will cause both its success and submitted states to becomefalse
. This makes it possible for users to edit and submit a given form many times in one session--just keep the editable Elements accessible in the UI after the first success (or provide a button that triggers the changed state).ReactiveForms Elements inside a Form Block affect the form's validity. They are reactively validated with SimpleSchema at the form-level, thanks to a shared schema context.
Due to the real-time nature of Meteor, one can only assume that while editing some existing data in a form, the original data might change before the edited work is submitted.
When data is changed remotely during a form session, there are three obvious ways to handle the experience:
This package supports all three of the above options, but special care has been taken to ensure a good experience in the case of #3.
Here's a working example of how easy it is to present a user with the option to accept or ignore remote changes.
This is purely focused on text inputs--the other types of form elements will have different experiences and constraints.
<template name="inputElement">
<input type={{type}} placeholder={{schema.instructions}} class="reactive-element" value={{value}}>
{{#if remoteValueChange}}
<p style="color:black">
This field has been updated remotely. Load the latest
<span title={{newRemoteValue}}>changes</span>?
<button class="accept-changes">Load</button> <button class="ignore-changes">Ignore</button>
</p>
{{/if}}
</template>
Template['inputElement'].events({
'click .accept-changes': function (e, t) {
e.preventDefault();
var inst = Template.instance();
inst[ReactiveForms.namespace].acceptValueChange();
},
'click .ignore-changes': function (e, t) {
e.preventDefault();
var inst = Template.instance();
inst[ReactiveForms.namespace].ignoreValueChange();
}
});
As you can see, we have access to the following template helpers:
{{remoteValueChange}}
{{newRemoteValue}}
name
attribute, but it could be used for a tooltip or anything else.We can control how we deal with remote changes using these template instance methods:
acceptValueChange()
ignoreValueChange()
remoteValueChange
to false.For more fine-grained control over how your form handles remote data changes, specify an onDataChange
hook
via a template helper (just like schema, action, and data):
Template['testForm'].helpers({
onDataChange: function() {
return function(oldData, newData) {
if (!_.isEqual(oldData, newData)) {
// Use one or more of the below methods.
// Usually, you should only need `this.refresh()`.
// Create an issue if you need something else here.
// Reset the form (equivalent to `callbacks.reset()` in the action function).
this.reset(true);
// Refresh unchanged Elements to reflect new data.
// Optionally: `this.refresh('dot.notation', customValue)`.
this.refresh();
// This sets the form's `changed` state to `true`.
this.changed();
}
};
}
});
This hook allows you to update your entire form during remote data changes without needing
to use passThroughData
on individual elements.
Here's the low-down on other Meteor forms packages and how they compare to this one.
While AutoForm strives to offer every option under the sun,
templates:forms
is minimalist in nature--it gives you what you need to build your own stuff, and doesn't make too many assumptions!
templates:forms
, as it aims to do much more.templates:forms
always keeps things self-contained in template instances.Know of another good forms package? Fork this repo, add it here, and create a PR!
Special thanks to steph643 for significant testing and review.
My goal with this package is to keep it simple and flexible, similar to core packages.
As such, it may already have everything it needs.
Please create issues to discuss feature contributions before creating a pull request.