Skip to content

Instantly share code, notes, and snippets.

@chrisdpeters
Last active August 10, 2017 13:13
Show Gist options
  • Save chrisdpeters/1a0aaccacffe79cd433701cd86a7a190 to your computer and use it in GitHub Desktop.
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/
<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>
<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