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:
Uman Shahzad
2017-06-27 06:58:04 +05:00
parent 39ac333b5d
commit cb034d4f2f
21 changed files with 1079 additions and 457 deletions

View File

@@ -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

View File

@@ -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'),

View File

@@ -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;
}
});
});

View File

@@ -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);
}
});
});

View File

@@ -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;
}

View File

@@ -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' ) { %>