Implement client-side registration form validation.
Input forms that need validation will have AJAX requests performed to get validation decisions live. All but a few important and common form fields perform generic validation; these will need a back-end handler in the future in order to have them validated through AJAX requests. Information is conveyed on focus and blur for both errors and successes.
This commit is contained in:
@@ -30,6 +30,17 @@
|
||||
confirm_email: 'xsy@edx.org',
|
||||
honor_code: true
|
||||
},
|
||||
$email = null,
|
||||
$name = null,
|
||||
$username = null,
|
||||
$password = null,
|
||||
$levelOfEducation = null,
|
||||
$gender = null,
|
||||
$yearOfBirth = null,
|
||||
$mailingAddress = null,
|
||||
$goals = null,
|
||||
$confirmEmail = null,
|
||||
$honorCode = null,
|
||||
THIRD_PARTY_AUTH = {
|
||||
currentProvider: null,
|
||||
providers: [
|
||||
@@ -49,9 +60,26 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
VALIDATION_DECISIONS_POSITIVE = {
|
||||
validation_decisions: {
|
||||
email: '',
|
||||
username: '',
|
||||
password: '',
|
||||
confirm_email: ''
|
||||
}
|
||||
},
|
||||
VALIDATION_DECISIONS_NEGATIVE = {
|
||||
validation_decisions: {
|
||||
email: 'Error.',
|
||||
username: 'Error.',
|
||||
password: 'Error.',
|
||||
confirm_email: 'Error'
|
||||
}
|
||||
},
|
||||
FORM_DESCRIPTION = {
|
||||
method: 'post',
|
||||
submit_url: '/user_api/v1/account/registration/',
|
||||
validation_url: '/api/user/v1/validation/registration',
|
||||
fields: [
|
||||
{
|
||||
placeholder: 'username@domain.com',
|
||||
@@ -110,10 +138,10 @@
|
||||
defaultValue: '',
|
||||
type: 'select',
|
||||
options: [
|
||||
{value: '', name: '--'},
|
||||
{value: 'p', name: 'Doctorate'},
|
||||
{value: 'm', name: "Master's or professional degree"},
|
||||
{value: 'b', name: "Bachelor's degree"}
|
||||
{value: '', name: '--'},
|
||||
{value: 'p', name: 'Doctorate'},
|
||||
{value: 'm', name: "Master's or professional degree"},
|
||||
{value: 'b', name: "Bachelor's degree"}
|
||||
],
|
||||
required: false,
|
||||
instructions: 'Select your education level.',
|
||||
@@ -126,10 +154,10 @@
|
||||
defaultValue: '',
|
||||
type: 'select',
|
||||
options: [
|
||||
{value: '', name: '--'},
|
||||
{value: 'm', name: 'Male'},
|
||||
{value: 'f', name: 'Female'},
|
||||
{value: 'o', name: 'Other'}
|
||||
{value: '', name: '--'},
|
||||
{value: 'm', name: 'Male'},
|
||||
{value: 'f', name: 'Female'},
|
||||
{value: 'o', name: 'Other'}
|
||||
],
|
||||
required: false,
|
||||
instructions: 'Select your gender.',
|
||||
@@ -142,10 +170,10 @@
|
||||
defaultValue: '',
|
||||
type: 'select',
|
||||
options: [
|
||||
{value: '', name: '--'},
|
||||
{value: 1900, name: '1900'},
|
||||
{value: 1950, name: '1950'},
|
||||
{value: 2014, name: '2014'}
|
||||
{value: '', name: '--'},
|
||||
{value: 1900, name: '1900'},
|
||||
{value: 1950, name: '1950'},
|
||||
{value: 2014, name: '2014'}
|
||||
],
|
||||
required: false,
|
||||
instructions: 'Select your year of birth.',
|
||||
@@ -185,7 +213,6 @@
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var createRegisterView = function(that) {
|
||||
// Initialize the register model
|
||||
model = new RegisterModel({}, {
|
||||
@@ -209,6 +236,43 @@
|
||||
view.on('auth-complete', function() {
|
||||
authComplete = true;
|
||||
});
|
||||
|
||||
// Target each form field.
|
||||
$email = $('#register-email');
|
||||
$confirmEmail = $('#register-confirm_email');
|
||||
$name = $('#register-name');
|
||||
$username = $('#register-username');
|
||||
$password = $('#register-password');
|
||||
$levelOfEducation = $('#register-level_of_education');
|
||||
$gender = $('#register-gender');
|
||||
$yearOfBirth = $('#register-year_of_birth');
|
||||
$mailingAddress = $('#register-mailing_address');
|
||||
$goals = $('#register-goals');
|
||||
$honorCode = $('#register-honor_code');
|
||||
};
|
||||
|
||||
var fillData = function() {
|
||||
$email.val(USER_DATA.email);
|
||||
$confirmEmail.val(USER_DATA.email);
|
||||
$name.val(USER_DATA.name);
|
||||
$username.val(USER_DATA.username);
|
||||
$password.val(USER_DATA.password);
|
||||
$levelOfEducation.val(USER_DATA.level_of_education);
|
||||
$gender.val(USER_DATA.gender);
|
||||
$yearOfBirth.val(USER_DATA.year_of_birth);
|
||||
$mailingAddress.val(USER_DATA.mailing_address);
|
||||
$goals.val(USER_DATA.goals);
|
||||
// Check the honor code checkbox
|
||||
$honorCode.prop('checked', USER_DATA.honor_code);
|
||||
};
|
||||
|
||||
var liveValidate = function($el, validationSuccess) {
|
||||
$el.focus();
|
||||
if (!_.isUndefined(validationSuccess) && !validationSuccess) {
|
||||
model.trigger('validation', $el, VALIDATION_DECISIONS_NEGATIVE);
|
||||
} else {
|
||||
model.trigger('validation', $el, VALIDATION_DECISIONS_POSITIVE);
|
||||
}
|
||||
};
|
||||
|
||||
var submitForm = function(validationSuccess) {
|
||||
@@ -216,19 +280,7 @@
|
||||
var clickEvent = $.Event('click');
|
||||
|
||||
// Simulate manual entry of registration form data
|
||||
$('#register-email').val(USER_DATA.email);
|
||||
$('#register-confirm_email').val(USER_DATA.email);
|
||||
$('#register-name').val(USER_DATA.name);
|
||||
$('#register-username').val(USER_DATA.username);
|
||||
$('#register-password').val(USER_DATA.password);
|
||||
$('#register-level_of_education').val(USER_DATA.level_of_education);
|
||||
$('#register-gender').val(USER_DATA.gender);
|
||||
$('#register-year_of_birth').val(USER_DATA.year_of_birth);
|
||||
$('#register-mailing_address').val(USER_DATA.mailing_address);
|
||||
$('#register-goals').val(USER_DATA.goals);
|
||||
|
||||
// Check the honor code checkbox
|
||||
$('#register-honor_code').prop('checked', USER_DATA.honor_code);
|
||||
fillData();
|
||||
|
||||
// If validationSuccess isn't passed, we avoid
|
||||
// spying on `view.validate` twice
|
||||
@@ -238,6 +290,10 @@
|
||||
isValid: validationSuccess,
|
||||
message: 'Submission was validated.'
|
||||
});
|
||||
// Successful validation means there's no need to use AJAX calls from liveValidate,
|
||||
if (validationSuccess) {
|
||||
spyOn(view, 'liveValidate').and.callFake(function() {});
|
||||
}
|
||||
}
|
||||
|
||||
// Submit the email address
|
||||
@@ -284,6 +340,7 @@
|
||||
if (param === '?course_id') {
|
||||
return encodeURIComponent(COURSE_ID);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
// Attempt to register
|
||||
@@ -308,17 +365,17 @@
|
||||
expect($('.button-oa2-facebook')).toBeVisible();
|
||||
});
|
||||
|
||||
it('validates registration form fields', function() {
|
||||
it('validates registration form fields on form submission', function() {
|
||||
createRegisterView(this);
|
||||
|
||||
// Submit the form, with successful validation
|
||||
submitForm(true);
|
||||
|
||||
// Verify that validation of form fields occurred
|
||||
expect(view.validate).toHaveBeenCalledWith($('#register-email')[0]);
|
||||
expect(view.validate).toHaveBeenCalledWith($('#register-name')[0]);
|
||||
expect(view.validate).toHaveBeenCalledWith($('#register-username')[0]);
|
||||
expect(view.validate).toHaveBeenCalledWith($('#register-password')[0]);
|
||||
expect(view.validate).toHaveBeenCalledWith($email[0]);
|
||||
expect(view.validate).toHaveBeenCalledWith($name[0]);
|
||||
expect(view.validate).toHaveBeenCalledWith($username[0]);
|
||||
expect(view.validate).toHaveBeenCalledWith($password[0]);
|
||||
|
||||
// Verify that no submission errors are visible
|
||||
expect(view.$formFeedback.find('.' + view.formErrorsJsHook).length).toEqual(0);
|
||||
@@ -327,7 +384,34 @@
|
||||
expect(view.$submitButton).toHaveAttr('disabled');
|
||||
});
|
||||
|
||||
it('displays registration form validation errors', function() {
|
||||
it('live validates registration form fields', function() {
|
||||
var requiredValidationFields = [$email, $confirmEmail, $username, $password],
|
||||
i,
|
||||
$el;
|
||||
createRegisterView(this);
|
||||
|
||||
for (i = 0; i < requiredValidationFields.length; ++i) {
|
||||
$el = requiredValidationFields[i];
|
||||
|
||||
// Perform successful live validations.
|
||||
liveValidate($el);
|
||||
|
||||
// Confirm success.
|
||||
expect($el).toHaveClass('success');
|
||||
|
||||
// Confirm that since we've blurred from each input, required text doesn't show.
|
||||
expect(view.getRequiredTextLabel($el)).toHaveClass('hidden');
|
||||
|
||||
// Confirm fa-check shows.
|
||||
expect(view.getIcon($el)).toHaveClass('fa-check');
|
||||
expect(view.getIcon($el)).toBeVisible();
|
||||
|
||||
// Confirm the error tip is empty.
|
||||
expect(view.getErrorTip($el).val().length).toBe(0);
|
||||
}
|
||||
});
|
||||
|
||||
it('displays registration form validation errors on form submission', function() {
|
||||
createRegisterView(this);
|
||||
|
||||
// Submit the form, with failed validation
|
||||
@@ -343,7 +427,34 @@
|
||||
expect(view.$submitButton).not.toHaveAttr('disabled');
|
||||
});
|
||||
|
||||
it('displays an error if the server returns an error while registering', function() {
|
||||
it('displays live registration form validation errors', function() {
|
||||
var requiredValidationFields = [$email, $confirmEmail, $username, $password],
|
||||
i,
|
||||
$el;
|
||||
createRegisterView(this);
|
||||
|
||||
for (i = 0; i < requiredValidationFields.length; ++i) {
|
||||
$el = requiredValidationFields[i];
|
||||
|
||||
// Perform invalid live validations.
|
||||
liveValidate($el, false);
|
||||
|
||||
// Confirm error.
|
||||
expect($el).toHaveClass('error');
|
||||
|
||||
// Confirm that since we've blurred from each input, required text still shows for errors.
|
||||
expect(view.getRequiredTextLabel($el)).not.toHaveClass('hidden');
|
||||
|
||||
// Confirm fa-times shows.
|
||||
expect(view.getIcon($el)).toHaveClass('fa-exclamation');
|
||||
expect(view.getIcon($el)).toBeVisible();
|
||||
|
||||
// Confirm the error tip shows an error message.
|
||||
expect(view.getErrorTip($el).val()).not.toBeEmpty();
|
||||
}
|
||||
});
|
||||
|
||||
it('displays an error on form submission if the server returns an error', function() {
|
||||
createRegisterView(this);
|
||||
|
||||
// Submit the form, with successful validation
|
||||
|
||||
@@ -79,6 +79,7 @@
|
||||
var buildIframe = function(link, modalSelector, contentSelector, tosLinkSelector) {
|
||||
// Create an iframe with contents from the link and set its height to match the content area
|
||||
return $('<iframe>', {
|
||||
title: 'Terms of Service and Honor Code',
|
||||
src: link.href,
|
||||
load: function() {
|
||||
var $iframeHead = $(this).contents().find('head'),
|
||||
|
||||
@@ -6,43 +6,30 @@
|
||||
'backbone',
|
||||
'common/js/utils/edx.utils.validate',
|
||||
'edx-ui-toolkit/js/utils/html-utils',
|
||||
'edx-ui-toolkit/js/utils/string-utils',
|
||||
'text!templates/student_account/form_errors.underscore'
|
||||
],
|
||||
function($, _, Backbone, EdxUtilsValidate, HtmlUtils, formErrorsTpl) {
|
||||
], function($, _, Backbone, EdxUtilsValidate, HtmlUtils, StringUtils, formErrorsTpl) {
|
||||
return Backbone.View.extend({
|
||||
tagName: 'form',
|
||||
|
||||
el: '',
|
||||
|
||||
tpl: '',
|
||||
|
||||
fieldTpl: '#form_field-tpl',
|
||||
|
||||
formErrorsTpl: formErrorsTpl,
|
||||
|
||||
formErrorsJsHook: 'js-form-errors',
|
||||
|
||||
defaultFormErrorsTitle: gettext('An error occurred.'),
|
||||
|
||||
events: {},
|
||||
|
||||
errors: [],
|
||||
|
||||
formType: '',
|
||||
|
||||
$form: {},
|
||||
|
||||
fields: [],
|
||||
|
||||
liveValidationFields: [],
|
||||
// String to append to required label fields
|
||||
requiredStr: '',
|
||||
|
||||
/*
|
||||
Translators: This string is appended to optional field labels on the student login, registration, and
|
||||
profile forms.
|
||||
Translators: This string is appended to optional field labels on the student login, registration, and
|
||||
profile forms.
|
||||
*/
|
||||
optionalStr: gettext('(optional)'),
|
||||
|
||||
submitButton: '',
|
||||
|
||||
initialize: function(data) {
|
||||
@@ -157,7 +144,7 @@
|
||||
$label,
|
||||
key = '',
|
||||
errors = [],
|
||||
test = {};
|
||||
validation = {};
|
||||
|
||||
for (i = 0; i < len; i++) {
|
||||
$el = $(elements[i]);
|
||||
@@ -171,13 +158,13 @@
|
||||
}
|
||||
|
||||
if (key) {
|
||||
test = this.validate(elements[i]);
|
||||
if (test.isValid) {
|
||||
validation = this.validate(elements[i]);
|
||||
if (validation.isValid) {
|
||||
obj[key] = $el.attr('type') === 'checkbox' ? $el.is(':checked') : $el.val();
|
||||
$el.removeClass('error');
|
||||
$label.removeClass('error');
|
||||
} else {
|
||||
errors.push(test.message);
|
||||
errors.push(validation.message);
|
||||
$el.addClass('error');
|
||||
$label.addClass('error');
|
||||
}
|
||||
@@ -190,7 +177,13 @@
|
||||
},
|
||||
|
||||
saveError: function(error) {
|
||||
this.errors = ['<li>' + error.responseText + '</li>'];
|
||||
this.errors = [
|
||||
StringUtils.interpolate(
|
||||
'<li>{error}</li>', {
|
||||
error: error.responseText
|
||||
}
|
||||
)
|
||||
];
|
||||
this.renderErrors(this.defaultFormErrorsTitle, this.errors);
|
||||
this.toggleDisableButton(false);
|
||||
},
|
||||
@@ -201,24 +194,48 @@
|
||||
renderErrors: function(title, errorMessages) {
|
||||
this.clearFormErrors();
|
||||
|
||||
this.renderFormFeedback(this.formErrorsTpl, {
|
||||
jsHook: this.formErrorsJsHook,
|
||||
title: title,
|
||||
messagesHtml: HtmlUtils.HTML(errorMessages.join(''))
|
||||
});
|
||||
if (title || errorMessages.length) {
|
||||
this.renderFormFeedback(this.formErrorsTpl, {
|
||||
jsHook: this.formErrorsJsHook,
|
||||
title: title,
|
||||
messagesHtml: HtmlUtils.HTML(errorMessages.join(''))
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
renderFormFeedback: function(template, context) {
|
||||
var tpl = HtmlUtils.template(template);
|
||||
HtmlUtils.prepend(this.$formFeedback, tpl(context));
|
||||
},
|
||||
|
||||
// Scroll to feedback container
|
||||
$('html,body').animate({
|
||||
scrollTop: this.$formFeedback.offset().top
|
||||
}, 'slow');
|
||||
doOnErrorList: function(id, action) {
|
||||
var i;
|
||||
for (i = 0; i < this.errors.length; ++i) {
|
||||
if (this.errors[i].includes(id)) {
|
||||
action(i);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Focus on the feedback container to ensure screen readers see the messages.
|
||||
this.$formFeedback.focus();
|
||||
updateError: function(error, id) {
|
||||
this.deleteError(id);
|
||||
this.addError(error, id);
|
||||
},
|
||||
|
||||
deleteError: function(id) {
|
||||
var self = this;
|
||||
this.doOnErrorList(id, function(index) {
|
||||
self.errors.splice(index, 1);
|
||||
});
|
||||
},
|
||||
|
||||
addError: function(error, id) {
|
||||
this.errors.push(StringUtils.interpolate(
|
||||
'<li id="{errorId}">{error}</li>', {
|
||||
errorId: id,
|
||||
error: error
|
||||
}
|
||||
));
|
||||
},
|
||||
|
||||
/* Allows extended views to add non-form attributes
|
||||
@@ -244,6 +261,14 @@
|
||||
this.clearFormErrors();
|
||||
} else {
|
||||
this.renderErrors(this.defaultFormErrorsTitle, this.errors);
|
||||
|
||||
// Scroll to feedback container
|
||||
$('html,body').animate({
|
||||
scrollTop: this.$formFeedback.offset().top
|
||||
}, 'slow');
|
||||
|
||||
// Focus on the feedback container to ensure screen readers see the messages.
|
||||
this.$formFeedback.focus();
|
||||
this.toggleDisableButton(false);
|
||||
}
|
||||
|
||||
@@ -285,6 +310,29 @@
|
||||
|
||||
validate: function($el) {
|
||||
return EdxUtilsValidate.validate($el);
|
||||
},
|
||||
|
||||
liveValidate: function($el, url, dataType, data, method, model) {
|
||||
$.ajax({
|
||||
url: url,
|
||||
dataType: dataType,
|
||||
data: data,
|
||||
method: method,
|
||||
success: function(response) {
|
||||
model.trigger('validation', $el, response);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
inLiveValidationFields: function($el) {
|
||||
var i,
|
||||
name = $el.attr('name') || false;
|
||||
for (i = 0; i < this.liveValidationFields.length; ++i) {
|
||||
if (this.liveValidationFields[i] === name) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,29 +4,45 @@
|
||||
'jquery',
|
||||
'underscore',
|
||||
'gettext',
|
||||
'edx-ui-toolkit/js/utils/string-utils',
|
||||
'js/student_account/views/FormView',
|
||||
'text!templates/student_account/form_status.underscore'
|
||||
],
|
||||
function($, _, gettext, FormView, formStatusTpl) {
|
||||
function(
|
||||
$, _, gettext,
|
||||
StringUtils,
|
||||
FormView,
|
||||
formStatusTpl
|
||||
) {
|
||||
return FormView.extend({
|
||||
el: '#register-form',
|
||||
|
||||
tpl: '#register-tpl',
|
||||
|
||||
validationUrl: '/api/user/v1/validation/registration',
|
||||
events: {
|
||||
'click .js-register': 'submitForm',
|
||||
'click .login-provider': 'thirdPartyAuth'
|
||||
'click .login-provider': 'thirdPartyAuth',
|
||||
'click input[required][type="checkbox"]': 'liveValidateHandler',
|
||||
'blur input[required], textarea[required], select[required]': 'liveValidateHandler',
|
||||
'focus input[required], textarea[required], select[required]': 'handleRequiredInputFocus'
|
||||
},
|
||||
|
||||
liveValidationFields: [
|
||||
'name',
|
||||
'username',
|
||||
'password',
|
||||
'email',
|
||||
'confirm_email',
|
||||
'country',
|
||||
'honor_code',
|
||||
'terms_of_service'
|
||||
],
|
||||
formType: 'register',
|
||||
|
||||
formStatusTpl: formStatusTpl,
|
||||
|
||||
authWarningJsHook: 'js-auth-warning',
|
||||
|
||||
defaultFormErrorsTitle: gettext('We couldn\'t create your account.'),
|
||||
|
||||
submitButton: '.js-register',
|
||||
positiveValidationIcon: 'fa-check',
|
||||
negativeValidationIcon: 'fa-exclamation',
|
||||
successfulValidationDisplaySeconds: 3,
|
||||
|
||||
preRender: function(data) {
|
||||
this.providers = data.thirdPartyAuth.providers || [];
|
||||
@@ -41,6 +57,7 @@
|
||||
this.autoRegisterWelcomeMessage = data.thirdPartyAuth.autoRegisterWelcomeMessage || '';
|
||||
|
||||
this.listenTo(this.model, 'sync', this.saveSuccess);
|
||||
this.listenTo(this.model, 'validation', this.renderLiveValidations);
|
||||
},
|
||||
|
||||
render: function(html) {
|
||||
@@ -79,6 +96,144 @@
|
||||
return this;
|
||||
},
|
||||
|
||||
hideRequiredMessageExceptOnError: function($el) {
|
||||
// We only handle blur if not in an error state.
|
||||
if (!$el.hasClass('error')) {
|
||||
this.hideRequiredMessage($el);
|
||||
}
|
||||
},
|
||||
|
||||
hideRequiredMessage: function($el) {
|
||||
this.doOnInputLabel($el, function($label) {
|
||||
$label.addClass('hidden');
|
||||
});
|
||||
},
|
||||
|
||||
doOnInputLabel: function($el, action) {
|
||||
var $label = this.getRequiredTextLabel($el);
|
||||
action($label);
|
||||
},
|
||||
|
||||
handleRequiredInputFocus: function(event) {
|
||||
var $el = $(event.currentTarget);
|
||||
// Avoid rendering for required checkboxes.
|
||||
if ($el.attr('type') !== 'checkbox') {
|
||||
this.renderRequiredMessage($el);
|
||||
}
|
||||
if ($el.hasClass('error')) {
|
||||
this.doOnInputLabel($el, function($label) {
|
||||
$label.addClass('error');
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
renderRequiredMessage: function($el) {
|
||||
this.doOnInputLabel($el, function($label) {
|
||||
$label.removeClass('hidden').text(gettext('(required)'));
|
||||
});
|
||||
},
|
||||
|
||||
getRequiredTextLabel: function($el) {
|
||||
return $('#' + $el.attr('id') + '-required-label');
|
||||
},
|
||||
|
||||
renderLiveValidations: function($el, decisions) {
|
||||
var $label = this.getLabel($el),
|
||||
$requiredTextLabel = this.getRequiredTextLabel($el),
|
||||
$icon = this.getIcon($el),
|
||||
$errorTip = this.getErrorTip($el),
|
||||
name = $el.attr('name'),
|
||||
type = $el.attr('type'),
|
||||
isCheckbox = type === 'checkbox',
|
||||
hasError = decisions.validation_decisions[name] !== '',
|
||||
error = isCheckbox ? '' : decisions.validation_decisions[name];
|
||||
|
||||
if (hasError) {
|
||||
this.renderLiveValidationError($el, $label, $requiredTextLabel, $icon, $errorTip, error);
|
||||
} else {
|
||||
this.renderLiveValidationSuccess($el, $label, $requiredTextLabel, $icon, $errorTip);
|
||||
}
|
||||
},
|
||||
|
||||
getLabel: function($el) {
|
||||
return this.$form.find('label[for=' + $el.attr('id') + ']');
|
||||
},
|
||||
|
||||
getIcon: function($el) {
|
||||
return $('#' + $el.attr('id') + '-validation-icon');
|
||||
},
|
||||
|
||||
getErrorTip: function($el) {
|
||||
return $('#' + $el.attr('id') + '-validation-error-msg');
|
||||
},
|
||||
|
||||
getFieldTimeout: function($el) {
|
||||
return $('#' + $el.attr('id')).attr('timeout-id') || null;
|
||||
},
|
||||
|
||||
setFieldTimeout: function($el, time, action) {
|
||||
$el.attr('timeout-id', setTimeout(action, time));
|
||||
},
|
||||
|
||||
clearFieldTimeout: function($el) {
|
||||
var timeout = this.getFieldTimeout($el);
|
||||
if (timeout) {
|
||||
clearTimeout(this.getFieldTimeout($el));
|
||||
$el.removeAttr('timeout-id');
|
||||
}
|
||||
},
|
||||
|
||||
renderLiveValidationError: function($el, $label, $req, $icon, $tip, error) {
|
||||
this.removeLiveValidationIndicators(
|
||||
$el, $label, $req, $icon,
|
||||
'success', this.positiveValidationIcon
|
||||
);
|
||||
this.addLiveValidationIndicators(
|
||||
$el, $label, $req, $icon, $tip,
|
||||
'error', this.negativeValidationIcon, error
|
||||
);
|
||||
this.renderRequiredMessage($el);
|
||||
},
|
||||
|
||||
renderLiveValidationSuccess: function($el, $label, $req, $icon, $tip) {
|
||||
var self = this,
|
||||
validationFadeTime = this.successfulValidationDisplaySeconds * 1000;
|
||||
this.removeLiveValidationIndicators(
|
||||
$el, $label, $req, $icon,
|
||||
'error', this.negativeValidationIcon
|
||||
);
|
||||
this.addLiveValidationIndicators(
|
||||
$el, $label, $req, $icon, $tip,
|
||||
'success', this.positiveValidationIcon, ''
|
||||
);
|
||||
this.hideRequiredMessage($el);
|
||||
|
||||
// Hide success indicators after some time.
|
||||
this.clearFieldTimeout($el);
|
||||
this.setFieldTimeout($el, validationFadeTime, function() {
|
||||
self.removeLiveValidationIndicators(
|
||||
$el, $label, $req, $icon,
|
||||
'success', self.positiveValidationIcon
|
||||
);
|
||||
self.clearFieldTimeout($el);
|
||||
});
|
||||
},
|
||||
|
||||
addLiveValidationIndicators: function($el, $label, $req, $icon, $tip, indicator, icon, msg) {
|
||||
$el.addClass(indicator);
|
||||
$label.addClass(indicator);
|
||||
$req.addClass(indicator);
|
||||
$icon.addClass(indicator + ' ' + icon);
|
||||
$tip.text(msg);
|
||||
},
|
||||
|
||||
removeLiveValidationIndicators: function($el, $label, $req, $icon, indicator, icon) {
|
||||
$el.removeClass(indicator);
|
||||
$label.removeClass(indicator);
|
||||
$req.removeClass(indicator);
|
||||
$icon.removeClass(indicator + ' ' + icon);
|
||||
},
|
||||
|
||||
thirdPartyAuth: function(event) {
|
||||
var providerUrl = $(event.currentTarget).data('provider-url') || '';
|
||||
|
||||
@@ -100,7 +255,11 @@
|
||||
function(errorList) {
|
||||
return _.map(
|
||||
errorList,
|
||||
function(errorItem) { return '<li>' + errorItem.user_message + '</li>'; }
|
||||
function(errorItem) {
|
||||
return StringUtils.interpolate('<li>{error}</li>', {
|
||||
error: errorItem.user_message
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
)
|
||||
@@ -135,32 +294,103 @@
|
||||
getFormData: function() {
|
||||
var obj = FormView.prototype.getFormData.apply(this, arguments),
|
||||
$form = this.$form,
|
||||
$label,
|
||||
$emailElement,
|
||||
$confirmEmailElement,
|
||||
email = '',
|
||||
confirmEmail = '';
|
||||
$emailElement = $form.find('input[name=email]'),
|
||||
$confirmEmail = $form.find('input[name=confirm_email]'),
|
||||
elements = $form[0].elements,
|
||||
$el,
|
||||
key = '',
|
||||
i;
|
||||
|
||||
$emailElement = $form.find('input[name=email]');
|
||||
$confirmEmailElement = $form.find('input[name=confirm_email]');
|
||||
for (i = 0; i < elements.length; i++) {
|
||||
$el = $(elements[i]);
|
||||
key = $el.attr('name') || false;
|
||||
|
||||
if ($confirmEmailElement.length) {
|
||||
email = $emailElement.val();
|
||||
confirmEmail = $confirmEmailElement.val();
|
||||
$label = $form.find('label[for=' + $confirmEmailElement.attr('id') + ']');
|
||||
// Due to a bug in firefox, whitespaces in email type field are not removed.
|
||||
// TODO: Remove this code once firefox bug is resolved.
|
||||
if (key === 'email') {
|
||||
$el.val($el.val().trim());
|
||||
}
|
||||
|
||||
if (confirmEmail !== '' && email !== confirmEmail) {
|
||||
this.errors.push('<li>' + $confirmEmailElement.data('errormsg-required') + '</li>');
|
||||
$confirmEmailElement.addClass('error');
|
||||
$label.addClass('error');
|
||||
} else if (confirmEmail !== '') {
|
||||
obj.confirm_email = confirmEmail;
|
||||
$confirmEmailElement.removeClass('error');
|
||||
$label.removeClass('error');
|
||||
// Simulate live validation.
|
||||
if ($el.attr('required')) {
|
||||
$el.blur();
|
||||
|
||||
// Special case: show required string for errors even if we're not focused.
|
||||
if ($el.hasClass('error')) {
|
||||
this.renderRequiredMessage($el);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($confirmEmail.length) {
|
||||
if (!$confirmEmail.val() || ($emailElement.val() !== $confirmEmail.val())) {
|
||||
this.errors.push(StringUtils.interpolate('<li>{error}</li>', {
|
||||
error: $confirmEmail.data('errormsg-required')
|
||||
}));
|
||||
}
|
||||
obj.confirm_email = $confirmEmail.val();
|
||||
}
|
||||
|
||||
return obj;
|
||||
},
|
||||
|
||||
liveValidateHandler: function(event) {
|
||||
var $el = $(event.currentTarget);
|
||||
// Until we get a back-end that can handle all available
|
||||
// registration fields, we do some generic validation here.
|
||||
if (this.inLiveValidationFields($el)) {
|
||||
if ($el.attr('type') === 'checkbox') {
|
||||
this.liveValidateCheckbox($el);
|
||||
} else {
|
||||
this.liveValidate($el);
|
||||
}
|
||||
} else {
|
||||
this.genericLiveValidateHandler($el);
|
||||
}
|
||||
// On blur, we do exactly as the function name says, no matter which input.
|
||||
this.hideRequiredMessageExceptOnError($el);
|
||||
},
|
||||
|
||||
liveValidate: function($el) {
|
||||
var data = {},
|
||||
field,
|
||||
i;
|
||||
for (i = 0; i < this.liveValidationFields.length; ++i) {
|
||||
field = this.liveValidationFields[i];
|
||||
data[field] = $('#register-' + field).val();
|
||||
}
|
||||
FormView.prototype.liveValidate(
|
||||
$el, this.validationUrl, 'json', data, 'POST', this.model
|
||||
);
|
||||
},
|
||||
|
||||
liveValidateCheckbox: function($checkbox) {
|
||||
var validationDecisions = {validation_decisions: {}},
|
||||
decisions = validationDecisions.validation_decisions,
|
||||
name = $checkbox.attr('name'),
|
||||
checked = $checkbox.is(':checked'),
|
||||
error = $checkbox.data('errormsg-required');
|
||||
decisions[name] = checked ? '' : error;
|
||||
this.renderLiveValidations($checkbox, validationDecisions);
|
||||
},
|
||||
|
||||
genericLiveValidateHandler: function($el) {
|
||||
var elementType = $el.attr('type');
|
||||
if (elementType === 'checkbox') {
|
||||
// We are already validating checkboxes in a generic way.
|
||||
this.liveValidateCheckbox($el);
|
||||
} else {
|
||||
this.genericLiveValidate($el);
|
||||
}
|
||||
},
|
||||
|
||||
genericLiveValidate: function($el) {
|
||||
var validationDecisions = {validation_decisions: {}},
|
||||
decisions = validationDecisions.validation_decisions,
|
||||
name = $el.attr('name'),
|
||||
error = $el.data('errormsg-required');
|
||||
decisions[name] = $el.val() ? '' : error;
|
||||
this.renderLiveValidations($el, validationDecisions);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -296,10 +296,6 @@
|
||||
display: inline;
|
||||
}
|
||||
|
||||
&.error {
|
||||
color: $red;
|
||||
}
|
||||
|
||||
&[for="register-data_sharing_consent"],
|
||||
&[for="register-honor_code"],
|
||||
&[for="register-terms_of_service"] {
|
||||
@@ -365,7 +361,22 @@
|
||||
}
|
||||
|
||||
&.error {
|
||||
border-color: $error-color;
|
||||
border-color: $red;
|
||||
}
|
||||
|
||||
&.success {
|
||||
border-color: $success-color-hover;
|
||||
}
|
||||
}
|
||||
|
||||
textarea,
|
||||
select {
|
||||
&.error {
|
||||
outline-color: $red;
|
||||
}
|
||||
|
||||
&.success {
|
||||
outline-color: $success-color-hover;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -384,9 +395,16 @@
|
||||
&:active, &:focus {
|
||||
outline: auto;
|
||||
}
|
||||
}
|
||||
|
||||
span,
|
||||
label {
|
||||
&.error {
|
||||
outline-color: $error-color;
|
||||
color: $red;
|
||||
}
|
||||
|
||||
&.success {
|
||||
color: $success-color-hover;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -394,6 +412,7 @@
|
||||
@extend %t-copy-sub1;
|
||||
color: $uxpl-gray-base;
|
||||
}
|
||||
|
||||
.tip {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
<div class="form-field <%=type%>-<%= name %>">
|
||||
<div class="form-field <%- type %>-<%- name %>">
|
||||
<% if ( type !== 'checkbox' ) { %>
|
||||
<label for="<%= form %>-<%= name %>">
|
||||
<span class="label-text"><%= label %></span>
|
||||
<% if ( required && requiredStr && (type !== 'hidden') ) { %><span class="label-required"><%= requiredStr %></span><% } %>
|
||||
<% if ( !required && optionalStr && (type !== 'hidden') ) { %><span class="label-optional"><%= optionalStr %></span><% } %>
|
||||
<label for="<%- form %>-<%- name %>">
|
||||
<span class="label-text"><%- label %></span>
|
||||
<% if ( required && type !== 'hidden' ) { %>
|
||||
<span id="<%- form %>-<%- name %>-required-label"
|
||||
class="label-required <% if ( !requiredStr ) { %>hidden<% } %>">
|
||||
<% if ( requiredStr ) { %><%- requiredStr %><% }%>
|
||||
</span>
|
||||
<span class="icon fa" id="<%- form %>-<%- name %>-validation-icon" aria-hidden="true"></span>
|
||||
<% } %>
|
||||
<% if ( !required && optionalStr && (type !== 'hidden') ) { %>
|
||||
<span class="label-optional" id="<%- form %>-<%- name %>-optional-label"><%- optionalStr %></span>
|
||||
<% } %>
|
||||
</label>
|
||||
<% if (supplementalLink && supplementalText) { %>
|
||||
<div class="supplemental-link">
|
||||
@@ -13,50 +21,59 @@
|
||||
<% } %>
|
||||
|
||||
<% if ( type === 'select' ) { %>
|
||||
<select id="<%= form %>-<%= name %>"
|
||||
name="<%= name %>"
|
||||
<select id="<%- form %>-<%- name %>"
|
||||
name="<%- name %>"
|
||||
class="input-inline"
|
||||
<% if ( instructions ) { %>
|
||||
aria-describedby="<%= form %>-<%= name %>-desc"
|
||||
aria-describedby="<%- form %>-<%- name %>-desc <%- form %>-<%- name %>-validation-error"
|
||||
<% } %>
|
||||
<% if ( typeof errorMessages !== 'undefined' ) {
|
||||
_.each(errorMessages, function( msg, type ) {%>
|
||||
data-errormsg-<%= type %>="<%= msg %>"
|
||||
data-errormsg-<%- type %>="<%- msg %>"
|
||||
<% });
|
||||
} %>
|
||||
<% if ( required ) { %> aria-required="true" required<% } %>>
|
||||
<% _.each(options, function(el) { %>
|
||||
<option value="<%= el.value%>"<% if ( el.default ) { %> data-isdefault="true" selected<% } %>><%= el.name %></option>
|
||||
<% }); %>
|
||||
<% if ( required ) { %> aria-required="true" required<% } %>
|
||||
>
|
||||
<% _.each(options, function(el) { %>
|
||||
<option value="<%- el.value%>"<% if ( el.default ) { %> data-isdefault="true" selected<% } %>><%- el.name %></option>
|
||||
<% }); %>
|
||||
</select>
|
||||
<% if ( instructions ) { %> <span class="tip tip-input" id="<%= form %>-<%= name %>-desc"><%= instructions %></span><% } %>
|
||||
<span id="<%- form %>-<%- name %>-validation-error" class="tip error" aria-live="assertive">
|
||||
<span class="sr-only">ERROR: </span>
|
||||
<span id="<%- form %>-<%- name %>-validation-error-msg"></span>
|
||||
</span>
|
||||
<% if ( instructions ) { %> <span class="tip tip-input" id="<%- form %>-<%- name %>-desc"><%- instructions %></span><% } %>
|
||||
<% if (supplementalLink && supplementalText) { %>
|
||||
<div class="supplemental-link">
|
||||
<a href="<%- supplementalLink %>" target="_blank"><%- supplementalText %></a>
|
||||
</div>
|
||||
<% } %>
|
||||
<% } else if ( type === 'textarea' ) { %>
|
||||
<textarea id="<%= form %>-<%= name %>"
|
||||
type="<%= type %>"
|
||||
name="<%= name %>"
|
||||
<textarea id="<%- form %>-<%- name %>"
|
||||
type="<%- type %>"
|
||||
name="<%- name %>"
|
||||
class="input-block"
|
||||
<% if ( instructions ) { %>
|
||||
aria-describedby="<%= form %>-<%= name %>-desc"
|
||||
aria-describedby="<%- form %>-<%- name %>-desc <%- form %>-<%- name %>-validation-error"
|
||||
<% } %>
|
||||
<% if ( restrictions.min_length ) { %> minlength="<%= restrictions.min_length %>"<% } %>
|
||||
<% if ( restrictions.max_length ) { %> maxlength="<%= restrictions.max_length %>"<% } %>
|
||||
<% if ( restrictions.min_length ) { %> minlength="<%- restrictions.min_length %>"<% } %>
|
||||
<% if ( restrictions.max_length ) { %> maxlength="<%- restrictions.max_length %>"<% } %>
|
||||
<% if ( typeof errorMessages !== 'undefined' ) {
|
||||
_.each(errorMessages, function( msg, type ) {%>
|
||||
data-errormsg-<%= type %>="<%= msg %>"
|
||||
data-errormsg-<%- type %>="<%- msg %>"
|
||||
<% });
|
||||
} %>
|
||||
<% if ( required ) { %> aria-required="true" required<% } %> ></textarea>
|
||||
<% if ( instructions ) { %> <span class="tip tip-input" id="<%= form %>-<%= name %>-desc"><%= instructions %></span><% } %>
|
||||
<% if (supplementalLink && supplementalText) { %>
|
||||
<div class="supplemental-link">
|
||||
<a href="<%- supplementalLink %>" target="_blank"><%- supplementalText %></a>
|
||||
</div>
|
||||
<% } %>
|
||||
<% if ( required ) { %> aria-required="true" required<% } %>></textarea>
|
||||
<span id="<%- form %>-<%- name %>-validation-error" class="tip error" aria-live="assertive">
|
||||
<span class="sr-only">ERROR: </span>
|
||||
<span id="<%- form %>-<%- name %>-validation-error-msg"></span>
|
||||
</span>
|
||||
<% if ( instructions ) { %> <span class="tip tip-input" id="<%- form %>-<%- name %>-desc"><%- instructions %></span><% } %>
|
||||
<% if (supplementalLink && supplementalText) { %>
|
||||
<div class="supplemental-link">
|
||||
<a href="<%- supplementalLink %>" target="_blank"><%- supplementalText %></a>
|
||||
</div>
|
||||
<% } %>
|
||||
<% } else { %>
|
||||
<% if ( type === 'checkbox' ) { %>
|
||||
<% if (supplementalLink && supplementalText) { %>
|
||||
@@ -65,30 +82,44 @@
|
||||
</div>
|
||||
<% } %>
|
||||
<% } %>
|
||||
<input id="<%= form %>-<%= name %>"
|
||||
type="<%= type %>"
|
||||
name="<%= name %>"
|
||||
<input id="<%- form %>-<%- name %>"
|
||||
type="<%- type %>"
|
||||
name="<%- name %>"
|
||||
class="input-block <% if ( type === 'checkbox' ) { %>checkbox<% } %>"
|
||||
<% if ( instructions ) { %> aria-describedby="<%= form %>-<%= name %>-desc" <% } %>
|
||||
<% if ( restrictions.min_length ) { %> minlength="<%= restrictions.min_length %>"<% } %>
|
||||
<% if ( restrictions.max_length ) { %> maxlength="<%= restrictions.max_length %>"<% } %>
|
||||
<% if ( instructions ) { %>
|
||||
aria-describedby="<%- form %>-<%- name %>-desc <%- form %>-<%- name %>-validation-error"
|
||||
<% } %>
|
||||
<% if ( restrictions.min_length ) { %> minlength="<%- restrictions.min_length %>"<% } %>
|
||||
<% if ( restrictions.max_length ) { %> maxlength="<%- restrictions.max_length %>"<% } %>
|
||||
<% if ( required ) { %> required<% } %>
|
||||
<% if ( typeof errorMessages !== 'undefined' ) {
|
||||
_.each(errorMessages, function( msg, type ) {%>
|
||||
data-errormsg-<%= type %>="<%= msg %>"
|
||||
data-errormsg-<%- type %>="<%- msg %>"
|
||||
<% });
|
||||
} %>
|
||||
<% if ( placeholder ) { %> placeholder="<%= placeholder %>"<% } %>
|
||||
<% if ( placeholder ) { %> placeholder="<%- placeholder %>"<% } %>
|
||||
value="<%- defaultValue %>"
|
||||
/>
|
||||
<% if ( type === 'checkbox' ) { %>
|
||||
<label for="<%= form %>-<%= name %>">
|
||||
<span class="label-text"><%= label %></span>
|
||||
<% if ( required && requiredStr ) { %><span class="label-required"><%= requiredStr %></span><% } %>
|
||||
<% if ( !required && optionalStr ) { %><span class="label-optional"><%= optionalStr %></span><% } %>
|
||||
<label for="<%- form %>-<%- name %>">
|
||||
<span class="label-text"><%- label %></span>
|
||||
<% if ( required && type !== 'hidden' ) { %>
|
||||
<span id="<%- form %>-<%- name %>-required-label"
|
||||
class="label-required <% if ( !requiredStr ) { %>hidden<% } %>">
|
||||
<% if ( requiredStr ) { %><%- requiredStr %><% }%>
|
||||
</span>
|
||||
<span class="icon fa" id="<%- form %>-<%- name %>-validation-icon" aria-hidden="true"></span>
|
||||
<% } %>
|
||||
<% if ( !required && optionalStr ) { %>
|
||||
<span class="label-optional" id="<%- form %>-<%- name %>-optional-label"><%- optionalStr %></span>
|
||||
<% } %>
|
||||
</label>
|
||||
<% } %>
|
||||
<% if ( instructions ) { %> <span class="tip tip-input" id="<%= form %>-<%= name %>-desc"><%= instructions %></span><% } %>
|
||||
<span id="<%- form %>-<%- name %>-validation-error" class="tip error" aria-live="assertive">
|
||||
<span class="sr-only">ERROR: </span>
|
||||
<span id="<%- form %>-<%- name %>-validation-error-msg"></span>
|
||||
</span>
|
||||
<% if ( instructions ) { %> <span class="tip tip-input" id="<%- form %>-<%- name %>-desc"><%- instructions %></span><% } %>
|
||||
<% } %>
|
||||
|
||||
<% if( form === 'login' && name === 'password' ) { %>
|
||||
|
||||
Reference in New Issue
Block a user