Skip to content

Commit

Permalink
Add required text to labels based on the model
Browse files Browse the repository at this point in the history
This commit adds the ability for the form to represent required fields
based on the validations present on the model.

Currently only supports ember-model-validators, but could be extended
easily through the required attribute.
  • Loading branch information
joegaudet committed Mar 1, 2023
1 parent 68b09b1 commit 60f690d
Show file tree
Hide file tree
Showing 11 changed files with 96 additions and 5 deletions.
9 changes: 8 additions & 1 deletion addon/components/field-for.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,14 @@
{{did-insert this.registerElement}}
>
{{#unless this.hideDefaultLabel}}
<LabelFor @label={{this.label}} @controlId={{this.controlId}} @infoText={{this.infoText}} />
<LabelFor
@label={{this.label}}
@controlId={{this.controlId}}
@infoText={{this.infoText}}
@required={{this.required}}
@form={{this.form}}
@requiredText={{this.form.requiredText}}
/>
{{/unless}}
{{#if this._showValue}}
{{#if this.displayValueComponent}}
Expand Down
4 changes: 3 additions & 1 deletion addon/components/field-for.js
Original file line number Diff line number Diff line change
Expand Up @@ -526,7 +526,9 @@ export default class FieldForComponent extends Component {
* @public
*/
@arg(bool)
required = false;
get required() {
return get(this, `form.model.validations.${this.for}.presence`) ?? false;
}

/**
* Whether or not this field is disabled
Expand Down
10 changes: 10 additions & 0 deletions addon/components/form-for.js
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,16 @@ export default class FormForComponent extends Component {
@arg(bool)
enforceRequiredFields = false;

@arg(bool)
get showRequiredIndicator() {
return this.formFor.showRequiredIndicator ?? false;
}

@arg(string)
get requiredText() {
return this.formFor.requiredText;
}

/**
* Resets the dirty model properties to their previous values on the form destruction. Turning this off will
* leave the model in a dirty state, even when the user navigates away.
Expand Down
8 changes: 6 additions & 2 deletions addon/components/label-for.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
{{#if this.customLabelComponent}}
{{component this.customLabelComponent for=@controlId label=@label}}
{{else}}
<label for="{{@controlId}}" ...attributes>
<label for="{{@controlId}}" data-test-label-for ...attributes>
{{#if (has-block)}}
{{yield @controlId}}
{{else}}
Expand All @@ -13,9 +13,13 @@
{{#if this.formFor.customInfoTextComponent}}
{{component this.formFor.customInfoTextComponent infoText=@infoText}}
{{else}}
<span title="{{@infoText}}">ℹ️</span>
<span class="ff-info-text" title="{{@infoText}}">ℹ️</span>
{{/if}}
{{/if}}

{{#if this.showRequiredIndicator}}
<span class="ff-required-text">{{this.requiredText}}</span>
{{/if}}
</label>
{{/if}}
{{/if}}
16 changes: 15 additions & 1 deletion addon/components/label-for.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
import { arg } from 'ember-arg-types';
import { object, string } from 'prop-types';
import { object, string, boolean } from 'prop-types';
import { next } from '@ember/runloop';

export default class LabelFor extends Component {
Expand All @@ -20,6 +20,20 @@ export default class LabelFor extends Component {
@arg(object)
field;

@arg(object)
form;

@arg(string)
requiredText;

@arg(boolean)
required = false;

@arg(boolean)
get showRequiredIndicator() {
return this.required && this.form?.showRequiredIndicator;
}

constructor() {
super(...arguments);

Expand Down
2 changes: 2 additions & 0 deletions addon/services/form-for.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ export default class FormForService extends Service {
this.customLabelComponent = this.config.customLabelComponent;
this.customInfoTextComponent = this.config.customInfoTextComponent;
this.preventsNavigationByDefault = this.config.preventsNavigationByDefault;
this.showRequiredIndicator = this.config.showRequiredIndicator;
this.requiredText = this.config.requiredText;

this.router.on('routeWillChange', (transition) => {
if (
Expand Down
2 changes: 2 additions & 0 deletions config/environment.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ module.exports = function (/* environment, appConfig */) {
useBemClass: false,
fieldForControlCalloutClasses: 'field-for-control-callout',
fieldForControlCalloutPosition: 'bottom left',
showRequiredIndicator: true,
requiredText: '*',

buttonClasses: '',
buttonActingClass: '',
Expand Down
6 changes: 6 additions & 0 deletions tests/dummy/app/templates/docs/forms.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,3 +262,9 @@ forms: ```form-for-model-name```
fields: ```form-for-model-name__field-for-field-name```

buttons: ```form-for-model-name__button-type-button```

## Required Field Text

If a form's model has validations (currently only ember-model-validator) then it can be configured to display a required
indicator in the label. This text can be configured at the environment level, and the form level. You can also override
the required value as an argument.
8 changes: 8 additions & 0 deletions tests/dummy/app/templates/docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ APP: {
formClasses: 'form',
fieldForControlCalloutClasses: 'field-for-control-callout',
fieldForControlCalloutPosition: 'bottom left',

// bem classes
bemClassPrefix: '',
useBemClass: false,

// required indicator
showRequiredIndicator: true,
requiredText: '*',

buttonClasses: '',
submitButtonClasses: '',
Expand Down
1 change: 1 addition & 0 deletions tests/dummy/config/environment.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ module.exports = function (environment) {
useBemClass: false,
fieldForControlCalloutClasses: 'field-for-control-callout',
fieldForControlCalloutPosition: 'bottom left',
requiredText: '*',

buttonClasses: 'a-button',
buttonActingClass: 'acting',
Expand Down
35 changes: 35 additions & 0 deletions tests/integration/components/form-for-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -703,4 +703,39 @@ module('Integration | Component | form for', function (hooks) {
assert.equal(this.validationOptions.only[0], 'foo');
assert.equal(this.validationOptions.only.length, 1);
});

test('displays the required text if the form is configured to do so and an attribute is marked required', async function (assert) {
this.model = { foo: null, bar: null, validations: { foo: { presence: true } } };

await render(hbs`
<Form
@for={{this.model}}
@enforceRequiredFields={{true}}
@showRequiredIndicator={{true}}
as |form|>
<form.field @for='foo' @label='test'/>
<form.submit />
</Form>
`);

assert.dom('[data-test-field-for="object_foo"] [data-test-label-for]').containsText('*');
});

test('lets you override the required text', async function (assert) {
this.model = { foo: null, bar: null, validations: { foo: { presence: true } } };

await render(hbs`
<Form
@for={{this.model}}
@enforceRequiredFields={{true}}
@showRequiredIndicator={{true}}
@requiredText="required"
as |form|>
<form.field @for='foo' @label='test'/>
<form.submit />
</Form>
`);

assert.dom('[data-test-field-for="object_foo"] [data-test-label-for]').containsText('required');
});
});

0 comments on commit 60f690d

Please sign in to comment.