section.content
  h1.content__heading Send Me a Message
  p.content__lede Use this handy contact form to get in touch with me.

  form.content__form.contact-form
  
    .testing
      p Does this do anything?

    .contact-form__input-group
      input#salutation-mr.contact-form__input.contact-form__input--radio(
        name="salutation"
        type="radio"
        value="Mr."
      )
      label.contact-form__label.contact-form__label--radio(
        for="salutation-mr"
      ) Mr.
      input#salutation-mrs.contact-form__input.contact-form__input--radio(
        name="salutation"
        type="radio"
        value="Mrs."
      )
      label.contact-form__label.contact-form__label--radio(
        for="salutation-mrs"
      ) Mrs.
      input#salutation-ms.contact-form__input.contact-form__input--radio(
        name="salutation"
        type="radio"
        value="Ms."
      )
      label.contact-form__label.contact-form__label--radio(
        for="salutation-ms"
      ) Ms.

    .contact-form__input-group
      label.contact-form__label(
        for="name"
      ) Full Name
      input#name.contact-form__input.contact-form__input--text(
        name="name"
        type="text"
      )

    .contact-form__input-group
      label.contact-form__label(
        for="email"
      ) Email Address
      input#email.contact-form__input.contact-form__input--email(
        name="email"
        type="email"
      )

    .contact-form__input-group
      label.contact-form__label(
        for="subject"
      ) How can I help you?
      select#subject.contact-form__input.contact-form__input--select(
        name="subject"
      )
        option I have a problem.
        option I have a general question.

    .contact-form__input-group
      label.contact-form__label(
        for="message"
      ) Enter a Message
      textarea#message.contact-form__input.contact-form__input--textarea(
        name="message"
        rows="6"
        cols="65"
      )

    .contact-form__input-group
      p.contact-form__label--checkbox-group Please send me:
      input#snacks-pizza.contact-form__input.contact-form__input--checkbox(
        name="snacks"
        type="checkbox"
        value="pizza"
      )
      label.contact-form__label.contact-form__label--checkbox(
        for="snacks-pizza"
      ) Pizza
      input#snacks-cake.contact-form__input.contact-form__input--checkbox(
        name="snacks"
        type="checkbox"
        value="cake"
      )
      label.contact-form__label.contact-form__label--checkbox(
        for="snacks-cake"
      ) Cake
      
    //- A hidden input to demonstrate that these are grabbed as well.
    input(
      name="secret"
      type="hidden"
      value="1b3a9374-1a8e-434e-90ab-21aa7b9b80e7"
    )

    button.contact-form__button(
      type="submit"
    ) Send It!

.results
  h2.results__heading Form Data
  pre.results__display-wrapper
    code.results__display
View Compiled
@use postcss-nested;
@use postcss-simple-vars;
@use postcss-cssnext;

$color-lightest: #f9fdfe;
$color-gray-light: #cdcfcf;
$color-gray-medium: #686a69;
$color-gray-dark: #414643;
$color-darkest: #2a2f2c;

/* A simple reset. */
*,::before,::after {
  margin: 0;
  box-sizing: border-box;
}
  
/* Heydon Pickering’s lobotomized owl. Details: https://bit.ly/1H7MXUD */
*+* {
  margin-top: 1rem;
}

/* Set up fonts, colors and all that jazz. */
body {
  background: $color-lightest;
  color: $color-gray-medium;
  font-family: 'Open Sans', sans-serif;
  font-size: 18px;
  line-height: 1.75;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* Headings use a different font because they’re hipsters. */
h1,h2 {
  color: $color-darkest;
  font-family: Lato, sans-serif;
  font-weight: 300;
  line-height: 1.125;
}

/* Set up general layout rules for outer containers. */
.content,.results {
  width: 90vw;
  max-width: 550px;
  margin: 8vh auto;
}

.content {
  &__heading {
    font-size: 125%;
  }
  
  &__lede {
    margin-top: 0.5rem;
    font-size: 87.5%;
  }
}

.results {
  &__heading {
    font-size: 110%;
  }
  
  &__display-wrapper {
    margin-top: 1rem;
    padding: 0.5rem 1rem;
    background: $color-lightest;
    border: 1px solid $color-gray-light;
    overflow-x: scroll;
  }
}

.contact-form {
  position: relative;
  display: block;
  margin: 0;
  padding: 1rem 0 2rem;
  border-top: 1px solid $color-gray-light;
  border-bottom: 1px solid $color-gray-light;
  overflow: hidden;
  
  &__input-group {
    margin-top: 0.25rem;
    padding: 0.5rem 1rem;
  }
  
  &__label {
    display: block;
    color: $color-gray-dark;
    font-family: Lato, sans-serif;
    font-size: 75%;
    line-height: 1.125;
    
    &--checkbox-group {
      display: inline-block;
      margin-right: 1rem;
      font-size: 75%;
    }
    
    &--checkbox,&--radio {
      display: inline-block;
      margin-left: 0.25rem;
    }
  }
  
  &__input {
    display: block;
    margin-top: 0;
    padding: 0.5rem 0.75rem;
    border: 1px solid $color-gray-light;
    width: 100%;
    font-family: 'Open Sans', sans-serif;
    font-size: 1rem;
    transition: 150ms border-color linear;
    
    &--checkbox,&--radio {
      display: inline-block;
      width: auto;
      
      &~& {
        margin-left: 1rem;
      }
    }
    
    &:focus,&:active {
      border-color: $color-gray-medium;
      outline: 0;
    }
  }
  
  &__button {
    display: block;
    margin: 0.5rem 1rem 0;
    padding: 0 1rem 0.125rem;
    background-color: $color-gray-medium;
    border: 0;
    color: $color-lightest;
    font-family: lato, sans-serif;
    font-size: 100%;
    letter-spacing: 0.05em;
    line-height: 1.5;
    text-transform: uppercase;
    transition: 150ms all linear;
    
    &:hover,&:active,&:focus {
      background: $color-darkest;
      cursor: pointer;
      outline: 0;
    }
  }
}
/**
 * Checks that an element has a non-empty `name` and `value` property.
 * @param  {Element} element  the element to check
 * @return {Bool}             true if the element is an input, false if not
 */
const isValidElement = element => {
  return element.name && element.value;
};

/**
 * Checks if an element’s value can be saved (e.g. not an unselected checkbox).
 * @param  {Element} element  the element to check
 * @return {Boolean}          true if the value should be added, false if not
 */
const isValidValue = element => {
  return (!['checkbox', 'radio'].includes(element.type) || element.checked);
};

/**
 * Checks if an input is a checkbox, because checkboxes allow multiple values.
 * @param  {Element} element  the element to check
 * @return {Boolean}          true if the element is a checkbox, false if not
 */
const isCheckbox = element => element.type === 'checkbox';

/**
 * Checks if an input is a `select` with the `multiple` attribute.
 * @param  {Element} element  the element to check
 * @return {Boolean}          true if the element is a multiselect, false if not
 */
const isMultiSelect = element => element.options && element.multiple;

/**
 * Retrieves the selected options from a multi-select as an array.
 * @param  {HTMLOptionsCollection} options  the options for the select
 * @return {Array}                          an array of selected option values
 */
const getSelectValues = options => [].reduce.call(options, (values, option) => {
  return option.selected ? values.concat(option.value) : values;
}, []);

/**
 * A more verbose implementation of `formToJSON()` to explain how it works.
 *
 * NOTE: This function is unused, and is only here for the purpose of explaining how
 * reducing form elements works.
 *
 * @param  {HTMLFormControlsCollection} elements  the form elements
 * @return {Object}                               form data as an object literal
 */
const formToJSON_deconstructed = elements => {
  
  // This is the function that is called on each element of the array.
  const reducerFunction = (data, element) => {
    
    // Add the current field to the object.
    data[element.name] = element.value;
    
    // For the demo only: show each step in the reducer’s progress.
    console.log(JSON.stringify(data));

    return data;
  };
  
  // This is used as the initial value of `data` in `reducerFunction()`.
  const reducerInitialValue = {};
  
  // To help visualize what happens, log the inital value, which we know is `{}`.
  console.log('Initial `data` value:', JSON.stringify(reducerInitialValue));
  
  // Now we reduce by `call`-ing `Array.prototype.reduce()` on `elements`.
  const formData = [].reduce.call(elements, reducerFunction, reducerInitialValue);
  
  // The result is then returned for use elsewhere.
  return formData;
};

/**
 * Retrieves input data from a form and returns it as a JSON object.
 * @param  {HTMLFormControlsCollection} elements  the form elements
 * @return {Object}                               form data as an object literal
 */
const formToJSON = elements => [].reduce.call(elements, (data, element) => {

  // Make sure the element has the required properties and should be added.
  if (isValidElement(element) && isValidValue(element)) {

    /*
     * Some fields allow for more than one value, so we need to check if this
     * is one of those fields and, if so, store the values as an array.
     */
    if (isCheckbox(element)) {
      data[element.name] = (data[element.name] || []).concat(element.value);
    } else if (isMultiSelect(element)) {
      data[element.name] = getSelectValues(element);
    } else {
      data[element.name] = element.value;
    }
  }

  return data;
}, {});

/**
 * A handler function to prevent default submission and run our custom script.
 * @param  {Event} event  the submit event triggered by the user
 * @return {void}
 */
const handleFormSubmit = event => {
  
  // Stop the form from submitting since we’re handling that with AJAX.
  event.preventDefault();
  
  // Call our function to get the form data.
  const data = formToJSON(form.elements);

  // Demo only: print the form data onscreen as a formatted JSON object.
  const dataContainer = document.getElementsByClassName('results__display')[0];
  
  // Use `JSON.stringify()` to make the output valid, human-readable JSON.
  dataContainer.textContent = JSON.stringify(data, null, "  ");
  
  // ...this is where we’d actually do something with the form data...
};

/*
 * This is where things actually get started. We find the form element using
 * its class name, then attach the `handleFormSubmit()` function to the 
 * `submit` event.
 */
const form = document.getElementsByClassName('contact-form')[0];
form.addEventListener('submit', handleFormSubmit);
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/6.13.0/polyfill.min.js