Skip to content

Instantly share code, notes, and snippets.

@jamesarosen
Last active June 1, 2016 22:48
Show Gist options
  • Save jamesarosen/9c0ae63fd1e5f8c0f5b6d2808545d46e to your computer and use it in GitHub Desktop.
Save jamesarosen/9c0ae63fd1e5f8c0f5b6d2808545d46e to your computer and use it in GitHub Desktop.
Inline Validations, Form Resetting, and Initial Values in Ember

Conditionally Show Validation Errors

I have a component for an <input> that (a) knows about validations and (b) knows how to only show validation errors if the user has touched the field or submitted the form:

// my-input/component.js
export default Ember.Component.extend({
  attributeBindings: [ 'isValid:data-is-valid', 'showValidationErrors:data-show-validation-errors' ],
  classNames: [ 'my-input' ],

  // isValid defined by Ember-Validations or some other mechanism

  showValidationErrors: false,
  value: undefined,

  didInsertElement() {
    this._super();
    const showErrors = this.set.bind(this, 'showValidationErrors', true);
    this.$('input')
      .on('focusOut.showValidationErrors', showErrors)
      .closest('form')
        .on('submit.showValidationErrors', showErrors);
  },

  willDestroyElement() {
    this.$('input')
      .off('focusOut.showValidationErrors')
      .closest('form')
        .off('submit.showValidationErrors');
    this._super();
  }
});

And its template:

{{!-- my-input/template.hbs --}}
{{input value=value ...}}
{{#if showValidationErrors}}
  {{#each errors}}
    ...
  {{/each}}
{{/if}}

And then I have some CSS like

.my-input[data-is-valid="false"][data-show-validation-errors="true"] { border-color: red; }

That works great! As soon as the user enters a field and then blurs from it, the input gets styled correctly and the errors show up.

Monkey Wrench / Spanner 1: Reset the Form

After using the above for many months successfully, I found a case where I needed to expand it a bit. I have a form for sending a message. When the user submits it, the form changes to a sending state, then a thank-you state, and then clears and resets to blank. The problem was that when I reset, each of these fields still thought they were touched, and thus started showing their validation errors. I solved this by watching for the reset event:

didInsertElement() {
  this._super();
  const showErrors = this.set.bind(this, 'showValidationErrors', true);
  const hideErrors = this.set.bind(this, 'showValidationErrors', false);
  this.$('input')
    .on('focusOut.showValidationErrors', showErrors)
    .closest('form')
      .on('submit.showValidationErrors', showErrors)
      .on('reset.showValidationErrors', hideErrors);
},

willDestroyElement() {
  this.$('input')
    .off('focusOut.showValidationErrors')
    .closest('form')
      .off('submit.showValidationErrors')
      .off('reset.showValidationErrors');
  this._super();
}

Now when the action on whatever outer component owns the form calls this.$('form')[0].reset(), the fields know to return to their default state.

Monkey Wrench / Spanner 2: Initial Values

But what if I have an <input /> that has an initial value? In pure HTML, if I write <input value='foo' />, then

  1. the initial value is "foo"
  2. when the user changes the value, the property changes, but the attribute remains "foo"
  3. when I reset the form, the property changes to the value of the attribute, "foo"

Unfortunately, Ember's {{input}} helper doesn't do anything with the attribute. My first instinct is to do it myself in didInsertElement:

// my-input/component.js:

value: undefined,

didInsertElement() {
  this._super();
  this.$('input').attr('value', this.get('value'));
  const showErrors = this.set.bind(this, 'showValidationErrors', true);
  this.$('input')
    .on('focusOut.showValidationErrors', showErrors)
    .closest('form')
      .on('submit.showValidationErrors', showErrors);
}

But this doesn't work because any number of things could cause that element to re-render, at which point it would set the "initial" value to the "current" value. What I really want is a self-documenting emulation of one-way binding:

// my-input/component.js:

initialValue: undefined,
onChange: Ember.K,
  
didInsertElement() {
  this.$('input').attr('value', this.get('initialValue'));
  // also call _super and bind the event handlers, as above
},

actions: {
  change() {
    this.onChange();
  }
}
{{!-- my-input/template.hbs --}}
{{input value=initialValue change=(action 'change')}}
{{!-- show errors as above --}}

This is particularly useful for readonly inputs, though see emberjs/ember.js#11828 for a bug about using readonly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment