Archive for the ‘Community’ Category


Building search forms with tableless models in CFWheels

This blog article was originally posted on Chris’ personal blog and is republished here with his permission.

In this post, I hope to persuade you that you will rarely ever need the Tag-based form helpers (textFieldTagselectTag, etc.) in your CFWheels apps ever again.

“How?” you ask.

The answer: through the use of a wonderful feature that we affectionately call tableless models.

How you’re probably used to coding search forms in CFWheels

So let’s code up an index form using the Tag-based helpers that you’re probably accustomed to using in this situation. The view’s job is to display a list of invoice records along with a form for narrowing by start date and end date:

<cfoutput>

#startFormTag(route="invoices", method="get")#
  #textFieldTag(name="startDate", value=params.startDate)#
  #textFieldTag(name="endDate", value=params.endDate)#
  #submitTag(value="Filter Invoices")#
#endFormTag()#

<table>
  <thead>
    <tr>
      <th>Invoice</th>
      <th>Date</th>
      <th>Amount</th>
    </tr>
  </thead>
  <tbody>
    <cfloop query="invoices">
      <tr>
        <td>#h(id)#</td>
        <td>#DateFormat(createdAt)#</td>
        <td>#DollarFormat(amount)#</td>
      </tr>
    </cfloop>
  </tbody>
</table>

</cfoutput>

This is pretty common, and I wouldn’t go as far to say that it’s wrong.

Let’s code what we need in the controller to wire everything up.

component extends="Controller" {
  function index() {
    param name="params.startDate" default="";
    param name="params.endDate" default="";
    
    local.where = [];
    
    if (IsDate(params.startDate)) {
      ArrayAppend(local.where, "createdAt >= '#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 < '#local.nextDay#'");
    }
    
    invoices = model("invoice").findAll(where=ArrayToList(local.where, " AND "));
  }
}

But wait! We can’t have a startDate that occurs after the endDate. We better add a check for that in the controller:

component extends="Controller" {
  function index() {
    param name="params.startDate" default="";
    param name="params.endDate" default="";
    
    local.where = [];
    
    // Let's make sure the start date and end date jive.
    if (IsDate(params.startDate) && IsDate(params.endDate) && params.startDate > params.endDate) {
      flashInsert(error="The start date must be on or before the end date.");
    }
    
    if (IsDate(params.startDate)) {
      ArrayAppend(local.where, "createdAt >= '#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 < '#local.nextDay#'");
    }
    
    invoices = model("invoice").findAll(where=ArrayToList(local.where, " AND "));
  }
}

That index 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.

Cleaning up the search form with tableless models

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!

All that we need to do is create a CFC in our models folder that represents this particular form. The initializer will contain a call to table(false), which tells CFWheels to not try to connect it to a database.

In addition to table(false), we can call all of the model validation initializers that we need to validate the data.

Lastly, we need to create a method that validates data passed into the model and runs the query if all is well.

Here is the finished product in models/InvoiceSearchForm.cfc:

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 = [];
    
    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 < '#local.nextDay#'");
    }
    
    this.results = model("invoice").findAll(where=ArrayToList(local.where, " AND "));
    return true;
  }
  
  private function startDateBeforeEndDateValidation() {
    if (IsDate(this.startDate) && IsDate(this.endDate) && this.startDate > this.endDate) {
      this.addError("startDate", "Start Date must be on or before End Date");
    }
  }
}

Notice that startDate and endDate become properties on the model in the this scope. This allows us to validate those properties and refer to them in an object-oriented manner.

When the run method is called, there will be a results property set on the object containing the search query.

Next, we rewire the controller to this much simpler form:

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");
    }
  }
}

Much cleaner, huh? Excluding whitespace and comments, this reduces the contents of the index action from 16 lines of actual code to 5.

This methodology also improves the view because we can now use textField instead of textFieldTag, and we can display validation errors near the affected form fields:

<cfoutput>

#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()#

<table>
  <thead>
    <tr>
      <th>Invoice</th>
      <th>Date</th>
      <th>Amount</th>
    </tr>
  </thead>
  <tbody>
    <cfloop query="search.results">
      <tr>
        <td>#h(id)#</td>
        <td>#DateFormat(createdAt)#</td>
        <td>#DollarFormat(amount)#</td>
      </tr>
    </cfloop>
  </tbody>
</table>

</cfoutput>

As a bonus, our InvoiceSearchForm model allows us to do things like set the labels/error message labels on the form fields using property, and allows us to do most of what models allow us to do: namely validations and callbacks.

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");
    
    //...
  }

  //...
}

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.

Other uses for tableless models

Here are some other ideas where tableless models are a Good Idea™:

Authentication forms
The model takes care of all authentication logic.

Password change/reset forms
Move interface-based concepts like validatesConfirmationOf out of the table-based user model and into a tableless model.

Database transactions involving multiple models
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.

Reports
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.

NoSQL and API integration
Are you saving your data somewhere other than a relational database? You can still model the business logic using CFWheels models.

Props

Most of this inspiration came from a similar concept known as Form Objects in Ruby on Rails. (There is a great Railscast about Form Objectshere too.)

Also, major kudos to Tony Petruzzi for adding this awesome feature into CFWheels, which made its way into the v1.3 release.

CFWheels v2.5.0 Released

This is a major milestone release of CFWheels v2.5.0 that has been in the works for over a year. As you can see nearly 34 PRs have been merged into the codebase which include many enhancements and bug fixes. In addition many changes have been made to the tooling used in the project.

Here are some of the highlights:

  • We have begun to publish SNAPSHOTS to ForgeBox.io on each successful commit to the develop branch.
  • The GitHub Actions CI scripts use the same configuration files as the local Docker testing suite. If you are inclined to contribute to the CFWheels project you will most likely want to be able to run the test suite locally in Docker containers to test your changes before you submit a PR. To run the local test suite simply type docker compose up in the root of your project. The source code is injected into the containers dynamically so it makes it easier to make changes and see them appear in the docker containers without rebuilding the containers. Look for more details on this to come in the future.
  • Every commit is now tested across a matrix of 20 combinations of CF Engines and Databases. The matrix includes CF Engines (Lucee 5, Lucee 6, Adobe ColdFusion 2016, Adobe ColdFusion 2018, Adobe ColdFusion 2021, and Adobe ColdFusion 2023) and databases (H2, MS SQL Server, PostgreSQL, and MySQL).
  • Each successful commit automatically builds two packages on ForgeBox. One for the default template and one for the core CFWheels folder.

Upgrading an Existing Project

The changes in this version are confined to the wheels directory so simply swapping out your wheels directory should be all you need to do to upgrade.

Changelog

Model Enhancements

  • PR-1183-Allow datasource argument in finders #1183 – [Adam Chapman]
  • PR-1201-Issue ORM create() fails object validation for not null columns with defaults #929 validate not nullable columns with default #1201 – [Adam Chapman]
  • PR-1202-Remove old oracle test workaround #1202 – [Adam Chapman]
  • PR-1205-issue-1182-adds-simplelock-to-sql-caching #1205 – [Adam Chapman]
  • PR-1222-Findall() performance bottleneck #1222 – [Adam Chapman]
  • PR-1223-refactor-queryCallback-with-inbuilt-query-functions #1223 – [Adam Chapman]
  • PR-1226-Invalid column not throwing exception in select argument #1226 – [Zain Ul Abideen]
  • PR-1265-improve-performance-refactor-out-listfind #1265 – [Adam Chapman]
  • PR-1260-Adds support for native query returnType #1260 – [Adam Chapman]
  • PR-1249-Removed the original IF/ELSE condition that invalidates calculated props and added condition #1240 – [Zain Ul Abideen]

View Enhancements

  • PR-1254-issue 908 enable paginationLinks() to set active class on parent #1254 – [Zain Ul Abideen]

Bug Fixes

  • PR-1227-Return a numeric value if the primary key is Numeric #1227 – [Zain Ul Abideen]
  • PR-1257-Checkbox bug when checkedvalue is not true #1257 – [Adam Chapman]
  • PR-1246-set the default route if it is not passed in the function #1246 – [Zain Ul Abideen]
  • PR-1256-issue 889 unable to duplicate component #1256 – [Zain Ul Abideen]
  • PR-1253-Issue 580 select ambiguous column name using the wheels alias #1253 – [Zain Ul Abideen]
  • PR-1245-Added afterFind callback hook in the findAll function in case of structs #1245 – [Zain Ul Abideen]
  • PR-1302-Check for Reload Password when setting a url IP exception #1302 – Peter Amiri

Miscellaneous

  • PR-1175-restoreTestRunnerApplicationScope setting #1175 – [Adam Chapman]
  • PR-1176-fix text in core readme file #1176 – [Per Djurner]
  • PR-1177-fix text in base template readme file #1177 – [Per Djurner]
  • PR-1178-fix text in default template file #1178 – [Per Djurner]
  • PR-1185-adds-root-docker-volume #1185 – [Adam Chapman]
  • PR-1200-Update the docker-compose command to docker compose v2 syntax #1200 – [Adam Chapman, Peter Amiri]
  • PR-1204-Add Lucee 6 to test matrix on local Docker test suite #1204 – [Peter Amiri]
  • PR-1203-ensure testing params maintained #1203 – [Adam Chapman]
  • PR-1228-Adding addClass attribute in the function textField #1228 – [Zain Ul Abideen]
  • PR-1230-Add Adobe 2021 Support to local Docker and GitHub Actions testing – #1230 – Peter Amiri
  • PR-1264-update Lucee 6 version used for tests to latest #1264 – [Zac Spitzer – * New Contributor *]
  • PR-1241-Fix spelling and remove whitespace from link #1241 – [John Bampton]
  • PR-1247-show the current git branch in the debug layout #1247 – [Michael Diederich]
  • PR-1250-Added test framework functions in the docs #1250 – [Zain Ul Abideen]
  • PR-1255-issue 1179 Downloaded the CDN files and changed paths in files #1255 – [Zain Ul Abideen]

Guides

  • PR-1198-Documentation-fixes #1198 – [Adam Chapman]

Download Zip File

CFWheels on CF Alive the Sequel

Back in February of 2019 David Belanger and Tom King from the CFWheels core team sat down with Michaela Light on the ColdFusion Alive Podcast to have a chat about CFWheels. A few weeks ago Peter Amiri had a chance to speak to Michaela Light about recent developments in the CFWheels community, how to contribute to the project, and the road map ahead.

Episode 122 of the ColdFusion Alive Podcast

You can view the episode notes on the TeraTech website.

The CFWheels Channel on CFML Slack Has Been Archived

Back in May 2022 we posted a blog article announcing that CFWheels has moved to GitHub Discussions. At the time this effected the retirement of the Google mailing list and redirecting the links on the home page to the new GitHub Discussions site. Now the time has come to also retire the CFWheels channel on the CFML Slack instance.

Feel free to go back and read the original post from May that laid out the reasoning for this move but we thought it was important to reiterate our thought process once again. At the core, the reasons for this move are to move our discussions closer to the code, allowing the poster and respondent to more easily link to specific branches, files, and even lines of code. Issues can be converted to discussions if they warrant further community input or discussions promoted to an issue once an issue or feature has had open consultation and next steps identified. Discussions can be marked as answered and the specific answer identified for future reference. And most importantly, all these discussions, collaborations, and consultations are searchable and discoverable by search engines so the community as a whole reaps the benefits.

We have seen how the move to GitHub discussions by other open source communities has benefited their community and brought their discussions closer to the codebase. We hope this move by us will be similarly fruitful.

CFWheels v2.4.0 Released

This version is the accumulation of bug fixes and minor enhancements over the last quarter. This release welcomes John Bampton and Coleman Sperando, two first time contributors to the project.

Download Zip

If updating from CFWheels 2.3.x:

It should be an easy upgrade, just swap out the wheels folder.

If you installed CFWheels with CommandBox and have a box.sjon file:

Enter install cfwheels in the root of your site to update your wheels folder to the latest.

Changelog

Bug Fixes

  • issue-1091-wheels-paths-in-error-template #1091 – [Adam Chapman]
  • issue-1082-validations should not trim properties #1082 – [Adam Chapman]
  • issue-1088-Adds SQL parsing regex tweak which correctly handles whitespace #1088 – [Adam Chapman, Adam Cameron]

Miscellaneous

  • Adds cfformat ignore marker comments around core “view” cfm files that contain html markup – [Adam Chapman]
  • Adds the ability to scroll large items horizontally in the test runner UI #1130 – [Adam Chapman]
  • Fix cfformat ignore markers #1129 – [Adam Chapman]
  • Enable finder model methods to returnAs “sql”, mainly for debugging #1141 – [Adam Chapman]
  • Show the Test Runner buttons in the CFWheels GUI on the Package List screen allowing the developer to run the entire test suite instead of one package at a time. – [Peter Amiri]
  • The Base Template now contains all necessary placeholders for the CLI to interact with the application and be able to inject code properly. – [Peter Amiri]
  • By default the Core tests will run in the application datasource, but the developer can setup a different database for running the Core tests to ensure there is no side effects from running the tests. If you do end up setting a different database for the coreTestDatasourceName, make sure to reload your application after running the Core tests. – [Peter Amiri]
  • Fix two broken links in README. [#1150] – [John Bampton – * New Contributor *]
  • Fix spelling [#1151][#1158] – [John Bampton – * New Contributor *]
  • Add .env parser to parse .env files and add the properties found in the file to this.env scope. #1157 – [Peter Amiri]
  • Update the local test suite to supported ARM architecture docker images to make the suite compatible with the Apple Silicon Macs. #1143 – [Peter Amiri]

Guides

  • Fix broken links throughout the guides. – [Peter Amiri]
  • Fixed mailto link in CONTRIBUTING.md #1123 – [Coleman Sperando * New Contributor *]
  • Fix test guides examples #1125 [Adam Chapman]
  • Fix typos in the guides #1161 [Adam Chapman]

Wheels CLI matures to Version 1.0

It’s hard to believe it took so long to get here but modern CFML development has come a long way thanks to tools like CommandBox and ForgeBox. The Wheels CLI is built as a CommandBox module and wouldn’t have even been possible without the support of the fine folks at Ortus Solutions.

The first commit to the repo for this project was committed back in July of 2016. It’s taken a while, that’s an understatement, to get here but Wheels itself jumped to 2.0, CommandBox matured, and we were able to put the plumbing in place to support the communication between the CLI and the running server. With nearly 300 commits in the repo, 25 commands in the CLI, and over 20 pages of documentation, it’s now time to take the alpha/beta label off send this baby out into the world.

Some of the more notable commands are wheels new to use our wizard to start a brand new project. With this command and the corresponding wheels generate app command, you can start a new Wheels project in a directory, specify the template to use, pick the CF engine to use, configure the datasource, and setup your reload password. In fact there’s a whole host of generate commands for every type of object you may want to create. There are a bunch of dbmigrate commands to interact with database migrations.

To install the CLI issue the following command:

box install cfwheels-cli

Don’t forget to check out the full CLI Commands section in the guides too.

CFWheels Added to the htmx Server-Side Examples Page

Back in March we published an example app where we took the TodoMVC spec and built a reference implementation of the app with CFWheels and htmx. Here is a quick graphic of the UI of the app.

Todo app implemented with CFWheels and htmx

This app has now been added to the list of Server-Side integration examples on the htmx website.

htmx at it’s core is html over the wire. Instead of returning JSON from backend APIs and consuming them with Javascript to build page interactivity, htmx takes a different approach. It expects actual html snippets to be returned that are swapped into the DOM. How it does it, is by extending html with a handful of additional attributes, that probably should have been there in the first place, which enable any HTML element to issue an AJAX call to the backend, specify what triggers the call, and specify how the returned HTML should be added to the DOM.

We’ll be doing more with HTMX in the future so it’s great to get CFWheels on the radars of the htmx project.

Two New Repositories Published

We have published two new repositories named cfwheels/cfwheels-www and cfwheels/cfwheels-api which handle the CFWheels.org landing page site and the api.cfwheels.org API documentation site respectively.

The cfwheels-api repository is a good example of how you can structure your own projects and keep sensitive information and the core framework files out of your project’s source control repository. The project used a .gitignore file that keeps these files out of the repo. It also uses a box.json file to specify some dependencies to pull the required files back in.

Using dependencies in a box.json file is nothing new but what’s interesting is that cfwheels/cfwheels-api has three dependencies defined. One is the core framework itself and pulls in the latest framework folder. The second is the newly published CFWheels DotEnvSettings Plugin which allows keeping sensitive information out of the repository by using a locally defined .env file. The last dependency is probably the most interesting, cause that one pulls in the Semantic Version module from ForgeBox which is a module that wasn’t specifically written for CFWheels but can be pulled in and used none the less. This opens up the potential of using many more modules from ForgeBox instead of being confined to just CFWheels Plugins.

CFWheels has moved to GitHub Discussions

Although Google Groups has served us well over the years, it’s started to lose some essential abilities and it’s looking a little long in the tooth.

We have decided that it would be a benefit to the community to migrate to GitHub Discussions. GitHub Discussions allows us to bring our community closer to where the code lives.

“People around the world are turning to open source—and to GitHub. It is becoming not only the home for developers, but a platform where people in many roles, doing diverse work, come to learn, engage with the community, and contribute to projects that advance the greater good.”

GitHub Octoverse 2020 Report

The benefits of moving to Github Discussions are numerous, but at a glance, we look forward to having better syntax highlighting, marking comments as answers, flagging threads as Answered, easily turning a discussion into an issue, and many more.

The Google Group will remain available, but we have disabled posting to the group, and we’ll treat it as a read-only archive of past discussions.

CFWheels Announces a Bug Bounty

We are happy to launch a new program that we hope will lead to a more stable framework for all of us. Effective immediately we are launching our Bug Bounty program. When we first conceived of the bounty program we were looking at programs from IssueHunt and BountySource and the main goal was to widen the field of contributors to the CFWheels project as well as crush some of the long standing bugs in the framework.

Most of these bugs are edge cases that don’t effect the core functionality of the framework and for the most part users have found work arounds for. In the past it’s been difficult to dedicate our limited resources towards some of these bugs but now that we have a growing list of monthly sponsors we would like to launch out bounty program to compensate contributors who are willing to tackle some of these.

The easiest way to get started participating in this program is to visit the issues list and look for the $50 Bounty tag. Pick an issue that interests you, clone the repo, and start working on your solution. When you think you have a working solution create a PR and submit it for review. Please read the Contributing to CFWheels chapter in the guides or the contributing guidelines in the repository for details on how to contribute to the CFWheels project.

We look forward to see how the community responds to this bounty program. Depending on how things go, we can envision expanding this program to include enhancements as well as sponsorship of individual enhancements.