Building search forms with tableless models in CFWheels

November 12, 2023

Posted in Community, Documentation, Inspiration, Tutorials

Posted By: Chris Peters

Tags: models

<!-- wp:quote --> <blockquote class="wp-block-quote"><!-- wp:paragraph --> <p>This blog article was originally posted on Chris' personal blog and is republished here with his permission.</p> <!-- /wp:paragraph --></blockquote> <!-- /wp:quote --> <!-- wp:paragraph --> <p>In this post, I hope to persuade you that you will rarely ever need the&nbsp;<code>Tag</code>-based form helpers (<code>textFieldTag</code>,&nbsp;<code>selectTag</code>, etc.) in your CFWheels apps ever again.</p> <!-- /wp:paragraph --> <!-- wp:paragraph --> <p>“How?” you ask.</p> <!-- /wp:paragraph --> <!-- wp:paragraph --> <p>The answer: through the use of a wonderful feature that we affectionately call&nbsp;<a href="http://docs.cfwheels.org/docs/object-relational-mapping#models-without-database-tables" target="_blank" rel="noreferrer noopener">tableless models</a>.</p> <!-- /wp:paragraph --> <!-- wp:heading --> <h2 class="wp-block-heading">How you’re probably used to coding search forms in CFWheels</h2> <!-- /wp:heading --> <!-- wp:paragraph --> <p>So let’s code up an index form using the&nbsp;<code>Tag</code>-based helpers that you’re probably accustomed to using in this situation. The view’s job is to display a list of&nbsp;<code>invoice</code>&nbsp;records along with a form for narrowing by start date and end date:</p> <!-- /wp:paragraph --> <!-- wp:code --> <pre class="wp-block-code"><code>&lt;cfoutput&gt; #startFormTag(route="invoices", method="get")# #textFieldTag(name="startDate", value=params.startDate)# #textFieldTag(name="endDate", value=params.endDate)# #submitTag(value="Filter Invoices")# #endFormTag()# &lt;table&gt; &lt;thead&gt; &lt;tr&gt; &lt;th&gt;Invoice&lt;/th&gt; &lt;th&gt;Date&lt;/th&gt; &lt;th&gt;Amount&lt;/th&gt; &lt;/tr&gt; &lt;/thead&gt; &lt;tbody&gt; &lt;cfloop query="invoices"&gt; &lt;tr&gt; &lt;td&gt;#h(id)#&lt;/td&gt; &lt;td&gt;#DateFormat(createdAt)#&lt;/td&gt; &lt;td&gt;#DollarFormat(amount)#&lt;/td&gt; &lt;/tr&gt; &lt;/cfloop&gt; &lt;/tbody&gt; &lt;/table&gt; &lt;/cfoutput&gt;</code></pre> <!-- /wp:code --> <!-- wp:paragraph --> <p>This is pretty common, and I wouldn’t go as far to say that it’s wrong.</p> <!-- /wp:paragraph --> <!-- wp:paragraph --> <p>Let’s code what we need in the controller to wire everything up.</p> <!-- /wp:paragraph --> <!-- wp:code --> <pre class="wp-block-code"><code>component extends="Controller" { function index() { param name="params.startDate" default=""; param name="params.endDate" default=""; local.where = &#91;]; if (IsDate(params.startDate)) { ArrayAppend(local.where, "createdAt &gt;= '#params.startDate#'"); } if (IsDate(params.endDate)) { local.nextDay = DateAdd("d", 1, params.endDate); local.nextDay = DateFormat(local.nextDay, "m/d/yyyy"); ArrayAppend(local.where, "createdAt &lt; '#local.nextDay#'"); } invoices = model("invoice").findAll(where=ArrayToList(local.where, " AND ")); } }</code></pre> <!-- /wp:code --> <!-- wp:paragraph --> <p>But wait! We can’t have a&nbsp;<code>startDate</code>&nbsp;that occurs after the&nbsp;<code>endDate</code>. We better add a check for that in the controller:</p> <!-- /wp:paragraph --> <!-- wp:code --> <pre class="wp-block-code"><code>component extends="Controller" { function index() { param name="params.startDate" default=""; param name="params.endDate" default=""; local.where = &#91;]; // Let's make sure the start date and end date jive. if (IsDate(params.startDate) &amp;&amp; IsDate(params.endDate) &amp;&amp; params.startDate &gt; params.endDate) { flashInsert(error="The start date must be on or before the end date."); } if (IsDate(params.startDate)) { ArrayAppend(local.where, "createdAt &gt;= '#params.startDate#'"); } if (IsDate(params.endDate)) { local.nextDay = DateAdd("d", 1, params.endDate); local.nextDay = DateFormat(local.nextDay, "m/d/yyyy"); ArrayAppend(local.where, "createdAt &lt; '#local.nextDay#'"); } invoices = model("invoice").findAll(where=ArrayToList(local.where, " AND ")); } }</code></pre> <!-- /wp:code --> <!-- wp:paragraph --> <p>That&nbsp;<code>index</code>&nbsp;action is getting pretty beefy at this point. And now we’re starting to validate our data in the controller, which can quickly turn into a tangled mess after we’ve added another field or two to the form.</p> <!-- /wp:paragraph --> <!-- wp:heading --> <h2 class="wp-block-heading">Cleaning up the search form with tableless models</h2> <!-- /wp:heading --> <!-- wp:paragraph --> <p>As it turns out, models in CFWheels come with a bunch of really helpful methods for validating data. And even though we’re not using this form to save data to a database, we can still use the model validations to validate our data. Hooray!</p> <!-- /wp:paragraph --> <!-- wp:paragraph --> <p>All that we need to do is create a CFC in our&nbsp;<code>models</code>&nbsp;folder that represents this particular form. The initializer will contain a call to&nbsp;<code>table(false)</code>, which tells CFWheels to not try to connect it to a database.</p> <!-- /wp:paragraph --> <!-- wp:paragraph --> <p>In addition to&nbsp;<code>table(false)</code>, we can call all of the model validation initializers that we need to validate the data.</p> <!-- /wp:paragraph --> <!-- wp:paragraph --> <p>Lastly, we need to create a method that validates data passed into the model and runs the query if all is well.</p> <!-- /wp:paragraph --> <!-- wp:paragraph --> <p>Here is the finished product in&nbsp;<code>models/InvoiceSearchForm.cfc</code>:</p> <!-- /wp:paragraph --> <!-- wp:code --> <pre class="wp-block-code"><code>component extends="Model" { function config() { // Make it tableless table(false); // Validations validatesFormatOf(properties="startDate,endDate", type="date", allowBlank=true); validate("startDateBeforeEndDateValidation"); } boolean function run() { // Run validations and abort if failed. if (!this.valid()) { this.results = QueryNew(""); return false; } // Continue with query if validation passed. local.where = &#91;]; if (IsDate(this.startDate)) { ArrayAppend(local.where, "createdAt >= '#this.startDate#'"); } if (IsDate(this.endDate)) { local.nextDay = DateAdd("d", 1, this.endDate); local.nextDay = DateFormat(local.nextDay, "m/d/yyyy"); ArrayAppend(local.where, "createdAt &lt; '#local.nextDay#'"); } this.results = model("invoice").findAll(where=ArrayToList(local.where, " AND ")); return true; } private function startDateBeforeEndDateValidation() { if (IsDate(this.startDate) &amp;&amp; IsDate(this.endDate) &amp;&amp; this.startDate > this.endDate) { this.addError("startDate", "Start Date must be on or before End Date"); } } }</code></pre> <!-- /wp:code --> <!-- wp:paragraph --> <p>Notice that&nbsp;<code>startDate</code>&nbsp;and&nbsp;<code>endDate</code>&nbsp;become properties on the model in the&nbsp;<code>this</code>&nbsp;scope. This allows us to validate those properties and refer to them in an object-oriented manner.</p> <!-- /wp:paragraph --> <!-- wp:paragraph --> <p>When the&nbsp;<code>run</code>&nbsp;method is called, there will be a&nbsp;<code>results</code>&nbsp;property set on the object containing the search query.</p> <!-- /wp:paragraph --> <!-- wp:paragraph --> <p>Next, we rewire the controller to this much simpler form:</p> <!-- /wp:paragraph --> <!-- wp:code --> <pre class="wp-block-code"><code>component extends="Controller" { function index() { // Note that moving this into an object named `search` will change the // `params` struct slightly. param name="params.search.startDate" default=""; param name="params.search.endDate" default=""; // We pass the `params.search` struct in as properties on the search form // object. search = model("invoiceSearchForm").new(argumentCollection=params.search); // This runs the search and adds an error message if validation fails. if (!search.run()) { flashInsert(error="There was an error with your search filters"); } } }</code></pre> <!-- /wp:code --> <!-- wp:paragraph --> <p>Much cleaner, huh? Excluding whitespace and comments, this reduces the contents of the&nbsp;<code>index</code>&nbsp;action from 16 lines of actual code to 5.</p> <!-- /wp:paragraph --> <!-- wp:paragraph --> <p>This methodology also improves the view because we can now use&nbsp;<code>textField</code>&nbsp;instead of&nbsp;<code>textFieldTag</code>, and we can display validation errors near the affected form fields:</p> <!-- /wp:paragraph --> <!-- wp:code --> <pre class="wp-block-code"><code>&lt;cfoutput&gt; #startFormTag(route="invoices", method="get")# #textField(objectName="search", property="startDate")# #errorMessageOn(objectName="search", property="startDate")# #textField(objectName="search", property="endDate")# #errorMessageOn(objectName="search", property="endDate")# #submitTag(value="Filter Invoices")# #endFormTag()# &lt;table&gt; &lt;thead&gt; &lt;tr&gt; &lt;th&gt;Invoice&lt;/th&gt; &lt;th&gt;Date&lt;/th&gt; &lt;th&gt;Amount&lt;/th&gt; &lt;/tr&gt; &lt;/thead&gt; &lt;tbody&gt; &lt;cfloop query="search.results"&gt; &lt;tr&gt; &lt;td&gt;#h(id)#&lt;/td&gt; &lt;td&gt;#DateFormat(createdAt)#&lt;/td&gt; &lt;td&gt;#DollarFormat(amount)#&lt;/td&gt; &lt;/tr&gt; &lt;/cfloop&gt; &lt;/tbody&gt; &lt;/table&gt; &lt;/cfoutput&gt;</code></pre> <!-- /wp:code --> <!-- wp:paragraph --> <p>As a bonus, our&nbsp;<code>InvoiceSearchForm</code>&nbsp;model allows us to do things like set the labels/error message labels on the form fields using&nbsp;<code>property</code>, and allows us to do most of what models allow us to do: namely validations and callbacks.</p> <!-- /wp:paragraph --> <!-- wp:code --> <pre class="wp-block-code"><code>component extends="Model" { function config() { table(false); // Set property labels for form fields and related error messages. property(name="startDate", label="Start"); property(name="endDate", label="End"); //... } //... }</code></pre> <!-- /wp:code --> <!-- wp:paragraph --> <p>I find this to be a nice pattern because it ties the form to the model in a fairly clean, object-oriented way: the model represents the form, so it makes sense for it to define how labels on the form should appear.</p> <!-- /wp:paragraph --> <!-- wp:heading --> <h2 class="wp-block-heading">Other uses for tableless models</h2> <!-- /wp:heading --> <!-- wp:paragraph --> <p>Here are some other ideas where tableless models are a Good Idea&#x2122;:</p> <!-- /wp:paragraph --> <!-- wp:paragraph --> <p><strong>Authentication forms</strong><br>The model takes care of all authentication logic.</p> <!-- /wp:paragraph --> <!-- wp:paragraph --> <p><strong>Password change/reset forms</strong><br>Move interface-based concepts like&nbsp;<code>validatesConfirmationOf</code>&nbsp;out of the table-based&nbsp;<code>user</code>&nbsp;model and into a tableless model.</p> <!-- /wp:paragraph --> <!-- wp:paragraph --> <p><strong>Database transactions involving multiple models</strong><br>Nested properties have their limits and logic related to them can really pollute your table-based models. Handle all of the logic in a model that’s intimately involved with the form.</p> <!-- /wp:paragraph --> <!-- wp:paragraph --> <p><strong>Reports</strong><br>Have you ever found yourself in a situation where you needed to run a query involving multiple database tables, but it was unclear which model to write the query in? Tableless models are a perfect way to avoid making a random decision.</p> <!-- /wp:paragraph --> <!-- wp:paragraph --> <p><strong>NoSQL and API integration</strong><br>Are you saving your data somewhere other than a relational database? You can still model the business logic using CFWheels models.</p> <!-- /wp:paragraph --> <!-- wp:heading --> <h2 class="wp-block-heading">Props</h2> <!-- /wp:heading --> <!-- wp:paragraph --> <p>Most of this inspiration came from a similar concept known as&nbsp;<a target="_blank" rel="noreferrer noopener" href="https://robots.thoughtbot.com/activemodel-form-objects">Form Objects</a>&nbsp;in Ruby on Rails. (There is a great&nbsp;<a target="_blank" rel="noreferrer noopener" href="http://railscasts.com/episodes/416-form-objects">Railscast about Form Objects</a>here too.)</p> <!-- /wp:paragraph --> <!-- wp:paragraph --> <p>Also, major kudos to Tony Petruzzi for adding this awesome feature into CFWheels, which made its way into the v1.3 release.</p> <!-- /wp:paragraph -->

Latest Blog Posts