MobX Form Data Abstraction

Separates form state from form rendering using MobX.

by Matt McCray

HTML

<script src="https://npmcdn.com/[email protected]/lib/mobx.umd.js"></script>
<script src="https://npmcdn.com/[email protected]/index.js"></script>
<script src="https://npmcdn.com/[email protected]"></script>
<body>
  <div id="app_root">One Moment..</div>
</body>

SCSS

body {
  font-family: "Helvetica Neue", Sans-Serif;  
  padding: 25px;
  
  h1 {
    margin-bottom: 25px;
    small {
      color: gray;
      margin-left: 1em;
    }
  }
}

.Property {
  font-size: 12px;
  
  &_Label {
    text-align: right;
    width: 100px;
    display: inline-block;
    margin-right: 5px;
    color: dimgray;
  }
  &_Value {
    display: inline-block;
  }
}

.Field {
  input {
    font-size: 110%;
  }
  
  &_ErrorMessage {
    color: crimson;
    font-size:80%;
  }
}

Babel + JSX

const { action, useStrict, observable, computed, asReference, toJS } = mobx
const { observer } = mobxReact
const { Component } = React
const DevTools = mobxDevtools.default


useStrict(true)

class FormField {
  name = null
  
  @observable initialValue = null
  @observable value = null
  @observable isTouched = false
  @observable validators = asReference([])
  
  @computed get isValid() {
  	return this.errors.length === 0
  }
  
  @computed get isDirty() {
  	return this.initialValue !== this.value
  }
  
  @computed get errors() {
  	return this.validators
      .map(validate => validate(this.value, this))
      .filter(result => typeof result === 'string')
  }
  
  @computed get hasErrors() {
  	return this.errors.length > 0
  }
  
  constructor(name, initialValue, ...validators) {
    if (isEmpty(initialValue)) {
      initialValue = ""
    }
    else if (typeof initialValue === 'function') {
    	validators.unshift(initialValue)
      initialValue = ""
    }
    
	  action('FormField Ctor', () => {
  		this.name = name
    	this.initialValue = initialValue
      this.value = initialValue
    	this.addValidator(...validators)
	  })()
  }
  
  @action('addValidator')
  addValidator(...validators) {
		validators.forEach(validator => {
    	this.validators.push(validator)
    })
  }
  
  @action('changeFromUI')
  changeFromUI(value) {
  	this.value = value
    this.isTouched = true
  }
  
  @action('setInitialValue')
  setInitialValue(value, reset = true) {
  	this.initialValue = value
    
    if (reset) {
    	this.value = value
      this.isTouched = false
    }
  }
  
  @action('reset')
  reset() {
  	this.value = this.initialValue
    this.isTouched = false
  }
}

class Form {
	@observable fieldNames = []
  @observable fields = asReference({})
  
	@computed get isValid() {
  	return this.fieldNames
    	.map(name => this.fields[name].isValid)
      .filter(isValid => isValid === false)
      .length === 0
  }

	@computed get isDirty() {
  	return...