Pillow can be thought of as the middleman between BedSheet and efanXtra. This article shows how the Bed Nap application can use it to automatically route request URLs to its efanXtra components.

Re-Route Index Page

A Pillow page is any efanXtra component that has been annotated with the @Page facet.

Upon startup Pillow scans your BedSheet application for pages and automatically sets up BedSheet Routes based on their class name.

So let's convert IndexPage to a Pillow page, it's as simple as adding a Page facet. But first download afPillow and add it as dependency:

fanr install -r http://eggbox.fantomfactory.org/fanr "afPillow 1.1.2 - 1.1"

Then update IndexPage:

using afIoc::Inject
using afEfanXtra::EfanComponent
using afPillow::Page

@Page
const mixin IndexPage : EfanComponent {
    @Inject abstract VisitService visitService
}

This is enough for Pillow to automatically route all requests to the URL /index, render the IndexPage as an efanXtra component and return the HTML to the browser.

Why the URL /index? Because Pillow looks at the class name Index and is clever enough to knock off the surplus Page suffix. (Note page classes and templates do not have to end in Page.) This then gives /index.

Welcome Pages

Does Pillow really route the URL /index to IndexPage? It could do, but actually, this happens:

/index --> Redirects to /
/      --> Renders IndexPage

This is due to the default Welcome Page Strategy. Welcome pages, also known as home pages or directory pages, are pages that are served when a directory URL is requested; that is, any URL that ends with a /.

Index is the default Welcome Page Name meaning when a directory URL is requested, Pillow internally adds Index to the URL and renders that page. Hence the URL / renders the IndexPage page.

But people and browsers will often add index, or index.html to directory URLs due to convention. So Pillow helpfully redirects these URLs to the shorter directory URL notation. This strategy is called onWithRedirects and is the default Welcome Page Strategy, others are available.

Re-Route View Page

Let's convert ViewPage into a Pillow page. Again, just add the @Page facet:

select all
using afEfanXtra::InitRender
using afEfanXtra::EfanComponent
using afPillow::Page

@Page
const mixin ViewPage : EfanComponent {
    abstract Visit visit

    @InitRender
    Void initRender(Visit visit) {
        this.visit = visit
    }
}

Note that the initRender() method is still there... that's because Pillow has a neat trick; it inspects the initRender() method (if there is one) and maps request URL segments to it!

Pillow looks at the above page and, because of the class name, maps it to /view. It then looks at the initRender() method, sees the one method parameter, and further maps it to /view/**.

Visit is said to be the context of the page, because it changes the context of what ViewPage renders. All we do with the visit is set it to a field so it can be used by the template.

Because this is such a widely used pattern (using the initRender() to set a page context field) Pillow has a shortcut for it, just annotate the field with @PageContext:

using afEfanXtra::InitRender
using afEfanXtra::EfanComponent
using afPillow::Page
using afPillow::PageContext

@Page
const mixin ViewPage : EfanComponent {
    @PageContext abstract Visit visit
}

Run It

Let's delete some now redundant code:

  • Delete the PageRoutes.fan class.
  • In AppModule:
    • Delete contribueRoutes()
    • Delete contributeEfanLibs()

And all our tests should still pass!

Bed Nap Test All

So lets start up the web application and try it out for real:

Bed Nap Test All

Err... what's that!?

Let's take closer look at the HTTP response headers:

select all
HTTP/1.1 200 OK
Content-Type: text/plain
Cache-Control: max-age=0, no-cache
Connection: keep-alive
Content-Length: 312
X-afPillow-renderedPage: bednap::IndexPage
Server: Wisp/1.0.66
Content-Encoding: gzip
Date: Mon, 01 Sep 2014 19:08:54 GMT
Vary: Accept-Encoding

The cause of our web page being rendered as plain text is the line:

Content-Type: text/plain

That's because Pillow doesn't know what kind of markup our template is generating. D'Oh!

So how do we tell it that our pages render HTML and not plain text?

Content Type

To tell Pillow what content the template markup represents we could set it directly with @Page.contentType, or...

We could rename our template files to have a double suffix / extension:

IndexPage.html.efan
ViewPage.html.efan

By doing this Pillow converts the inner extension .html to a MimeType and uses this as the returned Content-Type.

Try it. Rename the template files, re-build the application and refresh the browser. You should see rendered HTML again.

Tidy Up

Time to make our web app work better.

Page URLs

In IndexPage.html.efan we link to ViewPage by hard coding the URL to /view/<%= it.id%> which works... but it's very brittle and prone to error.

What if the page URL changed to view/visit/xxx, or the ID encoding changed to base64, or what if the app is run under a non-root WebMod (causing all the app URLs to have an extra path prefix), or what if...?

Problems.

Wouldn't it be great if we could just auto-generate these page URLs?

The good news is we can! We can use Pillow's Pages service to get a PageMeta instance for any given page and context. The PageMeta.pageUrl() method then returns the URL we want.

IndexPage.fan:

select all
using afIoc::Inject
using afEfanXtra::EfanComponent
using afPillow::Pages
using afPillow::Page

@Page
const mixin IndexPage : EfanComponent {
    @Inject abstract VisitService    visitService
    @Inject abstract Pages           pages

    Uri viewUrl(Visit visit) {
        pages.pageMeta(ViewPage#, [visit]).pageUrl
    }
}

IndexPage.html.efan:

...
    <td><a href="<%= viewUrl(it).encode %>" class="t-view">view</a></td>
...

The visit object is encoded via the VisitEncoder class that we wrote all the way back in IDs and ValueEncoders. That's good, for that means VisitEncoder is now used to both encode and decode our Visit objects to and from URL notation.

Let's do the same for ViewPage linking back to IndexPage.

IndexPage.fan:

select all
using afIoc::Inject
using afEfanXtra::InitRender
using afEfanXtra::EfanComponent
using afPillow::Page
using afPillow::Pages
using afPillow::PageContext

@Page
const mixin ViewPage : EfanComponent {
    @PageContext    abstract Visit    visit
    @Inject         abstract Pages    pages
}

IndexPage.html.efan:

...
    <div><a href="<%= pages[IndexPage#].pageUrl.encode %>">&lt; Back</a></div>
...

No need to create an extra method for that one, we just call the service direct from the template.

The nice thing about the IndexPage URL is that Pillow knows it's a Welcome Page (and the strategy is enabled) so the URL is returned as the abbreviated / and not the longer /index - which would lead to a redirect.

Testing which Page was Rendered

Looking back to the list of HTTP response headers you may have noticed this:

X-afPillow-renderedPage: bednap::IndexPage

When BedSheet is not in production mode and Pillow returns a page that it has rendered, it sets a X-afPillow-renderedPage response header. Tests can make use of this to ensure that a request URL renders the intended page.

In a Bounce test there is nothing more annoying than wasting time chasing up an Element not found error only to find it's because the wrong page was rendered! This often happens when either an error page is rendered, or a form validation error re-renders the same page and not the success page.

To get the Type of the rendered page you could add this method to WebTest:

Type renderedPage() {
    Type.find(client.lastResponse.headers["X-afPillow-renderedPage"])
}

It's then easy enough to assert that renderedPage() is equal to the expected page.

Source Code

All the source code for this tutorial is available on the Bed Nap Tutorial Bitbucket Repository.

Code for this particular article is available on the 08-Page-Routing-With-Pillow branch.

Use the following commands to check the code out locally:

C:\> hg clone https://bitbucket.org/fantomfactory/bed-nap-tutorial
C:\> cd bed-nap-tutorial
C:\> hg update 08-Page-Routing-With-Pillow

Don't forget, you can trial the finished tutorial application at Bed Nap.

Have fun!

Edits

  • 8 Aug 2016 - Updated tutorial to use BedSheet 1.5 & IoC 3.0.
  • 8 Aug 2015 - Updated tutorial to use BedSheet 1.4.
  • 4 Sep 2014 - Original article.


Discuss