Chapter 8: Form Formalities

In the previous chapter, we learned how to have Kitura display standard web pages via its templating system. In this chapter, we’ll use that knowledge to show some pages with forms on them, then demonstrate how to use Kitura to accept and act upon data from a form after the user submits it.

Web Forms: A Review

Before we start looking at the Kitura side of things, let’s do a quick review of how forms work in browsers. (But if you’re already pretty well informed on this stuff and find yourself getting bored, feel free to skip ahead to the next section.)

The HTML <form> tag has an action attribute which contains the address that the browser will contact when the form is submitted. It also has an attribute called method which can have a value of either get or post.

If method is set to get, then when the form is submitted, the browser will encode the names and values of the form’s fields and append them to the end of the address in the action attribute. It will then make an HTTP GET request to that address, and the request will have an empty body.

For example, given the following HTML:

<form method="get" action="/foo">
  <label>Search for: <input type="text" name="searchQuery" /></label><br />
  Media types:<br />
  <label><input type="checkbox" name="mediaType" value="text" />Text</label><br />
  <label><input type="checkbox" name="mediaType" value="audio" />Audio</label><br />
  <label><input type="checkbox" name="mediaType" value="video" />Video</label><br />
  <input type="submit" />
</form>

The browser will create a form which looks something like this.

Example form

Now let’s say we enter “Kitura” in the “Search for” field, check the “Text” checkbox, and then click the “Submit” button. The browser will build a query which looks something like:

http://example.com/foo?searchQuery=Kitura&mediaType=text

See how the name attributes of the <input> elements were combined with the values entered or selected by the user to build the URL?

So what happens if we go back to our form and change the method attribute of the <form> to post?

<form method="post" action="/foo">

When this form is submitted via an HTTP POST query, the browser will not put the form values in the URL. That means, if this page appears at the URL “http://example.com/“, the user will just be sent to “http://example.com/foo” no matter what values they enter or select in the form. The browser will, however, still serialize the data and send it as the body of the HTML request. This point is important because it means that the URLs created via forms that submit via GET are “bookmarkable” in the sense that, after submitting the from, if you add the resulting page to your browser’s bookmarks/favorites/hotlist and then revisit it later, you will see the same results of your submission. You could also share the URL with friends or family via email or text message or the like and know that, when they click on the web address, they will see the same results page that you did. This handy property does not hold true for results pages reached by a form that submits via POST; again, for our example form above, no matter what the user selects, the address of the resulting page will always be “http://example.com/foo”.

So if forms submitted by GET have this handy “bookmarkable” property, why aren’t all forms submitted by GET? Well, there are a couple of reasons why we’d still want to use POST for some forms. For example, there may be cases where we don’t want to have the form values inside the URL; for example, for a form a user would use to log in to a site that would have a password field. We don’t want that sensitive password to be sticking around in a URL for anyone to see or get access to! Also, note that browsers have a finite limit to the length of a URL that it will support. The limit varies by browser, but just as a rule of thumb, if there’s a chance someone could enter an arbitrarily large amount of data into your form - for example, if it has a <textarea> element for entering a large amount of text, a file upload field (an <input> tag with a type attribute of file), or just lots and lots of standard text fields - it’s best to have your form submit via POST rather than GET.

So when a POST request is done, as mentioned above, the form data is serialized into the body of a POST request to the server. There are two common methods by which this is done, and the method can be changed by setting an enctype value on the <form> tab. The default method used when an enctype is not present (as on our form above) is application/x-www-form-urlencoded, but the other common method is multipart/form-data. I won’t go into detail about how either of these work under the hood, but suffice it to say that if your form has a file upload field, you must set enctype to multipart/form-data. Otherwise, it’s safe to just let the browser use the default behavior by omitting the enctype attribute (which will cause it to use application/x-www-form-urlencoded by default).

So, to sum it up:

  • <form method="get">
    • Will create a “bookmarkable” URL
    • Should not be used with <textarea> or <input type="file">
    • Should never be used on forms with fields for passwords or other sensitive information
    • Should not be used on forms with many elements
  • <form method="post">
    • Will not create a “bookmarkable” URL
    • Can be used with large forms or forms with <textarea> elements
    • Should be used with forms with field for passwords or other sensitive information
    • Cannot be used with <input type="file">
  • <form method="post" enctype="multipart/form-data">
    • As above, except can be used with <input type="file">

Handling GET Submissions

All right, let’s get to coding. Start a new Kitura project. (We’ve been playing with that Chinook database project for a while, so if you’ve forgotten how to start a new one, feel free to go back and take a peek at chapter 1.)

In your Package.swift, import Kitura and KituraStencil. Let’s start by creating a route at the base path which just shows a template.

import Kitura
import KituraStencil

let router = Router()

router.setDefault(templateEngine: StencilTemplateEngine())

router.get("/") { _, response, next in
    try response.render("index", context: [:])
    next()
}

Kitura.addHTTPServer(onPort: 8080, with: router)
Kitura.run()

Create your Views directory and add an index.stencil which looks like the following. Part of it will look very familiar.

<!DOCTYPE html>
<html>
  <head>
    <title>Search Page</title>
  </head>
  <body>
    <form method="get" action="/search">
      <label>Search for: <input type="text" name="searchQuery" /></label><br />
      Media types:<br />
      <label><input type="checkbox" name="mediaType" value="text" />Text</label><br />
      <label><input type="checkbox" name="mediaType" value="audio" />Audio</label><br />
      <label><input type="checkbox" name="mediaType" value="video" />Video</label><br />
      <input type="submit" />
    </form>
  </body>
</html>

Note that our form is going to submit via GET in this case.

We now want to build a route handler to handle the form submission. So how do we find those values? Well, I actually already mentioned how in this book, way back in chapter 2. Remember? You can find them in the queryParameters property of the RouterRequest object. Try adding this to your project.

router.get("/search") { request, response, next in
    guard let query = request.queryParameters["searchQuery"] else {
        response.send("Please enter a search term.")
        return
    }
    guard let mediaType = request.queryParameters["mediaType"] else {
        response.send("Please select at least one media type.")
        return
    }
    response.send("You searched for \(query) in the \(mediaType) type! Here's some results.")
    next()
}

Now load up the search form page and play around a bit.

You might notice a quirk if you select more than one of the “Media types” checkboxes. The resulting URL will look something like this:

http://localhost:8080/search?searchQuery=Kitura&mediaType=text&mediaType=audio

Note there are two values associated with the mediaType key. The text will look something like this:

You searched for Kitura in the text,audio type! Here's some results.

Yes, when there are more than one values for a key in a query, Kitura concatenates the values with a comma in order that they all fit into a single string. To work around this and get the individual values in these cases where there might be multiples, we need to split the value that Kitura gives us on the comma character. (I think this is silly - what happens if one of the values itself has a comma? - and have submitted a pull request to the Kitura project to rectify this. The pull request was accepted, but the changes have not yet appeared in a Kitura release as of this writing.) Let’s update our handler to deal with this.

router.get("/search") { request, response, next in
    guard let query = request.queryParameters["searchQuery"] else {
        response.send("Please enter a search term.")
        return
    }
    guard let mediaType = request.queryParameters["mediaType"] else {
        response.send("Please select at least one media type.")
        return
    }
    let separateMediaTypes = mediaType.split(separator: ",")
    for type in separateMediaTypes {
        response.send("Here are \(query) results in the \(type) media type.\n")
    }
    next()
}

And the result:

Here are Kitura results in the text media type.
Here are Kitura results in the audio media type.

That’s better.

Handling POST Submissions

POST Submissions with enctype="application/x-www-form-urlencoded"

For this sort of form, it would probably be best to keep it as a GET form, since it’s fairly simple and we don’t mind if the results page is bookmarkable. But we’ll turn it into a POST form just so we can see how they would work.

Now that you got this far, handling a POST submission done with the default application/x-www-form-urlencoded encoding is simple. Start by opening up your index.stencil and changing the method attribute of the <form> tag to post and save.

When we expect an HTTP request to have a body we want to deal with, we want to add an instance of the BodyParser class to the relevant route as middleware. After doing that, we can access that data in the body parameter of the RouterRequest object. That parameter is a instance of the ParsedBody enum which has cases and computed property corresponding to the type of content in the body. The one we’re interested in here is the asURLEncoded parameter which returns an optional [String: String] dictionary. No worries if this all sounds like word salad; it’s simpler than it sounds. Let’s go to the code.

router.post("/search", middleware: BodyParser())
router.post("/search") { request, response, next in
    guard let postValues = request.body?.asURLEncoded else {
        response.send("That shouldn't have happened.")
        return
    }
    guard let query = postValues["searchQuery"] else {
        response.send("Please enter a search term.")
        return
    }
    guard let mediaType = postValues["mediaType"] else {
        response.send("Please select at least one media type.")
        return
    }
    let separateMediaTypes = mediaType.split(separator: ",")
    for type in separateMediaTypes {
        response.send("Here are \(query) results in the \(type) media type.\n")
    }
    next()
}

So note in that first line where we instantiate the BodyParser class and add it to handle POST requests made to the “/search” path. Yes, this is the same path we used in the code to handle GET requests above. Remember from Chapter 2 that Kitura lets us define different handlers for the same route segregated by the HTTP request method; the code we wrote above to handle GET requests won’t run when a POST request is made to “/search”, and vice versa. Also recall that we can omit a path when defining middleware or other route handlers, so what I often like to do is just have BodyParser kick into action on all POST requests, regardless of path, by doing:

router.post(middleware: BodyParser())

Though whether you’d like to take this approach is up to you. (If you ever try to write a POST handler and get confused why request.body is empty, odds are the problem is that you forgot to add this middleware. This has happened to me more times than I’d like to admit.)

Inside the standard request handler, we have:

    guard let postValues = request.body?.asURLEncoded else {

Here’s another quirk of Swift, specifically with the handling of optionals. Going into all the quirks of Swift optionals is outside the scope of this book, but suffice it to say that it’s something which often confuses new Swift developers - it sure confused me! The short of it here is that both the body property and the asURLEncoded property of the body property are optionals. The question mark after the body part means that the Swift compiler will check to make sure that body itself is not nil before it attempts to resolve its asURLEncoded property; if body is nil, it just sends nil down the line to postValues rather than trying to continue. The short of the short of it is that postValues will be nil if either request.body or request.body.asURLEncoded are nil.

Note that, just as with queryParameters, in the case that there are multiple values for one key in the data, those values will be concatenated with commas, so we do the same splitting as we do in our GET submission code.

Go ahead and build your project and test it out. You should see that the behavior is exactly the same as it was when you were making GET requests, except that the path won’t have the query string.

POST Submisssions with enctype="multipart/form-data"

Okay, let’s round out the trifecta by seeing how we’d take a form submission with the multipart/form-data encoding. Let’s tweak our template again to add the enctype attribute, and change the action path to something else while we’re at it. We’ll also change the action path to something else.

    <form method="post" action="/multipart" enctype="multipart/form-data">
      <label>Search for: <input type="text" name="searchQuery" /></label><br />
      Media types:<br />
      <label><input type="checkbox" name="mediaType" value="text" />Text</label><br />
      <label><input type="checkbox" name="mediaType" value="audio" />Audio</label><br />
      <label><input type="checkbox" name="mediaType" value="video" />Video</label><br />
      <label>A picture of something silly: <input type="file" name="sillyPicture" /></label><br />
      <input type="submit" />
    </form>

Now our router handler is going to be quite a bit different than the two cases above. We’re going to find our values in request.body?.asMultiPart, and it’s going to be an array of Part structs. Each struct will have a name property which corresponds to the name of our field in the HTML form, and a body property which contains the value the user entered as a ParsedBody enum. Most interesting for the case of file fields, there’s also a filename property which will contain the filename of an uploaded file (for other fields, it will be a blank String), and a type property which corresponds to the MIME type of the uploaded file. For example, if the file uploaded was a JPEG-formatted picture, it will be image/jpeg. (For non-file fields, this will be text/plain.)

So what we’ll do is loop through the parts and examine the name parameters to make sure all the parts we want can be found. We’ll use a switch case on the body parameters of the parts, which, recall, is a ParsedBody enum, even though (due to Swift quirkiness) we haven’t used it as one yet. You’ll see that this all makes the code quite a bit more verbose; another reason why you may wish to avoid using mutlipart/form-data-encoded forms when it’s not necessary (when there’s no file upload field).

router.post("/multipart", middleware: BodyParser())
router.post("/multipart") { request, response, next in
    guard let postValues = request.body?.asMultiPart else {
        response.send("That was unexpected.")
        return
    }
    var mediaTypes: [String] = []
    var searchQuery: String?
    var sillyPicture: Data?
    for part in postValues {
        switch part.body {
        case .text(let text):
            if part.name == "mediaType" {
                mediaTypes.append(text)
            }
            else if part.name == "searchQuery" {
                searchQuery = text
            }
        case .raw(let data):
            if part.name == "sillyPicture" {
                sillyPicture = data
            }
        default:
            response.send("Unexpected part type.")
        }
    }
    guard let _ = searchQuery else {
        response.send("Please enter a search term.")
        return
    }
    guard mediaTypes.isEmpty == false else {
        response.send("Please select at least one media type.")
        return
    }
    guard let _ = sillyPicture else {
        response.send("Please upload a silly picture.")
        return
    }
    for type in mediaTypes {
        response.send("Here are \(searchQuery!) results in the \(type) media type.\n")
    }
    // Do something interesting with the sillyPicture here…
    next()
}

Extra Credit

Want an extra challenge? Now that you know how forms work, go back and open up your music database project from previous chapters and add a search page allowing users to search for albums, artists, and songs by name. This will involve using the form handling stuff introduced in this chapter plus the templating and Kuery stuff from earlier chapters, so it should be a good demonstration of all the fun stuff you’ve learned so far. Have fun!

results matching ""

    No results matching ""