Progressively enhancing your CFWheels form with nested properties and jQuery http://blog.chrisdpeters.com/cfwheels-with-nested-properties-and-jquery/
<cfoutput> | |
<div id="address-#EncodeForHtml(arguments.current)#"> | |
<cfif not contact.addresses[arguments.current].isNew()> | |
#hiddenField( | |
objectName: "contact", | |
association: "addresses", | |
position: arguments.current, | |
property: "id" | |
)# | |
</cfif> | |
#hiddenField( | |
objectName: "contact", | |
association: "addresses", | |
position: arguments.current, | |
property: "position" | |
)# | |
#hiddenField( | |
objectName: "contact", | |
association: "addresses", | |
position: arguments.current, | |
property: "_delete", | |
data_delete: true | |
)# | |
#textField( | |
objectName: "contact", | |
association: "addresses", | |
position: arguments.current, | |
property: "street" | |
)# | |
#errorMessageOn( | |
objectName: "contact['addresses'][#arguments.current#]", | |
property: "street" | |
)# | |
#textField( | |
objectName: "contact", | |
association: "addresses", | |
position: arguments.current, | |
property: "city" | |
)# | |
#errorMessageOn( | |
objectName: "contact['addresses'][#arguments.current#]", | |
property: "city" | |
)# | |
<button | |
type="submit" | |
name="removeAddress" | |
value="#EncodeForHtml(arguments.current)#" | |
data-remove-contact-address | |
> | |
Remove Address | |
</button> | |
</div> | |
</cfoutput> |
<cfoutput> | |
<fieldset> | |
<legend>Contact</legend> | |
#textField(objectName: "contact", property: "firstName")# | |
#errorMessageOn(objectName: "contact", property: "firstName")# | |
#textField(objectName: "contact", property: "lastName")# | |
#errorMessageOn(objectName: "contact", property: "lastName")# | |
</fieldset> | |
<fieldset> | |
<legend>Addresses</legend> | |
<div id="contact-addresses"> | |
#includePartial(contact.addresses)# | |
</div> | |
<p> | |
<button id="new-address-button" type="submit" name="newAddress" value="true"> | |
+ New Address | |
</button> | |
</p> | |
</fieldset> | |
</cfoutput> |
component extends="Model" { | |
function init() { | |
belongsTo("contact"); | |
} | |
} |
(function($) { | |
$('#new-address-button').on('click', function(e) { | |
// Stuff from other example left out for brevity. | |
// ... | |
// Submit the entire form via AJAX. | |
$.ajax({ | |
url: contactForm.attr('action'), | |
type: 'post', | |
data: formData, | |
cache: false, | |
success: function(data, textStatus, jqXHR) { | |
var $responseData = $(data); | |
$('#contact-addresses').append($responseData); | |
$responseData.find('button[data-remove-contact-address]').on("click", function(e) { | |
addRemoveAddressHandler($(this), e); | |
}); | |
}, | |
error: function(jqXHR, textStatus, errorThrown) { | |
alert('There was an error adding the address.'); | |
}, | |
complete: function(jqXHR, textStatus) { | |
$this.prop('disabled', false); | |
$loader.hide(); | |
} | |
}); | |
}); | |
}(jQuery)); |
(function($) { | |
$('#new-address-button').on('click', function(e) { | |
e.preventDefault(); | |
var $this = $(this), | |
$contactForm = $('#contact-form'), | |
// Here, we're adding the add button to the form post | |
formData = $contactForm.serialize() + '&' + $this.attr('name') + '=' $this.val(), | |
responseData = ""; | |
$this.prop('disabled', true); | |
// This is up to you to implement. Try something like Spin.js | |
$loader.show(); | |
// Submit the entire form via AJAX. | |
$.ajax({ | |
url: contactForm.attr('action'), | |
type: 'post', | |
data: formData, | |
cache: false, | |
success: function(data, textStatus, jqXHR) { | |
responseData = $(data); | |
$('#contact-addresses').append(data); | |
}, | |
error: function(jqXHR, textStatus, errorThrown) { | |
alert('There was an error adding the address.'); | |
}, | |
complete: function(jqXHR, textStatus) { | |
$this.prop('disabled', false); | |
$loader.hide(); | |
} | |
}); | |
}); | |
}(jQuery)); |
(function($) { | |
// Functionality for adding a new address goes here, but I'm omitting it for brevity. | |
// ... | |
// Handler for removing an address. | |
function addRemoveAddressHandler($element, event) { | |
var $container = $element.parents('div'), | |
deletionField = $container.find('input[data-delete]'); | |
$container.fadeOut('normal', function() { | |
// If this isn't a new address, mark it for deletion and add deletion row with undo | |
if (deletionField.length) { | |
deletionField.val(true); | |
addRemovalNotice($container); | |
} | |
// If this is a new address, it can just be removed from the DOM | |
else { | |
$element.remove(); | |
} | |
}); | |
event.preventDefault(); | |
} | |
// Adds a notice indicating that the record will be deleted on save. | |
// Also adds an undo link and handler. | |
function addRemovalNotice($container) { | |
var containerId = $container.attr("id"); | |
$container.hide(); | |
$container.after( | |
'<div id="address-deletion-notice-' + containerId + '">' + | |
'This address will be deleted when you click the Save Changes button below. ' + | |
'<a href="#" data-address-deletion-undo>Undo</a>' + | |
'</tr>' | |
); | |
// Add undo link handler | |
$container.find('a[data-address-deletion-undo]').on('click', function(e) { | |
var $this = $(this), | |
$noticeContainer = $this.parents('div'), | |
containerId = $noticeContainer.attr("id").replace('address-deletion-notice-', ''), | |
$removedContainer = $("#" + containerId); | |
// Fade out notice container, remove it, unflag record for deletion, | |
// and fade in the removed container. | |
$noticeContainer.fadeOut('slow', function() { | |
$(this).remove(); | |
$container.find('input[data-delete]').val(false); | |
// Fade in the removed container | |
$container.fadeIn('slow'); | |
}); | |
e.preventDefault(); | |
}); | |
} | |
// Initialize click behavior for button | |
$("button[data-remove-contact-address]").on("click", function(e) { | |
addRemoveAddressHandler($(this), e); | |
}); | |
}(jQuery)); |
component extends="Model" { | |
function init() { | |
hasMany(name: "addresses", joinType: "outer"); | |
nestedProperties( | |
association: "addresses", | |
sortProperty: "position", | |
allowDelete: true | |
); | |
} | |
/** | |
* Removes an address at a given position. | |
*/ | |
function removeAddressAt(required numeric position) { | |
if (arguments.position >= ArrayLen(this.addresses)) { | |
// Delete record from database if it's persisted. | |
if (!this.addresses[arguments.position].isNew()) { | |
this.addresses[arguments.position].delete(); | |
} | |
// Either way, also remove from the array. | |
ArrayDeleteAt(this.addresses, arguments.position); | |
// Readjust address positions, or else we'll get some fun Java `null` | |
// errors later. | |
for (local.i = 1; local.i <= ArrayLen(this.addresses); local.i++) { | |
this.addresses[local.i].position = local.i; | |
} | |
} | |
} | |
} |
component extends="Model" { | |
function init() { | |
hasMany(name: "addresses", joinType: "outer"); | |
nestedProperties( | |
association: "addresses", | |
sortProperty: "position", | |
allowDelete: true | |
); | |
} | |
} |
component extends="Controller" { | |
// | |
// Constructor and actions omitted for brevity | |
// | |
/** | |
* Adds a new address record to the contact and loads new or edit form if | |
* requested. | |
*/ | |
private function addAddress() { | |
// Only run this logic if the "New Address" button was clicked. | |
if (StructKeyExists(params, "newAddress")) { | |
// On update, we have an existing contact record from the `findContact` | |
// filter that we can load the properties into. | |
if (StructKeyExists(variables, "contact")) { | |
contact.setProperties(params.contact); | |
} | |
// If we're working on a new contact, then the `findContact` filter didn't | |
// run before this. So we need a new contact record. | |
else { | |
contact = model("contact").new(params.contact); | |
} | |
// Make sure we have an array of addresses to work with. | |
if (!StructKeyExists(contact, "addresses") || !IsArray(contact.addresses)) { | |
contact.addresses = []; | |
} | |
// Now let's add the new address with its position populated. | |
ArrayAppend( | |
contact.addresses, | |
model("address").new( | |
position: ArrayLen(contact.addresses) + 1 | |
) | |
); | |
// For an AJAX request, we need to only return the `_address` | |
// partial with the new address record. | |
if (isAjax()) { | |
local.address = contact.addresses[ArrayLen(contact.addresses)]; | |
renderText( | |
includePartial( | |
partial: "address", | |
object: local.address, | |
current: local.address.position | |
) | |
); | |
} | |
// ...or render the full page if it's not AJAX. | |
else { | |
// Lastly, load the form with the new address populated. | |
renderPage(action: params.action == "create" ? "new" : "edit"); | |
} | |
} | |
} | |
} |
component extends="Controller" { | |
function init() { | |
verifies(params: "key", paramsTypes: "integer", only: "edit,update"); | |
verifies(params: "contact", paramsTypes: "struct", only: "create,update"); | |
filters(through: "findContact", only: "edit,update"); | |
filters(through: "addAddress", only: "create,update"); | |
} | |
// | |
// Actions omitted for brevity | |
// | |
/** | |
* Adds a new address record to the contact and loads new or edit form if | |
* requested. | |
*/ | |
private function addAddress() { | |
// Only run this logic if the "New Address" button was clicked. | |
if (StructKeyExists(params, "newAddress")) { | |
// On update, we have an existing contact record from the `findContact` | |
// filter that we can load the properties into. | |
if (StructKeyExists(variables, "contact")) { | |
contact.setProperties(params.contact); | |
} | |
// If we're working on a new contact, then the `findContact` filter didn't | |
// run before this. So we need a new contact record. | |
else { | |
contact = model("contact").new(params.contact); | |
} | |
// Make sure we have an array of addresses to work with. | |
if (!StructKeyExists(contact, "addresses") || !IsArray(contact.addresses)) { | |
contact.addresses = []; | |
} | |
// Now let's add the new address with its position populated. | |
ArrayAppend( | |
contact.addresses, | |
model("address").new( | |
position: ArrayLen(contact.addresses) + 1 | |
) | |
); | |
// Lastly, load the form with the new address populated. | |
renderPage(action: params.action == "create" ? "new" : "edit"); | |
} | |
} | |
} |
component extends="Controller" { | |
function init() { | |
verifies(params: "key", paramsTypes: "integer", only: "edit,update"); | |
verifies(params: "contact", paramsTypes: "struct", only: "create,update"); | |
filters(through: "findContact", only: "edit,update"); | |
filters(through: "addAddress", only: "create,update"); | |
filters(through: "removeAddress", only: "create,update"); | |
} | |
// | |
// Actions and `addAddress` filter omitted for brevity | |
// | |
/** | |
* Removes an address record or marks it for destruction and loads new or edit form if | |
* requested. | |
*/ | |
private function removeAddress() { | |
// Only run this logic if the "Remove Address" button was clicked. | |
if (StructKeyExists(params, "removeAddress") && IsNumeric(params.removeAddress)) { | |
// On update, we have an existing contact record from the `findContact` | |
// filter that we can load the properties into. | |
if (StructKeyExists(variables, "contact")) { | |
contact.setProperties(params.contact); | |
} | |
// If we're working on a new contact, then the `findContact` filter didn't | |
// run before this. So we need a new contact record. | |
else { | |
contact = model("contact").new(params.contact); | |
} | |
// Now let's remove the address by position. | |
contact.removeAddressAt(params.removeAddress); | |
// Lastly, load the form with the new address populated. | |
renderPage(action: params.action == "create" ? "new" : "edit"); | |
} | |
} | |
} |
component extends="Controller" { | |
function init() { | |
verifies(params: "key", paramsTypes: "integer", only: "edit,update"); | |
verifies(params: "contact", paramsTypes: "struct", only: "create,update"); | |
filters(through: "findContact", only: "edit,update"); | |
} | |
function new() { | |
contact = model("contact").new(addresses: []); | |
} | |
function create() { | |
contact = model("contact").new(params.contact); | |
if (contact.save()) { | |
flashInsert(success: "Contact created."); | |
redirectTo(route: "contact", key: contact.key()); | |
} | |
else { | |
flashInsert(error: "There was an error creating the contact."); | |
renderPage(action: "new"); | |
} | |
} | |
function edit() { | |
} | |
function update() { | |
if (contact.update(params.contact)) { | |
flashInsert(success: "Contact updated."); | |
redirectTo(route: "contact", key: contact.key()); | |
} | |
else { | |
flashInsert(error: "There was an error updating the contact."); | |
renderPage(action: "edit"); | |
} | |
} | |
/** | |
* Finds contact for form by `params.key`. | |
*/ | |
private function findContact() { | |
contact = model("contact").findByKey( | |
key: params.key, | |
include: "addresses", | |
order: "position" | |
); | |
if (!IsObject(contact)) { | |
Throw(type: "MyApp.RecordNotFound"); | |
} | |
} | |
} |
<cfset contentFor( | |
title: EncodeForHtml("Edit Contact: #contact.firstNameChangedFrom() #contact.lastNameChangedFrom()#") | |
)> | |
<cfoutput> | |
<h1>Edit Contact</h1> | |
#startFormTag(route: "contact", key: contact.key(), method: "put", id: "contact-form")# | |
#includePartial("form")# | |
<p> | |
#submitTag("Update Contact")# | |
</p> | |
#endFormTag()# | |
</cfoutput> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment