Views in Tailor

Templates

The simplest way to generate pages in response to a request is to create a Template. You can initialize a Template in the controller, with the data from the request, and then call its body method to build the body. That body method should add the contents of the response to its buffer attribute.

The only thing that you must set when creating a template is the controller parameter. You will often want to add other fields, though. The best practice is to have the controller extract the necessary data from the request and the database, and give those to the template when it creates it. This allows you to keep different parts of the logic separated, and design and test the two independently of each other.

Your templates will often be specific to the controller that is expected to render them. They will also often have similar purposes, like a template for an index page or for a form for editing a resource. A good convention for handling this is to scope them within the controller. For instance, if you declare a template with

extension HatsController {
  class IndexTemplate: Template {
  }
}

then you can just refer to it as IndexTemplate within the HatsController class. Swift does require that the every source file have a unique filename, unfortunately, so even if you keep all your templates for a controller in a dedicated directory, you will have to give them specific names like HatsIndexTemplate.swift.

Basic Tags

The tag method adds an HTML tag to a template's buffer. It takes in a tag name, a dictionary of attributes, and a block that adds in more content to go within the tag. For instance, if you had this code in your body method:

self.tag("p", ["class": "help-block"]) {
  self.tag("input", ["class": "help-button", "value": "Help"])
}

it would add this content to the buffer: <p class="help-block"><input class="help-button" value="Help"></input></p>.

There is a variant of the tag method that does not take any attributes, and another variant that takes the text to put inside the tag. There is also a text method that adds text directly into the buffer.

The link method puts in a link to another page in your site. It builds the path for you, based on a controller name, action, and request parameters. You can omit any of these, and it will default to matching the values for the current request. For instance, if you call link(actionName: "new"), that will generate a link to the new action in the current controller. The link method also takes attributes for the HTML tag, and a block that can generate the contents to go inside the tag.

Form Building

The FormBuilder class provides another set of helpers specifically for building forms for creating and updating model objects. Once you create a form builder, you create the form tag using the form method, which allows you to specify the path to submit the form to, the HTTP method, additional attributes for the tag, and finally a block for the body of the form.

Within the body of the form, you can use the input method to add in input tags. This serves as a wrapper around the form's inputBuilder, which is responsible for adding the tags. An input builder is a block that is given the form builder, the attribute name, the attribute value, input attributes, and validation errors. The default input builder will add in a label and an input element, but you will often want to give a custom input builder to add styling or the display of error messages.

Localization and Sanitization

You will generally want to put the text on your site through a localization system, so that you can translate it into different languages. Tailor's localization system is provided by the Localization class. This class provides a fetch method, which takes in a key and provides you the localized text. Keys should be in lowercase, with dots to separate different sections of the key. The fetch method may return nil, if there is no translated value available for the key.

The text method on templates automatically localizes the text, but it has a flag for disabling this behavior. The version of the tag method that takes text contents will also localize the text.

The Localization class itself does not provide any mechanism for storing or fetching translations. There are two subclasses that provide different strategies for this. PropertyListLocalization looks for the values in your application's configuration, under the section localization.content. For instance, if you are looking for a translation for hats.index.title in the locale en, it will look for a value in the configuration for the key localization.content.en.hats.index.title. You can find out more about configuring your app in the Configuration guide. The other available strategy is the DatabaseLocalization class. This looks for translations in a table called tailor_translations. This table should have a field for translation_key, locale, and translated_text, which should all be string types.

Localizations also allow you to interpolate dynamic content into them. The fetch method takes in an optional interpolations argument. If you provide a dictionary with name mapped to John, then any occurrence of \(name) in the translated text will be replaced with John.

Localizations support fallbacks from country-specific locales to global ones, and from other languages to English. For instance, if you're looking for a translation for the locale es-mx, and there is no translation available, it will look for one in the locale es, and then en. If you want to change the available fallbacks, you can use a custom Localization subclass that overrides the fallbackLocales method.

The localization for a template is specified by the controller. The default initializer for a controller will create a localization for the en locale, using a localization class specified in the application's configuration. The class should be specified in the localization.class configuration setting. The default is Tailor.PropertyListLocalization. If you want to use a different locale, perhaps based on a request parameter or user settings, you will have to set that localization in your controller's initializer. You should still set the localization settings. If you want to contruct a localization with that class in another locale, you can use the localization method in the Application class.

Tailor has some conventions for keys that can make them less repetitive. If your key starts with a dot, Tailor automatically prepends the name of the controller class an action. Let's say your app is called Haberdashery, and you are fetching a translation inside the create method of HatsController. You can just type localization.fetch(".success_message"), and it will look for the key haberdashery.hats_controller.create.success_message. Along the same lines, localization in templates are automatically prepended with the class name. If you are in the template HatsController.IndexTemplate, and you call localization.fetch(".title"), it will look for the key haberdashery.hats_controller.index_template.title.

Whenever you pull in text from a source outside of your code, whether it's a localization file or user-supplied content, you need to be careful to avoid accidentally allowing that text to alter the structure of the page or inject any javascript into the page. Tailor has a sanitization system to help you avoid these problems. The Sanitizer class provides a mechanism for sanitizing text and wrapping it in a SanitizedString to prove it has been sanitized. The sanitized string records all of the sanitization classes that have been run on it. The one you'll use most is Sanitizer.htmlSanitizer, which replaces angle brackets, quotation marks, ampersands, and backslashes with ampersand-escaped equivalents. You run text with a sanitizer by passing the string to the sanitizeString method. If you have text that you already know is safe, you can pass it through the accept method, which will add the sanitizer class to the text's list but will not modify its contents.

Any text that you provide in your templates is automatically run through an HTML sanitizer. If you want to avoid this, you can pass the text to the raw method, which will accept the text without sanitization.

Layouts

You'll often want to have all the pages in your site share a common layout, with a page's particular content embedded into it. The Layout class supports this pattern. A layout is a template that wraps around another template, and renders that template within it. You can create a custom layout for your app like you would any other template. When you reach the point where you want the page's content to appear, you can call self.renderTemplate(self.template). The Controller class provides a layout attribute to include layouts automatically. You just need to set a Layout subclass on this attribute, and any templates rendered in the controller will be wrapped in an instance of that layout class.

Examples

extension HatsController {
  class FormTemplate: Template {
    let hat: Hat

    init(controller: Controller, hat: Hat = Hat()) {
      self.hat = hat
      super.init(controller: controller)
    }

    override func body() {
      tag("h1", text: ".title")
      link(action: "index") { text(".back") }

      let form = FormBuilder(template: self, name: "hat")
      let action = (hat.id == nil ? "create" : "update")
      form.form(controller.pathFor(actionName: action) ?? "") {
        form.input("brimSize", hat.brimSize)
        form.input("color", hat.color)
        tag("input", ["type": "submit", "value": ".submit"])
      }
    }
  }
}