Ionic Contact Form Demo with Behat 3 Testing the Back-End


Today we are taking a whistle-stop tour of the Ionic Contact Form POST'ing to the Symfony 2 / FOS REST Bundle /api/contact end point.

I have set up a Behat 3 feature spec for the Contact POST, so let's take a look at that first:

# /src/ApiBundle/Features/contact.post.feature

Feature: Tweet Hours Contact API - POST

  In order to receive feedback from app users
  As a developer
  I need to POST new contact information

  Background:
    Given I have the request body:
      """
      {
        "name": "timmy testfield",
        "email": "timmy@tester.com",
        "message": "a long, interesting message from an app user"
      }
      """
      And I set header "Content-Type" with value "application/json"

    @current
    Scenario: POST with good data is accepted
      When I send a POST request to "/api/contact/"
      Then the response code should be 200

I find Behat features really self explanatory. I guess that's their key purpose though really - to be understandable by anyone in the business, including (relatively) non-technical folks. Like project managers.

However, this feature is for developers so it's a little geeky - 200 codes ahoy.

Essentially, what I am saying is that this API should allow a consumer of the API to POST in JSON data in the format above (the Given statement), and so long as it meets that sort of format / criteria then the REST consumer should get a 200 status code back. I guess it's more common to see 201 status code returned on POST, but in this case I'm not actually creating anything (that the consumer can see anyway), so 201 made no sense. Interested in wtf those status codes even mean? Click here. Light reading ahead.

REST Consumers - Ionic, Behat 3, Some kid and his Commodore 64

The nice thing about this kind of architecture is that we don't really care how the data is created / sent in, all that we care about is that when it gets here, it meets our requirements.

If you're unsure about the FOS REST side of things, check out my tutorial series on this exact topic. The most complex thing about REST is the mindset shift. Once you change your perception on how an application architecture should be to make it as useful to as many people as possible, the code itself is pretty straightforward. And better yet, if you follow my series it's covered by a set of Codeception tests.

As a result, as soon as that Behat 3 feature passes, we are golden. As long as we follow that same set of rules - must have these fields in the body, must have this Content-Type, must hit this endpoint - we can start accepting data from anywhere. Case in point, the way our Behat tests work behind the scenes (Guzzle) is different from how Angular / Ionic will be POST'ing in - $http.

The other thing to mention here is that whilst REST is certain the current fashion / trend according to the jobs boards, it's not the best solution to every problem. Use it when it makes sense.

Ionic Contact Form Example

Because code speaks a thousand words, and because I couldn't find a simple Angular / Ionic contact form example when I was first searching for this, let me share with you my controller / template code in the hope that it saves someone a few hours, at some point in the future :)

Note - this assumes you are following the same layout / convention as I am. It may not work verbatim. Rather, use it as a guideline.

// www/app/modules/core/controllers/contact.controller.js

angular.module('core')

    .controller('ContactController', ['$scope', '$http',  function($scope, $http) {

        $scope.sendContact = function() {

            var data = {
                name: this.contact.name,
                email: this.contact.email,
                message: this.contact.message
            };

            console.log(data);

            $http.post("http://192.168.1.189/app_dev.php/api/contact/", data);

        }

    }])
;

This controller works in conjunction with the following template:

<ion-view title="Contact">

    <ion-nav-buttons side="left">
        <button menu-toggle="left" class="button button-icon icon ion-navicon"></button>
    </ion-nav-buttons>

    <ion-content class="has-header padding">
        <form ng-submit="sendContact()">
            <div class="list">
                <label class="item item-input item-stacked-label">
                    <span class="input-label">Your Name</span>
                    <input type="text" ng-model="contact.name" placeholder="Jane Doe">
                </label>
                <label class="item item-input item-stacked-label">
                    <span class="input-label">Email</span>
                    <input type="text" ng-model="contact.email" placeholder="jane@email.com">
                </label>
                <label class="item item-input item-stacked-label">
                    <span class="input-label">Message</span>
                    <input type="text" ng-model="contact.message" placeholder="It would be great if you could add my #TweetHour">
                </label>
                <padding>
                    <button class="button button-balanced button-block">
                        Send
                    </button>
                </padding>
            </div>
        </form>
    </ion-content>
</ion-view>

There's a lot of text here that obscures something pretty simple.

Angular handles the hard part of matching up what is labelled with ng-model to the controller code - e.g. this.contact.email and we stick this on to a new object with keys that match up exactly with what our REST API expects.

One thing to point out - we use this.contact.email (for example), rather than $scope.contact.email. This took me a while hunting round StackOverflow to figure out, so hopefully that saves you a potential headache at some point in the future.

When we click the button, that in turns causes the form to submit, and the form uses ng-submit instead of the usual submit method to instead call the sendContact() function in our controller. The brackets are important there.

As the controller follows our convention exactly, all we then need to do is POST the result off - Angular / Ionic doesn't really care where, or what it's sending at this point. It just does as it's told.

And then we hit the API.

Nelmio CORS Bundle

One thing to note here is that whilst the Behat tests should pass, a call from a different machine will likely fail.

This is a CORS problem. One way to get around this is to add the NelmioCorsBundle to your project.

Follow the instructions, remembering to register the bundle in your AppKernel, and then add config similar to the following:

# app/config/config.yml

nelmio_cors:
    defaults:
        allow_credentials: false
        allow_origin: []
        allow_headers: []
        allow_methods: []
        expose_headers: []
        max_age: 0
        hosts: []
        origin_regex: false
    paths:
        '^/api/':
            allow_origin: ['*']
            allow_headers: ['X-Custom-Auth', 'content-type']
            allow_methods: ['POST', 'PUT', 'GET', 'DELETE']
            max_age: 3600
        '^/':
            origin_regex: true
            allow_origin: ['^http://localhost:[0-9]+']
            allow_headers: ['X-Custom-Auth']
            allow_methods: ['POST', 'PUT', 'GET', 'DELETE']
            max_age: 3600
            hosts: ['^api\.']

Your Behat test should still be passing after this, but not your Ionic app and other API consumers will also be able to send you some data.

Episodes