Skip to content

Customizing API forms

Mushfiq Rahman edited this page Sep 14, 2022 · 2 revisions

Add custom CSS and JavaScript

To render a form in Violet Rails, you have to use the snippet, {{ cms:helper render_form, <form_id_here> }} in your HTML. Forms come with default styles but they can be easily customized to fit the design of your website with custom CSS and JavaScript. Inspect the form markup in your browser devtools to see the classes that can be targeted to add custom styles and JavaScript functionality.

Styling the form with custom CSS

.form-group label {
  font-weight: bold;
  color: #6b2bc4;
}

.form-control::placeholder {
  color: #9e9e9e;
}

.form-control[type="text"],
.form-control[type="email"] {
  padding: 1rem;
}

.violet-cta-form .input[type="submit"] {
  background-color: #6b2bc4;
  color: #fff;
}

Using JavaScript for even more customization

Suppose, you need to change the order of form inputs:

function rearrangeFormInputs() {
  const form = document.querySelector(".violet-cta-form");
  // The first .form-group is the div containing the input elements
  // need to give this a different class to differentiate it from input elements having .form-group class
  const formControlsContainer = form.querySelector(".form-group");
  formControlsContainer.className = "form-controls-container";

  const formControls = form.querySelectorAll(".form-group");
  // the order in which we want the input elements to be arranged
  const formControlsOrder = {
    username: 0,
    email: 1,
    password: 2,
    password_confirmation: 3,
  };

  const orderedFormControls = new Array(formControls.length);

  formControls.forEach((control) => {
    // a form control will have a second class like "vr-username" and we need to extract "username"
    const inputName = control.classList[1].slice(3);
    const orderIndex = formControlsOrder[inputName];
    // inserting the input element at the index position we want
    orderedFormControls[orderIndex] = control;
  });

  formControlsContainer.replaceChildren(...orderedFormControls);
}

Script for rearranging the labels of checkbox and radio inputs:

function rearrangeCheckboxAndRadioLabels() {
  const inputContainers = document.querySelectorAll(
    ".violet-cta-form .checkbox-container"
  );

  inputContainers.forEach((inputContainer) => {
    const label = inputContainer.querySelector("span");
    inputContainer.appendChild(label);
  });
}

Script for hiding placeholder-text option in select elements:

function hidePlaceholderTextOptionForSelectElements() {
  const selectElements = document.querySelectorAll(".violet-cta-form select");
  selectElements.forEach((element) => {
    // the first option is an empty option which is used for placeholder text
    const emptyOption = element.querySelector("option");
    emptyOption.setAttribute("hidden", true);
  });
}

Script for prompting the user to solve reCAPTCHA (reCAPTCHA V2) by showing an alert when the user tries to submit the form without solving it:

const form = document.querySelector(".violet-cta-form");

function addRecaptchaAlert() {
  const recaptchaElement = form.querySelector(".g-recaptcha");
  const alertMarkup = `
		<p class="recaptcha-alert" style="display: none">
    	Please solve reCAPTCHA before submitting the form  
		</p>
	`;

  recaptchaElement.insertAdjacentHTML("afterend", alertMarkup);
}

function enhanceSubmitButtonToShowRecaptchaAlert() {
  const submitButton = form.querySelector('input[type="submit"]');
  const buttonWrapperMarkup = `
		<span class="submit-btn-wrapper">
    	<span class="click-area" title="reCAPTCHA needs to be solved" tabindex="0" role="button" aria-label="Submit"></span>
		</span>
	`;

  submitButton.insertAdjacentHTML("beforebegin", buttonWrapperMarkup);
  const submitButtonWrapper = form.querySelector(".submit-btn-wrapper");
  submitButtonWrapper.prepend(submitButton);

  const clickableArea = form.querySelector(".click-area");
  clickableArea.addEventListener("click", showRecaptchaAlert);
  clickableArea.addEventListener("keydown", function (e) {
    if (e.code !== "Enter" && e.code !== "Space") return;
    showRecaptchaAlert();
  });

  function showRecaptchaAlert() {
    if (submitButton.getAttribute("disabled")) {
      const recaptchaAlert = document.querySelector(".recaptcha-alert");
      recaptchaAlert.style.display = "block";
    }
  }
}

function hideRecaptchaAlertWhenInputIsFocused() {
  form.addEventListener("focusin", function (e) {
    if (!e.target.classList.contains("form-control")) {
      return;
    }

    const recaptchaAlert = form.querySelector(".recaptcha-alert");
    recaptchaAlert.style.display = "none";
  });
}

Styles relevant to showing reCAPTCHA alert:

.submit-btn-wrapper {
  position: relative;
}

/* this will be the clickable area when the button is disabled */
.submit-btn-wrapper .click-area {
  display: inline-block;
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  cursor: not-allowed;
  /* to make it stack on top of the button */
  z-index: 1;
}

/* this selects .click-area that comes immediately after a submit button that is not disabled */
input[type="submit"]:not(:disabled) + .click-area {
  transform: scale(0);
}

Script for showing a text field when the user selects a predefined option in a select dropdown or checkbox/radio group, e.g. "Other":

  • query all select input option elements and checkbox/radio inputs whose value is equal to the provided label, for example, <option value="Other">Other</option>
  • attach a change event listener to the parent of each selectInputOption
  • attach a change event listener to each selected checkbox/radio input
  • add the text field if a select input's value becomes the provided label OR it includes (in case of select2, the value will be an array) that label
  • in case of a selected checkbox/radio input, add the text field if it is checked
  • remove the text field otherwise
// adds a text input field when user selects an option, e.g. "Other" in a Select or checkbox group input
function addTextFieldWhenOptionIsSelected(optionLabel) {
  const elemIds = $(
    `.checkbox-container > input[value="${optionLabel}"][type="checkbox"]`
  ).map(function () {
    return "#" + $(this).attr("id");
  });

  const selectInputOptions = $(`select > option[value="${optionLabel}"]`);

  elemIds.each(function (_index, id) {
    const checkboxElem = $(id);

    checkboxElem.on("change", function () {
      const parent = $(this).parent(".checkbox-container");

      if ($(this).is(":checked")) {
        addTextField(this, parent);
      } else {
        removeTextField(this, parent, optionLabel);
      }
    });
  });

  selectInputOptions.each(function (_index, option) {
    const selectInput = $(option).parent();
    selectInput.on("change", function () {
      // in case of select2 input, the value will be in an array format
      if (
        selectInput.val() === optionLabel ||
        selectInput.val().includes(optionLabel)
      ) {
        addTextField(option, selectInput.parent());
      } else {
        removeTextField(option, selectInput.parent(), optionLabel);
      }
    });
  });

  function addTextField(element, elemParent) {
    const textField = $("<input>")
      .attr("type", "text")
      .attr("placeholder", "Please specify...")
      .addClass("form-control checkbox-text-field");

    $(elemParent).append(textField);

    // Changing option value according to user input value
    textField.on("keyup", function () {
      $(element).attr("value", $(this).val().trim());
    });
  }

  function removeTextField(elem, elemParent, optionLabel) {
    const textField = $(elemParent).find(
      'input.checkbox-text-field[type="text"]'
    );

    textField.remove();

    // Reverting back to the original value attribute.
    $(elem).attr("value", optionLabel);
  }
}
Clone this wiki locally