pod afPillow

Something for your web app to get its teeth into!

Mixins

PageMeta

(Service) - Returns details about the Pillow page currently being rendered.

Pages

(Service) - Methods for discovering Pillow pages and returning PageMeta instances.

PillowConfigIds

IocConfig values as provided by Pillow.

Enums

WelcomePageStrategy

Defines a strategy for handling the interaction between welcome page and directory URIs.

Facets

Page

Place on a EfanComponent to define a Pillow web page.

PageContext

Place on a field to mark it as a page context value.

PageEvent

Place on a method to mark it as a page event.

Errs

PillowErr

As thrown by Pillow.

Overview

Pillow is a web framework that maps HTTP request URLs to Pillow Pages, letting them react to RESTful events.

Pillow...

Pillow - Something for your web app to get its teeth into!

Install

Install Pillow with the Fantom Repository Manager ( fanr ):

C:\> fanr install -r http://repo.status302.com/fanr/ afPillow

To use in a Fantom project, add a dependency to build.fan:

depends = ["sys 1.0", ..., "afPillow 1.0"]

Documentation

Full API & fandocs are available on the Status302 repository.

Quick Start

1). Create a text file called Example.efan:

Hello Mum! I'm <%= age %> years old!

2). Create a text file called Example.fan:

using afIoc
using afBedSheet
using afEfanXtra
using afPillow

// ---- The only class you need! ----

@Page
const mixin Example : EfanComponent {

    @PageContext
    abstract Int age
}

// ---- Standard Main Class ----

class Main {
    Int main() {
        afBedSheet::Main().main([AppModule#.qname, "8069"])
    }
}

// ---- Support class, needed when running from a script ----

@SubModule { modules=[EfanXtraModule#, PillowModule#] }
class AppModule {
    @Contribute { serviceType=TemplateDirectories# }
    static Void contributeEfanDirs(Configuration config) {
        // Look for Example.efan in the same dir as this fantom file
        config.add(`./`)
    }
}

3). Run Example.fan as a Fantom script from the command line:

C:\> fan Example.fan

Efan Library: 'app' has 1 page(s):
  Example : /example

4). Point your browser at http://localhost:8069/example/42

Hello Mum! I'm 42 years old!

Usage

To create a web page, define an EfanComponent that is annotated with the Page facet. Example:

using afPillow::Page
using afEfanXtra::EfanComponent

@Page
const mixin Admin : EfanComponent {
    ...
}

Pages are efanXtra components and behave in exactly the same way.

Pillow will automatically route URLs with your page name, to your page. Camel casing class names results in a / delimiter. Examples:

`/admin`        --> Admin.fan
`/admin/secret` --> AdminSecret.fan

Or you can use the @Page facet to define an explicit URL.

Welcome Pages

Pillow supports the routing of welcome pages, also known as directory pages, through the WelcomePageStrategy.

When switched on, whenever a request is made for a directory URI (one that ends with a /slash/) then Pillow will render the directory's welcome page, which defaults to a page named Index. Examples:

`/`        --> Index.fan
`/admin/`  --> AdminIndex.fan

More can be read about directory URLs in the article: Should Your URLs Point to the Directory or the Index Page?

The welcome page strategy also supports redirects, where requests for legacy pages (like /index.html) are redirected to the directory URI. Redirects are preferred over serving up the same page for multiple URIs to avoid duplicate content.

Page Contexts

As seen in the Quick Start example, parts of the request path are automatically mapped to @PageContext fields. In our exmaple, the 42 in http://localhost:8069/example/42 is mapped to the age page context field.

Declaring page context fields is actually shorthard for assigning fields manually from the @InitRender method. The Quick Start example could be re-written long hand as:

@Page
const mixin Example : EfanComponent {
    abstract Int age

    @InitRender
    Void initRender(Int age) {
        this.age = age
    }
}

Note that a Pillow Page may choose to have either an @InitRender method or @PageContext fields, not both. Also note that page context objects need to be immutable ('const' classes).

Any @InitRender method parameter with a default value becomes an optional URL parameter. Example:

@Page
const mixin Example : EfanComponent {
    @InitRender
    Void initRender(Int age := 69) { .. }
}

Would respond to URLs of both:

/example
/example/42

Page Events

Page events allow pages to respond to RESTful actions by mapping URIs to page event methods. Page event methods are called in the context of the page they are defined. Denote page events with the @PageEvent facet.

Lets change our example so that the page context is a Str and introduce an event called loves:

@Page
const mixin Example : EfanComponent {

    @PageContext
    abstract Str name

    @PageEvent
    Void loves(Str meat) {
        echo("${name} loves ${meat}!")
    }
}

Event URIs follow the pattern:

<page name> / <page context(s)> / <event name> / <event context(s)>

So we can call the loves event with the URI http://localhost:8069/example/Emma/loves/sausage, which is broken down as:

example --> 'Example#' page type
Emma    --> 'name' field
loves   --> 'loves()' method
sausage --> 'meat' argument

Use PageMeta.eventUri(name, context) to generate event URIs that can be used in templates.

Event methods are invoked before anything is rendered. The default action, should the event method be Void or return null, is to re-render the containing page.

Event methods may return any BedSheet response object.

It is standard practice to prefix event methods with the word on, so the loves() method could also be written as:

@PageEvent
Void onLoves(Str meat) {
    echo("${name} loves ${meat}!")
}

But note that the event name (as used in the URL) is still love.

Page Meta

The PageMeta class holds information about the Pillow Page currently being rendered. Obviously, using PageMeta in a page class, returns information about itself! Which is quite handy.

Arguably the most useful method is pageUrl() which returns a URI that can be used, by a client, to render the page complete with the current page context. You can create new PageMeta instances with different page contexts by using the withContext() method. Using our example again:

@Page
const mixin Example : EfanComponent {
    @Inject abstract PageMeta pageMeta

    @PageContext
    abstract Int age

    Str renderLinkToSelf() {
        return "<a href='${pageMeta.pageUri}'>Link to ${age}</a>"
    }

    Str renderLinkTo69() {
        page69 := pageMeta.withContext([69]).pageUri
        return "<a href='${page69}'>Link to 69</a>"
    }
}

PageMeta instances are BedSheet response objects and may be returned from route handlers. The Pillow PageMeta handler will then render the Pillow page.

Use the Pages service to create PageMeta instances for any Pillow page.

Content Type

Page template files should use a double extension in their name, for example,

IndexPage.html.slim

The outer extension denotes the type of templating to use, Slim in our example. The innter extension is used to find the Content-Type that is sent in the HTTP response header. In our example, the Content-Type would be set to text/html.

If a double extension is not used, or not know, then the default content type, as defined by the config value, is used.

Or you can use the @Page facet to explicitly set the content type.

Page Routes

HTTP requests are routed to pages and events via standard BedSheet routes. All the Pillow routes are contributed under a single contribution called afPillow.pageRoutes. So to disable Pillow routing, simply remove this contribution:

@Contribute { serviceType=Routes# }
static Void contributeRoutes(Configuration config) {
    config.remove("afPillow.pageRoutes")
}

Should you wish to override any page route, contribute your own Route before the Pillow routes. That way your Route is processed first.

@Contribute { serviceType=Routes# }
static Void contributeRoutes(Configuration config) {
    config.add(Route(...)).before("afPillow.pageRoutes")
}

404 and Err Pages

Pillow pages may be used as BedSheet 404 Status and 500 Error pages. To do so, contribute a MethodCall func to Pages.renderPage():

To render Error404Page as a BedSheet 404 status page:

@Contribute { serviceType=HttpStatusResponses# }
static Void contribute404Response(Configuration config) {
    conf[404] = MethodCall(Pages#renderPage, [Error404Page#]).toImmutableFunc
}

To render Error500Page as a BedSheet error page:

@Contribute { serviceType=ErrResponses# }
static Void contributeErrResponses(Configuration config) {
    config[Err#] = MethodCall(Pages#renderPage, [Error500Page#]).toImmutableFunc
}

Note that you should also disable routing for those pages so they can't be accessed directly by a URL.

using afEfanXtra
using afPillow

@Page { disableRoutes=true }
const mixin Error404Page : EfanComponent { ... }

@Page { disableRoutes=true }
const mixin Error500Page : EfanComponent { ... }

Release Notes

v1.0.22

  • New: @InitRender method parameters with default values become optional URL parameters.
  • New: @PageContext fields may also be optional.
  • New: @PageEvent methods may also have optional and nullable URL parameters.
  • New: Added @Page.disableRoutes so individual pages can be omitted from Route generation.
  • Chg: PageMeta.eventUrl(...) may now take a Method or a Str as the event argument.
  • Chg: Made Pages.renderXXXX() methods public.
  • Chg: Caching HTTP headers are only set in prod mode.
  • Bug: Page context parameters are now correctly URL encoded and decoded - see URI Encoding / Decoding.

v1.0.20

  • Chg: Updated to IoC 2.0.0.
  • Chg: Converted PageMeta to a mixin.
  • Chg: Pillow BedSheet Routes are contributed via a single afPillow.pageRoutes contribution, and not placeholders.

v1.0.18

v1.0.16

  • Bug: Bodged release.

v1.0.14

  • New: Added @PageEvent.name which overrides the default method name.
  • New: Added PageMeta.eventMethods().
  • Chg: Updated to IoC 1.7.2 and BedSheet 1.3.12.
  • Chg: Renamed PageMeta.eventUri -> PageMeta.eventUrl.

v1.0.12

  • New: Added a default cache-control HTTP header as a config value.
  • Chg: Renamed PageMeta.pageUri -> PageMeta.pageUrl.

v1.0.10

  • New: Stack frames from Pillow, efanXtra and efan are marked as boring on BedSheet's Err500 page.
  • Chg: Updated to IoC 1.6.4.

v1.0.8

  • New: Using Bean Utils 0.0.2
  • New: Page state from events are saved and restored should the page be rendered as part of the same request.
  • Chg: Page events render the containing page by default.
  • Chg: Renamed @Page.uri -> @Page.url

v1.0.6

  • Chg: Updated to use efanXtra 1.1.0.
  • Chg: Pages.pageMeta() and Pages.get() now throw a NotFoundErr if the given page type could not be found.

v1.0.4

  • Chg: Page context may be nullable on Pages.pageMeta().
  • Chg: Added Pages.get() operator for easy PageMeta access.
  • Chg: Contributed Pillow Pages section to BedSheet's Err and Not Found pages.

v1.0.2

  • Chg: Page context values may now be mutable / non-const objects.

v1.0.0

  • New: Implemented WelcomePageStrategy.
  • New: Added helpful http response headers for testing.
  • Chg: PageUriResolver and ContentTypeResolver are now configurable.
  • Chg: Renamed ConfigId welcomePage -> welcomePageName.
  • Bug: Page URIs with no page context could be a directory URI.

v0.0.10

  • New: Added @PageEvent methods allowing URIs to be mapped to page methods.
  • New: Added @PageContext fields that can replace @InitRender methods.
  • New: Added the PageMeta class to wrap up, um, page meta data! Oh, and attached an instance to the rendering pages. (Deleted RenderPageMeta.)
  • New: Added PageMetaResponseProcessor that renders Pillow pages when PageMeta instances are returned as BedSheet response objects.
  • New: Added httpMethod field to @Page and @PageEvent
  • New: Added template uri to @Page
  • Chg: Page is now a facet, incorporating fields from @PageUri and @PageContentType (which have now been deleted).

v0.0.8

  • New: Use the @PageContentType facet to explicitly define the content type for your page.
  • New: Use a double extension (e.g. indexPage.xhtml.slim) to set the content type for the page.
  • Bug: @InitRender params could incorrectly match for directory index pages.

v0.0.6

  • New: Page uris and BedSheet routes are generated from the @InitRender method signature.
  • New: Directory uris may now serve welcome pages.
  • Chg: Updated to use BedSheet 1.2.
  • Chg: Renamed project to afPillow (from afBedSheetEfanExtra).
  • Chg: Reanmed EfanPageMeta to RenderPageMeta.
  • Chg: Renamed PageRoute to PageUri.

v0.0.4

  • New: Added @PageRoute facet that lets you specify a bespoke uri
  • New: Added EfanPageMeta which returns the active rendering page.

v0.0.2

  • New: Preview Release