Home

Awesome

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.

About

Build production-ready, reactive forms in minutes. Even complex workflows can be achieved with just a few lines of code.

Facts

Overview

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'}}

Examples

View the Live Example

Built with Bootstrap 3 and the sacha:spin package, it demonstrates how flexible and extensible this package is.

View a Package User's Example

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.

Install

meteor add templates:forms

This package works on the client-side only.

Usage

1. Provide a schema and an action function.

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:

Retrieve Form Values with 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:

Hopefully, this satisfies your needs.

2. Add ReactiveForms components.

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.

3. Register the ReactiveForms components if needed.

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.

API

ReactiveForms has only two API endpoints.

Add any custom template that satisfies the basic requirements (outlined below), and you're ready to go!

ReactiveForms.createElement()

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:

Element template requirements

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:

Element template helpers

Element templates have access to the following local helpers:

These helpers are available when a SimpleSchema is being used:

While inside a Form Block, these form-level helpers will be available:

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}}

Using nested Elements for cleaner code

As you build out your elements, you may start to feel like abstracting some common code.

Well, it only takes two steps:

  1. Create a partial Element template, but without the usual .reactive-element HTML element inside. You can use all the usual Element template helpers.

  2. 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.

Highlights

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.

ReactiveForms.createFormBlock()

Create a ReactiveForms Form Block from a compatible template.

ReactiveForms.createFormBlock({
  template: 'basicFormBlock',
  submitType: 'normal' // or 'enterKey', which captures that event in the form
});

Form Block template requirements

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 template helpers

Form Block templates have access to the following helpers:

Highlights

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 to true will cause both its success and submitted states to become false. 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.

Working With Reactive Form Data

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:

  1. Ignore the change and let the user submit their new form.
  2. Patch in the changes mid-session without any real prompt.
  3. Notify the user of remote changes and give them the opportunity to view and import those changes into the current session.

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:

We can control how we deal with remote changes using these template instance methods:

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.

Other Forms Packages

Here's the low-down on other Meteor forms packages and how they compare to this one.

AutoForm

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!

Know of another good forms package? Fork this repo, add it here, and create a PR!

Contributors

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.