Last active
August 10, 2017 13:13
-
-
Save chrisdpeters/1a0aaccacffe79cd433701cd86a7a190 to your computer and use it in GitHub Desktop.
Progressively enhancing your CFWheels form with nested properties and jQuery http://blog.chrisdpeters.com/cfwheels-with-nested-properties-and-jquery/
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
component extends="Model" { | |
function init() { | |
belongsTo("contact"); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(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)); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(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)); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(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)); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
component extends="Model" { | |
function init() { | |
hasMany(name: "addresses", joinType: "outer"); | |
nestedProperties( | |
association: "addresses", | |
sortProperty: "position", | |
allowDelete: true | |
); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<cfset contentFor(title: "New Contact")> | |
<cfoutput> | |
<h1>New Contact</h1> | |
#startFormTag(route: "contacts", id: "contact-form")# | |
#includePartial("form")# | |
<p> | |
#submitTag("Create Contact")# | |
</p> | |
#endFormTag()# | |
</cfoutput> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment