form framework

Based on AngularJS

by Richard Hunter

HTML

<div class="input-field">
  <input id="first-name" class="input" />
  <div class="help-text">Put your first name here</div>
  <div class="error-message">
    An error occurred
  </div>
</div>
<div class="input-field">
  <input id="last-name" class="input" />
  <div class="help-text">Put your last name here</div>
  <div class="error-message">
    An error occurred
  </div>
</div>

CSS

.input {
  border: solid 2px grey;
  outline: none;
}
.error-message {
  color: red;
  display: none;
}
.input.is-invalid.is-touched {
  border-color: red;
}
.input.is-invalid.is-touched ~ .error-message {
  display: block;
}
.input.is-invalid.is-touched ~ .help-text {
  display: none;
}

.input.is-valid.is-touched {
  border-color: limegreen;
}

.input-field {
  padding: 10px;
  border: 1px grey solid;
  margin: 10px;
}

JavaScript

function createWatcher(watcher, listener, data) {
  let value = watcher(data);
  listener(value, null, data);

  return (data) => {
    const newValue = watcher(data);

    if (newValue !== value) {
      listener(newValue, value);
      value = newValue;
    }
  };
}

class State {
  constructor(initial) {
    this.data = {
      ...initial
    };
    this.watchers = [];
  }

  watch(watcher, listener) {
    this.watchers.push(createWatcher(watcher, listener, this.data));
  }

  digest() {
    this.watchers.forEach(watcher => {
      watcher(this.data);
    });
  }

  inputField(key, value, valid) {
    this.data = {
      ...this.data,
      [key]: {
        ...this.data[key],
        value,
        valid,
      },
    };
    
    this.digest();
  }

  blurField(key) {
    this.data = {
      ...this.data,
      [key]: {
        ...this.data[key],
        touched: true,
      }
    };
    this.digest();
  }
}

const firstNameInitialValue = '';
const lastNameInitialValue = '';

const state = new State({
  firstName: {
    value: firstNameInitialValue,
    valid: isFirstNameValid(firstNameInitialValue),
    touched: false,
    errorMessage: 'An error occurred',
  },
  lastName: {
    value: lastNameInitialValue,
    valid: isLastNameValid(lastNameInitialValue),
    touched: false,
    errorMessage: 'You must include a last name',
  }
});


state.watch((data) => {
  return data.firstName.valid;
}, (newValue) => {
  const input = document.querySelector('#first-name');

  if (!newValue) {
    input.classList.remove('is-valid');
    input.classList.add('is-invalid');
  } else {
    input.classList.add('is-valid');
    input.classList.remove('is-invalid');
  }
});

state.watch((data) => {
  return data.firstName.touched;
}, newValue => {
  const input = document.querySelector('#first-name');

  if (newValue) {
    input.classList.add('is-touched');
    input.classList.remove('is-untouched');
  } else {
    input.classList.add('is-untouched');
   ...