<section class="section">
  <div class="container">
    <div class="notification is-link hide">
      <button class="delete"></button>
      Stuff happened...
    </div>
    
    
    <form name="someform">
      <div class="field">
        <label class="label">Name:</label>
        <div class="control">
          <input name="name" class="input" type="text">
        </div>
        <ul data-errors="name">
        </ul>
      </div>

      <div class="field">
        <label class="label">Email:</label>
        <div class="control">
          <input name="email" class="input" type="text">
        </div>
        <ul data-errors="email">
        </ul>
      </div>

      <div class="control">
        <button class="button is-info">Send</button>
      </div>
    </form>
  </div>
</section>
.hide {
  display: none;
}
function submit(event) {
  event.preventDefault();
  const input = collect_data(this);
  const formdata = validate_form(input);

  if (is_valid(formdata)) {
    clear_old_errors();
    toggle_notification();
  } else {
    Obj.map(show_errors, formdata);
  }
}

function setup() {
  document.forms.namedItem("someform").addEventListener("submit", submit);

  document
    .querySelectorAll("input")
    .forEach((el) => el.addEventListener("blur", clear_errors));

  document
    .querySelector(".notification .delete")
    .addEventListener("click", toggle_notification);
}

/*
 *
 *  Fantasy Land Utils
 *
 */

const Obj = {
  map(fn, data) {
    const result = {};
    for (let key in data) {
      result[key] = fn(data[key]);
    }

    return result;
  },
  ap(Fn, Data) {
    const result = {};
    for (let key in Data) {
      result[key] = Fn[key](Data[key]);
    }

    return result;
  }
};

/*
 *
 *  Validations
 *
 */
const validate_form = Obj.ap.bind(null, {
  name: validate.bind(null, [
    [long_enough, "Come on, try again."],
    [no_numbers, "Don't get smart. No numbers."]
  ]),
  email: validate.bind(null, [
    [long_enough, "Am I a joke to you?"],
    [is_email, "Totally not an email."]
  ])
});


function long_enough(input) {
  return input.length >= 2;
}

function is_email(input) {
  return input.includes("@");
}

function no_numbers(input) {
  return !/\d/.test(input);
}

function validate(validations, field) {
  const result = {...field};
  result.errors = [];

  for (let [validation, msg] of validations) {
    result.is_valid = validation(field.value);

    if (!result.is_valid) {
      result.errors.push(msg);
    }
  }

  return result;
}

function is_valid(formdata) {
  return Object.values(formdata).every((field) => field.is_valid);
}

/*
 *
 *  DOM Utilities
 *
 */

function collect_data(form_elm) {
  const result = {};
  const formdata = new FormData(form_elm);

  for (let entry of formdata.entries()) {
    result[entry[0]] = {
      field: entry[0],
      value: entry[1],      
    };
  }

  return result;
}

function show_errors(input) {
  const el = document.querySelector(`[data-errors=${input.field}]`);
  el.replaceChildren();

  for (let msg of input.errors) {
    const li = document.createElement("li");
    li.classList.add("help", "is-danger");
    li.textContent = msg;
    el.appendChild(li);
  }
}

function toggle_notification() {
  document.querySelector(".notification").classList.toggle("hide");
}

function clear_errors() {
  const name = this.getAttribute("name");
  document.querySelector(`[data-errors=${name}]`).replaceChildren();
}

function clear_old_errors() {
  document.querySelectorAll("input").forEach((el) => clear_errors.call(el));
}

setup();

External CSS

  1. https://cdnjs.cloudflare.com/ajax/libs/bulma/0.9.3/css/bulma.min.css

External JavaScript

This Pen doesn't use any external JavaScript resources.