From 40dec54689147e774d0bc783d5e19148712d47c4 Mon Sep 17 00:00:00 2001 From: Bartheleway Date: Tue, 3 Nov 2020 01:49:53 +0100 Subject: [PATCH 1/9] Fix backtracking on form --- .../paper-autocomplete/component.js | 95 +++++- addon/components/paper-form.js | 181 +++++++++--- addon/components/paper-input.js | 271 ++++++++++++------ addon/components/paper-select/component.js | 92 +++++- addon/mixins/child-mixin.js | 46 --- addon/mixins/parent-mixin.js | 25 -- addon/mixins/validation-mixin.js | 141 --------- addon/templates/components/paper-form.hbs | 24 +- addon/templates/components/paper-input.hbs | 4 - addon/utils/validation.js | 91 ++++++ package.json | 2 +- tests/dummy/app/templates/forms.hbs | 19 +- .../integration/components/paper-form-test.js | 101 ++++++- .../components/paper-input-test.js | 35 ++- 14 files changed, 726 insertions(+), 401 deletions(-) delete mode 100644 addon/mixins/child-mixin.js delete mode 100644 addon/mixins/parent-mixin.js delete mode 100644 addon/mixins/validation-mixin.js create mode 100644 addon/utils/validation.js diff --git a/addon/components/paper-autocomplete/component.js b/addon/components/paper-autocomplete/component.js index 9c8fe0c5d..dd756e7be 100644 --- a/addon/components/paper-autocomplete/component.js +++ b/addon/components/paper-autocomplete/component.js @@ -3,22 +3,90 @@ import template from './template'; import { tagName, layout } from '@ember-decorators/component'; import { action, computed } from '@ember/object'; +import { tracked } from '@glimmer/tracking' import calculatePosition from 'ember-paper/utils/calculate-ac-position'; -import ValidationMixin from 'ember-paper/mixins/validation-mixin'; - +import { invokeAction } from 'ember-invoke-action'; import { assert } from '@ember/debug'; import { indexOfOption } from 'ember-power-select/utils/group-utils'; + +import { buildComputedValidationMessages, notifyValidityChange } from 'ember-paper/utils/validation'; +import requiredValidator from 'ember-paper/validators/required'; +import minValidator from 'ember-paper/validators/min'; +import maxValidator from 'ember-paper/validators/max'; +import minlengthValidator from 'ember-paper/validators/minlength'; +import maxlengthValidator from 'ember-paper/validators/maxlength'; + +const validations = [ + requiredValidator, + minValidator, + maxValidator, + minlengthValidator, + maxlengthValidator +]; + @tagName('') @layout(template) -class PaperAutocomplete extends Component.extend(ValidationMixin) { +class PaperAutocomplete extends Component { + @tracked isTouched = false; + @computed('isTouched') + get formHasBeenValidated () { + return this.isTouched + } + + set formHasBeenValidated (value) { + this.isTouched = value + } + + validations = validations; + + @tracked + errorMessages + + @tracked + customValidations = [] + + @tracked + errors = [] + + @computed( + 'onSearchTextChange', + 'onSelectionChange', + 'searchText', + 'selected', + 'errors.[]', + 'customValidations.[]', + 'errorMessages', + requiredValidator.param, + minValidator.param, + maxValidator.param, + minlengthValidator.param, + maxlengthValidator.param + ) + get validationErrorMessages () { + const validationProperty = this.onSearchTextChange ? 'searchText' : 'selected'; + + return buildComputedValidationMessages.call(this, validationProperty) + } + + @computed.bool('validationErrorMessages.length') + hasErrorMessages + + @computed.reads('hasErrorMessages') + isInvalid + + @computed.not('isInvalid') + isValid + init() { this._initComponent(); super.init(...arguments); + + invokeAction(this, 'onRegister', this.get('elementId'), this.get('isValid'), this.get('isTouched'), this.get('isInvalidAndTouched')); } // Init autocomplete component @@ -31,6 +99,18 @@ class PaperAutocomplete extends Component.extend(ValidationMixin) { assert(' requires at least one of the `@onSelectionChange` or `@onSearchTextChange` functions to be provided.', hasTextChange || hasSelectionChange); } + destroy () { + const eltId = this.get('elementId') + + super.destroy(...arguments); + + invokeAction(this, 'onUnregister', eltId); + } + + notifyValidityChange() { + notifyValidityChange.call(this); + } + @action _onChange() { if (this.onSelectionChange) { @@ -40,15 +120,6 @@ class PaperAutocomplete extends Component.extend(ValidationMixin) { calculatePosition = calculatePosition; - @computed('onSearchTextChange', 'onSelectionChange') - get validationProperty() { - if (this.onSearchTextChange) { - return 'searchText'; - } else { - return 'selected'; - } - } - didReceiveAttrs() { super.didReceiveAttrs(...arguments); this.notifyValidityChange(); diff --git a/addon/components/paper-form.js b/addon/components/paper-form.js index 17f3b05fd..f22ec7775 100644 --- a/addon/components/paper-form.js +++ b/addon/components/paper-form.js @@ -4,57 +4,152 @@ import { not, and } from '@ember/object/computed'; import Component from '@ember/component'; -import { computed } from '@ember/object'; -import layout from '../templates/components/paper-form'; -import ParentMixin from 'ember-paper/mixins/parent-mixin'; +import { tagName, layout } from '@ember-decorators/component'; +import { set, action } from '@ember/object'; +import template from '../templates/components/paper-form'; +import { A } from '@ember/array'; import { invokeAction } from 'ember-invoke-action'; +import { tracked } from '@glimmer/tracking'; /** * @class PaperForm * @extends Ember.Component * @uses ParentMixin */ -export default Component.extend(ParentMixin, { - layout, - tagName: 'form', - - inputComponent: 'paper-input', - submitButtonComponent: 'paper-button', - selectComponent: 'paper-select', - autocompleteComponent: 'paper-autocomplete', - - isValid: not('isInvalid'), - isInvalid: computed('childComponents.@each.isInvalid', function() { - return this.get('childComponents').isAny('isInvalid'); - }), - - isTouched: computed('childComponents.@each.isTouched', function() { - return this.get('childComponents').isAny('isTouched'); - }), - - isInvalidAndTouched: and('isInvalid', 'isTouched'), - - submit() { - this.send('onSubmit'); - return false; - }, - - actions: { - onValidityChange() { - if (this.get('lastIsValid') !== this.get('isValid') || this.get('lastIsTouched') !== this.get('isTouched')) { - invokeAction(this, 'onValidityChange', this.get('isValid'), this.get('isTouched'), this.get('isInvalidAndTouched')); - this.set('lastIsValid', this.get('isValid')); - this.set('lastIsTouched', this.get('isTouched')); +@tagName('form') +@layout(template) +export default class PaperForm extends Component { + inputComponent = 'paper-input'; + submitButtonComponent = 'paper-button'; + selectComponent = 'paper-select'; + autocompleteComponent = 'paper-autocomplete'; + + finishFirstRender = false + + @tracked + formHasBeenValidated = false + + isValid = true + isTouched = false + + @not('isValid') + isInvalid + + @and('isInvalid', 'isTouched') + isInvalidAndTouched + + childComponents = A() + + updateValidity ({ childId, isValid, isTouched, isInvalidAndTouched }) { + const child = this.get('childComponents').findBy('childId', childId); + + if (child) { + const lastIsValid = child.isValid; + const lastIsTouched = child.isTouched; + + if ( + lastIsValid !== isValid + || lastIsTouched !== isTouched + ) { + set(child, 'isValid', isValid); + set(child, 'isTouched', isTouched); + set(child, 'isInvalidAndTouched', isInvalidAndTouched); + + this.notifyPropertyChange('childComponents') } - }, - onSubmit() { - if (this.get('isInvalid')) { - this.get('childComponents').setEach('isTouched', true); - invokeAction(this, 'onInvalid'); - } else { - invokeAction(this, 'onSubmit'); - this.get('childComponents').setEach('isTouched', false); + } + + this.triggerValidityChange() + } + + triggerValidityChange () { + const lastIsValid = this.get('isValid'); + const lastIsTouched = this.get('isTouched'); + + if ( + this.get('finishFirstRender') + && ( + lastIsValid !== this.getIsValid() + || lastIsTouched !== this.getIsTouched() + ) + ) { + this.setNewValidity() + } + } + + getIsValid () { + return this.get('childComponents').isEvery('isValid') + } + + getIsTouched () { + return this.get('childComponents').isAny('isTouched') + } + + setNewValidity () { + this.set('isValid', this.getIsValid()); + this.set('isTouched', this.getIsTouched()); + + invokeAction(this, 'onValidityChange', this.get('isValid'), this.get('isTouched'), this.get('isInvalidAndTouched')); + } + + @action + onChildValidityChange ({ elementId: childId, isValid, isTouched, isInvalidAndTouched }) { + if (!this.isDestroying) { + this.updateValidity({ + childId, + isValid, + isTouched, + isInvalidAndTouched + }) + } + } + + didRender () { + if (!this.get('finishFirstRender')) { + this.set('finishFirstRender', true) + + this.setNewValidity() + } + } + + submit (event) { + event.preventDefault() + + this.onInternalSubmit(...arguments) + } + + reset () { + this.set('formHasBeenValidated', false); + } + + @action + onInternalSubmit () { + if (this.get('isInvalid')) { + this.set('formHasBeenValidated', true); + + invokeAction(this, 'onInvalid'); + } else { + invokeAction(this, 'onSubmit'); + + this.set('formHasBeenValidated', false); + } + } + + @action + onRegister (childId) { + this.get('childComponents').pushObject({ childId }); + } + + @action + onUnregister (childId) { + if (!this.isDestroying) { + const child = this.get('childComponents').findBy('childId', childId) + + if (child) { + this.get('childComponents').removeObject(child); + + this.triggerValidityChange() } } } -}); +} diff --git a/addon/components/paper-input.js b/addon/components/paper-input.js index 59ce8925b..253eae482 100644 --- a/addon/components/paper-input.js +++ b/addon/components/paper-input.js @@ -1,33 +1,46 @@ /** * @module ember-paper */ -import { or, bool, and } from '@ember/object/computed'; +import { or, bool, and, not } from '@ember/object/computed'; import Component from '@ember/component'; -import { computed, set } from '@ember/object'; +import { tagName, layout } from '@ember-decorators/component'; +import { computed, set, action } from '@ember/object'; +import { tracked } from '@glimmer/tracking' import { isEmpty } from '@ember/utils'; import { run } from '@ember/runloop'; import { assert } from '@ember/debug'; -import layout from '../templates/components/paper-input'; +import template from '../templates/components/paper-input'; import FocusableMixin from 'ember-paper/mixins/focusable-mixin'; import ColorMixin from 'ember-paper/mixins/color-mixin'; -import ChildMixin from 'ember-paper/mixins/child-mixin'; -import ValidationMixin from 'ember-paper/mixins/validation-mixin'; +import { buildComputedValidationMessages, notifyValidityChange } from 'ember-paper/utils/validation'; +import requiredValidator from 'ember-paper/validators/required'; +import minValidator from 'ember-paper/validators/min'; +import maxValidator from 'ember-paper/validators/max'; +import minlengthValidator from 'ember-paper/validators/minlength'; +import maxlengthValidator from 'ember-paper/validators/maxlength'; import { invokeAction } from 'ember-invoke-action'; +const validations = [ + requiredValidator, + minValidator, + maxValidator, + minlengthValidator, + maxlengthValidator +]; + /** * @class PaperInput * @extends Ember.Component * @uses FocusableMixin - * @uses ChildMixin * @uses ColorMixin - * @uses ValidationMixin */ -export default Component.extend(FocusableMixin, ColorMixin, ChildMixin, ValidationMixin, { - layout, - tagName: 'md-input-container', - classNames: ['md-default-theme'], - classNameBindings: [ +@tagName('md-input-container') +@layout(template) +export default class PaperInput extends Component.extend(FocusableMixin, ColorMixin) { + classNames = ['md-default-theme']; + + classNameBindings = [ 'hasValue:md-input-has-value', 'isInvalidAndTouched:md-input-invalid', 'hasLeftIcon:md-icon-left', @@ -35,99 +48,163 @@ export default Component.extend(FocusableMixin, ColorMixin, ChildMixin, Validati 'focused:md-input-focused', 'block:md-block', 'placeholder:md-input-has-placeholder' - ], - type: 'text', - autofocus: false, - tabindex: null, - hideAllMessages: false, - isTouched: false, + ]; + + type = 'text'; + autofocus = false; + tabindex = null; + hideAllMessages = false; + + @tracked + isTouched = false; + + set formHasBeenValidated (value) { + this.set('isTouched', value) + } + + iconComponent = 'paper-icon'; + + validations = validations; - iconComponent: 'paper-icon', + @tracked + errorMessages + + @tracked + customValidations = [] + + @tracked + errors = [] + + @computed( + 'value', + 'errors.[]', + 'customValidations.[]', + 'errorMessages', + requiredValidator.param, + minValidator.param, + maxValidator.param, + minlengthValidator.param, + maxlengthValidator.param, + ) + get validationErrorMessages () { + return buildComputedValidationMessages.call(this, 'value') + } + + @bool('validationErrorMessages.length') + hasErrorMessages + + @not('isInvalid') + isValid // override validation mixin `isInvalid` to account for the native input validity - isInvalid: or('hasErrorMessages', 'isNativeInvalid'), + @or('hasErrorMessages', 'isNativeInvalid') + isInvalid - hasValue: computed('value', 'isNativeInvalid', function() { + @computed('value', 'isNativeInvalid') + get hasValue () { let value = this.get('value'); let isNativeInvalid = this.get('isNativeInvalid'); + return !isEmpty(value) || isNativeInvalid; - }), + } - shouldAddPlaceholder: computed('label', 'focused', function() { + @computed('label', 'focused') + get shouldAddPlaceholder () { // if has label, only add placeholder when focused return isEmpty(this.get('label')) || this.get('focused'); - }), + } - inputElementId: computed('elementId', { - get() { - return `input-${this.get('elementId')}`; - }, + @computed('elementId') + get inputElementId () { // elementId can be set from outside and it will override the computed value. // Please check the deprecations for further details // https://deprecations.emberjs.com/v3.x/#toc_computed-property-override - set(key, value) { - // To make sure the context updates properly, We are manually set value using @ember/object#set as recommended. - return set(this, "elementId", value); - } - }), - + return `input-${this.get('elementId')}`; + } + set inputElementId (value) { + // To make sure the context updates properly, We are manually set value using @ember/object#set as recommended. + return set(this, "elementId", value); + } - renderCharCount: computed('value', function() { + @computed('value') + get renderCharCount () { let currentLength = this.get('value') ? this.get('value').length : 0; return `${currentLength}/${this.get('maxlength')}`; - }), + } - hasLeftIcon: bool('icon'), - hasRightIcon: bool('iconRight'), - isInvalidAndTouched: and('isInvalid', 'isTouched'), + @bool('icon') + hasLeftIcon - validationProperty: 'value', // property that validations should be run on + @bool('iconRight') + hasRightIcon + + @and('isInvalid', 'isTouched') + isInvalidAndTouched // Lifecycle hooks - didReceiveAttrs() { - this._super(...arguments); + init () { + super.init(...arguments); + + invokeAction(this, 'onRegister', this.get('elementId')); + } + + didReceiveAttrs () { + super.didReceiveAttrs(...arguments); + assert('{{paper-input}} requires an `onChange` action or null for no action.', this.get('onChange') !== undefined); let { value, errors } = this.getProperties('value', 'errors'); let { _prevValue, _prevErrors } = this.getProperties('_prevValue', '_prevErrors'); + if (value !== _prevValue || errors !== _prevErrors) { this.notifyValidityChange(); } + this._prevValue = value; this._prevErrors = errors; - }, + } + + didInsertElement () { + super.didInsertElement(...arguments); - didInsertElement() { - this._super(...arguments); if (this.get('textarea')) { this._growTextareaOnResize = run.bind(this, this.growTextarea); window.addEventListener('resize', this._growTextareaOnResize); } - }, + } - didRender() { - this._super(...arguments); + didRender () { + super.didRender(...arguments); // setValue below ensures that the input value is the same as this.value this.setValue(this.get('value')); this.growTextarea(); - }, + } + + willDestroyElement () { + super.willDestroyElement(...arguments); - willDestroyElement() { - this._super(...arguments); if (this.get('textarea')) { window.removeEventListener('resize', this._growTextareaOnResize); this._growTextareaOnResize = null; } - }, + } + + destroy () { + const eltId = this.get('elementId') + + super.destroy(...arguments); + + invokeAction(this, 'onUnregister', eltId); + } - growTextarea() { + growTextarea () { if (this.get('textarea')) { - let inputElement = this.element.querySelector('input, textarea'); + const inputElement = this.element.querySelector('input, textarea'); inputElement.classList.add('md-no-flex'); inputElement.setAttribute('rows', 1); - let minRows = this.get('passThru.rows'); + const minRows = this.get('passThru.rows'); let height = this.getHeight(inputElement); if (minRows) { if (!this.lineHeight) { @@ -135,9 +212,11 @@ export default Component.extend(FocusableMixin, ColorMixin, ChildMixin, Validati this.lineHeight = inputElement.clientHeight; inputElement.style.minHeight = null; } + if (this.lineHeight) { height = Math.max(height, this.lineHeight * minRows); } + let proposedHeight = Math.round(height / this.lineHeight); let maxRows = this.get('passThru.maxRows') || Number.MAX_VALUE; let rowsToSet = Math.min(proposedHeight, maxRows); @@ -150,7 +229,6 @@ export default Component.extend(FocusableMixin, ColorMixin, ChildMixin, Validati } else { inputElement.classList.remove('md-textarea-scrollable'); } - } else { inputElement.style.height = 'auto'; inputElement.scrollTop = 0; @@ -162,50 +240,63 @@ export default Component.extend(FocusableMixin, ColorMixin, ChildMixin, Validati inputElement.classList.remove('md-no-flex'); } - }, + } + + getHeight (inputElement) { + const { offsetHeight } = inputElement; + const line = inputElement.scrollHeight - offsetHeight; - getHeight(inputElement) { - let { offsetHeight } = inputElement; - let line = inputElement.scrollHeight - offsetHeight; return offsetHeight + (line > 0 ? line : 0); - }, + } - setValue(value) { + setValue (value) { // normalize falsy values to empty string value = isEmpty(value) ? '' : value; if (this.element.querySelector('input, textarea').value !== value) { this.element.querySelector('input, textarea').value = value; } - }, - - actions: { - handleInput(e) { - invokeAction(this, 'onChange', e.target.value); - // setValue below ensures that the input value is the same as this.value - run.next(() => { - if (this.isDestroyed) { - return; - } - this.setValue(this.get('value')); - }); - this.growTextarea(); - let inputElement = this.element.querySelector('input'); - let isNativeInvalid = inputElement && inputElement.validity && inputElement.validity.badInput; - if (this.type === 'date' && e.target.value === '') { - // Chrome doesn't fire the onInput event when clearing the second and third date components. - // This means that we won't see another event when badInput becomes false if the user is clearing - // the date field. The reported value is empty, though, so we can already mark it as valid. - isNativeInvalid = false; + } + + notifyValidityChange () { + notifyValidityChange.call(this); + } + + @action + handleInput (e) { + invokeAction(this, 'onChange', e.target.value); + // setValue below ensures that the input value is the same as this.value + run.next(() => { + if (this.isDestroyed) { + return; } - this.set('isNativeInvalid', isNativeInvalid); - this.notifyValidityChange(); - }, + this.setValue(this.get('value')); + }); - handleBlur(e) { - invokeAction(this, 'onBlur', e); - this.set('isTouched', true); - this.notifyValidityChange(); + this.growTextarea(); + + let inputElement = this.element.querySelector('input'); + let isNativeInvalid = inputElement && inputElement.validity && inputElement.validity.badInput; + + if (this.type === 'date' && e.target.value === '') { + // Chrome doesn't fire the onInput event when clearing the second and third date components. + // This means that we won't see another event when badInput becomes false if the user is clearing + // the date field. The reported value is empty, though, so we can already mark it as valid. + isNativeInvalid = false; } + + this.set('isNativeInvalid', isNativeInvalid); + this.notifyValidityChange(); + } + + focusOut (e) { + invokeAction(this, 'onBlur', e); + + this.set('isTouched', true); + this.notifyValidityChange(); + } + + focusIn (e) { + invokeAction(this, 'onFocus', e); } -}); +} diff --git a/addon/components/paper-select/component.js b/addon/components/paper-select/component.js index d72528e46..68210c50b 100644 --- a/addon/components/paper-select/component.js +++ b/addon/components/paper-select/component.js @@ -3,10 +3,9 @@ import template from './template'; import { tagName, layout } from '@ember-decorators/component'; import { action } from '@ember/object'; -import { and } from '@ember/object/computed'; -import ChildMixin from 'ember-paper/mixins/child-mixin'; - -import ValidationMixin from 'ember-paper/mixins/validation-mixin'; +import { tracked } from '@glimmer/tracking' +import { computed } from '@ember/object'; +import { invokeAction } from 'ember-invoke-action'; import clamp from 'ember-paper/utils/clamp'; @@ -21,20 +20,97 @@ function getOffsetRect(node) { } : { left: 0, top: 0, width: 0, height: 0 }; } +import { buildComputedValidationMessages, notifyValidityChange } from 'ember-paper/utils/validation'; +import requiredValidator from 'ember-paper/validators/required'; +import minValidator from 'ember-paper/validators/min'; +import maxValidator from 'ember-paper/validators/max'; +import minlengthValidator from 'ember-paper/validators/minlength'; +import maxlengthValidator from 'ember-paper/validators/maxlength'; + +const validations = [ + requiredValidator, + minValidator, + maxValidator, + minlengthValidator, + maxlengthValidator +]; + @tagName('') @layout(template) -class PaperSelect extends Component.extend(ValidationMixin, ChildMixin) { +class PaperSelect extends Component { - validationProperty = 'selected'; + @tracked isTouched = false; isFocused = false; - @and('isInvalid', 'isTouched') + @computed('isTouched') + get formHasBeenValidated () { + return this.isTouched + } + + set formHasBeenValidated (value) { + this.isTouched = value + } + + @tracked + errorMessages + + @tracked + customValidations = [] + + @tracked + errors = [] + + @computed.and('isInvalid', 'isTouched') isInvalidAndTouched; - @and('isFocused', 'selected') + @computed.and('isFocused', 'selected') isFocusedAndSelected; + validations = validations; + + @computed( + 'selected', + 'errors.[]', + 'customValidations.[]', + 'errorMessages', + requiredValidator.param, + minValidator.param, + maxValidator.param, + minlengthValidator.param, + maxlengthValidator.param + ) + get validationErrorMessages () { + return buildComputedValidationMessages.call(this, 'selected') + } + + @computed.bool('validationErrorMessages.length') + hasErrorMessages + + @computed.reads('hasErrorMessages') + isInvalid + + @computed.not('isInvalid') + isValid + + init() { + super.init(...arguments); + + invokeAction(this, 'onRegister', this.get('elementId'), this.get('isValid'), this.get('isTouched'), this.get('isInvalidAndTouched')); + } + + destroy () { + const eltId = this.get('elementId') + + super.destroy(...arguments); + + invokeAction(this, 'onUnregister', eltId); + } + + notifyValidityChange() { + notifyValidityChange.call(this); + } + didReceiveAttrs() { super.didReceiveAttrs(...arguments); this.notifyValidityChange(); diff --git a/addon/mixins/child-mixin.js b/addon/mixins/child-mixin.js deleted file mode 100644 index b7ee8806d..000000000 --- a/addon/mixins/child-mixin.js +++ /dev/null @@ -1,46 +0,0 @@ -/** - * @module ember-paper - */ -import Mixin from '@ember/object/mixin'; - -import { computed } from '@ember/object'; -import ParentMixin from 'ember-paper/mixins/parent-mixin'; - -/** - * @class ChildMixin - * @extends Ember.Mixin - */ -export default Mixin.create({ - - // override to look for a specific parent class - parentClass: ParentMixin, - - // this will typically be overriden when yielding a contextual component - parentComponent: computed({ - get() { - if (this._parentComponent !== undefined) { - return this._parentComponent; - } - - return this.nearestOfType(this.get('parentClass')); - }, - - set(key, value) { - return this._parentComponent = value; - } - }), - - init() { - this._super(...arguments); - if (this.get('parentComponent')) { - this.get('parentComponent').register(this); - } - }, - - willDestroyElement() { - this._super(...arguments); - if (this.get('parentComponent')) { - this.get('parentComponent').unregister(this); - } - } -}); diff --git a/addon/mixins/parent-mixin.js b/addon/mixins/parent-mixin.js deleted file mode 100644 index bf3df43ca..000000000 --- a/addon/mixins/parent-mixin.js +++ /dev/null @@ -1,25 +0,0 @@ -/** - * @module ember-paper - */ -import Mixin from '@ember/object/mixin'; - -import { computed } from '@ember/object'; -import { A } from '@ember/array'; - -/** - * @class ParentMixin - * @extends Ember.Mixin - */ -export default Mixin.create({ - childComponents: computed(function() { - return A(); - }), - - register(child) { - this.get('childComponents').pushObject(child); - }, - - unregister(child) { - this.get('childComponents').removeObject(child); - } -}); diff --git a/addon/mixins/validation-mixin.js b/addon/mixins/validation-mixin.js deleted file mode 100644 index f2a43015f..000000000 --- a/addon/mixins/validation-mixin.js +++ /dev/null @@ -1,141 +0,0 @@ -/** - * @module ember-paper - */ - -import Mixin from '@ember/object/mixin'; -import { assert, warn } from '@ember/debug'; -import { isArray, A } from '@ember/array'; -import { get, computed, defineProperty } from '@ember/object'; -import { bool, reads, not } from '@ember/object/computed'; -import { loc } from '@ember/string'; -import { isBlank } from '@ember/utils'; -import requiredValidator from 'ember-paper/validators/required'; -import minValidator from 'ember-paper/validators/min'; -import maxValidator from 'ember-paper/validators/max'; -import minlengthValidator from 'ember-paper/validators/minlength'; -import maxlengthValidator from 'ember-paper/validators/maxlength'; -import { invokeAction } from 'ember-invoke-action'; - -/** - * In order to make validation generic it is required that components using the validation mixin - * specify what property the validation logic should be based on. - * - * @public - * - * @return computed property that depends on the supplied property name - */ -function buildComputedValidationMessages(property, validations = [], customValidations = []) { - let validationParams = validations.map((v) => get(v, 'param')).filter((v) => !isBlank(v)); - let customValidationParams = customValidations.map((v) => get(v, 'param')).filter((v) => !isBlank(v)); - - return computed(property, 'errors.[]', 'customValidations.[]', ...validationParams, ...customValidationParams, function() { - let validations = A(); - let messages = A(); - - // built-in validations - validations.pushObjects(this.validations()); - - // custom validations - let customValidations = this.get('customValidations'); - assert('`customValidations` must be an array', isArray(customValidations)); - validations.pushObjects(customValidations); - - // execute validations - let currentValue = this.get(property); - validations.forEach((validation) => { - assert('validation must include a `validate(value)` function', validation && validation.validate && typeof validation.validate === 'function'); - try { - let valParam = get(validation, 'param'); - let paramValue = valParam ? this.get(valParam) : undefined; - if (!validation.validate(currentValue, paramValue)) { - let message = this.get(`errorMessages.${valParam}`) || get(validation, 'message'); - messages.pushObject({ - message: loc(message.string || message, paramValue, currentValue) - }); - } - } catch(error) { - warn(`Exception with validation: ${validation} ${error}`, false); - } - }); - - // error messages array - let errors = this.get('errors') || []; - assert('`errors` must be an array', isArray(errors)); - messages.pushObjects(errors.map((e) => { - return get(e, 'message') ? e : { message: e }; - })); - - return messages; - }); -} - -/** - * @class ValidationMixin - * @extends Ember.Mixin - */ -export default Mixin.create({ - validationErrorMessages: null, - lastIsInvalid: undefined, - validationProperty: null, // property that validation should be based on - - init() { - this._super(...arguments); - assert('validationProperty must be set', this.get('validationProperty')); - if (!this.get('validationErrorMessages')) { - let computedValidationMessages = buildComputedValidationMessages( - this.get('validationProperty'), - this.validations(), - this.get('customValidations') - ); - defineProperty(this, 'validationErrorMessages', computedValidationMessages); - } - }, - - hasErrorMessages: bool('validationErrorMessages.length'), - - /** - * The result of isInvalid is appropriate for controlling the display of - * validation error messages. It also may be used to distinguish whether - * the input would be considered valid after it is touched. - * - * @public - * - * @return {boolean} Whether the input is or would be invalid. - * false: input is valid (touched or not), or is no longer rendered - * true: input has been touched and is invalid. - */ - isInvalid: reads('hasErrorMessages'), - isValid: not('isInvalid'), - - /** - * Return the built-in validations. - * - * May be overridden to provide additional built-in validations. Be sure to - * call this._super() to retrieve the standard validations. - * - * @public - */ - validations() { - return [ - requiredValidator, - minValidator, - maxValidator, - minlengthValidator, - maxlengthValidator - ]; - }, - - notifyValidityChange() { - let isValid = this.get('isValid'); - let lastIsValid = this.get('lastIsValid'); - let isTouched = this.get('isTouched'); - let lastIsTouched = this.get('lastIsTouched'); - if (lastIsValid !== isValid || lastIsTouched !== isTouched) { - invokeAction(this, 'onValidityChange', isValid); - this.set('lastIsValid', isValid); - this.set('lastIsTouched', isTouched); - } - }, - customValidations: [], - errors: [] -}); diff --git a/addon/templates/components/paper-form.hbs b/addon/templates/components/paper-form.hbs index 726b0c954..a792fc301 100644 --- a/addon/templates/components/paper-form.hbs +++ b/addon/templates/components/paper-form.hbs @@ -3,20 +3,30 @@ isInvalid=isInvalid isTouched=isTouched isInvalidAndTouched=isInvalidAndTouched + formHasBeenValidated=formHasBeenValidated input=(component inputComponent - parentComponent=this - onValidityChange=(action "onValidityChange") + onValidityChange=(action "onChildValidityChange") + onRegister=(action "onRegister") + onUnregister=(action "onUnregister") + formHasBeenValidated=formHasBeenValidated ) submit-button=(component submitButtonComponent type="submit" ) select=(component selectComponent - parentComponent=this - onValidityChange=(action "onValidityChange") + onValidityChange=(action "onChildValidityChange") + onRegister=(action "onRegister") + onUnregister=(action "onUnregister") + formHasBeenValidated=formHasBeenValidated ) autocomplete=(component autocompleteComponent - parentComponent=this - onValidityChange=(action "onValidityChange") + onValidityChange=(action "onChildValidityChange") + onRegister=(action "onRegister") + onUnregister=(action "onUnregister") + formHasBeenValidated=formHasBeenValidated ) - onSubmit=(action "onSubmit") + onSubmit=(action "onInternalSubmit") + register=(action "onRegister") + unregister=(action "onUnregister") + validityChange=(action "onChildValidityChange") )}} diff --git a/addon/templates/components/paper-input.hbs b/addon/templates/components/paper-input.hbs index 09f0d93cf..f80ce9291 100644 --- a/addon/templates/components/paper-input.hbs +++ b/addon/templates/components/paper-input.hbs @@ -14,8 +14,6 @@ disabled={{disabled}} autofocus={{autofocus}} aria-describedby={{concat elementId "-char-count " elementId "-error-messages"}} - onfocus={{onFocus}} - onblur={{action "handleBlur"}} onkeydown={{onKeyDown}} onkeyup={{onKeyUp}} onclick={{onClick}} @@ -44,8 +42,6 @@ disabled={{disabled}} autofocus={{autofocus}} aria-describedby={{concat elementId "-char-count " elementId "-error-messages"}} - onfocus={{onFocus}} - onblur={{action "handleBlur"}} onkeydown={{onKeyDown}} onkeyup={{onKeyUp}} onclick={{onClick}} diff --git a/addon/utils/validation.js b/addon/utils/validation.js new file mode 100644 index 000000000..51fa5cfe3 --- /dev/null +++ b/addon/utils/validation.js @@ -0,0 +1,91 @@ +import { assert, warn } from '@ember/debug'; +import { isArray, A } from '@ember/array'; +import { get } from '@ember/object'; +import { loc } from '@ember/string'; +import { invokeAction } from 'ember-invoke-action'; + +/** + * In order to make validation generic it is required that components using the validation mixin + * specify what property the validation logic should be based on. + * + * @public + * + * @return computed property that depends on the supplied property name + */ +export function buildComputedValidationMessages(property) { + const validators = A(); + const messages = A(); + + // built-in validations + validators.pushObjects(this.get('validations')); + + // custom validations + let customValidators = this.get('customValidations'); + + assert('`customValidations` must be an array', isArray(customValidators)); + + validators.pushObjects(customValidators); + + // execute validations + let currentValue = this.get(property); + + validators.forEach((validation) => { + assert('validation must include a `validate(value)` function', validation && validation.validate && typeof validation.validate === 'function'); + + try { + let valParam = get(validation, 'param'); + let paramValue = valParam ? this.get(valParam) : undefined; + + if (!validation.validate(currentValue, paramValue)) { + const message = this.get(`errorMessages.${valParam}`) || get(validation, 'message'); + + messages.pushObject({ + message: loc(message.string || message, paramValue, currentValue) + }); + } + } catch(error) { + warn(`Exception with validation: ${validation} ${error}`, false, { + id: 'ember-paper-compute-validation-message' + }); + } + }); + + // error messages array + let errors = this.get('errors') || []; + + assert('`errors` must be an array', isArray(errors)); + + messages.pushObjects(errors.map((e) => { + return get(e, 'message') ? e : { message: e }; + })); + + return messages; +} + +export function notifyValidityChange() { + const isValid = this.get('isValid'); + const lastIsValid = this.get('lastIsValid'); + const isTouched = this.get('isTouched'); + const lastIsTouched = this.get('lastIsTouched'); + const isInvalidAndTouched = this.get('isInvalidAndTouched'); + + if ( + lastIsValid !== isValid + || lastIsTouched !== isTouched + ) { + invokeAction(this, 'onValidityChange', { + elementId: this.get('elementId'), + isValid, + isTouched, + isInvalidAndTouched + }); + + this.set('lastIsValid', isValid); + this.set('lastIsTouched', isTouched); + } +} + +export default { + buildComputedValidationMessages, + notifyValidityChange +}; diff --git a/package.json b/package.json index 00d1ace04..4f3fb0346 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,6 @@ "@ember/jquery": "^1.1.0", "@ember/optional-features": "^1.3.0", "@glimmer/component": "^1.0.0", - "@glimmer/tracking": "^1.0.0", "babel-eslint": "^8.2.6", "broccoli-asset-rev": "^3.0.0", "ember-cli": "~3.16.0", @@ -56,6 +55,7 @@ "qunit-dom": "^1.0.0" }, "dependencies": { + "@glimmer/tracking": "^1.0.0", "@html-next/vertical-collection": "^1.0.0", "angular-material-styles": "1.1.21", "broccoli-file-creator": "^2.1.1", diff --git a/tests/dummy/app/templates/forms.hbs b/tests/dummy/app/templates/forms.hbs index 75e98cbe8..e33491f32 100644 --- a/tests/dummy/app/templates/forms.hbs +++ b/tests/dummy/app/templates/forms.hbs @@ -91,6 +91,10 @@ +

+ You can pass a customValidations array property to each input component (see paper-input for more details). +

+

If you prefer, you can trigger the submit action without using a submit button. paper-form also yields an onSubmit action you @@ -103,16 +107,21 @@

- You can also extend paper-input to make your own custom components - (money inputs, phone inputs, etc.) and the form validation will still - work out of the box! - Just replace paper-input with your component's name. + Extending paper-input component will give you the opportunity to override some default behavior. + You can also work with any components you'd like. + For this, paper-form yields three actions and one property : +

    +
  • register: to inform the form your component is influencing the global valid state
  • +
  • unregister: to detach your component when it destroys (usually called in willDestroyElement hook)
  • +
  • validityChange: to inform the form that the validity of your component changed
  • +
  • formHasBeenValidated: changes when form is submitted / resetted
  • +

{{! BEGIN-SNIPPET form.custom-components }}
- +
Submit diff --git a/tests/integration/components/paper-form-test.js b/tests/integration/components/paper-form-test.js index e402a0ac1..5c93bc51b 100644 --- a/tests/integration/components/paper-form-test.js +++ b/tests/integration/components/paper-form-test.js @@ -1,7 +1,7 @@ import Component from '@ember/component'; import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; -import { render, triggerEvent, click } from '@ember/test-helpers'; +import { render, triggerEvent, click, waitFor, setupOnerror } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; module('Integration | Component | paper form', function(hooks) { @@ -10,6 +10,8 @@ module('Integration | Component | paper form', function(hooks) { test('`isInvalid` and `isValid` work as expected', async function(assert) { assert.expect(4); + this.set('errors', []); + await render(hbs` {{#paper-form as |form|}} {{form.input value=foo onChange=(action (mut foo)) label="Foo"}} @@ -97,6 +99,8 @@ module('Integration | Component | paper form', function(hooks) { assert.notOk(isInvalidAndTouched); }); + this.set('errors', []); + await render(hbs` {{#paper-form onValidityChange=(action onValidityChange) as |form|}} {{form.input value=foo onChange=(action (mut foo)) label="Foo"}} @@ -128,6 +132,92 @@ module('Integration | Component | paper form', function(hooks) { }); + test('form `onValidityChange` action is invoked on dynamic component removal', async function(assert) { + // paper-form triggers `onValidityChange` when components are added / deleted + // so we expect three runs: one for first render / one for delete / one for add back + assert.expect(9); + + this.set('onValidityChange', (isValid, isTouched, isInvalidAndTouched) => { + assert.notOk(isValid); + assert.notOk(isTouched); + assert.notOk(isInvalidAndTouched); + }); + + this.set('showChild', true); + + await render(hbs` + {{#paper-form onValidityChange=(action onValidityChange) as |form|}} + {{form.input value=foo onChange=(action (mut foo)) label="Foo"}} + + {{#if showChild}} + {{form.input class="show" value=bar onChange=(action (mut bar)) label="Bar" required=true}} + {{else}} +
+ {{/if}} + {{/paper-form}} + `); + + this.set('onValidityChange', (isValid, isTouched, isInvalidAndTouched) => { + assert.ok(isValid); + assert.notOk(isTouched); + assert.notOk(isInvalidAndTouched); + }); + + this.set('showChild', false); + + await waitFor('.not-show', { timeout: 5000 }); + + this.set('onValidityChange', (isValid, isTouched, isInvalidAndTouched) => { + assert.notOk(isValid); + assert.notOk(isTouched); + assert.notOk(isInvalidAndTouched); + }); + + this.set('showChild', true); + + await waitFor('.show', { timeout: 5000 }); + }); + + test('form backtracking isValid property', async function(assert) { + assert.expect(6); + + setupOnerror(function (error) { + assert.notOk(error) + }) + + this.set('onValidityChange', (isValid, isTouched, isInvalidAndTouched) => { + assert.notOk(isValid); + assert.notOk(isTouched); + assert.notOk(isInvalidAndTouched); + }); + + this.set('showChild', true); + + await render(hbs` + {{#if showChild}} + {{#paper-form class="show" onValidityChange=(action onValidityChange) as |form|}} + {{form.input value=foo onChange=(action (mut foo)) label="Foo"}} + {{form.input value=bar onChange=(action (mut bar)) label="Bar" required=true}} + {{/paper-form}} + {{else}} + {{#paper-form class="not-show" onValidityChange=(action onValidityChange) as |form|}} + {{form.input value=foo2 onChange=(action (mut foo2)) label="Foo2"}} + {{form.input value=bar2 onChange=(action (mut bar2)) label="Bar2"}} + {{/paper-form}} + {{/if}} + `); + + this.set('onValidityChange', (isValid, isTouched, isInvalidAndTouched) => { + assert.ok(isValid); + assert.notOk(isTouched); + assert.notOk(isInvalidAndTouched); + }); + + this.set('showChild', false); + + await waitFor('.not-show', { timeout: 5000 }); + }); + test('form is reset after submit action is invoked', async function(assert) { assert.expect(3); @@ -136,8 +226,7 @@ module('Integration | Component | paper form', function(hooks) { {{form.input value=foo onChange=(action (mut foo)) label="Foo"}} {{form.input value=bar onChange=(action (mut bar)) label="Bar"}} - - + {{/paper-form}} `); @@ -157,10 +246,12 @@ module('Integration | Component | paper form', function(hooks) { test('works without using contextual components', async function(assert) { assert.expect(4); + this.set('errors', []) + await render(hbs` {{#paper-form as |form|}} - {{paper-input value=foo onChange=(action (mut foo)) label="Foo"}} - {{paper-input value=bar onChange=(action (mut bar)) label="Bar" errors=errors}} + {{paper-input value=foo onRegister=(action form.register) onUnegister=(action form.unregister) onValidityChange=(action form.validityChange) formHasBeenValidated=form.formHasBeenValidated onChange=(action (mut foo)) label="Foo"}} + {{paper-input value=bar onRegister=(action form.register) onUnegister=(action form.unregister) onValidityChange=(action form.validityChange) formHasBeenValidated=form.formHasBeenValidated onChange=(action (mut bar)) label="Bar" errors=errors}} {{#if form.isInvalid}}
Form is invalid!
diff --git a/tests/integration/components/paper-input-test.js b/tests/integration/components/paper-input-test.js index 7341b635d..a9c8e497b 100644 --- a/tests/integration/components/paper-input-test.js +++ b/tests/integration/components/paper-input-test.js @@ -288,13 +288,13 @@ module('Integration | Component | paper-input', function(hooks) { this.value = 'aaabbbccc'; this.customValidations = [{ param: 'notinclude', - message: 'You can\'t include the substring %@.', - validate: (value, notinclude) => typeof value === 'string' && value.indexOf(notinclude) === -1 + message: 'You can\'t include the substring cc.', + validate: (value) => typeof value === 'string' && value.indexOf('cc') === -1 }]; await render(hbs` {{paper-input value=value onChange=dummyOnChange isTouched=true - maxlength=8 customValidations=customValidations notinclude="cc"}} + maxlength=8 customValidations=customValidations}} `); assert.dom('.paper-input-error').exists({ count: 2 }, 'renders two errors'); @@ -321,15 +321,14 @@ module('Integration | Component | paper-input', function(hooks) { assert.dom('.paper-input-error:first-child').hasText('This is required.'); }); - test('changing param in custom validations works', async function(assert) { + test('changing custom validations works', async function(assert) { assert.expect(6); this.value = 'aaabbbccc'; - this.notinclude = 'cc'; this.customValidations = [{ param: 'notinclude', - message: 'You can\'t include the substring %@.', - validate: (value, notinclude) => typeof value === 'string' && value.indexOf(notinclude) === -1 + message: 'You can\'t include the substring cc.', + validate: (value) => typeof value === 'string' && value.indexOf('cc') === -1 }]; await render(hbs` @@ -341,7 +340,11 @@ module('Integration | Component | paper-input', function(hooks) { assert.dom('.paper-input-error:first-child').hasText('Must not exceed 8 characters.'); assert.dom('.paper-input-error:last-child').hasText("You can't include the substring cc."); - this.set('notinclude', 'bb'); + this.set('customValidations', [{ + param: 'notinclude', + message: 'You can\'t include the substring bb.', + validate: (value) => typeof value === 'string' && value.indexOf('bb') === -1 + }]); assert.dom('.paper-input-error').exists({ count: 2 }); assert.dom('.paper-input-error:first-child').hasText('Must not exceed 8 characters.'); @@ -373,16 +376,16 @@ module('Integration | Component | paper-input', function(hooks) { this.value = 'aaabbbccc'; this.customValidations = [{ param: 'notinclude', - message: 'You can\'t include the substring %@.', - validate: (value, notinclude) => typeof value === 'string' && value.indexOf(notinclude) === -1 + message: 'You can\'t include the substring cc.', + validate: (value) => typeof value === 'string' && value.indexOf('cc') === -1 }]; await render(hbs` {{paper-input value=value onChange=dummyOnChange isTouched=true - maxlength=8 customValidations=customValidations notinclude="cc" + maxlength=8 customValidations=customValidations errorMessages=(hash maxlength="Too small, baby!" - notinclude="Can't have %@, baby!" + notinclude="Can't have cc, baby!" )}} `); @@ -402,7 +405,9 @@ module('Integration | Component | paper-input', function(hooks) { attribute: 'foo' }]; - await render(hbs`{{paper-input onChange=dummyOnChange errors=errors isTouched=true}}`); + await render(hbs`{{paper-input onChange=dummyOnChange errors=errors}}`); + + await triggerEvent('input:first-of-type', 'blur'); assert.dom('.paper-input-error').exists({ count: 2 }, 'renders two errors'); assert.dom('.paper-input-error:first-child').hasText('foo should be a number.'); @@ -417,7 +422,9 @@ module('Integration | Component | paper-input', function(hooks) { 'foo should be smaller than 12.' ]; - await render(hbs`{{paper-input onChange=dummyOnChange errors=errors isTouched=true}}`); + await render(hbs`{{paper-input onChange=dummyOnChange errors=errors}}`); + + await triggerEvent('input:first-of-type', 'blur'); assert.dom('.paper-input-error').exists({ count: 2 }, 'renders two errors'); assert.dom('.paper-input-error:first-child').hasText('foo should be a number.'); From b218848ad29b9e00ec7b883c7632ff2a5fc18dfb Mon Sep 17 00:00:00 2001 From: Bartheleway Date: Tue, 3 Nov 2020 02:40:43 +0100 Subject: [PATCH 2/9] Remove getWithDefault --- addon/components/paper-icon.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/addon/components/paper-icon.js b/addon/components/paper-icon.js index 6ad12c4a5..7e582d61d 100644 --- a/addon/components/paper-icon.js +++ b/addon/components/paper-icon.js @@ -27,8 +27,9 @@ let PaperIconComponent = Component.extend(ColorMixin, { reverseSpin: false, iconClass: computed('icon', 'positionalIcon', function() { - let icon = this.getWithDefault('positionalIcon', this.get('icon')); - return icon; + let icon = this.get('positionalIcon'); + + return icon === undefined ? this.get('icon') : icon; }), 'aria-hidden': false, From a8cdff76c61f86325f1231934bde556c84ab5904 Mon Sep 17 00:00:00 2001 From: Bartheleway Date: Sun, 8 Nov 2020 18:55:36 +0100 Subject: [PATCH 3/9] Fix use object get / set for compatibility issues --- addon/components/paper-form.js | 26 ++++++++++----------- addon/utils/validation.js | 42 +++++++++++++++++----------------- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/addon/components/paper-form.js b/addon/components/paper-form.js index f22ec7775..b94ba1986 100644 --- a/addon/components/paper-form.js +++ b/addon/components/paper-form.js @@ -5,7 +5,7 @@ import { not, and } from '@ember/object/computed'; import Component from '@ember/component'; import { tagName, layout } from '@ember-decorators/component'; -import { set, action } from '@ember/object'; +import { get, set, action } from '@ember/object'; import template from '../templates/components/paper-form'; import { A } from '@ember/array'; import { invokeAction } from 'ember-invoke-action'; @@ -41,7 +41,7 @@ export default class PaperForm extends Component { childComponents = A() updateValidity ({ childId, isValid, isTouched, isInvalidAndTouched }) { - const child = this.get('childComponents').findBy('childId', childId); + const child = get(this, 'childComponents').findBy('childId', childId); if (child) { const lastIsValid = child.isValid; @@ -63,11 +63,11 @@ export default class PaperForm extends Component { } triggerValidityChange () { - const lastIsValid = this.get('isValid'); - const lastIsTouched = this.get('isTouched'); + const lastIsValid = get(this, 'isValid'); + const lastIsTouched = get(this, 'isTouched'); if ( - this.get('finishFirstRender') + get(this, 'finishFirstRender') && ( lastIsValid !== this.getIsValid() || lastIsTouched !== this.getIsTouched() @@ -78,18 +78,18 @@ export default class PaperForm extends Component { } getIsValid () { - return this.get('childComponents').isEvery('isValid') + return get(this, 'childComponents').isEvery('isValid') } getIsTouched () { - return this.get('childComponents').isAny('isTouched') + return get(this, 'childComponents').isAny('isTouched') } setNewValidity () { this.set('isValid', this.getIsValid()); this.set('isTouched', this.getIsTouched()); - invokeAction(this, 'onValidityChange', this.get('isValid'), this.get('isTouched'), this.get('isInvalidAndTouched')); + invokeAction(this, 'onValidityChange', get(this, 'isValid'), get(this, 'isTouched'), get(this, 'isInvalidAndTouched')); } @action @@ -105,7 +105,7 @@ export default class PaperForm extends Component { } didRender () { - if (!this.get('finishFirstRender')) { + if (!get(this, 'finishFirstRender')) { this.set('finishFirstRender', true) this.setNewValidity() @@ -124,7 +124,7 @@ export default class PaperForm extends Component { @action onInternalSubmit () { - if (this.get('isInvalid')) { + if (get(this, 'isInvalid')) { this.set('formHasBeenValidated', true); invokeAction(this, 'onInvalid'); @@ -137,16 +137,16 @@ export default class PaperForm extends Component { @action onRegister (childId) { - this.get('childComponents').pushObject({ childId }); + get(this, 'childComponents').pushObject({ childId }); } @action onUnregister (childId) { if (!this.isDestroying) { - const child = this.get('childComponents').findBy('childId', childId) + const child = get(this, 'childComponents').findBy('childId', childId) if (child) { - this.get('childComponents').removeObject(child); + get(this, 'childComponents').removeObject(child); this.triggerValidityChange() } diff --git a/addon/utils/validation.js b/addon/utils/validation.js index 51fa5cfe3..8bba31fa2 100644 --- a/addon/utils/validation.js +++ b/addon/utils/validation.js @@ -1,6 +1,6 @@ import { assert, warn } from '@ember/debug'; -import { isArray, A } from '@ember/array'; -import { get } from '@ember/object'; +import { isArray } from '@ember/array'; +import { get, set } from '@ember/object'; import { loc } from '@ember/string'; import { invokeAction } from 'ember-invoke-action'; @@ -13,33 +13,33 @@ import { invokeAction } from 'ember-invoke-action'; * @return computed property that depends on the supplied property name */ export function buildComputedValidationMessages(property) { - const validators = A(); - const messages = A(); + const validators = []; + const messages = []; // built-in validations - validators.pushObjects(this.get('validations')); + validators.push(...get(this, 'validations')); // custom validations - let customValidators = this.get('customValidations'); + let customValidators = get(this, 'customValidations'); assert('`customValidations` must be an array', isArray(customValidators)); - validators.pushObjects(customValidators); + validators.push(...customValidators); // execute validations - let currentValue = this.get(property); + let currentValue = get(this, property); validators.forEach((validation) => { assert('validation must include a `validate(value)` function', validation && validation.validate && typeof validation.validate === 'function'); try { let valParam = get(validation, 'param'); - let paramValue = valParam ? this.get(valParam) : undefined; + let paramValue = valParam ? get(this, valParam) : undefined; if (!validation.validate(currentValue, paramValue)) { - const message = this.get(`errorMessages.${valParam}`) || get(validation, 'message'); + const message = get(this, `errorMessages.${valParam}`) || get(validation, 'message'); - messages.pushObject({ + messages.push({ message: loc(message.string || message, paramValue, currentValue) }); } @@ -51,11 +51,11 @@ export function buildComputedValidationMessages(property) { }); // error messages array - let errors = this.get('errors') || []; + let errors = get(this, 'errors') || []; assert('`errors` must be an array', isArray(errors)); - messages.pushObjects(errors.map((e) => { + messages.push(...errors.map((e) => { return get(e, 'message') ? e : { message: e }; })); @@ -63,25 +63,25 @@ export function buildComputedValidationMessages(property) { } export function notifyValidityChange() { - const isValid = this.get('isValid'); - const lastIsValid = this.get('lastIsValid'); - const isTouched = this.get('isTouched'); - const lastIsTouched = this.get('lastIsTouched'); - const isInvalidAndTouched = this.get('isInvalidAndTouched'); + const isValid = get(this, 'isValid'); + const lastIsValid = get(this, 'lastIsValid'); + const isTouched = get(this, 'isTouched'); + const lastIsTouched = get(this, 'lastIsTouched'); + const isInvalidAndTouched = get(this, 'isInvalidAndTouched'); if ( lastIsValid !== isValid || lastIsTouched !== isTouched ) { invokeAction(this, 'onValidityChange', { - elementId: this.get('elementId'), + elementId: get(this, 'elementId'), isValid, isTouched, isInvalidAndTouched }); - this.set('lastIsValid', isValid); - this.set('lastIsTouched', isTouched); + set(this, 'lastIsValid', isValid); + set(this, 'lastIsTouched', isTouched); } } From 7e2c7f4737357828b095b25dcc1710ffd8da89e2 Mon Sep 17 00:00:00 2001 From: Bartheleway Date: Sun, 8 Nov 2020 19:01:01 +0100 Subject: [PATCH 4/9] Move to new structure --- addon/components/{paper-input.js => paper-input/component.js} | 2 +- .../paper-input.hbs => components/paper-input/template.hbs} | 0 app/components/paper-input.js | 4 +--- 3 files changed, 2 insertions(+), 4 deletions(-) rename addon/components/{paper-input.js => paper-input/component.js} (99%) rename addon/{templates/components/paper-input.hbs => components/paper-input/template.hbs} (100%) diff --git a/addon/components/paper-input.js b/addon/components/paper-input/component.js similarity index 99% rename from addon/components/paper-input.js rename to addon/components/paper-input/component.js index 253eae482..f67bbc866 100644 --- a/addon/components/paper-input.js +++ b/addon/components/paper-input/component.js @@ -10,7 +10,7 @@ import { tracked } from '@glimmer/tracking' import { isEmpty } from '@ember/utils'; import { run } from '@ember/runloop'; import { assert } from '@ember/debug'; -import template from '../templates/components/paper-input'; +import template from './template'; import FocusableMixin from 'ember-paper/mixins/focusable-mixin'; import ColorMixin from 'ember-paper/mixins/color-mixin'; import { buildComputedValidationMessages, notifyValidityChange } from 'ember-paper/utils/validation'; diff --git a/addon/templates/components/paper-input.hbs b/addon/components/paper-input/template.hbs similarity index 100% rename from addon/templates/components/paper-input.hbs rename to addon/components/paper-input/template.hbs diff --git a/app/components/paper-input.js b/app/components/paper-input.js index 2195498b6..4448dc11d 100644 --- a/app/components/paper-input.js +++ b/app/components/paper-input.js @@ -1,3 +1 @@ -import PaperInput from 'ember-paper/components/paper-input'; - -export default PaperInput; +export { default } from 'ember-paper/components/paper-input/component'; From 15cf8c73099c694048216889059384edd0bb9d66 Mon Sep 17 00:00:00 2001 From: Bartheleway Date: Sun, 8 Nov 2020 19:03:36 +0100 Subject: [PATCH 5/9] Use new this / @ and prepare migration to glimmer --- addon/components/paper-input/component.js | 202 +++++++++++++--------- addon/components/paper-input/template.hbs | 178 ++++++++++--------- addon/utils/color-class-bindings.js | 18 ++ 3 files changed, 227 insertions(+), 171 deletions(-) create mode 100644 addon/utils/color-class-bindings.js diff --git a/addon/components/paper-input/component.js b/addon/components/paper-input/component.js index f67bbc866..f64c2afe8 100644 --- a/addon/components/paper-input/component.js +++ b/addon/components/paper-input/component.js @@ -5,14 +5,13 @@ import { or, bool, and, not } from '@ember/object/computed'; import Component from '@ember/component'; import { tagName, layout } from '@ember-decorators/component'; -import { computed, set, action } from '@ember/object'; +import { computed, set, get } from '@ember/object'; import { tracked } from '@glimmer/tracking' import { isEmpty } from '@ember/utils'; import { run } from '@ember/runloop'; import { assert } from '@ember/debug'; import template from './template'; -import FocusableMixin from 'ember-paper/mixins/focusable-mixin'; -import ColorMixin from 'ember-paper/mixins/color-mixin'; +import colorClassNameBindings from 'ember-paper/utils/color-class-bindings'; import { buildComputedValidationMessages, notifyValidityChange } from 'ember-paper/utils/validation'; import requiredValidator from 'ember-paper/validators/required'; import minValidator from 'ember-paper/validators/min'; @@ -32,28 +31,68 @@ const validations = [ /** * @class PaperInput * @extends Ember.Component - * @uses FocusableMixin - * @uses ColorMixin */ @tagName('md-input-container') @layout(template) -export default class PaperInput extends Component.extend(FocusableMixin, ColorMixin) { - classNames = ['md-default-theme']; - - classNameBindings = [ - 'hasValue:md-input-has-value', - 'isInvalidAndTouched:md-input-invalid', - 'hasLeftIcon:md-icon-left', - 'hasRightIcon:md-icon-right', - 'focused:md-input-focused', - 'block:md-block', - 'placeholder:md-input-has-placeholder' - ]; +export default class PaperInput extends Component { + classNameBindings = ['computedClasses'] + + @computed( + 'hasValue', + 'isInvalidAndTouched', + 'hasLeftIcon', + 'hasRightIcon', + 'focused', + 'block', + 'placeholder', + ...colorClassNameBindings.map(binding => binding.param) + ) + get computedClasses () { + const classes = [ + 'md-default-theme' + ] + + if (get(this, 'hasValue')) { + classes.push('md-input-has-value') + } + + if (get(this, 'isInvalidAndTouched')) { + classes.push('md-input-invalid') + } + + if (get(this, 'hasLeftIcon')) { + classes.push('md-icon-left') + } + + if (get(this, 'hasRightIcon')) { + classes.push('md-icon-right') + } + + if (get(this, 'focused')) { + classes.push('md-input-focused') + } + + if (get(this, 'block')) { + classes.push('md-block') + } + + if (get(this, 'placeholder')) { + classes.push('md-input-has-placeholder') + } + + colorClassNameBindings.forEach(binding => { + if (this[binding.param]) { + classes.push(binding.class) + } + }) + + return classes.join(' ') + } type = 'text'; - autofocus = false; - tabindex = null; - hideAllMessages = false; + + @tracked + focused = false; @tracked isTouched = false; @@ -67,27 +106,26 @@ export default class PaperInput extends Component.extend(FocusableMixin, ColorMi validations = validations; @tracked - errorMessages + errorMessages; @tracked - customValidations = [] + value = ''; @tracked - errors = [] + customValidations = []; + + @tracked + errors = []; @computed( 'value', 'errors.[]', 'customValidations.[]', 'errorMessages', - requiredValidator.param, - minValidator.param, - maxValidator.param, - minlengthValidator.param, - maxlengthValidator.param, + ...validations.map(validator => validator.param), ) get validationErrorMessages () { - return buildComputedValidationMessages.call(this, 'value') + return buildComputedValidationMessages.call(this, 'value'); } @bool('validationErrorMessages.length') @@ -102,8 +140,8 @@ export default class PaperInput extends Component.extend(FocusableMixin, ColorMi @computed('value', 'isNativeInvalid') get hasValue () { - let value = this.get('value'); - let isNativeInvalid = this.get('isNativeInvalid'); + const value = get(this, 'value'); + const isNativeInvalid = get(this, 'isNativeInvalid'); return !isEmpty(value) || isNativeInvalid; } @@ -111,26 +149,15 @@ export default class PaperInput extends Component.extend(FocusableMixin, ColorMi @computed('label', 'focused') get shouldAddPlaceholder () { // if has label, only add placeholder when focused - return isEmpty(this.get('label')) || this.get('focused'); + return isEmpty(get(this, 'label')) || get(this, 'focused'); } - @computed('elementId') - get inputElementId () { - // elementId can be set from outside and it will override the computed value. - // Please check the deprecations for further details - // https://deprecations.emberjs.com/v3.x/#toc_computed-property-override - return `input-${this.get('elementId')}`; - } + inputElementId - set inputElementId (value) { - // To make sure the context updates properly, We are manually set value using @ember/object#set as recommended. - return set(this, "elementId", value); - } - - @computed('value') + @computed('value', 'maxlength') get renderCharCount () { - let currentLength = this.get('value') ? this.get('value').length : 0; - return `${currentLength}/${this.get('maxlength')}`; + let currentLength = get(this, 'value') ? get(this, 'value.length') : 0; + return `${currentLength}/${get(this, 'maxlength')}`; } @bool('icon') @@ -146,66 +173,63 @@ export default class PaperInput extends Component.extend(FocusableMixin, ColorMi init () { super.init(...arguments); - invokeAction(this, 'onRegister', this.get('elementId')); + assert('{{paper-input}} requires an `onChange` action or null for no action.', get(this, 'onChange') !== undefined); + + if (get(this, 'onRegister')) { + invokeAction(this, 'onRegister', get(this, 'elementId')); + } + + if (!get(this, 'inputElementId')) { + set(this, 'inputElementId', `input-${get(this, 'elementId')}`) + } } didReceiveAttrs () { super.didReceiveAttrs(...arguments); - assert('{{paper-input}} requires an `onChange` action or null for no action.', this.get('onChange') !== undefined); - - let { value, errors } = this.getProperties('value', 'errors'); - let { _prevValue, _prevErrors } = this.getProperties('_prevValue', '_prevErrors'); - - if (value !== _prevValue || errors !== _prevErrors) { - this.notifyValidityChange(); - } - - this._prevValue = value; - this._prevErrors = errors; + this.notifyValidityChange(); } didInsertElement () { super.didInsertElement(...arguments); - if (this.get('textarea')) { - this._growTextareaOnResize = run.bind(this, this.growTextarea); - window.addEventListener('resize', this._growTextareaOnResize); - } - } + if (get(this, 'textarea')) { + set(this, '_growTextareaOnResize', run.bind(this, this.growTextarea)); - didRender () { - super.didRender(...arguments); - // setValue below ensures that the input value is the same as this.value - this.setValue(this.get('value')); - this.growTextarea(); + window.addEventListener('resize', get(this, '_growTextareaOnResize')); + } } willDestroyElement () { super.willDestroyElement(...arguments); - if (this.get('textarea')) { - window.removeEventListener('resize', this._growTextareaOnResize); - this._growTextareaOnResize = null; + if (get(this, 'textarea')) { + window.removeEventListener('resize', get(this, '_growTextareaOnResize')); + + set(this, '_growTextareaOnResize', null); } } - destroy () { - const eltId = this.get('elementId') + willDestroy () { + const eltId = get(this, 'elementId'); - super.destroy(...arguments); + super.willDestroy(...arguments); - invokeAction(this, 'onUnregister', eltId); + if (get(this, 'onUnregister')) { + invokeAction(this, 'onUnregister', eltId); + } } growTextarea () { - if (this.get('textarea')) { + if (get(this, 'textarea')) { const inputElement = this.element.querySelector('input, textarea'); + inputElement.classList.add('md-no-flex'); inputElement.setAttribute('rows', 1); - const minRows = this.get('passThru.rows'); + const minRows = get(this, 'passThru.rows'); let height = this.getHeight(inputElement); + if (minRows) { if (!this.lineHeight) { inputElement.style.minHeight = 0; @@ -218,7 +242,7 @@ export default class PaperInput extends Component.extend(FocusableMixin, ColorMi } let proposedHeight = Math.round(height / this.lineHeight); - let maxRows = this.get('passThru.maxRows') || Number.MAX_VALUE; + let maxRows = get(this, 'passThru.maxRows') || Number.MAX_VALUE; let rowsToSet = Math.min(proposedHeight, maxRows); inputElement.style.height = `${this.lineHeight * rowsToSet}px`; @@ -232,7 +256,9 @@ export default class PaperInput extends Component.extend(FocusableMixin, ColorMi } else { inputElement.style.height = 'auto'; inputElement.scrollTop = 0; + let height = this.getHeight(inputElement); + if (height) { inputElement.style.height = `${height}px`; } @@ -262,41 +288,45 @@ export default class PaperInput extends Component.extend(FocusableMixin, ColorMi notifyValidityChange.call(this); } - @action - handleInput (e) { + input (e) { invokeAction(this, 'onChange', e.target.value); + // setValue below ensures that the input value is the same as this.value run.next(() => { if (this.isDestroyed) { return; } - this.setValue(this.get('value')); + this.setValue(get(this, 'value')); }); this.growTextarea(); - let inputElement = this.element.querySelector('input'); + const inputElement = this.element.querySelector('input'); let isNativeInvalid = inputElement && inputElement.validity && inputElement.validity.badInput; - if (this.type === 'date' && e.target.value === '') { + if (get(this, 'type') === 'date' && e.target.value === '') { // Chrome doesn't fire the onInput event when clearing the second and third date components. // This means that we won't see another event when badInput becomes false if the user is clearing // the date field. The reported value is empty, though, so we can already mark it as valid. isNativeInvalid = false; } - this.set('isNativeInvalid', isNativeInvalid); + set(this, 'isNativeInvalid', isNativeInvalid); this.notifyValidityChange(); } focusOut (e) { + set(this, 'isTouched', true); + set(this, 'focused', true); + invokeAction(this, 'onBlur', e); - this.set('isTouched', true); this.notifyValidityChange(); } focusIn (e) { + set(this, 'focused', false); + invokeAction(this, 'onFocus', e); } } diff --git a/addon/components/paper-input/template.hbs b/addon/components/paper-input/template.hbs index f80ce9291..f460e52c0 100644 --- a/addon/components/paper-input/template.hbs +++ b/addon/components/paper-input/template.hbs @@ -1,94 +1,102 @@ -{{#if label}} - +{{#if @label}} + {{/if}} -{{#if icon}} - {{component iconComponent icon}} +{{#if @icon}} + {{component this.iconComponent @icon}} {{/if}} -{{#if textarea}} +{{#if @textarea}} + name={{@passThru.name}} + rows={{@passThru.rows}} + cols={{@passThru.cols}} + maxlength={{@passThru.maxlength}} + tabindex={{@passThru.tabindex}} + title={{@title}} + required={{@passThru.required}} + selectionEnd={{@passThru.selectionEnd}} + selectionStart={{@passThru.selectionStart}} + selectionDirection={{@passThru.selectionDirection}} + wrap={{@passThru.wrap}} + readonly={{@passThru.readonly}} + form={{@passThru.form}} + spellcheck={{@passThru.spellcheck}} + {{did-insert this.bindResize}} + {{did-update this.growTextarea}} + {{will-destroy this.unbindResize}} + ...attributes + >{{@value}} {{else}} + accept={{@passThru.accept}} + autocomplete={{@passThru.autocomplete}} + autocorrect={{@passThru.autocorrect}} + autocapitalize={{@passThru.autocapitalize}} + autosave={{@passThru.autosave}} + form={{@passThru.form}} + formaction={{@passThru.formaction}} + formenctype={{@passThru.formenctype}} + formmethod={{@passThru.formmethod}} + formnovalidate={{@passThru.formnovalidate}} + formtarget={{@passThru.formtarget}} + height={{@passThru.height}} + inputmode={{@passThru.inputmode}} + min={{@passThru.min}} + maxlength={{@passThru.maxlength}} + max={{@passThru.max}} + multiple={{@passThru.multiple}} + name={{@passThru.name}} + pattern={{@passThru.pattern}} + readonly={{@passThru.readonly}} + required={{@passThru.required}} + selectionDirection={{@passThru.selectionDirection}} + size={{@passThru.size}} + spellcheck={{@passThru.spellcheck}} + step={{@passThru.step}} + tabindex={{@passThru.tabindex}} + title={{@title}} + width={{@passThru.width}} + ...attributes + > {{/if}} -{{#unless hideAllMessages}} -
+{{#unless @hideAllMessages}} +
{{#if maxlength}} -
{{renderCharCount}}
+
{{this.renderCharCount}}
{{/if}}
- {{#if isInvalidAndTouched}} -
- {{#each validationErrorMessages as |error index|}} -
+ {{#if this.isInvalidAndTouched}} +
+ {{#each this.validationErrorMessages as |error index|}} +
{{error.message}}
{{/each}} @@ -97,14 +105,14 @@ {{/unless}} {{yield (hash - charCount=currentLength - isInvalid=isInvalid - isTouched=isTouched - isInvalidAndTouched=isInvalidAndTouched - hasValue=hasValue - validationErrorMessages=validationErrorMessages + charCount=this.currentLength + isInvalid=this.isInvalid + isTouched=this.isTouched + isInvalidAndTouched=this.isInvalidAndTouched + hasValue=this.hasValue + validationErrorMessages=this.validationErrorMessages )}} -{{#if iconRight}} - {{component iconComponent iconRight}} +{{#if @iconRight}} + {{component iconComponent @iconRight}} {{/if}} diff --git a/addon/utils/color-class-bindings.js b/addon/utils/color-class-bindings.js new file mode 100644 index 000000000..38db24281 --- /dev/null +++ b/addon/utils/color-class-bindings.js @@ -0,0 +1,18 @@ +/** + * @module ember-paper + */ + +export default [ + { + param: 'warn', + class: 'md-warn', + }, + { + param: 'accent', + class: 'md-accent', + }, + { + param: 'primary', + class: 'md-primary' + }, +]; From 8f00e070b56460c3b292153538f64759cbfa1a29 Mon Sep 17 00:00:00 2001 From: Bartheleway Date: Sun, 8 Nov 2020 19:04:11 +0100 Subject: [PATCH 6/9] Fix backtracking when user enters input --- addon/utils/validation.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/addon/utils/validation.js b/addon/utils/validation.js index 8bba31fa2..852d3500f 100644 --- a/addon/utils/validation.js +++ b/addon/utils/validation.js @@ -1,5 +1,6 @@ import { assert, warn } from '@ember/debug'; import { isArray } from '@ember/array'; +import { run } from '@ember/runloop'; import { get, set } from '@ember/object'; import { loc } from '@ember/string'; import { invokeAction } from 'ember-invoke-action'; @@ -73,11 +74,15 @@ export function notifyValidityChange() { lastIsValid !== isValid || lastIsTouched !== isTouched ) { - invokeAction(this, 'onValidityChange', { - elementId: get(this, 'elementId'), - isValid, - isTouched, - isInvalidAndTouched + run.next(() => { + if (!this.isDestroyed) { + invokeAction(this, 'onValidityChange', { + elementId: get(this, 'elementId'), + isValid, + isTouched, + isInvalidAndTouched + }); + } }); set(this, 'lastIsValid', isValid); From 29a37c2bf62bd58fe70470d0c549f600e8e45ec4 Mon Sep 17 00:00:00 2001 From: Bartheleway Date: Sun, 8 Nov 2020 19:50:54 +0100 Subject: [PATCH 7/9] Move run.next to form as it's the only concerned component --- addon/components/paper-form.js | 11 ++++++++--- addon/utils/validation.js | 15 +++++---------- tests/integration/components/paper-form-test.js | 6 +++++- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/addon/components/paper-form.js b/addon/components/paper-form.js index b94ba1986..127aec3a4 100644 --- a/addon/components/paper-form.js +++ b/addon/components/paper-form.js @@ -5,6 +5,7 @@ import { not, and } from '@ember/object/computed'; import Component from '@ember/component'; import { tagName, layout } from '@ember-decorators/component'; +import { run } from '@ember/runloop'; import { get, set, action } from '@ember/object'; import template from '../templates/components/paper-form'; import { A } from '@ember/array'; @@ -86,10 +87,14 @@ export default class PaperForm extends Component { } setNewValidity () { - this.set('isValid', this.getIsValid()); - this.set('isTouched', this.getIsTouched()); + run.next(() => { + if (!get(this, 'isDestroying')) { + this.set('isValid', this.getIsValid()); + this.set('isTouched', this.getIsTouched()); - invokeAction(this, 'onValidityChange', get(this, 'isValid'), get(this, 'isTouched'), get(this, 'isInvalidAndTouched')); + invokeAction(this, 'onValidityChange', get(this, 'isValid'), get(this, 'isTouched'), get(this, 'isInvalidAndTouched')); + } + }) } @action diff --git a/addon/utils/validation.js b/addon/utils/validation.js index 852d3500f..8bba31fa2 100644 --- a/addon/utils/validation.js +++ b/addon/utils/validation.js @@ -1,6 +1,5 @@ import { assert, warn } from '@ember/debug'; import { isArray } from '@ember/array'; -import { run } from '@ember/runloop'; import { get, set } from '@ember/object'; import { loc } from '@ember/string'; import { invokeAction } from 'ember-invoke-action'; @@ -74,15 +73,11 @@ export function notifyValidityChange() { lastIsValid !== isValid || lastIsTouched !== isTouched ) { - run.next(() => { - if (!this.isDestroyed) { - invokeAction(this, 'onValidityChange', { - elementId: get(this, 'elementId'), - isValid, - isTouched, - isInvalidAndTouched - }); - } + invokeAction(this, 'onValidityChange', { + elementId: get(this, 'elementId'), + isValid, + isTouched, + isInvalidAndTouched }); set(this, 'lastIsValid', isValid); diff --git a/tests/integration/components/paper-form-test.js b/tests/integration/components/paper-form-test.js index 5c93bc51b..28d9f420e 100644 --- a/tests/integration/components/paper-form-test.js +++ b/tests/integration/components/paper-form-test.js @@ -1,7 +1,7 @@ import Component from '@ember/component'; import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; -import { render, triggerEvent, click, waitFor, setupOnerror } from '@ember/test-helpers'; +import { render, triggerEvent, click, waitFor, settled, setupOnerror } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; module('Integration | Component | paper form', function(hooks) { @@ -38,6 +38,8 @@ module('Integration | Component | paper form', function(hooks) { attribute: 'foo' }]); + await settled(); + assert.dom('.invalid-div').exists({ count: 1 }); assert.dom('.valid-div').doesNotExist(); }); @@ -274,6 +276,8 @@ module('Integration | Component | paper form', function(hooks) { attribute: 'foo' }]); + await settled(); + assert.dom('.invalid-div').exists({ count: 1 }); assert.dom('.valid-div').doesNotExist(); }); From eb64ddc391dcd19d7a94b516a59bc554e08a8f6a Mon Sep 17 00:00:00 2001 From: Bartheleway Date: Sat, 28 Nov 2020 16:20:00 +0100 Subject: [PATCH 8/9] Fix package.lock to reflect package modifications --- package-lock.json | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9fe547b66..2b19104ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1961,8 +1961,7 @@ "@glimmer/env": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/@glimmer/env/-/env-0.1.7.tgz", - "integrity": "sha1-/S0rVakCnGs3psk16MiHGucN+gc=", - "dev": true + "integrity": "sha1-/S0rVakCnGs3psk16MiHGucN+gc=" }, "@glimmer/interfaces": { "version": "0.45.1", @@ -1986,7 +1985,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/@glimmer/tracking/-/tracking-1.0.0.tgz", "integrity": "sha512-OuF04ihYD/Rjvf++Rf7MzJVnawMSax/SZXEj4rlsQoMRwtQafgtkWjlFBcbBNQkJ3rev1zzfNN+3mdD2BFIaNg==", - "dev": true, "requires": { "@glimmer/env": "^0.1.7", "@glimmer/validator": "^0.44.0" @@ -2001,8 +1999,7 @@ "@glimmer/validator": { "version": "0.44.0", "resolved": "https://registry.npmjs.org/@glimmer/validator/-/validator-0.44.0.tgz", - "integrity": "sha512-i01plR0EgFVz69GDrEuFgq1NheIjZcyTy3c7q+w7d096ddPVeVcRzU3LKaqCfovvLJ+6lJx40j45ecycASUUyw==", - "dev": true + "integrity": "sha512-i01plR0EgFVz69GDrEuFgq1NheIjZcyTy3c7q+w7d096ddPVeVcRzU3LKaqCfovvLJ+6lJx40j45ecycASUUyw==" }, "@html-next/vertical-collection": { "version": "1.0.0", From e1ae932f19ce384853d18d5125eb5a81a3ab30ba Mon Sep 17 00:00:00 2001 From: Bartheleway Date: Mon, 7 Dec 2020 20:42:19 +0100 Subject: [PATCH 9/9] Fix remove glimmer anticipated stuff --- addon/components/paper-input/template.hbs | 3 --- 1 file changed, 3 deletions(-) diff --git a/addon/components/paper-input/template.hbs b/addon/components/paper-input/template.hbs index f460e52c0..14aed1ae5 100644 --- a/addon/components/paper-input/template.hbs +++ b/addon/components/paper-input/template.hbs @@ -32,9 +32,6 @@ readonly={{@passThru.readonly}} form={{@passThru.form}} spellcheck={{@passThru.spellcheck}} - {{did-insert this.bindResize}} - {{did-update this.growTextarea}} - {{will-destroy this.unbindResize}} ...attributes >{{@value}} {{else}}