0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-01-27 22:49:56 -05:00

Refactored <GhBenefitItem>

refs https://github.com/TryGhost/Ghost/issues/14101

- migrated component to Glimmer
- swapped usage of `ValidationState` mixin for `{{validation-status}}` modifier
  - updated modifier to accept custom error/success classes
- removed unnecessary/unused code in the `gh-benefit-item.js` backing class
This commit is contained in:
Kevin Ansfield 2022-12-09 17:06:49 +00:00
parent 25c530293f
commit c3487fea41
4 changed files with 199 additions and 106 deletions

View file

@ -1,39 +1,46 @@
{{#unless this.benefitItem.isNew}}
<span class="gh-blognav-grab">
{{svg-jar "grab"}}
<span class="sr-only">Reorder</span>
</span>
{{/unless}}
<div
class="gh-blognav-item {{unless @benefitItem.isNew "gh-blognav-item--sortable"}}"
{{validation-status errorClass="gh-blognav-item--error" errors=@benefitItem.errors}}
...attributes
>
{{#unless @benefitItem.isNew}}
<span class="gh-blognav-grab">
{{svg-jar "grab"}}
<span class="sr-only">Reorder</span>
</span>
{{/unless}}
<div class="gh-blognav-line {{unless this.name "placeholder"}}">
{{svg-jar "check-2"}}
<span
class="gh-blognav-label"
{{validation-status errors=this.benefitItem.errors property="name" hasValidated=this.benefitItem.hasValidated}}
>
<GhTrimFocusInput
@shouldFocus={{this.benefitItem.last}}
@placeholder={{if this.isFreeTier "Access to all public posts" "Expert analysis"}}
@value={{readonly this.name}}
@input={{action "updateLabel" value="target.value"}}
@keyPress={{action "clearLabelErrors"}}
@stopEnterKeyDownPropagation={{true}}
@focus-out={{action "updateLabel" this.name}}
data-test-input="benefit-label" />
<div class="gh-blognav-line {{unless @benefitItem.name "placeholder"}}">
{{svg-jar "check-2"}}
<span
class="gh-blognav-label"
{{validation-status errors=@benefitItem.errors property="name" hasValidated=@benefitItem.hasValidated}}
>
<GhTrimFocusInput
@shouldFocus={{@benefitItem.last}}
@placeholder={{if this.isFreeTier "Access to all public posts" "Expert analysis"}}
@value={{readonly @benefitItem.name}}
@input={{this.handleLabelInput}}
@keyPress={{this.clearLabelErrors}}
@stopEnterKeyDownPropagation={{true}}
@focus-out={{fn this.updateLabel @benefitItem.name}}
data-test-input="benefit-label" />
<GhErrorMessage
@errors={{this.benefitItem.errors}}
@property="name"
data-test-error="benefit-label" />
</span>
</div>
<GhErrorMessage
@errors={{@benefitItem.errors}}
@property="name"
data-test-error="benefit-label" />
</span>
</div>
{{#if this.benefitItem.isNew}}
<button type="button" class="gh-blognav-add" {{action "addItem" this.benefitItem}} data-test-button="add-benefit">
{{svg-jar "plus"}}<span class="sr-only">Add</span>
</button>
{{else}}
<button type="button" class="gh-blognav-delete" {{action "deleteItem" this.benefitItem}} data-test-button="delete-benefit">
{{svg-jar "trash"}}<span class="sr-only">Delete</span>
</button>
{{/if}}
{{#if @benefitItem.isNew}}
<button type="button" class="gh-blognav-add" {{on "click" (fn @addItem @benefitItem)}} data-test-button="add-benefit">
{{svg-jar "plus"}}<span class="sr-only">Add</span>
</button>
{{else}}
<button type="button" class="gh-blognav-delete" {{on "click" (fn @deleteItem @benefitItem)}} data-test-button="delete-benefit">
{{svg-jar "trash"}}<span class="sr-only">Delete</span>
</button>
{{/if}}
</div>

View file

@ -1,58 +1,19 @@
import Component from '@ember/component';
import ValidationState from 'ghost-admin/mixins/validation-state';
import boundOneWay from 'ghost-admin/utils/bound-one-way';
import {computed} from '@ember/object';
import {readOnly} from '@ember/object/computed';
import {run} from '@ember/runloop';
import Component from '@glimmer/component';
import {action} from '@ember/object';
export default Component.extend(ValidationState, {
classNames: 'gh-blognav-item',
classNameBindings: ['errorClass', 'benefitItem.isNew::gh-blognav-item--sortable'],
new: false,
// closure actions
addItem() {},
deleteItem() {},
updateLabel() {},
name: boundOneWay('benefitItem.name'),
errors: readOnly('benefitItem.errors'),
errorClass: computed('hasError', function () {
return this.hasError ? 'gh-blognav-item--error' : '';
}),
actions: {
addItem(item) {
this.addItem(item);
},
deleteItem(item) {
this.deleteItem(item);
},
updateLabel(value) {
this.set('name', value);
return this.updateLabel(value, this.benefitItem);
},
clearLabelErrors() {
if (this.get('benefitItem.errors')) {
this.get('benefitItem.errors').remove('name');
}
}
},
keyPress(event) {
// enter key
if (event.keyCode === 13) {
event.preventDefault();
if (this.get('benefitItem.isNew')) {
run.scheduleOnce('actions', this, this.send, 'addItem', this.benefitItem);
} else {
run.scheduleOnce('actions', this, this.send, 'focusItem', this.benefitItem);
}
}
export default class GhBenefitItem extends Component {
@action
handleLabelInput(event) {
this.updateLabel(event.target.value);
}
});
@action
updateLabel(value) {
this.args.updateLabel(value, this.args.benefitItem);
}
@action
clearLabelErrors() {
this.args.benefitItem.errors?.remove('name');
}
}

View file

@ -1,12 +1,18 @@
import Modifier from 'ember-modifier';
import {isEmpty} from '@ember/utils';
const errorClass = 'error';
const successClass = 'success';
const ERROR_CLASS = 'error';
const SUCCESS_CLASS = 'success';
export default class ValidationStatusModifier extends Modifier {
modify(element, positional, {errors, property, hasValidated}) {
const validationClass = this.errorClass(errors, property, hasValidated);
modify(element, positional, {errors, property, hasValidated, errorClass = ERROR_CLASS, successClass = SUCCESS_CLASS}) {
const hasError = this.hasError(errors, property, hasValidated);
let validationClass = '';
if (!property || hasValidated?.includes(property)) {
validationClass = hasError ? errorClass : successClass;
}
element.classList.remove(errorClass);
element.classList.remove(successClass);
@ -16,16 +22,6 @@ export default class ValidationStatusModifier extends Modifier {
}
}
errorClass(errors, property, hasValidated) {
const hasError = this.hasError(errors, property, hasValidated);
if (hasValidated && hasValidated.includes(property)) {
return hasError ? errorClass : successClass;
} else {
return '';
}
}
hasError(errors, property, hasValidated) {
// if we aren't looking at a specific property we always want an error class
if (!property && errors && !errors.get('isEmpty')) {

View file

@ -0,0 +1,129 @@
import DS from 'ember-data'; // eslint-disable-line
import EmberObject from '@ember/object';
import hbs from 'htmlbars-inline-precompile';
import {expect} from 'chai';
import {find, render} from '@ember/test-helpers';
import {settled} from '@ember/test-helpers';
import {setupRenderingTest} from 'ember-mocha';
const {Errors} = DS;
describe('Integration: Modifier: validation-status', function () {
setupRenderingTest();
this.beforeEach(function () {
let testObject = EmberObject.create();
testObject.name = 'Test';
testObject.hasValidated = [];
testObject.errors = Errors.create();
this.set('testObject', testObject);
});
it('handles missing params', async function () {
await render(hbs`<div class="test" {{validation-status}}></div>`);
expect(find('.test')).to.exist;
});
describe('with errors/property/hasValidated params', function () {
it('has no success/error class by default', async function () {
await render(hbs`
<div
class="test"
{{validation-status
property="name"
errors=this.testObject.errors
hasValidated=this.testObject.hasValidated
}}
></div>
`);
expect(find('.test').classList).to.have.length(1);
});
it('has success class when valid', async function () {
await render(hbs`
<div
class="test"
{{validation-status
property="name"
errors=this.testObject.errors
hasValidated=this.testObject.hasValidated
}}
></div>
`);
this.testObject.hasValidated.pushObject('name'); // pushObject vs push because this is an EmberArray and we're testing tracking
await settled();
expect(find('.test')).to.have.class('success');
expect(find('.test')).to.not.have.class('error');
});
it('has error class when invalid', async function () {
await render(hbs`
<div
class="test"
{{validation-status
property="name"
errors=this.testObject.errors
hasValidated=this.testObject.hasValidated
}}
></div>
`);
this.testObject.hasValidated.pushObject('name'); // pushObject vs push because this is an EmberArray and we're testing tracking
this.testObject.errors.add('name', 'has error');
await settled();
expect(find('.test')).to.have.class('error');
expect(find('.test')).to.not.have.class('success');
});
it('always has error class when no property is passed', async function () {
await render(hbs`
<div
class="test"
{{validation-status
errors=this.testObject.errors
hasValidated=this.testObject.hasValidated
}}
></div>
`);
this.testObject.hasValidated.pushObject('different'); // pushObject vs push because this is an EmberArray and we're testing tracking
this.testObject.errors.add('different', 'has error');
await settled();
expect(find('.test')).to.have.class('error');
expect(find('.test')).to.not.have.class('success');
});
it('can have custom success/error classes', async function () {
await render(hbs`
<div
class="test"
{{validation-status
property="name"
errors=this.testObject.errors
hasValidated=this.testObject.hasValidated
errorClass="custom-error"
successClass="custom-success"
}}
></div>
`);
this.testObject.hasValidated.pushObject('name'); // pushObject vs push because this is an EmberArray and we're testing tracking
await settled();
expect(find('.test')).to.have.class('custom-success');
expect(find('.test')).to.not.have.class('success');
this.testObject.errors.add('name', 'invalid');
await settled();
expect(find('.test')).to.have.class('custom-error');
expect(find('.test')).to.not.have.class('error');
});
});
});