Here's an example of what we do for an invoicing app. I'm retyping as I modify some things for clarity, so excuse any typos. I hope it gets the point across.
The gist is that this works with and without JavaScript. Without JavaScript, the entire page reloads with a new invoice item appended.
If there is JavaScript, jQuery sends an AJAX request to get the HTML for only the new invoice item that needs to be added.
If you have any questions or if I left something out, let me know.
controllers/Invoices.cfc
component extends="Controller" {
function init() {
// This interrupts a create or update to add a new invoice item
filters(through="$addInvoiceItem", only="create,update");
}
function new() {
// Regular new logic here
}
function create() {
// Regular create logic here
}
function edit() {
// Regular edit logic here
}
function update() {
// Regular update logic here
}
private function $addInvoiceItem() {
if (StructKeyExists(params, "newInvoiceItem") && Len(params.newInvoiceItem)) {
// Method in model that loads submitted data and adds a new item
invoice = model("invoice").addInvoiceItem(argumentCollection=params.invoice);
// For an AJAX request, we need to only pass the added item to the partial
if (isAjax()) {
invoice.invoiceItems = [
invoice.invoiceItems[ArrayLen(invoice.invoiceItems)]
]
// This will load `views/invoices/_invoiceitem.cfm`
renderText(includePartial(invoice.invoiceItems));
}
// Otherwise, handle regular HTML requests
else {
switch (params.action) {
case "create":
renderPage(action="new");
break;
case "update":
renderPage(action="edit");
break;
}
}
}
}
}
models/Invoice.cfc
component extends="Model" {
function init() {
// Associations
hasMany("invoiceItems");
// Nested properties
nestedProperties(property="invoiceItems", allowDelete=true, sortProperty="position");
}
function addInvoiceItem() {
// Load invoice object
if (StructKeyExists(arguments, "id") && IsNumeric(arguments.id)) {
local.invoice = model("invoice").findOne(where="id=#arguments.id#", include="invoiceItems");
local.invoice.setProperties(arguments);
}
else {
local.invoice = model("invoice").new(arguments);
}
if (!StructKeyExists(local.invoice, "invoiceItems")) {
local.invoice.invoiceItems = [];
}
// Add new item to invoice
ArrayAppend(
local.invoice.invoiceItems,
model("invoiceItem").new(position=ArrayLen(local.invoice.invoiceItems))
);
return local.invoice;
}
}
javascripts/invoices.js
$(document).ready(function() {
// Add button does AJAX post
$("#new-invoice-item-button").on("click", function(e) {
var $this = $(this),
$invoiceForm = $("#invoice-form"),
formData = $invoiceForm.serialize() + "&newInvoiceItem=%2B%20Add%20Item",
$invoiceTableLastRow = $("#invoice-items tr:last");
// Disable button
$this.prop("disabled", true);
// Call form action to get new invoice item row
$.ajax({
url: $invoiceForm.attr("action"),
type: "post",
data: formData,
cache: false,
success: function(data, textStatus, jqXHR) {
data = $(data);
$invoiceTableLastRow.after(data);
},
error: function(jqXHR, textStatus, errorThrown) {
alert("There was an error adding the item.");
},
complete: function(jqXHR, textStatus) {
// Re-enable field
$this.prop("disabled", false);
}
});
e.preventDefault();
});
});
views/invoices/_invoiceitem.cfm
<!--- Just showing a couple example fields here for the table row that is generated --->
<tr>
<td>
#textField(
label=false,
objectName="invoice",
association="invoiceItems",
position=arguments.current,
property="description",
append=""
)#
</td>
<td>
#textField(
label=false,
objectName="invoice",
association="invoiceItems",
position=arguments.current,
property="amount",
append=""
)#
#hiddenField(
objectName="invoice",
association="invoiceItems",
position=arguments.current,
property="id"
)#
#hiddenField(
objectName="invoice",
association="invoiceItems",
position="arguments.current,
property="position"
)#
</td>
</tr>