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:
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.
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.
But what if I have an <input />
that has an initial value? In pure HTML, if I write <input value='foo' />
, then
- the initial value is
"foo"
- when the user changes the value, the property changes, but the attribute remains
"foo"
- 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();
}
}
This is particularly useful for readonly
inputs, though see emberjs/ember.js#11828 for a bug about using readonly
.