From 526178a767402eb902a78c2e9012d123aa0ee2cc Mon Sep 17 00:00:00 2001 From: Seamus Johnston Date: Fri, 4 Nov 2022 08:17:30 -0500 Subject: [PATCH] Write JavaScript to check domain availability --- src/registrar/assets/js/get-gov.js | 132 ++++++++++++++++-- .../templates/application_contact.html | 2 +- 2 files changed, 122 insertions(+), 12 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index b3f0af84b..0f07e205b 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -1,13 +1,26 @@ +/** + * @file get-gov.js includes custom code for the .gov registrar. + * + * Constants and helper functions are at the top. + * Event handlers are in the middle. + * Initialization (run-on-load) stuff goes at the bottom. + */ + /** Strings announced to assistive technology users. */ var ARIA = { QUESTION_REMOVED: "Previous follow-up question removed", QUESTION_ADDED: "New follow-up question required" } +var DEFAULT_ERROR = "Please check this field for errors."; + var REQUIRED = "required"; var INPUT = "input"; -/** Helper function. Makes an element invisible. */ +// <<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>> +// Helper functions. + +/** Makes an element invisible. */ function makeHidden(el) { el.style.position = "absolute"; el.style.left = "-100vw"; @@ -18,7 +31,7 @@ function makeHidden(el) { el.style.visibility = "hidden"; } -/** Helper function. Makes visible a perviously hidden element. */ +/** Makes visible a perviously hidden element. */ function makeVisible(el) { el.style.position = "relative"; el.style.left = "unset"; @@ -33,19 +46,19 @@ function forEachChild(el, selector, func) { } } -/** Helper function. Removes `required` attribute from input. */ +/** Removes `required` attribute from input. */ const removeRequired = input => input.removeAttribute(REQUIRED); -/** Helper function. Adds `required` attribute to input. */ +/** Adds `required` attribute to input. */ const setRequired = input => input.setAttribute(REQUIRED, ""); -/** Helper function. Removes `checked` attribute from input. */ +/** Removes `checked` attribute from input. */ const removeChecked = input => input.checked = false; -/** Helper function. Adds `checked` attribute to input. */ +/** Adds `checked` attribute to input. */ const setChecked = input => input.checked = true; -/** Helper function. Creates and returns a live region element. */ +/** Creates and returns a live region element. */ function createLiveRegion(id) { const liveRegion = document.createElement("div"); liveRegion.setAttribute("role", "region"); @@ -64,8 +77,8 @@ var radioToggles = {}; /** - * Helper function. Tracks state of selected radio button. - * + * Tracks state of selected radio button. + * * This is required due to JavaScript not having a native * event trigger for "deselect" on radio buttons. Tracking * which button has been deselected (and hiding the associated @@ -75,7 +88,7 @@ function rememberSelected(radioButton) { selected[radioButton.name] = radioButton; } -/** Helper function. Announces changes to assistive technology users. */ +/** Announces changes to assistive technology users. */ function announce(id, text) { const liveRegion = document.getElementById(id + "-live-region"); liveRegion.innerHTML = text; @@ -123,6 +136,70 @@ function revealToggleable(e) { } } +/** + * Slow down event handlers by limiting how frequently they fire. + * + * A wait period must occur with no activity (activity means "this + * debounce function being called") before the handler is invoked. + * + * @param {Function} handler - any JS function + * @param {number} cooldown - the wait period, in milliseconds + */ +function debounce(handler, cooldown=600) { + let timeout; + return function(...args) { + const context = this; + clearTimeout(timeout); + timeout = setTimeout(() => handler.apply(context, args), cooldown); + } +} + +/** Asyncronously fetches JSON. No error handling. */ +function fetchJSON(endpoint, callback, url="/api/v1/") { + const xhr = new XMLHttpRequest(); + xhr.open('GET', url + endpoint); + xhr.send(); + xhr.onload = function() { + if (xhr.status != 200) return; + callback(JSON.parse(xhr.response)); + }; + // nothing, don't care + // xhr.onerror = function() { }; +} + +/** Modifies CSS and HTML when an input is valid/invalid. */ +function toggleInputValidity(el, valid, msg=DEFAULT_ERROR) { + if (valid) { + el.setCustomValidity(""); + el.removeAttribute("aria-invalid"); + el.classList.remove('usa-input--error'); + } else { + el.classList.remove('usa-input--success'); + el.setAttribute("aria-invalid", "true"); + el.setCustomValidity(msg); + // this is here for testing: in actual use, we might not want to + // visually display these errors until the user tries to submit + el.classList.add('usa-input--error'); + } +} + +function _checkDomainAvailability(e) { + const callback = (response) => { + toggleInputValidity(e.target, (response && response.available)); + if (e.target.validity.valid) { + e.target.classList.add('usa-input--success'); + // do other stuff, like display a toast? + } + } + fetchJSON(`available/${e.target.value}`, callback); +} + +/** Call the API to see if the domain is good. */ +const checkDomainAvailability = debounce(_checkDomainAvailability); + +// <<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>> +// Event handlers. + /** On radio button selection change, handles associated toggleables. */ function handleToggle(e) { // hide any previously visible HTML associated with previously selected radio buttons @@ -131,9 +208,42 @@ function handleToggle(e) { revealToggleable(e); } +/** On input change, handles running any associated validators. */ +function handleInputValidation(e) { + const attribute = e.target.getAttribute("validate") || ""; + if (!attribute.length) return; + const validators = attribute.split(" "); + let isInvalid = false; + for (const validator of validators) { + switch (validator) { + case "domain": + checkDomainAvailability(e); + break; + } + } + toggleInputValidity(e.target, !isInvalid); +} + +// <<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>> +// Initialization code. + +/** + * An IIFE that will attach validators to inputs. + * + * It looks for elements with `validate=" "` and adds + * change handlers for each known type. + */ + (function validatorsInit() { + "use strict"; + const needsValidation = document.querySelectorAll('[validate]'); + for(const input of needsValidation) { + input.addEventListener('input', handleInputValidation); + } +})(); + /** * An IIFE that will hide any elements with `hide-on-load` attribute. - * + * * Why not start with `hidden`? Because this is for use with form questions: * if Javascript fails, users will still need access to those questions. */ diff --git a/src/registrar/templates/application_contact.html b/src/registrar/templates/application_contact.html index d27dfe803..08dbd0e37 100644 --- a/src/registrar/templates/application_contact.html +++ b/src/registrar/templates/application_contact.html @@ -23,7 +23,7 @@ of a larger entity. If so, enter information about your part of the larger entit
{{ wizard.form.street_address|add_label_class:"usa-label" }} - {{ wizard.form.street_address|add_class:"usa-input" }} + {{ wizard.form.street_address|add_class:"usa-input"|attr:"validate:domain" }}
{% if wizard.steps.prev %}