Update step rendering

This commit is contained in:
David Craig 2015-08-10 15:18:50 +12:00
parent 3217695713
commit 65651387e0
6 changed files with 134 additions and 274 deletions

View File

@ -44,6 +44,13 @@ class UserForm extends Form {
return $this->controller->LastEdited; return $this->controller->LastEdited;
} }
/**
* @return boolean
*/
public function getDisplayErrorMessagesAtTop() {
return $this->controller->DisplayErrorMessagesAtTop;
}
/** /**
* @return array * @return array
*/ */
@ -66,11 +73,18 @@ class UserForm extends Form {
public function getFormSteps() { public function getFormSteps() {
$steps = new ArrayList(); $steps = new ArrayList();
foreach ($this->controller->Fields()->filter('ClassName', 'EditableFormStep') as $step) { foreach ($this->controller->Fields() as $field) {
$steps->push(array( if ($field instanceof EditableFormStep) {
'Title' => $step->Title, $steps->push($field->getFormField());
'Fields' => $this->getFormFields($step) continue;
)); }
if(empty($steps->last())) {
trigger_error('Missing first step in form', E_USER_WARNING);
$steps->push(CompositeField::create());
}
$steps->last()->push($field->getFormField());
} }
return $steps; return $steps;
@ -81,18 +95,12 @@ class UserForm extends Form {
* by using {@link updateFormFields()} on an {@link Extension} subclass which * by using {@link updateFormFields()} on an {@link Extension} subclass which
* is applied to this controller. * is applied to this controller.
* *
* @param EditableFormStep $parent
*
* @return FieldList * @return FieldList
*/ */
public function getFormFields($parent = null) { public function getFormFields() {
if(!$parent) {
$parent = $this->controller;
}
$fields = new FieldList(); $fields = new FieldList();
foreach($parent->Fields() as $editableField) { foreach($this->controller->Fields() as $editableField) {
// get the raw form field from the editable version // get the raw form field from the editable version
$field = $editableField->getFormField(); $field = $editableField->getFormField();

View File

@ -367,26 +367,10 @@ class UserDefinedForm_Controller extends Page_Controller {
$form = UserForm::create($this); $form = UserForm::create($this);
$this->generateConditionalJavascript(); $this->generateConditionalJavascript();
$this->generateValidationJavascript($form);
return $form; return $form;
} }
/**
* Build jQuery validation script and require as a custom script
*
* @param UserForm $form
*/
public function generateValidationJavascript(UserForm $form) {
// set the custom script for this form
Requirements::customScript(
$this
->customise(array('Form' => $form))
->renderWith('ValidationScript'),
'UserFormsValidation'
);
}
/** /**
* Generate the javascript for the conditional field show / hiding logic. * Generate the javascript for the conditional field show / hiding logic.
* *

View File

@ -18,14 +18,6 @@ class EditableFormStep extends EditableFormField {
*/ */
private static $plural_name = 'Steps'; private static $plural_name = 'Steps';
/**
* @config
* @var array
*/
private static $has_many = array(
'Fields' => 'EditableFormField'
);
/** /**
* @return FieldList * @return FieldList
*/ */
@ -45,7 +37,7 @@ class EditableFormStep extends EditableFormField {
* @return FormField * @return FormField
*/ */
public function getFormField() { public function getFormField() {
return false; return CompositeField::create()->setTitle($this->Title);
} }
/** /**

View File

@ -8,7 +8,7 @@ jQuery(function ($) {
FORM_ID: 'UserForm_Form', // $Form.FormName.JS FORM_ID: 'UserForm_Form', // $Form.FormName.JS
ERROR_CONTAINER_ID: '', // $ErrorContainerID.JS ERROR_CONTAINER_ID: '', // $ErrorContainerID.JS
ENABLE_LIVE_VALIDATION: false, // $EnableLiveValidation ENABLE_LIVE_VALIDATION: false, // $EnableLiveValidation
DISPLAY_ERROR_MESSAGES_AT_TOP: true, // $DisplayErrorMessagesAtTop DISPLAY_ERROR_MESSAGES_AT_TOP: false, // $DisplayErrorMessagesAtTop
HIDE_FIELD_LABELS: false, // $HideFieldLabels HIDE_FIELD_LABELS: false, // $HideFieldLabels
MESSAGES: {} // var meaasges MESSAGES: {} // var meaasges
}; };
@ -63,9 +63,50 @@ jQuery(function ($) {
self.jumpToStep(stepNumber - 1); self.jumpToStep(stepNumber - 1);
}); });
this.$el.validate(this.validationOptions);
return this; return this;
} }
/*
* Default options for step validation. These get extended in main().
*/
UserForm.prototype.validationOptions = {
ignore: ':hidden',
errorClass: 'required',
errorElement: 'span',
errorPlacement: function (error, element) {
error.addClass('message');
if(element.is(':radio') || element.parents('.checkboxset').length > 0) {
error.insertAfter(element.closest('ul'));
} else {
error.insertAfter(element);
}
},
success: function (error) {
var errorId = $(error).attr('id');
error.remove();
if (CONSTANTS.DISPLAY_ERROR_MESSAGES_AT_TOP) {
// Pass the field's ID with the event.
$('.userform').trigger('userform.form.valid', [errorId.substr(0, errorId.indexOf('-error'))]);
}
},
messages: CONSTANTS.MESSAGES,
rules: {
// TODO
// <% loop $Fields %>
// <% if $Validation %><% if ClassName == EditableCheckboxGroupField %>
// '{$Name.JS}[]': {$ValidationJSON.RAW},
// <% else %>
// '{$Name.JS}': {$ValidationJSON.RAW},
// <% end_if %><% end_if %>
// <% end_loop %>
}
};
/** /**
* @func UserForm.addStep * @func UserForm.addStep
* @param {object} step - An instance of FormStep. * @param {object} step - An instance of FormStep.
@ -157,18 +198,40 @@ jQuery(function ($) {
return this; return this;
} }
/**
* @func hasErrors
* @return boolean
* @desc Checks if the error container has any error messages.
*/
ErrorContainer.prototype.hasErrors = function () {
return this.$el.find('.error-list').children().length > 0;
};
/**
* @func removeErrorMessage
* @desc Removes an error message from the error container.
*/
ErrorContainer.prototype.removeErrorMessage = function (fieldId) {
this.$el.find('#' + fieldId + '-top-error').remove();
// If there are no more error then hide the container.
if (!this.hasErrors()) {
this.hide();
}
};
/** /**
* @func ErrorContainer.updateErrorMessage * @func ErrorContainer.updateErrorMessage
* @param {object} input - The jQuery input object which contains the field to validate. * @param {object} $input - The jQuery input object which contains the field to validate.
* @param {object} message - The error message to display (html escaped). * @param {object} message - The error message to display (html escaped).
* @desc Update an error message (displayed at the top of the form). * @desc Update an error message (displayed at the top of the form).
*/ */
ErrorContainer.prototype.updateErrorMessage = function (input, message) { ErrorContainer.prototype.updateErrorMessage = function ($input, message) {
var inputID = input.attr('id'), var inputID = $input.attr('id'),
anchor = '#' + inputID, anchor = '#' + inputID,
elementID = inputID + '-top-error', elementID = inputID + '-top-error',
messageElement = $('#' + elementID), messageElement = $('#' + elementID),
describedBy = input.attr('aria-describedby'); describedBy = $input.attr('aria-describedby');
// The 'message' param will be an empty string if the field is valid. // The 'message' param will be an empty string if the field is valid.
if (!message) { if (!message) {
@ -186,7 +249,7 @@ jQuery(function ($) {
messageElement.show().find('a').html(message); messageElement.show().find('a').html(message);
} else { } else {
// Generate better link to field // Generate better link to field
input.closest('.field[id]').each(function(){ $input.closest('.field[id]').each(function(){
anchor = '#' + $(this).attr('id'); anchor = '#' + $(this).attr('id');
}); });
@ -202,14 +265,14 @@ jQuery(function ($) {
// link back to original input via aria // link back to original input via aria
// Respect existing non-error aria-describedby // Respect existing non-error aria-describedby
if ( !describedBy ) { if (!describedBy) {
describedBy = elementID; describedBy = elementID;
} else if ( !describedBy.match( new RegExp( "\\b" + elementID + "\\b" ) ) ) { } else if (!describedBy.match(new RegExp('\\b' + elementID + '\\b'))) {
// Add to end of list if not already present // Add to end of list if not already present
describedBy += " " + elementID; describedBy += " " + elementID;
} }
input.attr('aria-describedby', describedBy); $input.attr('aria-describedby', describedBy);
} }
}; };
@ -225,10 +288,6 @@ jQuery(function ($) {
this.$el = element instanceof jQuery ? element : $(element); this.$el = element instanceof jQuery ? element : $(element);
if (CONSTANTS.DISPLAY_ERROR_MESSAGES_AT_TOP) {
this.errorContainer = new ErrorContainer(this.$el.find('.error-container'));
}
// Has the step been viewed by the user? // Has the step been viewed by the user?
this.viewed = false; this.viewed = false;
@ -244,51 +303,31 @@ jQuery(function ($) {
self.$el.trigger('userform.step.next'); self.$el.trigger('userform.step.next');
}); });
// Set up validation for the step. if (CONSTANTS.DISPLAY_ERROR_MESSAGES_AT_TOP) {
this.$el.validate(this.validationOptions); this.errorContainer = new ErrorContainer(this.$el.find('.error-container'));
// Listen for errors on the UserForm.
this.$el.closest('.userform').on('userform.form.error', function (e, validator) {
// The step only cares about errors if it's currently visible.
if (!self.$el.is(':visible')) {
return;
}
// Add or update each error in the list.
$.each(validator.errorList, function (i, error) {
self.errorContainer.updateErrorMessage($(error.element), error.message);
});
});
// Listen for fields becoming valid
this.$el.closest('.userform').on('userform.form.valid', function (e, fieldId) {
self.errorContainer.removeErrorMessage(fieldId);
});
}
return this; return this;
} }
/*
* Default options for step validation. These get extended in main().
*/
FormStep.prototype.validationOptions = {
ignore: ':hidden',
errorClass: 'required',
errorElement: 'span',
errorPlacement: function (error, element) {
debugger;
error.addClass('message');
if(element.is(':radio') || element.parents('.checkboxset').length > 0) {
error.insertAfter(element.closest('ul'));
} else {
error.insertAfter(element);
}
if (CONSTANTS.DISPLAY_ERROR_MESSAGES_AT_TOP) {
// TODO
this.errorContainer.updateErrorMessage(element, error.html());
}
},
success: function (error) {
error.remove();
},
messages: CONSTANTS.MESSAGES,
rules: {
// TODO
// <% loop $Fields %>
// <% if $Validation %><% if ClassName == EditableCheckboxGroupField %>
// '{$Name.JS}[]': {$ValidationJSON.RAW},
// <% else %>
// '{$Name.JS}': {$ValidationJSON.RAW},
// <% end_if %><% end_if %>
// <% end_loop %>
}
};
/** /**
* @func ProgressBar * @func ProgressBar
* @constructor * @constructor
@ -360,8 +399,8 @@ jQuery(function ($) {
* @desc Bootstraps the front-end. * @desc Bootstraps the front-end.
*/ */
function main() { function main() {
var userform = new UserForm($('.userform')), var userform = null,
progressBar = new ProgressBar($('#userform-progress')); progressBar = null;
// Extend classes with common functionality. // Extend classes with common functionality.
$.extend(FormStep.prototype, commonMixin); $.extend(FormStep.prototype, commonMixin);
@ -370,7 +409,7 @@ jQuery(function ($) {
// Extend the default validation options with conditional options // Extend the default validation options with conditional options
// that are set by the user in the CMS. // that are set by the user in the CMS.
if (CONSTANTS.ENABLE_LIVE_VALIDATION) { if (CONSTANTS.ENABLE_LIVE_VALIDATION) {
$.extend(FormStep.prototype.validationOptions, { $.extend(UserForm.prototype.validationOptions, {
onfocusout: function (element) { onfocusout: function (element) {
this.element(element); this.element(element);
} }
@ -378,24 +417,18 @@ jQuery(function ($) {
} }
if (CONSTANTS.DISPLAY_ERROR_MESSAGES_AT_TOP) { if (CONSTANTS.DISPLAY_ERROR_MESSAGES_AT_TOP) {
$.extend(FormStep.prototype.validationOptions, { $.extend(UserForm.prototype.validationOptions, {
// Callback for custom code when an invalid form / step is submitted.
invalidHandler: function (event, validator) { invalidHandler: function (event, validator) {
var errorList = $('#' + CONSTANTS.ERROR_CONTAINER_ID + ' ul'); $('.userform').trigger('userform.form.error', [validator]);
// Update the error list with errors from the validator.
// We do this because top messages are not part of the regular
// error message life cycle, which jquery.validate handles for us.
errorList.empty();
$.each(validator.errorList, function () {
// TODO
this.errorContainer.updateErrorMessage($(this.element), this.message);
});
}, },
onfocusout: false onfocusout: false
}); });
} }
userform = new UserForm($('.userform'));
progressBar = new ProgressBar($('#userform-progress'));
// Conditionally hide field labels and use HTML5 placeholder instead. // Conditionally hide field labels and use HTML5 placeholder instead.
if (CONSTANTS.HIDE_FIELD_LABELS) { if (CONSTANTS.HIDE_FIELD_LABELS) {
$('#' + CONSTANTS.FORM_ID + ' label.left').each(function () { $('#' + CONSTANTS.FORM_ID + ' label.left').each(function () {

View File

@ -13,18 +13,18 @@
<% loop $FormSteps %> <% loop $FormSteps %>
<fieldset class="form-step"> <fieldset class="form-step">
<% if $DisplayErrorMessagesAtTop %> <% if $Top.DisplayErrorMessagesAtTop %>
<fieldset class="error-container" aria-hidden="true" style="display: none;"> <fieldset class="error-container" aria-hidden="true" style="display: none;">
<div> <div>
<h4></h4> <h4></h4>
<ul></ul> <ul class="error-list"></ul>
</div> </div>
</fieldset> </fieldset>
<% end_if %> <% end_if %>
<h2>$Title</h2> <h2>$Title</h2>
<% loop $Fields %> <% loop $Children %>
$FieldHolder $FieldHolder
<% end_loop %> <% end_loop %>

View File

@ -1,157 +0,0 @@
(function($) {
$(document).ready(function() {
var formId = "{$Form.FormName.JS}",
errorContainerId = "{$ErrorContainerID.JS}",
errorContainer = $('<fieldset><div><h4></h4><ul></ul></div></fieldset>');
var messages = {<% loop $Fields %><% if $ErrorMessage && not $SetsOwnError %><% if $ClassName == 'EditableCheckboxGroupField' %>
'{$Name.JS}[]': '{$ErrorMessage.JS}'<% if not Last %>,<% end_if %><% else %>
'{$Name.JS}': '{$ErrorMessage.JS}'<% if not Last %>,<% end_if %><% end_if %><% end_if %><% end_loop %>
};
$(document).on("click", "input.text[data-showcalendar]", function() {
$(this).ssDatepicker();
if($(this).data('datepicker')) {
$(this).datepicker('show');
}
});
$("#" + formId).validate({
ignore: ':hidden',
errorClass: "required",
errorElement: "span",
errorPlacement: function(error, element) {
error.addClass('message');
if(element.is(":radio") || element.parents(".checkboxset").length > 0) {
error.insertAfter(element.closest("ul"));
} else {
error.insertAfter(element);
}
<% if $DisplayErrorMessagesAtTop %>
applyTopErrorMessage(element, error.html());
<% end_if %>
},
success: function (error) {
error.remove();
},
messages: messages,
rules: {
<% loop $Fields %>
<% if $Validation %><% if ClassName == EditableCheckboxGroupField %>
'{$Name.JS}[]': {$ValidationJSON.RAW},
<% else %>
'{$Name.JS}': {$ValidationJSON.RAW},
<% end_if %><% end_if %>
<% end_loop %>
}
/*
* Conditional options.
* Using leading commas so we don't get a trailing comma on
* the last option. Trailing commas can break IE.
*/
<% if $EnableLiveValidation %>
// Enable live validation
,onfocusout: function (element) { this.element(element); }
<% end_if %>
<% if $DisplayErrorMessagesAtTop %>
,invalidHandler: function (event, validator) {
var errorList = $('#' + errorContainerId + ' ul');
// Update the error list with errors from the validator.
// We do this because top messages are not part of the regular
// error message life cycle, which jquery.validate handles for us.
errorList.empty();
$.each(validator.errorList, function () {
applyTopErrorMessage($(this.element), this.message);
});
}
,onfocusout: false
<% end_if %>
});
<% if $HideFieldLabels %>
// Hide field labels (use HTML5 placeholder instead)
$("#" + formId + "label.left").each(function() {
$("#"+$(this).attr("for"))
.attr("placeholder", $(this).text());
$(this).remove();
});
Placeholders.init();
<% end_if %>
<% if $DisplayErrorMessagesAtTop %>
/**
* @applyTopErrorMessage
* @param {jQuery} input - The jQuery input object which contains the field to validate
* @param {string} message - The error message to display (html escaped)
* @desc Update an error message (displayed at the top of the form).
*/
function applyTopErrorMessage(input, message) {
var inputID = input.attr('id'),
anchor = '#' + inputID,
elementID = inputID + '-top-error',
errorContainer = $('#' + errorContainerId),
messageElement = $('#' + elementID),
describedBy = input.attr('aria-describedby');
// The 'message' param will be an empty string if the field is valid.
if (!message) {
// Style issues as fixed if they already exist
messageElement.addClass('fixed');
return;
}
messageElement.removeClass('fixed');
errorContainer.show();
if (messageElement.length === 1) {
// Update the existing error message.
messageElement.show().find('a').html(message);
} else {
// Generate better link to field
input.closest('.field[id]').each(function(){
anchor = '#' + $(this).attr('id');
});
// Add a new error message
messageElement = $('<li><a></a></li>');
messageElement
.attr('id', elementID)
.find('a')
.attr('href', location.pathname + location.search + anchor)
.html(message);
errorContainer
.find('ul')
.append(messageElement);
// link back to original input via aria
// Respect existing non-error aria-describedby
if ( !describedBy ) {
describedBy = elementID;
} else if ( !describedBy.match( new RegExp( "\\b" + elementID + "\\b" ) ) ) {
// Add to end of list if not already present
describedBy += " " + elementID;
}
input.attr( "aria-describedby", describedBy );
}
}
// Build container
errorContainer
.hide()
.attr('id', errorContainerId)
.find('h4')
.text(ss.i18n._t(
"UserForms.ERROR_CONTAINER_HEADER",
"Please correct the following errors and try again:"
));
$('#' + formId).prepend(errorContainer);
<% end_if %>
});
})(jQuery);