cfWheels Relationships (Part 5)

cfwheelsheaderHasManycheckbox() is obviously only one way of updating these relationships. The other main way is via cunning uses of includePartial().

Let’s return to our contact’s email addresses – remember the contact ‘hasMany’ of those. We need a way to a) list their existing email addresses, b) add new ones (preferably within the same update form as the main contact details and c) edit or delete existing addresses.

When you need to display/update more information that what a checkbox could hold, includePartial() comes to the rescue. Whilst I’m demonstrating this with the email address field, which just has a single value, there’s nothing stopping you adding additional fields for this example.

In our Contact’s edit form, we can simply do this:

<fieldset>
<legend>Email</legend>
    #includePartial(contact.emailaddresses)#
    #includePartial("emailaddressNew")#     
</fieldset>

And then create a partial called _emailaddress.cfm in the /views/contacts/ folder, containing:

<cfoutput>
#btextField(
    objectName="contact",
    association="emailaddresses",
    position=arguments.current,
    property="email",
    label="Email Address",
    class="span4"
)#
</cfoutput>

This should look pretty familiar – after all, it’s *almost* the same as a bog standard textField on a model, except we’re listing the association, and crucially, the postition.

So if we’ve got multiple email addresses, what does the generated HTML look like?

<label class="control-label" for="contact-emailaddresses-2-email">Email Address</label>
<input class="span4" id="contact-emailaddresses-2-email" maxlength="500" name="contact[emailaddresses][2][email]" value="joe3@bloggers.com" type="text">
<label class="control-label" for="contact-emailaddresses-3-email">Email Address</label>
<input class="span4" id="contact-emailaddresses-3-email" maxlength="500" name="contact[emailaddresses][3][email]" value="joe2@bloggers.com" type="text">

The [emailaddresses][3][email] part of the naming convention translates to “object[association][id][property]“.

And when submitted, with params.contact, we can see a nested struct:

If we’ve included the association when we do the contact updating, i.e (contact.update(params.contact);) these properties will automatically update, with no extra code.

That’s all good for existing entries, but what about adding new ones? Wouldn’t it be nice if we could just “reinclude” that partial to add a new form? If you try that, you’ll hit the ‘arguments.position’ error – as that partial is expected a numeric value for the position in the array that the property is in. Even if you try and set a tickCount as a unique identifier, you’ll hit snags too. This is probably the most annoying thing (currently, i.e wheels 1.1.8) – if someone’s got an elegant solution to this, please let me know :) At the moment I fall back to either adding via a separate params struct, and testing for it’s existence, or doing it via Javascript.

Right, that’s all for now – thoughts on improving this greatly recieved.

cfWheels Relationships (part 4)

cfwheelsheaderSo far we’ve setup the database tables and the models, so now we need to start thinking about getting and updating this contacts data, including the associated nested properties.

Outputting a simple add/edit form should hopefully be familiar to you by now (if not I recommend checking out the cfwheels screencasts: http://cfwheels.org/screencasts ). When updating our nested properties, there are several methods, each with pros & cons.

Firstly, let’s have a look at HasManyCheckBox();

The hasManyCheckbox() helper is really, really useful. I’m going to use our technologies relationship, as this is just the sort of data which is designed to be displayed/updated in this fashion.


#hasManyCheckBox(
    objectName="Contact",
    association="contactTechnologies",
        keys="#contact.key()#,#id#",
        label=name)#

Now what does this actually do? The loop itself should be obvious – we’re looping over *all* the potential values for technologies – after all, we want to be able to list those entries which aren’t ticked too: but we need to be able to make sure that the entries which are present in the join table are properly ticked. ObjectName=”Contact” is our parent object – the one which holds the nested properties. Association is the name of the nested property itself. Next, you have the most important part, the composite key. Remember when I mentioned about the column key order in the database construction for our join table? *This* is why it’s important: the order of the keys here *MUST* reflect the order in the database schema. I’ve used contact.key() which allows me to do this on a new model (which won’t actually have a primary key per se – yet).

So what does this wonderful piece of code actually output?

<label class="checkbox" for="Contact-contactTechnologies-1-1-_delete">ColdFusion<input id="Contact-contactTechnologies-1-1-_delete" type="checkbox" name="Contact[contactTechnologies][1,1][_delete]" value="0" checked="checked" /><input id="Contact-contactTechnologies-1-1-_delete-checkbox" type="hidden" name="Contact[contactTechnologies][1,1][_delete]($checkbox)" value="1" /></label>

<label class="checkbox" for="Contact-contactTechnologies-1-4-_delete">CSS<input id="Contact-contactTechnologies-1-4-_delete" type="checkbox" name="Contact[contactTechnologies][1,4][_delete]" value="0" /><input id="Contact-contactTechnologies-1-4-_delete-checkbox" type="hidden" name="Contact[contactTechnologies][1,4][_delete]($checkbox)" value="1" /></label>

<label class="checkbox" for="Contact-contactTechnologies-1-5-_delete">HTML5<input id="Contact-contactTechnologies-1-5-_delete" type="checkbox" name="Contact[contactTechnologies][1,5][_delete]" value="0" /><input id="Contact-contactTechnologies-1-5-_delete-checkbox" type="hidden" name="Contact[contactTechnologies][1,5][_delete]($checkbox)" value="1" /></label>

<label class="checkbox" for="Contact-contactTechnologies-1-6-_delete">jQuery<input id="Contact-contactTechnologies-1-6-_delete" type="checkbox" name="Contact[contactTechnologies][1,6][_delete]" value="0" /><input id="Contact-contactTechnologies-1-6-_delete-checkbox" type="hidden" name="Contact[contactTechnologies][1,6][_delete]($checkbox)" value="1" /></label>

etc. As you should be able to see from the above, there’s actually some fairly complex stuff going on: imagine it as cfWheels by default adding all the associations & relationships, then deleting all the relationships, then by checking a checkbox, you save that association from being deleted. Looking at the dumped params struct makes this more obvious:

The ’1,1′, ’1,2′ struct keys are the composite keys: 1 representing the contactid, and the second representing the technology (in this case). If the checkbox is checked, then the _DELETE sub struct returns false.

What’s nice about this approach is that you don’t have to do anything extra; when submitting and creating the final contact object, you just have to make sure you include the associations.

‘Why that’s awesome!’ I hear you say. Yes, yes it is. BUT. It doesn’t help us in a few areas.

1) We can’t add new values to the list easily – i.e I can’t add a new technology
2) We can’t edit the associated properties easily – i.e I can’t edit an existing technology

Of those two points, I would argue editing existing properties from within the contact update/add form is perhaps not the place for it – these properties could apply to other contacts, so pushing them out into their own form is probably better from a user perspective – there’s then no chance of the user thinking ‘oh I’ll just change ColdFusion to Railo’ and then the horror when all other contacts then list Railo too.

Adding new values to the technology list is however something I see happening. To do this, I’ve found the easiest solution is to just create a new form field tag, and then test for the values when updating the contact object.

This means when we submit our update or create request, we need to test for the existence of the field, return the created key, and then add the association manually.

I’ve found the easiest way to do this (so far) is to add it to a new private function: So our update function calls our new checking function ‘checkForNewTechnologies()’:

contact.update(params.contact);
	if(contact.hasErrors()){
         renderPage(action="edit");
	}
        else {
	checkForNewTechnologies();
	 flashInsert(success="The contact was updated successfully.");
	 redirectTo(action="view", key=contact.id);
}

Which looks like:

<cffunction name="checkForNewTechnologies" access="private">
<cfscript>
  if(structkeyexists(params, "newtechnology") AND len(params.newtechnology) GT 2) {
		nTechnology=model("technology").create(
			name=params.newtechnology
		  );
		nTechnologyAssociation=model("contacttechnology").create(
			contactid=contact.key(), technologyid=nTechnology.key()
		 );
  }
</cfscript>
</cffunction>

Obviously, this is just one way of many to do it. You could add the new technology via Ajax, return the generated ID, then replicate the form HTML via JS to pass the new value in along with the other checkboxes: this is quite a cumbersome technique as you rely on recreating the wheels generated HTML manually, but might have it’s uses.

Incoming search terms:

  • cfwheels one to one relation

cfWheels Relationships (part 2)

cfwheelsheaderNow it’s just a matter of representing these relationships in cfWheels so we can create/update/delete some data.

models/Contact.cfc

<cfcomponent extends="Model" output="false">
    <cffunction name="init">
		<cfscript>
                // Model init functions
		property(name="firstname", label="First Name");
		property(name="lastname", label="Last Name");

		//Calculated Properties
		property(name="fullname", sql="CONCAT(firstname, ' ', lastname)");

		// Relationships
		hasMany(name="emailaddresses", dependent="deleteAll");
		hasMany(name="contacttechnologies", dependent="deleteAll");
		hasMany(name="contactcompanies", dependent="deleteAll");

		// Nested properties
		nestedProperties(associations="
                      emailaddresses,
                      contacttechnologies,
                      contactcompanies", allowDelete=true);

        </cfscript>
    </cffunction>
</cfcomponent>

So I’ve done a couple of things here worthy of note: I’ve set some default values for the firstname and lastname properties – namely the label name: I want this to display as ‘First Name’ rather than the default ‘firstname’ which is what the label=”" attribute is for. I’ve also added a very simple calculated property, of fullname, which takes the first and last names and puts them together at a database level. You can imagine how this can become very useful indeed – if I had prefix, suffix, middlename columns, I could build up various additional properties using those too.

Lastly, you’ll see the relationships & nested properties; emailaddresses has the ‘dependent=”deleteAll” attribute, which means when I delete a contact, the dependent email addresses will also be deleted. As the other relationships are also dependent to that contact, they’ll be deleted too – but bear in mind, this won’t delete the ‘source’ data, i.e the technologies or companies tables which is crucial as these are being used by other entities in the application. The nested properties call allows us to literally ‘nest’ models *within* the contacts model. More on this later.

models/Emailaddress.cfc

<cfcomponent extends="Model" output="false">
    <cffunction name="init">
		<cfscript>
        // Model init functions
		 belongsTo("contact");
        </cfscript>
    </cffunction>
</cfcomponent>

As this is our simplest relationship, an Email address is very easily represented with a simple ‘belongsTo’ call.

models/Technology.cfc

<cfcomponent extends="Model" output="false">
    <cffunction name="init">
		<cfscript>
		// Relationships
		hasMany(name="contacttechnologies");
        </cfscript>
    </cffunction>
</cfcomponent>

As this is ‘on the other end’ of the join for contact-technologies, we need to refer the technology model back to the join model.

models/Technology.cfc

<cfcomponent extends="Model" output="false">
    <cffunction name="init">
		<cfscript>
       	// Relationships
		belongsTo(name="contact");
		belongsTo(name="technology");
        </cfscript>
    </cffunction>
</cfcomponent>

And here is the join model – it ‘belongsTo’ both contact.cfc and technology.cfc and allows us to join the two.

models/Company.cfc

<cfcomponent extends="Model" output="false">
    <cffunction name="init">
		<cfscript>
		// Relationships
		hasMany(name="contactcompanies");
        </cfscript>
    </cffunction>
</cfcomponent>

models/Contactcompany.cfc

<cfcomponent extends="Model" output="false">
    <cffunction name="init">
		<cfscript>
       	// Relationships
		belongsTo(name="contact");
		belongsTo(name="company");
        </cfscript>
    </cffunction>
</cfcomponent>

Lastly, our company and contactcompany models.

Visualising the Models

I tend to find being able to visualise the data structure more useful than relying solely on code. What do these models look like if we just create a new one?

Let’s take the Contacts model, and simply call:

contact=model("contact").new();

The result?

As expected, but… what about our email addresses?  How do we get them into our nice new contact call?

We need to create a new instance of the emailaddress model, and pass it to the .new() call as a named argument. As there may be more than one email address, we need to put our email address model within an array, so cfWheels can keep track of multiple nested properties.

If we try:

newEmailaddress[1]=model("emailaddress").new();
contact=model("contact").new(emailaddresses=newEmailaddress);

We get:

So we now have an (empty) nested email address model within the new contact. Handy.

We can also extend this concept for the other relationships:

newEmailaddress[1]=model("emailaddress").new();
newContactCompany[1]=model("contactcompany").new();
newContactTechnology[1]=model("contacttechnology").new();

contact=model("contact").new(
	emailaddresses=newEmailaddress,
	contactCompanies=newContactCompany,
	contactTechnologies=newContactTechnology
);

Well, this is all very well, but what do these models look like with some data in them?

Incoming search terms:

  • cfscript cfcomponent
  • cfwheels deleteall where nested
  • cfwheels hasmany
  • relationships

cfWheels Relationships (part 3)

cfwheelsheaderLet’s look at an example call to a contact, but without any of it’s relationships.

contact=model("contact").findOne(where="id=1");

So, the simplest possible call – find me one contact row where id = 1.

Well, it’s good to see our createdAt field got autofilled, and our calculated property of fullname made it through too. deletedAt is null, which is also good. The above call would return false if deletedAt contained a value.

Let’s do the same call, but get those email addresses:

contact=model("contact").findOne(where="id=1", include="emailaddresses");

That’s better! Incidentally, if there were no email addresses, we’d get an empty array like so:

Let’s extend this to the other relationships:

contact=model("contact").findOne(where="id=1",
		include="emailaddresses,contactCompanies,contactTechnologies");

So here we can see the join table relationship nested properties too. Now, here’s the catch. How do we get the data from the otherside of the join?

We can’t do this, as we’re returning a single entity with objects:

contact=model("contact").findOne(where="id=1",
include="emailaddresses,contactCompanies(company),contactTechnologies(technology);

We *can* do it if we return this record as a query (which is the default for a ‘findAll’ call):

contact=model("contact").findAll(where="id=1",
include="emailaddresses,contactCompanies(company),contactTechnologies(technology),
returnAs="query");

However, whether you actually want to do this will be dependent on your application. Sometimes, this will make perfect sense, especially if you’re calling data in any sort of tabular fashion. But if you’re calling your model in order to populate a form (for example) then this complicates matters. More on ‘hasManyCheckBox’ and associated solutions later.

Incoming search terms:

  • cfwheels relationships part 3

cfWheels Relationships (part 1)

cfwheelsheaderNo, this isn’t going to be a guide about getting on with your loved one (unless of course, your loved one is a certain open source coldFusion framework) but will specifically look at all the different types of relationships in cfWheels, and what they mean in terms of real world application design. I’m going to assume you have a working knowledge of cfWheels (objects/models/controllers/views/routes etc) as the documentation over at cfwheels.org is very good, and well worth digging into.

For this series, I’m going to take the old default example application of a ‘people’ database (or in this context, a contacts database).

Normalising Data

The old addage ‘DRY’ (or Don’t Repeat Yourself) applies just as much to your data, as it does to your controller & view code. I don’t consider myself a DBA by any stretch of the imagination, but it simply makes sense – don’t duplicate data (except in extreme circumstances – there I said it). With that in mind, let’s have a think about what this application should do, and how this might reflect our model conventions and model relationships.

The Contact model:

Well, at it’s core, a contacts directory needs, yes, you guessed it, a list of people. Let’s call them ‘Contacts’. At a mimimum, we’re going to need a contact.cfc model. This should contain any information which is unique to a contact, and has one value.

The table ‘contacts’ might look like this:

contacts
—————-
id [key, int, autoincrement]
firstname [varchar, 255]
lastname [varchar, 255]
createdAt [datetime]
updatedAt [datetime]
deletedAt [datetime]

Pretty simple – an id to reference with, firstname & lastname fields which we can add a calculated property to later to get a ‘fullname’ value, and three cfWheels specific fields, which will auto update – when it’s created, updated or deleted. Naturally, our id field will need to autoincrement, and be an integer.

So what about that contact’s data? Let’s assume we want to store the following data about this contact:

  • One or more email addresses, which are specific to that contact. This is a ‘one to many‘ relationship – one contact, multiple email addresses. It could be said a contact ‘hasMany‘ email addresses, and that those email addresses ‘belongTo‘ that contact.
  • A set of related technologies such as ‘mysql’, ‘php’, ‘coldFusion’, ‘cfWheels’. A contact might have many of these, and likewise, ‘mysql’ might need to refer to multiple contacts. So whilst a contact ‘hasMany’ technologies, they’re not exclusive to that contact.
  • A company relationship, such as ‘Adobe’, ‘Microsoft’, ‘Apple’ – we’d want to reuse these companies for other contacts too. Companies might also contain additional data such a postal address. We’d also want to store data about that relationship to that company – i.e a contacts postition, and be able to easily retrieve data about that company when looking at a contact.

Let’s take all these in turn, as each presents different challenges when referring back to our contact.

One or more email addresses: So a new table is called for to store our email addresses.

emailaddresses
——————-
id [key, int, autoincrement]
email [varchar, 255]
createdAt [datetime]
contactid [int]

As before, an id to reference with, an email column to store the actual value, and a createdAt so we know when it was added. To me, this data is less important to keep track of in terms of audit, so I’m ignoring the updatedAt/deletedAt fields. The contactid field here is the (excuse the pun) ‘key’ to the whole excercise. This ties in the email address to the contact, after we setup our model relationships later. This is probably the simplest relation type in this example.

A set of related technologies: We’ll need a table to store our ‘technologies’ – this will be a very simple table.

technologies
—————
id [key, int, autoincrement]
name [varchar, 255]

That’s all we need to store this, it’s essentially a glorified list of categories. But how do we store which contact has a technology, and which technologies refer to what contacts? We need a join table, which will simply be the referring id’s of the contact and the technology.

contacttechnologies
—————
contactid [key, int]
technologyid [key, int]

Note how I’ve named the table – and also the order of the columns. As “contact” has come first in the column name, I’ve made sure the column order reflects this – the reason why becomes obvious later on. As both columns are keys, this means we can’t have any duplicate rows – that’s a good thing – we don’t want random duplicate relationships popping up anywhere.

So this is what it looks like so far:

Company relationships

Our company relationships mean we need two things – one, a place to store our companies, and two, a join table to store the relationship in the same fashion as contacts – technologies as before.

companies (this is the same table structure as contacts for this example)
—————-
id [key, int, autoincrement]
name [varchar, 255]
createdAt [datetime]
updatedAt [datetime]
deletedAt [datetime]

contactcompanies
—————
contactid [key, int]
companyid [key, int]

This pattern should start to look a little familiar – it’s basically the same as the email address – contact relationship.

What does our db structure look like now?

Next, we’re going to look at representing these relationships within cfWheels.

Build your own interactive map

The Oxford Internet Institute have been playing around with their own Visualization Library which is out there as open source. One of the things I’ve been doing recently is building the back end to a customiser, which takes your data and preferences and gives you a nicely compiled version.

The back end is written in bespoke ColdFusion, but am in the process of moving it over to cfWheels – having a framework with good documentation really helps when there’s more than one person working on it!

Anyway, it’s now in Beta, so give it a shot – http://api.oii.ox.ac.uk/InteractiveVisBuilder/ – as always, any feedback greatly appreciated!

Fun with isotope.js

Just released this – http://www.oxfordmartin.ox.ac.uk/labs/bigquestions – which uses the fab isotope.js library. A bit of fun / experiment – cfWheels powering the back end for that whole site; was stupidly easy to get wheels to return HTML via ajax to populate the grid.

Go have a play – some interesting videos there too.

Incoming search terms:

  • oxford internet institute coldfusion
  • #contactemail# meaning in coldfusion
  • isotopejs

Creating an Ajax mySQL Full Text Search in cfWheels

I’m quite a fan of mySQL full text search – don’t get me wrong, it’s not exactly Google, and it has some annoying limitations, but for a quick and easy search function, it’s not half bad. It’s particularly useful for intranets, or just listings in admin section when you might have to delve through a few thousand records otherwise.

This example uses cfWheels, and most importantly, the cfWheels plugin Remote Form Helper.

Step One: Set up an index on your Table in MySQL.

Note: you can only do this on myISAM tables, not innodb.
I usually do this is navicat, where it’s very painless to do indeed. (simply right click a table, select Design table, then select indexes: give the index a name, and select the columns in the table you want to search: make sure the index type is set to fulltext).

For the ‘navicat-less’, try something like this:

CREATE TABLE `articles` 
(
  `id` int(11) NOT NULL default '0',
  `title` varchar(125) default NULL,
  `topic` varchar(25) NOT NULL default '',
  `author` varchar(25) NOT NULL default '',
  `createdAt` datetime NOT NULL default '0000-00-00 00:00:00',
  `body` text NOT NULL,
);

Then add the index:

ALTER TABLE articles ADD FULLTEXT(title,body);

The attributes in the Fulltext() function are the column names we wish to search (I believe there’s a limit of 16 columns).

In your mySQL conf file, add this line under the [mysqld] section:
ft_min_word_len=3

This sets mySQL to search for words of 3 characters or more, rather than the annoying default of 4.

Step 2: cfWheels setup
Download and install/activate/configure the Remote Form Helper plugin.
Make sure you remember to add the line:

addFormat(extension="js", mimeType="text/javascript");

To your config/settings.cfm file as mentioned in the plugin documentation, and also include the wheel.jquery.js file distributed with the plugin. (oh and jQuery itself, obviously).

Step 3: Search Views & Controllers

We need to create at least three files.

Firstly, our controller, /controllers/Search.cfc:

<cfcomponent extends="controller">
<cffunction name="init">
<cfscript>
// This provides bit is essential!
 provides("html, json, js"); 
</cfscript>
</cffunction>

<cffunction name="q" hint="The Main Search Router">
<cfscript>
// If search terms is incoming, and is an ajax request
if(structkeyexists(params, 's') AND len(params.s) GTE 3 AND isAjax())
{   
// searchArticles() function returns our HTML directly to the data attribute 
renderWith(data=searchArticles(params.s), template="articles");
}
else
{ renderNothing() };
</cfscript> 
</cffunction>

<cffunction name="searchArticles" access="private" hint="Our FullText Search">
 <cfargument name="s" required="yes" type="string" hint="The Search Term">
 <cfquery name="q" datasource="#application.wheels.datasourcename#" maxrows="50">
 SELECT id, title, topic, author, body, createdAt FROM articles
 WHERE MATCH (title,body) AGAINST (<cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.s#">);
 </cfquery>
 <cfloop query="q">
  <cfset q.content[currentrow]=formatResult(string=content, highlightT=arguments.s)>
 </cfloop>
 <cfreturn q />
</cffunction>

<cffunction name="formatResult" access="private" returntype="string" hint="Returns a shortened teaser of the main content, with highlighted search terms">
 <cfargument name="string" type="string" hint="The String to truncate">
 <cfargument name="highlightT" type="string" hint="The term to highlight">
  <cfset var newString="">
  <cfset newString=highlight(text=truncate(stripTags(arguments.string), 400), phrases=arguments.highlightT)>
  <cfreturn newString />
</cffunction>

Next, some view files:

/views/search/index.cfm (where our search form and results output will live)

<cfparam name="params.s" default="">       
<cfoutput> 
<!---Form--->
<!--- NOTE THE remote=true attribute!--->
#startFormTag(controller="search", action="q", id="advancedSearchForm", remote="true")#
#textFieldTag(name="s", placeholder="Search..", label="Search For", value=params.s)#
#submitTag(value="Search")#
#endFormTag()#
<!---Output--->
<div id="results"></div> 
</cfoutput> 

/views/search/articles.js.cfm – This formats our returned dataset for return to the page

<!--- loop over and save the output in a variable--->
<cfsavecontent variable="resultSet">
<cfoutput>
<h2>Articles which match your search:  (#arguments.data.recordcount# results)</h2>
<cfloop query="arguments.data">
<cfoutput>
  <h3>#linkTo(text=title, controller="articles", action="view", key=id)#</h3>
  <h4>#Dateformat(createdAt, 'dd mmm yyyy')#</h4>
  <p>#content#</p>
</cfoutput>
</cfloop>
</cfsavecontent>

<!--- Use the plugin to shove back the results to the page --->
<cfoutput>
#pageReplaceHTML(selector="##results", content=resultSet)#
</cfoutput>

That’s it!
So the form at /views/search/index.cfm posts via ajax to the ‘q’ function in /controllers/Search.cfc.
That q() function returns the html via renderWith(), and the remote form helpers pageReplaceHTML() function posts the results to the results div on the calling page.

The nice part about this approach is that you can use that q() part with a switch/case statement to call any function to return data as you need. You could even go to a different search source, such as a Google Search Appliance, which I’ll cover soon.

Incoming search terms:

  • ajax text search mysql
  • cfwheels ajax example
  • cfwheels cfparam ajax
  • mysql fulltext search example coldfusion
  • text field in ajax mysql full-text search

Nested Layouts in CFWheels

Must confess I’ve been struggling with this one today, even with the great documentation at cfwheels.org.

Thanks to the cfwheels list, I’ve cracked it though (blogging the solution for posterity!).

I wanted to created a truly nested layout, i.e a master layout with <html> & <body> tags, and then a sub layout in my /views/main/ (with main being the controller name in this instance) which then included the default content on /views/main/index/. The problem was that when you add a layout file in /views/main/layout.cfm it overrides the default layout in /views/layout.cfm.

Turns out you need to call the parent template in the sub template and inject the content using contentFor() before you make the call.

So in my /views/layout.cfm

<html>
<body>
<div id="master-wrapper"> 
<cfoutput>#includeContent("mainbody")#</cfoutput>
</div>
</body>
</html>

and in my /views/main/layout.cfm

<cfoutput>  
<cfsavecontent variable="mainbody">
<div id="controller-layout">
#includeContent()#
</div>
</cfsavecontent>
<!--- Inject the above variable into the parent layout --->
<cfset contentFor(mainbody=mainbody)>
<!--- Include the parent layout --->
#includeLayout("/layout.cfm")#
</cfoutput>

and in my standard view of /views/main/index.cfm

<div id="main-view">
<p>I am the content in the main index file</p>
</div>

Results in…

<!-- Master Layout Start -->
<html>
<body>
<div id="master-wrapper"> 
  <!-- Sub Layout Start -->
  <div id="controller-layout">
    <!-- Standard View Start -->
    <div id="main-view">
    <p>I am the content in the main index file</p>
    </div>
    <!-- Standard View End -->
  </div>
  <!-- Sub Layout End -->
</div>
</body>
</html>
<!-- Master Layout End --> 

Notes: this means you’d need a layout.cfm in every views folder just to set the mainbody variable, otherwise your content never gets called by the top level layout. I’ve tried getting this working with the default body variable but haven’t had much success yet.

Incoming search terms:

  • CFwheels table sorting
  • cfwheels template
  • includepartial() cfwheels
  • html searchbox cfwheels
  • example web application in cfwheels
  • cfwheels test controller
  • can generate database tables from the models in CFWheels?
  • cfwheels search form
  • cfwheels record value is not insert table-into
  • cfwheels layout