From 9718ce8f7e3be89fc47fadaf3db392d3aa30aa1e Mon Sep 17 00:00:00 2001 From: Vladimir Date: Thu, 2 Jan 2020 18:25:31 +0100 Subject: [PATCH 1/4] ADD: Client-side Sign Up form validation Changes: - Removed realtime_usernmae_validation.js - CSS classes for valid and invalid input element - Minor changes in _create_form.html.erb The old sign up form would only validate a username. This change provides a nice UI to the user while filling the form. Resolves #3439 --- app/assets/javascripts/application.js | 1 - .../realtime_username_validation.js | 25 --- app/assets/javascripts/validation.js | 205 +++++++++++++----- app/assets/stylesheets/style.css | 23 +- app/views/users/_create_form.html.erb | 28 ++- 5 files changed, 191 insertions(+), 91 deletions(-) delete mode 100644 app/assets/javascripts/realtime_username_validation.js diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index e0a7f98b45..9f34b05886 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -55,7 +55,6 @@ //= require wikis.js //= require header_footer.js //= require keybindings.js -//= require realtime_username_validation.js //= require jquery-validation/dist/jquery.validate.js //= require validation.js //= require submit_form_ajax.js \ No newline at end of file diff --git a/app/assets/javascripts/realtime_username_validation.js b/app/assets/javascripts/realtime_username_validation.js deleted file mode 100644 index f2ede4dd1b..0000000000 --- a/app/assets/javascripts/realtime_username_validation.js +++ /dev/null @@ -1,25 +0,0 @@ -$("document").ready(function() { - $("input[name='user[username]']").on("change", function(e) { - var username = e.target.value; - if (username === "") { - $(".username-check").empty(); - } else { - $(".username-check").append(' '); - $.get("/api/srch/profiles?query=" + username, function(data) { - if (data.items) { - $.map(data.items, function(userData) { - if (userData.doc_title === username) { - $(".username-check").empty(); - $(".username-check").append("Username already exists."); - $(".username-check").css("color", "red"); - } - }); - } else { - $(".username-check").empty(); - $(".username-check").append("Username is available."); - $(".username-check").css("color", "green"); - } - }); - } - }); -}); diff --git a/app/assets/javascripts/validation.js b/app/assets/javascripts/validation.js index a0fb66606a..e60da2540b 100644 --- a/app/assets/javascripts/validation.js +++ b/app/assets/javascripts/validation.js @@ -1,5 +1,5 @@ -$(document).ready(function () { - $("#edit-form").validate({ +$(document).ready(function() { + $('#edit-form').validate({ rules: { email: { required: true, @@ -10,21 +10,21 @@ $(document).ready(function () { minlength: 8 }, password2: { - equalTo: "#password1" + equalTo: '#password1' } }, messages: { password1: { - required: "Please enter password", - minlength: "Password should be minimum 8 characters long" + required: 'Please enter password', + minlength: 'Password should be minimum 8 characters long' }, email: { - required: "Please enter email", - email: "Invalid email address" + required: 'Please enter email', + email: 'Invalid email address' }, password2: { - required: "Please enter password", - minlength: "Password should be minimum 8 characters long", + required: 'Please enter password', + minlength: 'Password should be minimum 8 characters long', equalTo: "Passwords doesn't match" } }, @@ -32,47 +32,152 @@ $(document).ready(function () { form.submit(); } }); +}); - $("#create-form").validate({ - rules: { - username: { - required: true, - minlength: 3 - }, - email: { - required: true, - email: true - }, - password1: { - required: true, - minlength: 8 - }, - password2: { - equalTo: "#password1" - } - }, - messages: { - username: { - required: "Please enter username", - minlength: "Username should be minimum 3 characters long" - }, - password1: { - required: "Please enter password", - minlength: "Password should be minimum 8 characters long" - }, - email: { - required: "Please enter email", - email: "Invalid email address" - }, - password2: { - required: "Please enter password", - minlength: "Password should be minimum 8 characters long", - equalTo: "Passwords doesn't match" - } - }, - submitHandler: function(form) { - form.submit(); +$(document).ready(function() { + var signUpForm = document.querySelector('#create-form'); + + if (!signUpForm) return; + + var validationTracker = {}; + var submitBtn = document.querySelector("#create-form [type='submit']"); + var usernameElement = document.querySelector("[name='user[username]']"); + var emailElement = document.querySelector("[name='user[email]']"); + var passwordElement = document.querySelector("[name='user[password]']"); + var confirmPasswordElement = document.querySelector( + "[name='user[password_confirmation]']" + ); + + isFormValid(); + + usernameElement.addEventListener('input', validateUsername); + emailElement.addEventListener('input', validateEmail); + passwordElement.addEventListener('input', validatePassword); + confirmPasswordElement.addEventListener('input', validateConfirmPassword); + + function validateUsername(e) { + var username = e.target.value; + + if (username.length < 3) { + restoreOriginalStyle(this); + } else { + $.get('/api/srch/profiles?query=' + username, function(data) { + if (data.items) { + $.map(data.items, function(userData) { + if (userData.doc_title === username) { + updateUI(usernameElement, false, 'Username already exists'); + } else { + updateUI(usernameElement, true); + } + }); + } else { + updateUI(usernameElement, true); + } + }); } - }); + } + + function validateEmail(e) { + var email = e.target.value; + var regexp = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; + var isValid = regexp.test(email); + + updateUI(emailElement, isValid, 'Invalid email'); + } + + function validatePassword(e) { + var password = e.target.value; + + if (!isPasswordValid(this, password)) return; + + if (password === confirmPasswordElement.value) { + updateUI(confirmPasswordElement, true); + } + + updateUI(passwordElement, true); + } + + function validateConfirmPassword(e) { + var password = passwordElement.value; + var confirmPassword = e.target.value; + + if (!isPasswordValid(this, confirmPassword)) return; + + if (confirmPassword !== password) { + updateUI(confirmPasswordElement, false, 'Passwords must be equal'); + return; + } + + updateUI(confirmPasswordElement, true); + } + + // Password is valid if it is a least 8 characaters long and contains at least 1 number + function isPasswordValid(element, password) { + var doesContainNum = /\d+/g.test(password); + var isValid = password.length >= 8 && doesContainNum; + + if (!isValid) { + updateUI(element, isValid, 'Minimum 8 characters including 1 number'); + } + + return isValid; + } + + function updateUI(element, valid, errorMsg) { + var elementName = element.getAttribute('name'); + + if (valid) { + validationTracker[elementName] = true; + styleElement(element, 'form-element-invalid', 'form-element-valid'); + removeErrorMsg(element); + } else { + validationTracker[elementName] = false; + styleElement(element, 'form-element-valid', 'form-element-invalid'); + renderErrorMsg(element, errorMsg); + } + + isFormValid(); + } + + function renderErrorMsg(element, message) { + if (!message) return; + + var errorMsgElement = element.nextElementSibling; + errorMsgElement.textContent = message; + errorMsgElement.style.color = 'red'; + errorMsgElement.classList.remove('invisible'); + } + + function removeErrorMsg(element) { + var errorMsgElement = element.nextElementSibling; + errorMsgElement.classList.add('invisible'); + } + + function restoreOriginalStyle(element) { + element.classList.remove('form-element-valid'); + element.classList.remove('form-element-invalid'); + } + + function styleElement(element, classToRemove, classToAdd) { + if (element.classList.contains(classToRemove)) { + element.classList.remove(classToRemove); + } + + element.classList.add(classToAdd); + } + + function disableSubmitBtn() { + submitBtn.setAttribute('disabled', ''); + } + + function enableSubmitBtn() { + submitBtn.removeAttribute('disabled'); + } + + function isFormValid() { + var isValid = Object.values(validationTracker).filter(Boolean).length === 4; -}); \ No newline at end of file + if (isValid) enableSubmitBtn(); + else disableSubmitBtn(); + } +}); diff --git a/app/assets/stylesheets/style.css b/app/assets/stylesheets/style.css index 124a2ce7b2..37a303b9df 100644 --- a/app/assets/stylesheets/style.css +++ b/app/assets/stylesheets/style.css @@ -339,6 +339,25 @@ a .fa-white, } /* Styles for specific areas of the site */ +/* Remove blue background on Chrome autocomplete */ +input:-webkit-autofill, +input:-webkit-autofill:hover, +input:-webkit-autofill:focus, +input:-webkit-autofill:active { + -webkit-box-shadow: 0 0 0 30px white inset !important; +} + +.form-element-valid { + background-color: white !important; + box-shadow: none !important; + border: green 2px solid !important; +} + +.form-element-invalid { + background-color: white !important; + box-shadow: none !important; + border: red 2px solid !important; +} .note-show .main-image { margin-bottom: 14px; @@ -585,7 +604,7 @@ textarea, input { background: rgba(255,255,255,0.8); z-index: 9999; border-radius: 4px; - padding: 10px; + padding: 10px; } .btn-toolbar .btn-outline-secondary @@ -597,5 +616,3 @@ textarea, input { { color: #f1f1f1 !important; } - - diff --git a/app/views/users/_create_form.html.erb b/app/views/users/_create_form.html.erb index 64a34753e1..cd7fc9b0ff 100644 --- a/app/views/users/_create_form.html.erb +++ b/app/views/users/_create_form.html.erb @@ -1,5 +1,5 @@
- <%= form_for :user, :as => :user, :url => "/register", :html => {:class => "container", :id => "create-form"} do |f| %> + <%= form_for :user, :as => :user, :url => "/register", :html => {:class => "container", :id => "create-form", :'data-toggle' => "validator"} do |f| %> <% if f.error_messages != "" %>
<%= f.error_messages %>
<% end %> @@ -23,15 +23,16 @@
<%= f.text_field :username, { tabindex: 1, placeholder: "Username", class: 'form-control', id: 'username-signup' } %> - +
- +
<%= f.text_field :email, { tabindex: 3, placeholder: "you@email.com", class: 'form-control', id: 'email' } %> +
- +
@@ -54,8 +55,9 @@ id: 'password1', onpaste: 'return false;' } %> +
- +
<%= f.password_field :password_confirmation, { placeholder: I18n.t('users._form.confirm_password'), @@ -64,6 +66,7 @@ id: 'password-confirmation', onpaste: 'return false;' } %> +
@@ -111,18 +114,19 @@ + <% end %> - + From c1cdb39f75b49bfa571fd486166d10e6c504c086 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Sat, 4 Jan 2020 00:12:40 +0100 Subject: [PATCH 4/4] ADD: Tests for signup modal form validation --- test/system/screenshots_test.rb | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/test/system/screenshots_test.rb b/test/system/screenshots_test.rb index a4d09bc5df..f90bf3d73e 100644 --- a/test/system/screenshots_test.rb +++ b/test/system/screenshots_test.rb @@ -15,6 +15,28 @@ class ScreenshotsTest < ApplicationSystemTestCase take_screenshot end + test 'signup modal form validation' do + visit '/' + click_on 'Sign up' + + fill_in 'user[username]', with: 'Bob' + fill_in 'user[email]', with: 'Invalid@email' + fill_in 'user[password]', with: 'tooshort' + fill_in 'user[password_confirmation]', with: 'password' + + username_error_msg = find("#username-signup ~ small").text + email_error_msg = find("#email ~ small").text + password_error_msg = find("#password1 ~ small").text + confirm_password_error_msg = find("#password-confirmation ~ small").text + + assert_equal( username_error_msg, "Username already exists" ) + assert_equal( email_error_msg, "Invalid email" ) + assert_equal( password_error_msg, "Please make sure password is atleast 8 characters long with minimum one numeric value" ) + assert_equal( confirm_password_error_msg, "Passwords must be equal" ) + + take_screenshot + end + test 'login modal' do visit '/' click_on 'Login' @@ -88,7 +110,7 @@ class ScreenshotsTest < ApplicationSystemTestCase visit '/questions' take_screenshot end - + test 'questions_shadow' do visit '/questions_shadow' take_screenshot @@ -108,7 +130,7 @@ class ScreenshotsTest < ApplicationSystemTestCase visit '/comments' take_screenshot end - + test 'wiki revisions' do visit "/wiki/revisions/#{nodes(:about).slug}" click_on '1' @@ -117,7 +139,7 @@ class ScreenshotsTest < ApplicationSystemTestCase test 'wiki page with inline grids' do node = nodes(:place) # /wiki/chicago page - node.add_tag('place', users(:bob)) # lets get a map on this page! + node.add_tag('place', users(:bob)) # lets get a map on this page! node.add_tag('lon:-71.4', users(:bob)) node.add_tag('lat:41.7', users(:bob)) revision = node.latest @@ -160,5 +182,5 @@ class ScreenshotsTest < ApplicationSystemTestCase visit node.path take_screenshot end - + end