From 65651387e0abd9b9b880fd246ced05fa6a68e79e Mon Sep 17 00:00:00 2001 From: David Craig Date: Mon, 10 Aug 2015 15:18:50 +1200 Subject: [PATCH] Update step rendering --- code/forms/UserForm.php | 34 ++-- code/model/UserDefinedForm.php | 16 -- .../editableformfields/EditableFormStep.php | 10 +- javascript/UserForm.js | 175 +++++++++++------- templates/UserForm.ss | 16 +- templates/ValidationScript.ss | 157 ---------------- 6 files changed, 134 insertions(+), 274 deletions(-) delete mode 100644 templates/ValidationScript.ss diff --git a/code/forms/UserForm.php b/code/forms/UserForm.php index ce21a3a..7787f12 100644 --- a/code/forms/UserForm.php +++ b/code/forms/UserForm.php @@ -44,6 +44,13 @@ class UserForm extends Form { return $this->controller->LastEdited; } + /** + * @return boolean + */ + public function getDisplayErrorMessagesAtTop() { + return $this->controller->DisplayErrorMessagesAtTop; + } + /** * @return array */ @@ -66,11 +73,18 @@ class UserForm extends Form { public function getFormSteps() { $steps = new ArrayList(); - foreach ($this->controller->Fields()->filter('ClassName', 'EditableFormStep') as $step) { - $steps->push(array( - 'Title' => $step->Title, - 'Fields' => $this->getFormFields($step) - )); + foreach ($this->controller->Fields() as $field) { + if ($field instanceof EditableFormStep) { + $steps->push($field->getFormField()); + 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; @@ -81,18 +95,12 @@ class UserForm extends Form { * by using {@link updateFormFields()} on an {@link Extension} subclass which * is applied to this controller. * - * @param EditableFormStep $parent - * * @return FieldList */ - public function getFormFields($parent = null) { - if(!$parent) { - $parent = $this->controller; - } - + public function getFormFields() { $fields = new FieldList(); - foreach($parent->Fields() as $editableField) { + foreach($this->controller->Fields() as $editableField) { // get the raw form field from the editable version $field = $editableField->getFormField(); diff --git a/code/model/UserDefinedForm.php b/code/model/UserDefinedForm.php index a271286..9e3e2d1 100755 --- a/code/model/UserDefinedForm.php +++ b/code/model/UserDefinedForm.php @@ -367,26 +367,10 @@ class UserDefinedForm_Controller extends Page_Controller { $form = UserForm::create($this); $this->generateConditionalJavascript(); - $this->generateValidationJavascript($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. * diff --git a/code/model/editableformfields/EditableFormStep.php b/code/model/editableformfields/EditableFormStep.php index c0b666f..242422e 100644 --- a/code/model/editableformfields/EditableFormStep.php +++ b/code/model/editableformfields/EditableFormStep.php @@ -18,14 +18,6 @@ class EditableFormStep extends EditableFormField { */ private static $plural_name = 'Steps'; - /** - * @config - * @var array - */ - private static $has_many = array( - 'Fields' => 'EditableFormField' - ); - /** * @return FieldList */ @@ -45,7 +37,7 @@ class EditableFormStep extends EditableFormField { * @return FormField */ public function getFormField() { - return false; + return CompositeField::create()->setTitle($this->Title); } /** diff --git a/javascript/UserForm.js b/javascript/UserForm.js index 76f3532..df5baa3 100644 --- a/javascript/UserForm.js +++ b/javascript/UserForm.js @@ -8,7 +8,7 @@ jQuery(function ($) { FORM_ID: 'UserForm_Form', // $Form.FormName.JS ERROR_CONTAINER_ID: '', // $ErrorContainerID.JS ENABLE_LIVE_VALIDATION: false, // $EnableLiveValidation - DISPLAY_ERROR_MESSAGES_AT_TOP: true, // $DisplayErrorMessagesAtTop + DISPLAY_ERROR_MESSAGES_AT_TOP: false, // $DisplayErrorMessagesAtTop HIDE_FIELD_LABELS: false, // $HideFieldLabels MESSAGES: {} // var meaasges }; @@ -63,9 +63,50 @@ jQuery(function ($) { self.jumpToStep(stepNumber - 1); }); + this.$el.validate(this.validationOptions); + 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 * @param {object} step - An instance of FormStep. @@ -157,18 +198,40 @@ jQuery(function ($) { 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 - * @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). * @desc Update an error message (displayed at the top of the form). */ - ErrorContainer.prototype.updateErrorMessage = function (input, message) { - var inputID = input.attr('id'), + ErrorContainer.prototype.updateErrorMessage = function ($input, message) { + var inputID = $input.attr('id'), anchor = '#' + inputID, elementID = inputID + '-top-error', 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. if (!message) { @@ -186,10 +249,10 @@ jQuery(function ($) { messageElement.show().find('a').html(message); } else { // Generate better link to field - input.closest('.field[id]').each(function(){ + $input.closest('.field[id]').each(function(){ anchor = '#' + $(this).attr('id'); }); - + // Add a new error message messageElement = $('
  • '); messageElement @@ -199,17 +262,17 @@ jQuery(function ($) { .html(message); this.$el.find('ul').append(messageElement); - + // link back to original input via aria // Respect existing non-error aria-describedby - if ( !describedBy ) { + if (!describedBy) { 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 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); - if (CONSTANTS.DISPLAY_ERROR_MESSAGES_AT_TOP) { - this.errorContainer = new ErrorContainer(this.$el.find('.error-container')); - } - // Has the step been viewed by the user? this.viewed = false; @@ -244,51 +303,31 @@ jQuery(function ($) { self.$el.trigger('userform.step.next'); }); - // Set up validation for the step. - this.$el.validate(this.validationOptions); + if (CONSTANTS.DISPLAY_ERROR_MESSAGES_AT_TOP) { + 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; } - /* - * 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 * @constructor @@ -360,8 +399,8 @@ jQuery(function ($) { * @desc Bootstraps the front-end. */ function main() { - var userform = new UserForm($('.userform')), - progressBar = new ProgressBar($('#userform-progress')); + var userform = null, + progressBar = null; // Extend classes with common functionality. $.extend(FormStep.prototype, commonMixin); @@ -370,7 +409,7 @@ jQuery(function ($) { // Extend the default validation options with conditional options // that are set by the user in the CMS. if (CONSTANTS.ENABLE_LIVE_VALIDATION) { - $.extend(FormStep.prototype.validationOptions, { + $.extend(UserForm.prototype.validationOptions, { onfocusout: function (element) { this.element(element); } @@ -378,24 +417,18 @@ jQuery(function ($) { } 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) { - var errorList = $('#' + CONSTANTS.ERROR_CONTAINER_ID + ' 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 () { - // TODO - this.errorContainer.updateErrorMessage($(this.element), this.message); - }); + $('.userform').trigger('userform.form.error', [validator]); }, onfocusout: false }); } + userform = new UserForm($('.userform')); + progressBar = new ProgressBar($('#userform-progress')); + // Conditionally hide field labels and use HTML5 placeholder instead. if (CONSTANTS.HIDE_FIELD_LABELS) { $('#' + CONSTANTS.FORM_ID + ' label.left').each(function () { diff --git a/templates/UserForm.ss b/templates/UserForm.ss index a60e2b9..8aa1154 100644 --- a/templates/UserForm.ss +++ b/templates/UserForm.ss @@ -13,18 +13,18 @@ <% loop $FormSteps %>
    - <% if $DisplayErrorMessagesAtTop %> - + <% if $Top.DisplayErrorMessagesAtTop %> + <% end_if %>

    $Title

    - <% loop $Fields %> + <% loop $Children %> $FieldHolder <% end_loop %> diff --git a/templates/ValidationScript.ss b/templates/ValidationScript.ss deleted file mode 100644 index 2e02cb8..0000000 --- a/templates/ValidationScript.ss +++ /dev/null @@ -1,157 +0,0 @@ -(function($) { - $(document).ready(function() { - var formId = "{$Form.FormName.JS}", - errorContainerId = "{$ErrorContainerID.JS}", - errorContainer = $('

      '); - - 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 = $('
    • '); - 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);