/vapor-forms

Brings simple, dynamic and re-usable web form handling to Vapor.

Primary LanguageSwift

VaporForms

Brings simple, dynamic and re-usable web form handling to Vapor.

This library is being used in production and should be safe, but as an early release the API is subject to change.

Don't forget to add to your providers if you want to use built-in Leaf tags:

import Vapor
import VaporForms

let drop = Droplet()
try drop.addProvider(VaporForms.Provider.self)

Features

Create a Fieldset on the fly:

let fieldset = Fieldset([
  "firstName": StringField(label: "First Name"),
  "lastName": StringField(label: "Last Name"),
])

and add validation:

let fieldset = Fieldset([
  "firstName": StringField(),
  "lastName": StringField(),
  "email": StringField(String.EmailValidator()),
], requiring: ["email"])

You can add multiple validators, too:

let fieldset = Fieldset([
  "firstName": StringField(label: "First Name",
    String.MinimumLengthValidator(characters: 3),
    String.MaximumLengthValidator(characters: 255),
  ),
  "lastName": StringField(label: "Last Name",
    String.MinimumLengthValidator(characters: 3),
    String.MaximumLengthValidator(characters: 255),
  ),
  "email": StringField(String.EmailValidator()),
], requiring: ["email"])

And even add whole-fieldset validation after individual field validators have run:

static let loginFieldset = Fieldset([
  "username": StringField(label: "Username"),
  "password": StringField(label: "Password"),
], requiring: ["username", "password"]) { fieldset in
  let loginResult = validateCredentials(username: fieldset.values["username"]!.string!, password: fieldset.values["password"]!.string!)
  if !loginResult {
    fieldset.errors["password"].append(FieldError.validationFailed(message: "Username and password not valid"))
  }
}

Validate from a request:

fieldset.validate(request.data)

or even from a simple object:

fieldset.validate([
  "firstName": "Peter",
  "lastName": "Pan",
])

Validation results:

switch fieldset.validate(request.data) {
case .success(let validatedData):
  // validatedData is guaranteed to contain correct field names and values.
  let user = User(
    firstName: validatedData["firstName"]!.string!,
    lastName: validatedData["lastName"]!.string!,
  )
case .failure:
  // Use the field names and failed validation messages in `fieldset.errors`,
  // and the passed-in values in `fieldset.values` to re-render your form.
  // If a single field fails multiple validators, you'll receive
  // an error string for each rather than just failing at the first
  // validator.
}

Gain strongly-typed results by wrapping the Fieldset in a re-usable Form.

struct UserForm: Form {
  let firstName: String
  let lastName: String
  let email: String
  
  static let fieldset = Fieldset([
    "firstName": StringField(),
    "lastName": StringField(),
    "email": StringField(String.EmailValidator()),
  ], requiring: ["firstName", "lastName", "email"])
  
  init(validatedData: [String: Node]) throws {
    // validatedData is guaranteed to contain correct field names and values.
    firstName = validatedData["firstName"]!.string!
    lastName = validatedData["lastName"]!.string!
    email = validatedData["email"]!.string!
  }
}

Now you can easily and type-safely access the results of your form validation.

drop.get { request in
  do {
    let form = try UserForm(validating: request.data)
    // Return to your view, or use the properties to save a Model instance.
    return "Hello \(form.firstName) \(form.lastName)"
  } catch FormError.validationFailed(let fieldset) {
    // Use the leaf tags on the fieldset to re-render your form.
    return try drop.view.make("index", [
      "fieldset": fieldset,
    ])
  }
}

Rendering a form with validation error messages:

<label>Name</label>
<input type='text' name='name' value='#valueForField(fieldset, "name")'>
#ifFieldHasErrors(fieldset, "name") { <ul class="errorlist"> }
#loopErrorsForField(fieldset, "name", "message") { <li>#(message)</li> }
#ifFieldHasErrors(fieldset, "name") { </ul> }

Rendering a select:

<select name='colour'>
  <option hidden></option>
  <option #valueForField(fieldset, "colour") { #equal(self, "red") { selected } }>red</option>
  <option #valueForField(fieldset, "colour") { #equal(self, "blue") { selected } }>blue</option>
  <option #valueForField(fieldset, "colour") { #equal(self, "green") { selected } }>green</option>
</select>

Documentation

See the extensive tests file for full usage while in early development. Built-in validators are in the Validators directory. Proper documentation to come.

Known issues

So far, everything works as it says on the tin.

There are some unfortunate design aspects, though, which the author hopes to straighten out.

One of Swift's greatest assets is strong typing, but this library largely bypasses all those benefits. This is due to limitations in both Swift's introspection mechanism, and the author's general intelligence. The Form protocol is an attempt to resolve this lack; in theory, when the end-user fills out their fields property and init method correctly there should be no problems, but it would be nice for the compiler to catch any typos before the app runs. Using an enum for field names would be a good idea.

The majority of the library uses ...ValidationResult enums to return useful information about the success or failure of validation. However, the Form protocol also throws because the mapping of validated data to instance property is implemented by the end-user and errors may arise.

Vapor's Node is heavily used, as is Content. Unfortunately, the built-in validation is (despite the author's best efforts) almost completely unused. Future work may be able to converge the two validation mechanisms enough that this library doesn't need to supply its own.