Building a contact form

← Previous: Working with cursors

So you want a contact form on your site. A pretty common requirement. Maybe it's not a contact form; maybe you want to accept submissions of stories, or product ideas. The same principles apply.

Doing it the easy way: using apostrophe-pieces-submit-widgets

A module is available that allows users to submit any type of piece you wish to permit. You can specify the subset of fields that are appropriate for them, and avoid the work of building your own solution. Then just add the widget to the appropriate pages, and site visitors will see forms.

This works well for contact forms, since you can just define a piece type with an appropriate schema. So check out the apostrophe-pieces-submit-widgets module before doing anything more complex!

Doing it your way: a note on CSRF protection

Apostrophe provides tools that can help you build contact forms, including the apostrophe-pieces-submit-widgets module and other techniques. And we'll look at them. But first: of course you can also do your own thing.

Apostrophe sites are still node and Express apps, and you still have HTML5, JavaScript, lodash and jQuery at your disposal on the browser side. Wing it if you want to, especially in "project level" code that's not part of a reusable Apostrophe module.

Just one thing you'll need to know before you wing it: "plain old form submissions" not executed by jQuery aren't going to work, not right out of the box. That's because Apostrophe adds Cross Site Request Forgery (CSRF) protection, as standard middleware. Let's look at how to make that work for your code too.

Submitting "plain old forms"

When you create an ordinary form element with the POST method and point it at an Express route with an action attribute, you'll find that you get a mysterious 403 Forbidden error. That's because your form does not contain the CSRF token.

If you really want to use a "plain old form submission," you can configure Apostrophe to let your route through:

// in app.js
modules: {
  apostrophe-express: {
    csrf: {
      exceptions: [ '/my-post-route-url' ]
    }
  }
}

Submitting AJAX forms with jQuery

There's a better way, though. Just use jQuery to submit your form, like this...

$('.my-form').submit(function() {
  $.post('/my-post-route-url', $('.my-form').serialize(), function(result) {
    if (result.status === 'ok') {
      $('.my-form .thank-you').show();
    }
  });
  return false;
});

When you do that, a jQuery AJAX handler supplied by Apostrophe automatically sends a CSRF token with your data, and it just works.

This way you can immediately respond "in the page" rather than waiting for a full-page refresh.

Don't forget to return false; or call preventDefault on the event.

Adding routes for your custom form handlers

Either way, you'll want an Express route on the server side to process the data. In any module, you might write:

self.route('post', 'submit', function(req, res) {
  // Access req.body here
  // Send back an AJAX response with `res.send()` as you normally do with Express
});

That creates a route at /modules/your-module-name/submit.

Or you can use Express directly:

self.apos.app.post('/my-post-route-url', function(req, res) {
  // Access req.body here
  // Send back an AJAX response with `res.send()` as you normally do with Express
});

If you are allowing "plain old form submissions," you'll want to use res.redirect afterwards to bring the user back to a useful page. You might want to send along data.url in a hidden field in your form for this purpose.

Going deeper with Apostrophe: creating a contact-form module

That being said... depending on your ambitions, Apostrophe may have a better way to offer. Using pieces and widgets, you can create a solution in which Apostrophe does most of the work.

And actually, we've already built that solution for you! But perhaps you'd like to understand that path more deeply and take it in new directions. If so, the rest of this article is for you.

Apostrophe provides tools to help you render forms, sanitize the user's entries, submit them, and save them where the results can be easily viewed and managed.

Remember pieces? Pieces are great! All we need is a way to accept form submissions to create them.

Let's start by creating a contact-form module.

// in app.js
  modules: {
    // ... other modules ...
    'contact-form': {}
  }
// in lib/modules/contact-form/index.js
var async = require('async');

module.exports = {
  extend: 'apostrophe-pieces',
  name: 'contact-form',
  label: 'Contact Form',
  alias: 'contactForm',
  addFields: [
    {
      name: 'name',
      type: 'string',
      label: 'Your Name',
      required: true
    },
    {
      name: 'email',
      type: 'string',
      label: 'Your Email',
      required: true
    },
    {
      name: 'title',
      type: 'string',
      label: 'Subject',
      required: true
    },
    {
      name: 'body',
      type: 'string',
      label: 'Message',
      textarea: true,
    }
  ],
  permissionsFields: false,

  afterConstruct: function(self) {
    self.setSubmitSchema();
  },

  construct: function(self, options) {

    self.setSubmitSchema = function() {
      self.submitSchema = self.apos.schemas.subset(self.schema,
        [ 'name', 'email', 'title', 'body' ]
      );
    };

    self.submit = function(req, callback) {
      var piece = {};
      return async.series([
        convert,
        insert
      ], callback);
      function convert(callback) {
        return self.apos.schemas.convert(req, self.schema, 'form', req.body, piece, callback);
      }
      function insert(callback) {
        return self.insert(req, piece, { permissions: false }, callback);
      }
    };

  }
};

What's going on in this code?

"Great, but nobody's calling self.submit!" Well no, not in this module. We'll call it from contact-form-widgets.

Displaying and submitting the form: contact-form-widgets

"What the heck do widgets have to do with contact forms?"

They're a pretty great way to allow users to add contact forms wherever you want them!

Of course, you'll want to manage that reasonably, by only adding the widget to appropriate areas in suitable page templates. If you really want to lock it down, you can introduce it only with apos.singleton().

Widgets also have some plumbing that's really helpful for what we need to do.

// in app.js
  modules: {
    // Other modules, then ...
    'contact-form': {},
    'contact-form-widgets': {}
  }
// in lib/modules/contact-form-widgets/index.js
module.exports = {

  extend: 'apostrophe-widgets',
  label: 'Contact Form',
  contextualOnly: true,
  scene: 'user',

  construct: function(self, options) {

    self.forms = self.apos.contactForm;

    self.output = function(widget, options) {
      return self.partial(self.template, {
        widget: widget,
        options: options,
        manager: self,
        schema: self.forms.submitSchema
      });
    };

    self.pushAsset('script', 'always', { when: 'always' });
    self.pushAsset('stylesheet', 'always', { when: 'always' });

    self.route('post', 'submit', function(req, res) {
      return self.forms.submit(req, function(err) {
        return res.send({ status: err ? 'error' : 'ok' });
      });
    });

    var superGetCreateSingletonOptions = self.getCreateSingletonOptions;
    self.getCreateSingletonOptions = function(req) {
      var options = superGetCreateSingletonOptions(req);
      options.submitSchema = self.forms.submitSchema;
      options.piece = self.forms.newInstance();
      return options;
    };

  }
};

What's going on this code?

Here's the markup for our form widget:

{# in lib/modules/contact-form-widgets/views/widget.html #}
{% import "apostrophe-schemas:macros.html" as schemas %}

<form class="contact-form" data-contact-form>
  <h4>Contact Us</h4>
  {{ schemas.fields(data.schema, { tabs: false }) }}
  <button type="submit">Send Message</button>
  {# Later gets hoisted out and becomes visible #}
  <div class="thank-you" data-thank-you>
    <h4>Thank you for getting in touch! We'll respond soon.</h4>
  </div>
</form>

What's going on in this template?

Here are some simple styles to get the form working:

// in lib/modules/contact-form-widgets/public/css/always.less

.contact-form
{
  padding: 20px 0;
  width: 400px;
  margin: auto;
  fieldset {
    margin: 1em 0;
  }
  label {
    display: inline-block;
    width: 200px;
  }
  textarea {
    width: 200px;
    height: 5em;
  }
  .thank-you {
    // later gets hoisted out and becomes visible
    display: none;
  }
}

And here is the JavaScript that powers the form on the browser side. We don't want to use a plain old form submission; we want to use Apostrophe's schemas to sanitize the form first and pass on the data:

// in lib/modules/contact-form-widgets/public/js/always.js

apos.define('contact-form-widgets', {

  extend: 'apostrophe-widgets',

  construct: function(self, options) {

    self.play = function($widget, data, options) {

      var $form = $widget.find('[data-contact-form]');
      var schema = self.options.submitSchema;
      var piece = _.cloneDeep(self.options.piece);

      return apos.schemas.populate($form, self.schema, self.piece, function(err) {
        if (err) {
          alert('A problem occurred setting up the contact form.');
          return;
        }
        enableSubmit();
      });

      function enableSubmit() {
        $form.on('submit', function() {
          submit();
          return false;
        });
      }

      function submit() {
        return async.series([
          convert,
          submitToServer
        ], function(err) {
          if (err) {
            alert('Something was not right. Please review your submission.');
          } else {
            // Replace the form with its formerly hidden thank you message
            $form.replaceWith($form.find('[data-thank-you]'));
          }
        });
        function convert(callback) {
          return apos.schemas.convert($form, schema, piece, callback);
        }
        function submitToServer(callback) {
          return self.api('submit', piece, function(data) {
            if (data.status === 'ok') {
              // All is well
              return callback(null);
            }
            // API-level error
            return callback('error');
          }, function(err) {
            // Transport-level error
            return callback(err);
          });
        }
      }
    };
  }
});

What's going on in this code?

Adding the widget to a page template

Now you can test it out by adding the widget to a page template:

  {{
    apos.area(data.page, 'body', {
      widgets: {
        // Other widgets perhaps...
        'contact-form': {}
      }
    })
  }}

You can add as many instances of the contact form around the site as you wish.

Viewing the results

To see the submitted forms, just access "Contact Forms" via the Apostrophe admin bar. Since they are pieces, you can manage and update them as you see fit.

More ideas: moderating submitted content

There's a lot more that we can do, now that we have a form powered by schemas. The showFields feature of select fields can be used to selectively show and hide parts of the form based on the user's input so far. And we can go beyond contact forms, allowing users to submit stories and other types of content. Just set the def property of the published field to false and you'll be able to manage the incoming submissions via the appropriate filter in the "Manage" view.

Next: Accessing the database directly →