3.0.0-SNAPSHOT

Layouts

Wheels allows you to create layouts so that you don't need to header and footer code on every single view template.

Introduction

As a red-blooded CFML developer, you're used to creating include files like header.cfm and footer.cfm, and then using <cfinclude> on every single page to include them. The popular way to do it looks something like this:

<cfinclude template="/includes/header.cfm">

<p>Some page content</p>

<cfinclude template="/includes/footer.cfm">

Does that mean that you should <cfinclude> your headers and footers in every view in your Wheels app? Heck no! If the structure of your pages ever changed, you would need to edit every single page on your site to make the fix.

Layouts to the rescue!

Implementing a Layout

In your Wheels installation, layout files are stored either directly in the app/views folder or contained in one of the controllers' view folders. Let's go over how layouts work, starting with the simplest way and then moving on to more complex setups.

Let's say that you want to define one layout to be used by every view in your application. You would accomplish this by editing the default layout. The default layout is the app/views/layout.cfm file. In a fresh install of Wheels, you'll notice that it only contains a few lines of code:

<html>
    <body>
        <cfoutput>#includeContent()#</cfoutput>
    </body>
</html>

The call to includeContent() Simple Example represents the output of your page generated by your view files. Whatever code you put before this snippet will be run before the view. Similarly, whatever code you put after the snippet will be run afterward

Simple Example

For most purposes, this means that you could write code for your page header before the snippet, and write code for the footer after. Here is a simple example of wrapping your view's content with a header and footer.

<html>
<head>
<title><cfoutput>#title#</cfoutput></title>
</head>

<body>

<div id="container">
    <div id="navigation">
        <ul>
            <cfoutput>
            <li>#linkTo(text="Home", route="main")#</li>
            <li>#linkTo(text="About Us", route="about")#</li>
            <li>#linkTo(text="Contact Us", route="contact")#</li>
            </cfoutput>
        </ul>
    </div>
    <div id="content">
        <cfoutput>#includeContent()#</cfoutput>
    </div>
</div>

</body>
</html>

As you can see, we just wrote code that wraps every view's content with the layout. Pretty cool, huh?

Use of Variables in the Layout

Just like views in Wheels, any variable declared by your application's controller can be used within your layouts. In addition to that, any variables that you set in view templates are accessible to the layouts as well.

Notice in the above code example that there is a variable called title being output in between the <title> tags. This would require that any controller or view using this particular layout would need to set a variable named title.

To help document this, you can use <cfparam> tags at the top of your layout files. That way any developer using your layout in the future could easily see which variables need to be set by the controller.

Here's an example:

<!--- Title is required --->
<cfparam name="title" type="string">

<cfoutput>

<html>
<head>
<title>#title#</title>
</head>

<body>

<!--- View's Content --->
<h1>#title#</h1>
#contentForLayout()#

</body>
</html>

</cfoutput>

There's also a different way to set variables that goes hand in hand with the includeContent() function that you may prefer. It's the contentFor() function. We'll dig into how that one works later in this chapter.

The Default Layout

One layout file that is already created for you is app/views/layout.cfm. Think of it as the default layout to be used by any given controller.

If you're writing code for a controller called press and there is no layout created for press, Wheels will automatically use app/views/layout.cfm as the layout.

If you implement a layout for the press controller, then that layout will be used instead of the default layout.

So, how exactly do you implement a layout meant specifically for just one controller? Well, that's next...

Overriding the Default Layout with a Controller-Specific Layout

Let's pretend that you want to create a layout to be used only in a controller called blog. To do this, you would simply create the layout and save it as app/views/blog/layout.cfm.

As you can see, the convention is to place your layout file together with the other view files for the controller.

Overriding the Default Layout Using the usesLayout() Function

However, if you need to override the name of the layout file or its location in the folder structure, you can specify what layout file to use with the usesLayout() function in the controller's init function instead.

function config(){
  usesLayout("blogLayoutOne");
}

With that code placed in the app/controllers/Blog.cfc file, all actions will now wrap their contents with the bloglayoutone.cfm file instead of the layout.cfm file.

The usesLayout() function also accepts except, only, and useDefault arguments for further customization.

function config(){
  usesLayout("blogLayoutOne", except="home");
}

That code tells Wheels to apply the blogLayoutOne layout for any actions in this controller except for the homeaction. In the case of the home action, it will fall back to the default behavior (i.e. using the app/views/blog/layout.cfmfile).

If for some reason you do not want the default behavior to be used when conditions aren't met, you can set the useDefault argument to false.

function config(){
  usesLayout(name="blogLayoutOne", except="home", useDefault=false);
}

You can even instruct Wheels to run a specific function that will determine the layout handling.

function config(){
  usesLayout("resolveLayout");
}

function resolveLayout(){
    switch(chapter){
    case "index":
    break;
        return "index_layout";
    case "show":
      return "show_layout";
    break;
  }
}

It's worth repeating here that everything inside the controller's config() function runs only once per application and controller. This is why you can't perform the logic that decides which layout to use directly inside the config()function itself. Instead, you tell Wheels to always run the resolveLayout function on each incoming request.

Overriding the Default Layout at the Action Level

Another option for overriding layouts is to use the layout argument of the renderView() function.

As you may already know, Wheels's renderView() function is the last thing called in your actions. This function is run automatically by Wheels, so most of the time, you won't need to call it explicitly in your code.

Take a look at this example action, called display:

function display(){
  renderView(layout="visitorLayout");
}

This assumes that you want for your action to use the layout stored at app/views/blog/visitorlayout.cfm.

The default behavior for the layout argument is to look for the file in the current controller's view folder, so here we're still assuming that the display action is in the blog controller. The .cfm extension will also be added automatically by Wheels, so you don't need to specifically include that part.

If you want Wheels to locate the file in a different folder, you can start the layout argument with a forward slash, /. By doing this, Wheels will know you want to start the search in the app/views folder. Let's say that you're storing all miscellaneous layouts in its own folder, simply called layouts. You would display one of them with the following code:

function display(){
  renderView(layout="/layouts/plain");
}

Note that setting the layout argument on renderView() will override any settings you may have made with the usesLayout() function. This gives you finer-grained control.

Using No Layout

If you don't want for a given template to be wrapped by a layout at all, you may want to consider creating the page as a partial. See the chapter about Partials for more information.

Another alternative is to use the renderView() function and set the layout argument to false.

You can also create a separate layout that only contains the call to the includeContent() function in it and reference it as described above in Using a Different Layout. This may end up a little ugly though if you start getting a lot of small identical files like this, but the option is there for you at least.

Lastly, if your view needs to return XML code or other data for JavaScript calls, then you should reference the renderNothing() and renderText() functions to see which would be best used by your action.

Nested/Inherited Layouts

Like many templating languages, Wheels offers the ability to create layout files that can be "inherited" by other layout files. The end goal is to create a "parent" layout that has missing sections intended to be filled in by "child" layouts.

Wheels's approach involves the use of includeLayout() and the contentFor() function that we mentioned briefly earlier in this chapter.

The includeLayout() function simply includes another layout file. The common usage for this is to include a parent layout from a child layout.

So what about the contentFor() function? Well, as you may recall the code in the default layout file in Wheels contains this:

<cfoutput>#includeContent()#</cfoutput>

The includeContent() function accepts a name argument. The reason we don't have to use it above is because it defaults to body. This body variable has been set internally in the Wheels framework code with the use of the contentFor() function.

The point I'm trying to make here is that we can use this same functionality to set any type of content for, as an example, sections in our layout files. So, armed with this new knowledge, let's create some nested layout awesomeness.

Say we have a global layout and want to fill out content in it from controller specific layouts, here's how we can do it.

The child layout:

contentFor(pageTitle="My custom title");

<cfoutput>#includeLayout("layout")#</cfoutput>

Note: If your parent file is one of the default ones named layout.cfm you can actually remove the "layout" string above since the default for includeLayout() is layout anyway. We're just including it here for completeness and to show that you can of course achieve this regardless of what your parent layout file is named.

The parent layout:

<html>
    <head>
        <title><cfoutput>#includeContent("pageTitle")#</cfoutput></title>
    </head>
    <body>
        <cfoutput>#includeContent("body")#</cfoutput>
    </body>
</html>

Similar to above, you can remove "body" because that is the default on the includeContent() function.

That was a fairly basic example of how you can achieve nested layouts in Wheels to DRY up your code. You can of course expand on this by having entire sections of HTML (like a sub menu for example) be created by the child layouts. Also, as a reminder, don't forget that you can use includePartial() from inside your layout files as well to further keep things DRY.

Layouts for Emails and Partials

Besides having layouts for view pages in Wheels, you can also have them on emails that you send out and partial files that you include. We have chosen to speak about these in their respective chapters though: Sending Email and Partials.

Last updated

Logo